mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-01 15:58:37 +00:00
This PR makes the NodePrepareResources() and NodeUnprepareResource() calls of the kubeletplugin API for DynamicResourceAllocation symmetrical. It wasn't clear how one would use the set of CDIDevices passed back in the NodeUnprepareResource() of the v1alpha1 API, and the new API now passes back the full ResourceHandle that was originally passed to the Prepare() call. Passing the ResourceHandle is strictly more informative and a plugin could always (re)derive the set of CDIDevice from it. This is a breaking change, but this release is scheduled to break multiple APIs for DynamicResourceAllocation, so it makes sense to do this now instead of later. Signed-off-by: Kevin Klues <kklues@nvidia.com>
209 lines
5.9 KiB
Go
209 lines
5.9 KiB
Go
/*
|
|
Copyright 2022 The Kubernetes 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 app
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
|
|
"k8s.io/dynamic-resource-allocation/kubeletplugin"
|
|
"k8s.io/klog/v2"
|
|
drapbv1 "k8s.io/kubelet/pkg/apis/dra/v1alpha2"
|
|
)
|
|
|
|
type ExamplePlugin struct {
|
|
logger klog.Logger
|
|
d kubeletplugin.DRAPlugin
|
|
fileOps FileOperations
|
|
|
|
cdiDir string
|
|
driverName string
|
|
nodeName string
|
|
|
|
mutex sync.Mutex
|
|
prepared map[ClaimID]bool
|
|
}
|
|
|
|
// ClaimID contains both claim name and UID to simplify debugging. The
|
|
// namespace is not included because it is random in E2E tests and the UID is
|
|
// sufficient to make the ClaimID unique.
|
|
type ClaimID struct {
|
|
Name string
|
|
UID string
|
|
}
|
|
|
|
var _ drapbv1.NodeServer = &ExamplePlugin{}
|
|
|
|
// getJSONFilePath returns the absolute path where CDI file is/should be.
|
|
func (ex *ExamplePlugin) getJSONFilePath(claimUID string) string {
|
|
return filepath.Join(ex.cdiDir, fmt.Sprintf("%s-%s.json", ex.driverName, claimUID))
|
|
}
|
|
|
|
// FileOperations defines optional callbacks for handling CDI files.
|
|
type FileOperations struct {
|
|
// Create must overwrite the file.
|
|
Create func(name string, content []byte) error
|
|
|
|
// Remove must remove the file. It must not return an error when the
|
|
// file does not exist.
|
|
Remove func(name string) error
|
|
}
|
|
|
|
// StartPlugin sets up the servers that are necessary for a DRA kubelet plugin.
|
|
func StartPlugin(logger klog.Logger, cdiDir, driverName string, nodeName string, fileOps FileOperations, opts ...kubeletplugin.Option) (*ExamplePlugin, error) {
|
|
if fileOps.Create == nil {
|
|
fileOps.Create = func(name string, content []byte) error {
|
|
return os.WriteFile(name, content, os.FileMode(0644))
|
|
}
|
|
}
|
|
if fileOps.Remove == nil {
|
|
fileOps.Remove = func(name string) error {
|
|
if err := os.Remove(name); err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
ex := &ExamplePlugin{
|
|
logger: logger,
|
|
fileOps: fileOps,
|
|
cdiDir: cdiDir,
|
|
driverName: driverName,
|
|
nodeName: nodeName,
|
|
prepared: make(map[ClaimID]bool),
|
|
}
|
|
|
|
opts = append(opts,
|
|
kubeletplugin.Logger(logger),
|
|
kubeletplugin.DriverName(driverName),
|
|
)
|
|
d, err := kubeletplugin.Start(ex, opts...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("start kubelet plugin: %w", err)
|
|
}
|
|
ex.d = d
|
|
|
|
return ex, nil
|
|
}
|
|
|
|
// stop ensures that all servers are stopped and resources freed.
|
|
func (ex *ExamplePlugin) Stop() {
|
|
ex.d.Stop()
|
|
}
|
|
|
|
func (ex *ExamplePlugin) IsRegistered() bool {
|
|
status := ex.d.RegistrationStatus()
|
|
if status == nil {
|
|
return false
|
|
}
|
|
return status.PluginRegistered
|
|
}
|
|
|
|
// NodePrepareResource ensures that the CDI file for the claim exists. It uses
|
|
// a deterministic name to simplify NodeUnprepareResource (no need to remember
|
|
// or discover the name) and idempotency (when called again, the file simply
|
|
// gets written again).
|
|
func (ex *ExamplePlugin) NodePrepareResource(ctx context.Context, req *drapbv1.NodePrepareResourceRequest) (*drapbv1.NodePrepareResourceResponse, error) {
|
|
logger := klog.FromContext(ctx)
|
|
|
|
// Determine environment variables.
|
|
var p parameters
|
|
if err := json.Unmarshal([]byte(req.ResourceHandle), &p); err != nil {
|
|
return nil, fmt.Errorf("unmarshal resource handle: %w", err)
|
|
}
|
|
|
|
// Sanity check scheduling.
|
|
if p.NodeName != "" && ex.nodeName != "" && p.NodeName != ex.nodeName {
|
|
return nil, fmt.Errorf("claim was allocated for %q, cannot be prepared on %q", p.NodeName, ex.nodeName)
|
|
}
|
|
|
|
// CDI wants env variables as set of strings.
|
|
envs := []string{}
|
|
for key, val := range p.EnvVars {
|
|
envs = append(envs, key+"="+val)
|
|
}
|
|
|
|
deviceName := "claim-" + req.ClaimUid
|
|
vendor := ex.driverName
|
|
class := "test"
|
|
spec := &spec{
|
|
Version: "0.3.0", // This has to be a version accepted by the runtimes.
|
|
Kind: vendor + "/" + class,
|
|
// At least one device is required and its entry must have more
|
|
// than just the name.
|
|
Devices: []device{
|
|
{
|
|
Name: deviceName,
|
|
ContainerEdits: containerEdits{
|
|
Env: envs,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
filePath := ex.getJSONFilePath(req.ClaimUid)
|
|
buffer, err := json.Marshal(spec)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal spec: %w", err)
|
|
}
|
|
if err := ex.fileOps.Create(filePath, buffer); err != nil {
|
|
return nil, fmt.Errorf("failed to write CDI file %v", err)
|
|
}
|
|
|
|
dev := vendor + "/" + class + "=" + deviceName
|
|
resp := &drapbv1.NodePrepareResourceResponse{CdiDevices: []string{dev}}
|
|
|
|
ex.mutex.Lock()
|
|
defer ex.mutex.Unlock()
|
|
ex.prepared[ClaimID{Name: req.ClaimName, UID: req.ClaimUid}] = true
|
|
|
|
logger.V(3).Info("CDI file created", "path", filePath, "device", dev)
|
|
return resp, nil
|
|
}
|
|
|
|
// NodeUnprepareResource removes the CDI file created by
|
|
// NodePrepareResource. It's idempotent, therefore it is not an error when that
|
|
// file is already gone.
|
|
func (ex *ExamplePlugin) NodeUnprepareResource(ctx context.Context, req *drapbv1.NodeUnprepareResourceRequest) (*drapbv1.NodeUnprepareResourceResponse, error) {
|
|
logger := klog.FromContext(ctx)
|
|
|
|
filePath := ex.getJSONFilePath(req.ClaimUid)
|
|
if err := ex.fileOps.Remove(filePath); err != nil {
|
|
return nil, fmt.Errorf("error removing CDI file: %w", err)
|
|
}
|
|
logger.V(3).Info("CDI file removed", "path", filePath)
|
|
|
|
ex.mutex.Lock()
|
|
defer ex.mutex.Unlock()
|
|
delete(ex.prepared, ClaimID{Name: req.ClaimName, UID: req.ClaimUid})
|
|
|
|
return &drapbv1.NodeUnprepareResourceResponse{}, nil
|
|
}
|
|
|
|
func (ex *ExamplePlugin) GetPreparedResources() []ClaimID {
|
|
ex.mutex.Lock()
|
|
defer ex.mutex.Unlock()
|
|
var prepared []ClaimID
|
|
for claimID := range ex.prepared {
|
|
prepared = append(prepared, claimID)
|
|
}
|
|
return prepared
|
|
}
|