DRA Network Framework and Plugin SDK
A framework-first implementation plan with separate VF and bond plugins and a shared VLAN, ipvlan, and macvlan service
DRA Network Framework and Plugin SDK
Status: Proposed implementation blueprint; not a released SDK or a working node-network implementation.
Revision: 2026-09-16.
Companions: Allocation and topology design and Device discovery design.
Scope: Build the common execution framework and Go SDK first, then use them to implement small NetworkPlugins. Preserve the companions’ four lifecycle hooks, typed ports, immutable revisions, and allocation ownership model.
Examples: Go package paths, SDK helpers, effect executors, and custom resource shapes below are proposed project APIs. The Go examples were checked with a planning-only test harness; they do not implement gRPC serving, durable execution, or Linux networking. Section 16 states the validation boundary.
1. Selected Implementation Approach
Implement three node-local services exposing five logical NetworkPlugins:
| Service process / artifact | Logical plugin names | Initial role |
|---|---|---|
networkplugin-vf | sriov | Prepare and attach an exclusively allocated kernel VF |
networkplugin-bond | bond | Create a bond from interfaces already attached to the sandbox |
networkplugin-links | vlan, ipvlan, macvlan | One shared implementation package and process with three independent contracts |
Shared service does not mean shared allocation. VLAN, ipvlan, and macvlan share packaging, code, and SDK infrastructure. Their allocation semantics, supported configurations, state partitions, and compatibility rules remain distinct.
The network DRA authority still publishes inventory and constraints; kube-scheduler allocates the roots; the node coordinator authorizes and executes the selected topology. Plugins must not choose replacement hardware or register independent DRA inventories for the same resources.
1.1 What stays unchanged, and what this document adds
| Inherited from the companion designs | Newly proposed here |
|---|---|
| Four mutating NetworkPlugin RPCs | A Go handler facade that returns local, serializable effect plans |
| Separate read-only inspection service | An SDK host that implements transport, inspection, replay, and common validation |
| Named typed ports and allocation provenance | A reusable Linux effect kit for link ownership, moves, membership, and restoration |
| Coordinator and plugin journals | A common write-ahead runner with explicit effect-level recovery |
| Logical plugins may share a process | Three services; the link service hosts three identity-bound endpoints |
| Immutable plugin and topology revisions | Per-contract state partitions and a shared-service upgrade policy |
The SDK is a convenience implementation of the existing project wire contract. A plugin written in another language may implement the same wire protocol without this Go SDK. There is no requirement to add fields to upstream Kubernetes APIs.
1.2 The first end-to-end examples
Two exclusive VF allocations:
sriov: vf0 -- net1 --\
bond: bond0 -- vlan: data0
sriov: vf1 -- net2 --/
One shared-parent allocation, separate example:
macvlan: allocated parent share -- secondary0
Alternative shared-parent mode, separate example:
ipvlan: allocated parent share -- secondary0The last two examples are alternatives, not permission to run both modes on the same host parent at the same time. Hosting them in one process does not bypass the published compatibility domains. This also matches the documented macvlan/ipvlan restriction on a common master. E6
These examples provide layer-2 interface realization only. They do not allocate addresses, replace the primary network, install a default route, configure switch ports, or promise RDMA bonding. IPAM and address installation remain separate plugins under the allocation design.
2. Process Boundaries and Logical Plugin Registration
2.1 Preserve the existing protobuf
Appendix A of the allocation design defines a call targeting one plugin endpoint. GetInfo returns one plugin_name, and mutation requests do not contain an arbitrary plugin-name routing field.
Use one identity-bound Unix socket per logical plugin, even when several sockets belong to one process:
node coordinator
|
+-- sriov.sock ----> networkplugin-vf
|
+-- bond.sock -----> networkplugin-bond
|
+-- vlan.sock -----+
+-- ipvlan.sock ---+--> networkplugin-links
+-- macvlan.sock --+The SDK binds the logical identity to the server adapter registered on that socket. For example, GetInfo on vlan.sock returns the VLAN contract, while GetInfo on ipvlan.sock returns the ipvlan contract. Both may report the same artifact digest but must report their own contract digest.
This is ordinary local RPC composition, not Go’s dynamic plugin package. No mutation is dispatched by interpreting a tenant-supplied config.type, socket path, or executable name.
2.2 Three deployment artifacts, five registry entries
Keep the companion’s NetworkPlugin registry shape and publish five logical entries. The vlan, ipvlan, and macvlan entries point to the same approved link-service image digest. Local registration associates each identity with its actual socket; the workload does not choose that path.
| Registry name | Artifact | Initial stepKinds | Preparation output | Connection output |
|---|---|---|---|---|
sriov | VF service | [Allocation] | Internal prepared-VF state | interface: KernelInterface/v1 |
bond | Bond service | [Operation] | None | interface: KernelInterface/v1 |
vlan | Link service | [Operation] | None | interface: KernelInterface/v1 |
macvlan | Link service | [Allocation] | parent: SharedParent/v1 | interface: KernelInterface/v1 |
ipvlan | Link service | [Allocation] | parent: SharedParent/v1 | interface: KernelInterface/v1 |
Macvlan and ipvlan are Allocation roots in this initial SDK profile because creating a child on a shared host parent must be charged to an allocated share. Supporting them as derived operations on an already-owned sandbox interface requires a separately declared contract and effects-budget validation; it is not silently enabled by adding an input port.
Preparation outputs are not ordinary connection outputs. The node coordinator passes a root’s prepared state to that same root’s connection call. An unrelated operation cannot use a SharedParent/v1 preparation output as a way to create uncharged host children.
3. Framework Responsibilities and Repository Layout
3.1 Put common correctness in the SDK
| Layer | Responsibility | Plugin author should not duplicate |
|---|---|---|
| Control-plane compiler | Validate graph, effects budget, descriptors, root selectors, and immutable revisions | DeviceClass generation or allocation selection |
| Node coordinator | Resolve actual claim allocations, authorize the sandbox, retain namespaces, coordinate resource ownership and readiness | Kubelet/NRI integration and cross-plugin claim ownership |
| SDK RPC host | Authenticate calls, validate envelopes, bind plugin identity, enforce phase rules, translate errors | Hand-written gRPC dispatch and inconsistent retry handling |
| SDK operation runner | Persist intent, replay responses, serialize duplicate calls, enforce generations, execute/undo effects | A separate journal and idempotency scheme in every plugin |
| SDK resource services | Verify handles and grants, resolve namespaces, validate original ownership, coordinate locks | Using untrusted interface names as authority |
| Linux effect kit | Native VF/link effects, before-images, observation, safe compensation | Repeated low-level link creation and cleanup patterns |
| Logical plugin handler | Declare its contract and assemble the supported operations | Generic transport, Kubernetes clients, or runtime callbacks |
The framework cannot invent the inverse of an arbitrary kernel action. Every registered effect executor must implement its own observation, before-image, mutation, and recovery rules. Reuse those executors where the semantics genuinely match.
3.2 Suggested package layout
networkplugins/
api/networkplugin/v1alpha1/ # Generated from the companion protobuf
sdk/
handler.go # Public handler facade and call views
descriptor.go # Contracts, strict schemas, ports, effect permissions
plan.go # Serializable plans, validation, stable output IDs
host/ # Identity-bound gRPC servers and inspection
runner/ # Intent, execution, replay, undo, fencing
store/ # Durable transactional storage interface
resources/ # Verified handles, grants, ownership references
namespaces/ # Verified namespace entry/open/restore
testkit/ # Fake kernel, durable test stores, fault injection
effects/
builders/ # Typed descriptions of effects; no syscalls
linux/ # Registered native effect executors
ownership/ # Prepared-parent entitlement effects
plugins/
vf/ # Logical sriov plugin
bond/ # Logical bond plugin
links/ # VLAN/ipvlan/macvlan handler family
cmd/
networkplugin-vf/
networkplugin-bond/
networkplugin-links/
internal/
claimresolver/ # Kubernetes-specific integration, not plugin code
coordinator/ # Cross-plugin preparation/attachment transactions
runtimeadapter/ # NRI integration and mandatory readiness checks
contracts/ # Versioned descriptor and schema bundles
examples/ # Small topologies and SDK usageFor readability, the examples import effect builders as example.com/networkplugins/effects. That placeholder module is not a published dependency. Concrete package paths can be finalized when creating the repository.
Do not put Kubernetes or NRI clients in each logical plugin. The coordinator supplies the verified allocation and sandbox context; the plugin receives only the authority and observations needed for its effects.
4. The Proposed Go SDK Facade
4.1 Keep the four hooks, return local plans
The public lifecycle remains unchanged:
| Hook | VF | Bond | VLAN | Macvlan / ipvlan |
|---|---|---|---|---|
NodePrepare | Snapshot and prepare the allocated VF | Rejected | Rejected | Hold one authorized shared-parent entitlement |
ConnectNetwork | Move/connect the prepared VF | Create bond and memberships | Create VLAN on an authorized sandbox parent | Create one child for the prepared share |
RemoveNetwork | Return VF to prepared state | Undo memberships and delete owned bond | Delete owned VLAN | Delete owned child |
NodeUnprepare | Restore preparation changes | Rejected | Rejected | Release this preparation’s parent reference |
The new SDK facade returns a plan, not the protobuf response directly:
package sdk
import "context"
// Proposed SDK facade. These return LOCAL plans, not protobuf responses.
// The SDK adapter executes the plan and builds the existing RPC response.
type Handler interface {
Describe() Descriptor
NodePrepare(context.Context, *PrepareCall) (Plan, error)
ConnectNetwork(context.Context, *ConnectCall) (Plan, error)
RemoveNetwork(context.Context, *RemoveCall) (CleanupPlan, error)
NodeUnprepare(context.Context, *UnprepareCall) (CleanupPlan, error)
}
// A serializable description: no closures, shell commands, or live FDs.
type Plan struct {
Effects []EffectSpec
Exports map[string][]OutputRef
}
type EffectSpec struct {
ID string
Kind string // Versioned executor, e.g. linux/create-bond/v1.
Payload []byte // Canonical JSON validated by that executor's schema.
}
type OutputRef struct {
EffectID string
Port string
}
type CleanupPlan struct {
TargetOperationID string
TargetHook string // NodePrepare or ConnectNetwork.
}The RPC adapter runs that plan through the common operation runner and then builds the existing NodePrepareResponse, ConnectNetworkResponse, or CleanupResponse. This is an internal implementation choice, not an extra planning RPC or a change to the companion’s wire format.
A planning method must not mutate hardware. effects.CreateBond(...), for example, constructs an effect description; it does not create the link immediately. No effect may run until the entire local plan has been validated and durably recorded.
The plan builder retains validation/serialization errors and reports them from Build(). Effect IDs are unique within the hook plan; output references identify named ports from earlier effects. Execution is sequential within a step in the initial implementation. The topology coordinator controls ordering between steps.
4.2 Call views and prerequisite checks
The production SDK constructs call views only after authenticating and validating a request. The methods below are proposed conveniences, not trust granted by ordinary exported struct fields:
| Call-view helper | Required production check |
|---|---|
RequireExclusiveVF(ctx) | Allocation is an exclusive VF, stable physical identity matches retained inventory, and grants permit the requested preparation |
RequireSharedUnit(ctx, name, capacity) | Logical plugin/persona matches; actual allocated share has the required quantity; no unsupported extra consumption or conflicting parent contract |
RequirePreparedRoot(ctx, name) | Preparation is durably complete, matches allocation and logical identity, and remains usable for this sandbox |
RequireLocalInterfaces(ctx, port, min, max) | Correct port type/cardinality, verified namespace, authorized provenance, and permitted access mode |
RequireDistinctResources(...) | No repeated resource or physical-device identity, including two handles that alias the same VF |
InterfaceName("interface") | The coordinator assigned a valid, collision-free name for this output in this sandbox |
UndoConnect(...) / UndoPrepare(...) | Cleanup targets the original operation and its recorded plan; dependency, generation, and ownership checks still apply |
These checks complement, not replace, the actual effect executor’s revalidation immediately before mutation. Names, ifindexes, PCI addresses, and caller-returned state are observations; none is sufficient authority on its own.
Config decoding requires one JSON object, no duplicate or unknown keys, required fields, correct types, bounded sizes, and cross-field validation against the locked schema. DecodeConfig[T] performs typed decoding after that schema layer. Go zero values alone cannot distinguish all missing required fields.
4.3 Descriptors must be complete
Describe() in the small examples returns a descriptor reference, including ConfigSchemaID. The SDK host expands it from the artifact’s pinned schema bundle into the complete descriptor required by the allocation design: supported step kinds, config schema, input/output types and cardinality, allowed effects, preparation semantics, and state-format compatibility.
A missing descriptor bundle fails registration. A three-field Go descriptor summary must never be advertised as a complete production contract.
The examples deliberately select a small implementation profile:
- Kernel VF only;
rdmaAccessmust be false. - Active-backup bond, two to eight members, positive monitor interval.
- VLAN ID 1–4094 on a sandbox-local parent, layer-2-only.
- Macvlan bridge mode and ipvlan L2 mode on approved host parents.
These are project support choices, not the full Linux feature set. In particular, eight bond members is not presented as a kernel maximum. The full allocation document permits broader capabilities such as LACP; a restricted artifact must advertise its narrower schema and a distinct locked contract digest rather than claim to implement the broader contract unchanged. E4
4.4 Plan outputs versus wire resource handles
An OutputRef is a local reference such as create-bond/interface. After execution, the SDK resolves it into a ResourceHandle with the original wire fields: stable ID, type, owner scope/step, attachment identity, boot identity, root provenance, and validated description JSON.
The same output ID remains valid through later effects in the plan. Observations are refreshed at completion; creating a bond and then bringing it up does not mint a second bond handle. An update-only effect preserves the input’s identity.
The coordinator verifies output provenance and granted effects before accepting the RPC result. It does not trust a plugin simply because the response lists an allocation-root ID. Invalid output after mutation is a failed/uncertain operation requiring recovery, not permission to forget its effects.
5. Build the Durable Runner Before Native Plugins
5.1 Two journals, one operation identity
The coordinator records what it authorized and requested. Each plugin service records what it actually attempted and changed. A lost RPC response must not make completed side effects invisible.
Partition a plugin journal by logical identity and state-contract family, then preparation/attachment scope and operation ID. Store the exact artifact digest, contract digest, semantic digest, plan, before-images, effect progress, and replayable response in the record. Cleanup locates that original record even when the caller never received PluginState.
The shared link service may use one transactional database with three logical partitions. Different logical partitions do not imply different physical lock domains: macvlan and ipvlan referencing one physical parent must still coordinate through the same node authority.
5.2 Execution sequence
Authenticate peer and identity-bound endpoint.
Validate operation envelope, locked contract, allocation, sandbox, and grants.
Acquire scope serialization and the required resource ownership guards.
Check persisted generation/epoch and any terminal cleanup fence.
- A stale create cannot bypass a later removal.
- A matching completed request may replay its saved response.
- A duplicate in-progress request joins/reports that operation.
For a new intent:
Build a deterministic, side-effect-free plan.
Validate all effect kinds, inputs, permissions, and output provenance.
Commit the plan and operation intent durably.
For each effect:
Capture and persist its original state before mutation.
Mark the effect Applying durably.
Recheck identity, authority, and namespace availability.
Apply or reconcile this effect's owned state.
Observe and persist the effect's result.
Validate final exports and postconditions.
Persist the complete replayable RPC response.
Only then return success.Authentication and terminal-generation checks happen before replay. Replaying an old successful connection response after cleanup must not resurrect its validity.
Define semantic-digest inputs explicitly: logical plugin identity, hook, immutable preparation/attachment identity, allocation/config/input-handle meaning, assigned names, and desired generation. Trace IDs, RPC deadlines, and refreshed authentication-token bytes are not semantic configuration. Reusing an operation ID with different semantics is rejected.
5.3 Effect executor contract
Each effect kind has a versioned schema and four internal behaviors:
| Internal behavior | Purpose |
|---|---|
| Capture | Read identity and before-image; no external mutation |
| Observe | Determine whether the intended effect is absent, present as owned, partially applied, or conflicting |
| Apply | Perform the authorized mutation, using persistent evidence to avoid duplicate effects |
| Revert | Compensate only this effect’s owned changes using its before-image and current identity |
These are internal SDK effect operations, not additional NetworkPlugin RPC hooks. InspectOperation remains read-only; repair occurs only inside a replayed lifecycle intent or its authorized cleanup.
An effect such as PrepareVF is a convenience builder for a versioned subplan, not one opaque unjournaled function. Binding changes, VF attribute changes, namespace moves, renames, and link-state changes each need recoverable progress boundaries. A crash can occur between any two syscalls.
A useful per-effect progression is:
Planned -> Captured -> Applying -> Applied
| |
+--> Undoing <-+
|
Undone
Unverifiable ownership or lost state -> QuarantinedApplying without a durable result is uncertain. Recovery probes identity and ownership, not merely whether a link with the expected name exists. Link creation should attach a deterministic ownership marker at creation where the backend supports it; any gap between creation and marking requires an explicit reconciliation strategy. A foreign link with the expected name must never be adopted or deleted automatically.
5.4 Fencing is not just a generation comparison
The SDK records the highest accepted generation and coordinator epoch for each scope. Removal establishes a durable terminal fence against earlier connection work. It then waits for local workers to quiesce or resolves their uncertain effects before reporting successful cleanup.
An epoch does not fence a Linux syscall already running in an old process. A replacement service/coordinator must establish that the old writer is stopped or excluded by the node’s ownership mechanism, then reconcile its journal. Do not grant new ownership solely because a timeout expired or a Lease changed owner.
The node authority serializes conflicting work across plugin services. Resolve physical resource/domain keys before mutation, acquire multiple keys in a deterministic order, and hold them for the required operation boundary. Per-process sync.Mutex keyed by an interface name is not cross-plugin ownership control. Shared-parent references persist for preparation lifetime; they are different from short mutation locks.
5.5 Cleanup rules
UndoConnect loads the recorded original connection plan and compensates attempted effects in reverse order, including the effect that failed or timed out. UndoPrepare does the equivalent for preparation after the coordinator’s attachment-cleanup conditions pass.
Do not rebuild an undo plan from current configuration or the current plugin’s preferred defaults. Cleanup must work without the original success response, and it must retain the original implementation/state reader until safe retirement.
A cleanup response is successful only after every required effect is removed or verified absent. A queued cleanup request, transport error, or missing journal entry is not proof of absence. CleanupPending and quarantine states remain visible to the coordinator; they prevent unsafe physical reuse even when scheduler allocation state has changed.
A gRPC deadline or cancellation does not roll back a hardware mutation. Apply bounded retries to the same operation identity, disable mutation hedging, and implement duplicate suppression even when a transport can retry calls. E1 E2 E3
6. Separate Plugin Example: Kernel SR-IOV VFs
6.1 Contract and restoration boundaries
The sriov plugin operates only on the VF in its allocation binding. It never searches for a free VF, changes sriov_numvfs, resets a PF, or changes a PF-wide mode to make an allocation work.
| Phase | This plugin owns | Must remain untouched |
|---|---|---|
| Prepare | Original VF state snapshot, approved per-VF settings, local preparation reference | Other VFs and unrelated PF-wide configuration |
| Connect | Moving and naming the prepared VF in this sandbox; connection-scoped link state | The claim’s allocation and preparation lifetime |
| Remove | Returning the VF to its prepared state after downstream dependencies are gone | Claim-scoped preparation and any still-required device access |
| Unprepare | Restoring original preparation changes and releasing this preparation | Other preparations or scheduler allocation status |
The kernel-only example returns no CDI device IDs. Adding VFIO or RDMA access is a separate advertised profile and must prepare that access at NodePrepare, not introduce it during a derived operation.
6.2 Small handler
effects.PrepareVF builds the checked preparation subplan. effects.MoveVF builds a connection subplan that remembers both namespaces, names, and the prepared state; its compensation returns the VF rather than deleting it.
package vf
import (
"context"
"fmt"
"example.com/networkplugins/effects"
"example.com/networkplugins/sdk"
)
type Plugin struct{}
func New() *Plugin { return &Plugin{} }
func (*Plugin) Describe() sdk.Descriptor {
return sdk.Descriptor{
Name: "sriov", ContractVersion: "v1alpha1",
StepKinds: []sdk.StepKind{sdk.Allocation},
ConfigSchemaID: "sriov-kernel/v1alpha1",
}
}
type Config struct {
MTU int `json:"mtu"`
VF struct {
VLAN int `json:"vlan"`
SpoofCheck *bool `json:"spoofCheck,omitempty"`
Trust *bool `json:"trust,omitempty"`
} `json:"vf"`
RDMAAccess bool `json:"rdmaAccess"`
}
func (*Plugin) NodePrepare(ctx context.Context, c *sdk.PrepareCall) (sdk.Plan, error) {
if err := c.RequireExclusiveVF(ctx); err != nil {
return sdk.Plan{}, err
}
cfg, err := sdk.DecodeConfig[Config](c.ConfigJSON)
if err != nil {
return sdk.Plan{}, err
}
if cfg.RDMAAccess {
return sdk.Plan{}, fmt.Errorf("RDMA access is outside this kernel-VF profile")
}
p := sdk.NewPlan()
p.Add("prepare-vf", effects.PrepareVF(c.Allocation, cfg, c.Requirements))
return p.Build()
}
func (*Plugin) ConnectNetwork(ctx context.Context, c *sdk.ConnectCall) (sdk.Plan, error) {
prepared, err := c.RequirePreparedRoot(ctx, "sriov")
if err != nil {
return sdk.Plan{}, err
}
name, err := c.InterfaceName("interface")
if err != nil {
return sdk.Plan{}, err
}
p := sdk.NewPlan()
moved := p.Add("move-vf", effects.MoveVF(prepared, c.Sandbox, name))
p.Export("interface", moved.Output("interface"))
return p.Build()
}
func (*Plugin) RemoveNetwork(_ context.Context, c *sdk.RemoveCall) (sdk.CleanupPlan, error) {
return sdk.UndoConnect(c) // Replays the ORIGINAL effect plan, not current config.
}
func (*Plugin) NodeUnprepare(_ context.Context, c *sdk.UnprepareCall) (sdk.CleanupPlan, error) {
return sdk.UndoPrepare(c) // SDK verifies all attachment cleanup prerequisites.
}
var _ sdk.Handler = (*Plugin)(nil)The compiler propagates downstream requirements, such as a bond’s MTU, into c.Requirements. Preparation validates and applies only authorized VF-scoped requirements. Host-parent MTU or PF-global state is not implicitly mutable just because a child needs a larger MTU.
Use pointer booleans for settings where omission differs from an explicit false. The effect records precisely which fields it changes. During restore, a conflicting external writer causes a diagnostic/recovery decision, not an unconditional overwrite with an old snapshot.
7. Separate Plugin Example: Bond
7.1 A bond owns membership, not its VFs
The bond plugin is Operation-only. It receives two or more distinct KernelInterface/v1 handles in the sandbox and an AttachMember grant for each. It creates the bond, attaches those members, and returns the bond’s handle.
It does not move host interfaces, choose devices, release root preparations, or assign IP addresses. A member already owned by another master or carrying incompatible external configuration is rejected rather than forcibly detached or cleared.
The sample uses active-backup mode with explicit monitoring. LACP would also require the corresponding plugin support and a compatible external fabric configuration; neither follows from creating a bond object. E4
7.2 Small handler
The registered schema requires mode: active-backup, a positive monitorIntervalMs, and a supported MTU before this handler is called. Broader modes can be added under an explicitly expanded locked contract.
package bond
import (
"context"
"example.com/networkplugins/effects"
"example.com/networkplugins/sdk"
)
type Plugin struct{ sdk.OperationOnly }
func New() *Plugin { return &Plugin{} }
func (*Plugin) Describe() sdk.Descriptor {
return sdk.Descriptor{
Name: "bond", ContractVersion: "v1alpha1",
StepKinds: []sdk.StepKind{sdk.Operation},
ConfigSchemaID: "bond/v1alpha1",
}
}
type Config struct {
Mode string `json:"mode"`
MonitorIntervalMs int `json:"monitorIntervalMs"`
MTU int `json:"mtu"`
}
func (*Plugin) ConnectNetwork(ctx context.Context, c *sdk.ConnectCall) (sdk.Plan, error) {
members, err := c.RequireLocalInterfaces(ctx, "members", 2, 8)
if err != nil {
return sdk.Plan{}, err
}
// Includes physical identity, not just different user-visible port names.
if err := c.RequireDistinctResources(members); err != nil {
return sdk.Plan{}, err
}
cfg, err := sdk.DecodeConfig[Config](c.ConfigJSON)
if err != nil {
return sdk.Plan{}, err
}
name, err := c.InterfaceName("interface")
if err != nil {
return sdk.Plan{}, err
}
p := sdk.NewPlan()
created := p.Add("create-bond", effects.CreateBond(c.Sandbox, name, cfg))
for i, member := range members {
p.AddIndexed("attach-member", i,
effects.AttachMember(created.Output("interface"), member))
}
p.Add("activate-bond", effects.SetLinkUp(created.Output("interface")))
p.Export("interface", created.Output("interface"))
return p.Build()
}
func (*Plugin) RemoveNetwork(_ context.Context, c *sdk.RemoveCall) (sdk.CleanupPlan, error) {
return sdk.UndoConnect(c)
}
var _ sdk.Handler = (*Plugin)(nil)OperationOnly implements prepare/unprepare by returning the project’s wrong-step-kind error, translated to gRPC FAILED_PRECONDITION. It does not turn invalid lifecycle calls into successful no-ops.
For two members, the effect plan is:
create-bond
attach-member-0
attach-member-1
activate-bondBefore each membership change, the backend captures the member’s relevant link state and the changes that bonding may induce. Removal reverses the activation and memberships before deleting the owned bond. The coordinator first requires successful cleanup of dependent VLANs. Members are still root-owned interfaces; their later return to the host belongs to the VF plugin.
A successful bond RPC returns only the bond output, with member references in its typed description. It does not concatenate VF outputs into an inherited interface array.
8. Shared Plugin Example: VLAN, Ipvlan, and Macvlan
8.1 One implementation family, three explicit contracts
The common implementation shares:
- Strict call/config validation, namespace resolution, journals, and response replay.
- Child-link identity generation, ownership markers, link activation, and deletion verification.
- A versioned link-effect implementation library and conformance tests.
It does not share an untyped catch-all config or a global “delete all children” cleanup routine.
| Logical plugin | Parent source | Initial mode | Owned effect |
|---|---|---|---|
vlan | parent input from an earlier sandbox step | VLAN ID 1–4094 | One VLAN on that parent |
macvlan | This root’s prepared host-parent entitlement | bridge | One child for the allocated macvlan unit |
ipvlan | This root’s prepared host-parent entitlement | l2 | One child for the allocated ipvlan unit |
The VLAN example does not allocate IP addresses automatically. Macvlan/ipvlan preparation records one share-specific reference and checks the existing parent; it does not reset the parent or bring down existing users. The initial shared-parent contract requires an already provisioned parent with sufficient MTU and an allowed configuration.
8.2 Identity, config, and constructor
The kind is selected by trusted service registration, never by a request field that can switch a VLAN endpoint into an ipvlan operation.
package links
import (
"context"
"fmt"
"example.com/networkplugins/effects"
"example.com/networkplugins/sdk"
)
type Kind string
const (
VLAN Kind = "vlan"
IPVLAN Kind = "ipvlan"
MACVLAN Kind = "macvlan"
)
type Plugin struct{ kind Kind }
func New(kind Kind) (*Plugin, error) {
switch kind {
case VLAN, IPVLAN, MACVLAN:
return &Plugin{kind: kind}, nil
default:
return nil, fmt.Errorf("unsupported logical link plugin %q", kind)
}
}
func (p *Plugin) Describe() sdk.Descriptor {
kind := sdk.Allocation
if p.kind == VLAN {
kind = sdk.Operation
}
return sdk.Descriptor{
Name: string(p.kind), ContractVersion: "v1alpha1",
StepKinds: []sdk.StepKind{kind},
ConfigSchemaID: string(p.kind) + "/v1alpha1",
}
}
type Config struct {
VLANID *int `json:"vlanId,omitempty"`
Mode string `json:"mode,omitempty"`
MTU int `json:"mtu"`
}
func (p *Plugin) config(raw []byte) (Config, error) {
cfg, err := sdk.DecodeConfig[Config](raw)
if err != nil {
return Config{}, err
}
if cfg.MTU < 68 || cfg.MTU > 65535 {
return Config{}, fmt.Errorf("mtu is outside the descriptor bounds")
}
switch p.kind {
case VLAN:
if cfg.VLANID == nil || *cfg.VLANID < 1 || *cfg.VLANID > 4094 || cfg.Mode != "" {
return Config{}, fmt.Errorf("vlan requires vlanId 1..4094 and forbids mode")
}
case MACVLAN:
if cfg.VLANID != nil || cfg.Mode != "bridge" {
return Config{}, fmt.Errorf("this macvlan profile requires mode=bridge")
}
case IPVLAN:
if cfg.VLANID != nil || cfg.Mode != "l2" {
return Config{}, fmt.Errorf("this ipvlan profile requires mode=l2")
}
}
return cfg, nil
}A VLAN config containing mode is invalid. A macvlan or ipvlan config containing vlanId is invalid. Native link types have different semantics even though the code can reuse lifecycle helpers.
Ipvlan mode is also a parent-wide compatibility concern. The initial profile accepts only L2; adding L3/L3S requires a deliberate parent-mode and inventory/persona contract, not just expanding a string enum in the handler. E5
8.3 Root-only preparation for shared host parents
func (p *Plugin) NodePrepare(ctx context.Context, c *sdk.PrepareCall) (sdk.Plan, error) {
if p.kind == VLAN {
return sdk.Plan{}, sdk.ErrWrongStepKind
}
cfg, err := p.config(c.ConfigJSON)
if err != nil {
return sdk.Plan{}, err
}
capacity := "dra.networking/macvlans"
if p.kind == IPVLAN {
capacity = "dra.networking/ipvlans"
}
if err := c.RequireSharedUnit(ctx, string(p.kind), capacity); err != nil {
return sdk.Plan{}, err
}
plan := sdk.NewPlan()
hold := plan.Add("hold-parent",
effects.HoldSharedParent(c.Allocation, string(p.kind), cfg))
plan.Export("parent", hold.Output("parent"))
return plan.Build()
}HoldSharedParent validates the allocation’s actual quantity and returns a SharedParent/v1 preparation output. The node authority records the corresponding preparation reference under the physical parent/domain identity. The plugin service records its own durable receipt; neither side treats a process-local counter as the ownership authority.
This reference is not a new scheduler reservation. Capacity was reserved through DRA. A root preparation acquires permission to realize that specific allocation, and the SinglePod attachment rule prevents one share from producing simultaneous children in two sandboxes.
8.4 Connection and cleanup
func (p *Plugin) ConnectNetwork(ctx context.Context, c *sdk.ConnectCall) (sdk.Plan, error) {
cfg, err := p.config(c.ConfigJSON)
if err != nil {
return sdk.Plan{}, err
}
name, err := c.InterfaceName("interface")
if err != nil {
return sdk.Plan{}, err
}
plan := sdk.NewPlan()
var created sdk.EffectRef
if p.kind == VLAN {
parents, err := c.RequireLocalInterfaces(ctx, "parent", 1, 1)
if err != nil {
return sdk.Plan{}, err
}
created = plan.Add("create-child",
effects.CreateVLAN(parents[0], c.Sandbox, name, cfg))
} else {
prepared, err := c.RequirePreparedRoot(ctx, string(p.kind))
if err != nil {
return sdk.Plan{}, err
}
created = plan.Add("create-child",
effects.CreateSharedChild(prepared, c.Sandbox, name, string(p.kind), cfg))
}
plan.Add("activate-child", effects.SetLinkUp(created.Output("interface")))
plan.Export("interface", created.Output("interface"))
return plan.Build()
}
func (*Plugin) RemoveNetwork(_ context.Context, c *sdk.RemoveCall) (sdk.CleanupPlan, error) {
return sdk.UndoConnect(c)
}
func (p *Plugin) NodeUnprepare(_ context.Context, c *sdk.UnprepareCall) (sdk.CleanupPlan, error) {
if p.kind == VLAN {
return sdk.CleanupPlan{}, sdk.ErrWrongStepKind
}
return sdk.UndoPrepare(c)
}
var _ sdk.Handler = (*Plugin)(nil)CreateVLAN is restricted here to a verified sandbox-local KernelInterface/v1. CreateSharedChild consumes only the parent already bound to the root preparation and preserves its logical mode. Both return the child’s handle, not ownership of the parent.
The native effect backend must prove which namespace contains the parent and which contains the child; it cannot assume they are identical. If creation requires an intermediate link in the host namespace followed by a move, both phases are journaled, identifiable, and recoverable.
For macvlan/ipvlan, RemoveNetwork deletes this attachment’s child. NodeUnprepare releases only this preparation’s parent reference after cleanup. Neither operation deletes, resets, or reconfigures the shared host parent. A disappeared child does not by itself prove its IPAM lease or other downstream effects have been cleaned; those dependents have their own receipts.
9. Registering the Three Services
9.1 Small registration example
The following three functions run in three different command binaries. They are shown together only to make the packaging distinction clear.
package examples
import (
"example.com/networkplugins/plugins/bond"
"example.com/networkplugins/plugins/links"
"example.com/networkplugins/plugins/vf"
"example.com/networkplugins/sdk"
)
// These functions run in THREE DIFFERENT service processes.
// Each Host is constructed with its own durable store, effect registry,
// authenticated peer policy, pinned schema bundle, and artifact-specific run dir.
func RegisterVF(h *sdk.Host) error {
return h.Register(sdk.Endpoint{
SocketName: "sriov.sock", StateNamespace: "sriov/v1alpha1",
Handler: vf.New(),
})
}
func RegisterBond(h *sdk.Host) error {
return h.Register(sdk.Endpoint{
SocketName: "bond.sock", StateNamespace: "bond/v1alpha1",
Handler: bond.New(),
})
}
func RegisterLinks(h *sdk.Host) error {
for _, kind := range []links.Kind{links.VLAN, links.IPVLAN, links.MACVLAN} {
handler, err := links.New(kind)
if err != nil {
return err
}
if err := h.Register(sdk.Endpoint{
SocketName: string(kind) + ".sock",
StateNamespace: string(kind) + "/v1alpha1",
Handler: handler,
}); err != nil {
return err
}
}
return nil
}The proposed Host startup path validates all registrations and pinned descriptor/effect bundles before serving requests. If one registration is invalid, startup fails rather than exposing a partially configured set as fully ready.
A command’s remaining plumbing is conventional: parse administrator-provided service configuration, open its single-writer durable store, construct approved effect executors and peer authentication, register handlers, then serve until shutdown. Do not mark the service ready before state recovery and identity checks complete. Shutdown stops admitting new work, drains or records in-flight work, and leaves incomplete operations recoverable.
9.2 Local deployment layout
/run/dra-networkplugins/<vf-artifact-id>/sriov.sock
/run/dra-networkplugins/<bond-artifact-id>/bond.sock
/run/dra-networkplugins/<links-artifact-id>/vlan.sock
/run/dra-networkplugins/<links-artifact-id>/ipvlan.sock
/run/dra-networkplugins/<links-artifact-id>/macvlan.sock
/var/lib/dra-networkplugins/vf/ # One service writer
/var/lib/dra-networkplugins/bond/ # One service writer
/var/lib/dra-networkplugins/links/ # One service writer, three logical partitionsThese are proposed administrator-owned paths. Service identity, permissions, mount visibility, and state ownership must be set by deployment, not inferred from a path string.
An artifact-specific socket directory prevents accidentally reconnecting an old pinned plan to a newly replaced binary at the same path. Persistent data must outlive ordinary container replacement. Do not open the same service database from two independent writers during an upgrade.
The common link service has shared fate: restarting it interrupts all three logical endpoints. That is the operational cost of shared packaging. Per-logical readiness and state partitions improve diagnosis but do not provide process isolation. The same handlers can later be deployed separately without changing topology pluginRef names.
10. Small Topology Examples
The custom resources below follow the companion topology shape. They are design examples, not installable CRD definitions. The controller adds mandatory driver/plugin-support selectors and resolves friendly plugin references into immutable revisions; it does not rely on users supplying those checks correctly.
10.1 Two VFs, a bond, and a VLAN
This example combines all three services in four graph steps. The resulting interface is layer-2-ready only; address assignment is intentionally absent.
apiVersion: networking.dra.io/v1alpha1
kind: NetworkTopology
metadata:
name: sdk-vf-bond-vlan
spec:
runtimeProfile: nri-secondary-v1
attachmentPolicy: SinglePod
execution:
maxParallelSteps: 1
primaryNetworkPolicy: Preserve
steps:
- name: vf0
kind: Allocation
pluginRef: {name: sriov, contractVersion: v1alpha1}
allocation:
sharing: Exclusive
selector:
cel: >-
device.attributes["dra.networking"].type == "vf" &&
device.attributes["dra.networking"].pfName == "enp3s0f0"
interfaceName: net1
config:
mtu: 1500
vf: {vlan: 0, spoofCheck: false, trust: true}
rdmaAccess: false
- name: vf1
kind: Allocation
pluginRef: {name: sriov, contractVersion: v1alpha1}
allocation:
sharing: Exclusive
selector:
cel: >-
device.attributes["dra.networking"].type == "vf" &&
device.attributes["dra.networking"].pfName == "enp3s0f1"
interfaceName: net2
config:
mtu: 1500
vf: {vlan: 0, spoofCheck: false, trust: true}
rdmaAccess: false
- name: bond0
kind: Operation
pluginRef: {name: bond, contractVersion: v1alpha1}
inputs:
members:
- from: {step: vf0, output: interface}
- from: {step: vf1, output: interface}
interfaceName: bond0
config:
mode: active-backup
monitorIntervalMs: 100
mtu: 1500
- name: data-vlan
kind: Operation
pluginRef: {name: vlan, contractVersion: v1alpha1}
inputs:
parent:
from: {step: bond0, output: interface}
interfaceName: data0
config: {vlanId: 100, mtu: 1500}
exports:
data:
from: {step: data-vlan, output: interface}The selected VFs must support the required MTU and permitted per-VF settings. The example explicitly requests trust: true and spoofCheck: false as an administrator-approved demonstration profile; these are not SDK defaults or a requirement for every bond. The native bond/VF implementation must define and test its MAC handling and failover behavior against the chosen VF policy. A more restrictive policy is preferable when supported by that tested configuration. This example deliberately uses two named PFs as administrator selection criteria; it does not assert that different PF names prove different physical NIC failure domains.
The controller generates two root DeviceClasses, one for vf0 and one for vf1, and stores their authoritative root/revision bindings. Bond and VLAN do not receive new DeviceClasses or root allocations. Users request the generated revision-specific classes using ExactCount: 1 per root, as in the allocation companion.
Expected NetworkPlugin calls:
DRA NodePrepareResources:
sriov(vf0).NodePrepare
sriov(vf1).NodePrepare
Current sandbox attachment:
sriov(vf0).ConnectNetwork -> net1
sriov(vf1).ConnectNetwork -> net2
bond(bond0).ConnectNetwork -> bond0
vlan(data-vlan).ConnectNetwork -> data0
coordinator commits attachment Ready
Safe attachment removal:
vlan(data-vlan).RemoveNetwork
bond(bond0).RemoveNetwork
sriov(vf1).RemoveNetwork
sriov(vf0).RemoveNetwork
DRA NodeUnprepareResources, after cleanup:
sriov(vf1).NodeUnprepare
sriov(vf0).NodeUnprepareBoth VF calls use one logical sriov endpoint but have different root identities and journals. The VLAN call uses a different logical endpoint hosted by the common link service. Successful removal order is not sufficient on its own: failed/uncertain steps must also complete cleanup before their ancestors can be released.
10.2 One macvlan allocation through the shared link service
apiVersion: networking.dra.io/v1alpha1
kind: NetworkTopology
metadata:
name: sdk-macvlan
spec:
runtimeProfile: nri-secondary-v1
attachmentPolicy: SinglePod
execution:
maxParallelSteps: 1
primaryNetworkPolicy: Preserve
steps:
- name: secondary
kind: Allocation
pluginRef: {name: macvlan, contractVersion: v1alpha1}
allocation:
sharing: Shared
selector:
cel: >-
device.attributes["dra.networking"].type == "pf" &&
device.attributes["dra.networking"].ifName == "enp3s0f2"
capacityRequests:
dra.networking/macvlans: "1"
interfaceName: secondary0
config: {mode: bridge, mtu: 1500}
exports:
secondary:
from: {step: secondary, output: interface}The compiler requires a shared macvlan persona that authorizes one child. capacityRequests is a project compile/admission requirement, not a field secretly embedded into a DeviceClass to make the scheduler consume extra capacity. The generated claim example must include the matching request, or omission must be explicitly accepted only when the pinned device default reserves the same one unit.
The lifecycle is:
NodePrepare: record entitlement to one macvlan unit on the allocated parent
ConnectNetwork: create one secondary0 in the authorized sandbox
RemoveNetwork: delete that child; parent remains
NodeUnprepare: release this preparation reference; parent remains10.3 One ipvlan allocation through the same service
apiVersion: networking.dra.io/v1alpha1
kind: NetworkTopology
metadata:
name: sdk-ipvlan
spec:
runtimeProfile: nri-secondary-v1
attachmentPolicy: SinglePod
execution:
maxParallelSteps: 1
primaryNetworkPolicy: Preserve
steps:
- name: secondary
kind: Allocation
pluginRef: {name: ipvlan, contractVersion: v1alpha1}
allocation:
sharing: Shared
selector:
cel: >-
device.attributes["dra.networking"].type == "pf" &&
device.attributes["dra.networking"].ifName == "enp3s0f2"
capacityRequests:
dra.networking/ipvlans: "1"
interfaceName: secondary0
config: {mode: l2, mtu: 1500}
exports:
secondary:
from: {step: secondary, output: interface}This resolves to ipvlan.sock, not macvlan.sock with a mutable type switch. A claim for the macvlan persona cannot be reused to execute this topology.
10.4 Discovery declarations remain per logical plugin
The following two policies are alternatives for one interface and share a comparison domain while declaring disjoint mode groups:
apiVersion: networking.dra.io/v1alpha1
kind: DeviceExposurePolicy
metadata:
name: sdk-parent-macvlan
spec:
action: expose
selector:
cel: >-
device.attributes["dra.networking"].type == "pf" &&
device.attributes["dra.networking"].ifName == "enp3s0f2"
exposure:
deviceNameSuffix: "-macvlan"
allowMultipleAllocations: true
capacity:
macvlans:
value: "64"
requestPolicy:
default: "1"
validValues: ["1"]
supportedNetworkPlugins:
- name: macvlan
consumePerAllocation: {macvlans: "1"}
compatibility:
- scope: interface
domain: interface-mode
groups: [macvlan]
---
apiVersion: networking.dra.io/v1alpha1
kind: DeviceExposurePolicy
metadata:
name: sdk-parent-ipvlan
spec:
action: expose
selector:
cel: >-
device.attributes["dra.networking"].type == "pf" &&
device.attributes["dra.networking"].ifName == "enp3s0f2"
exposure:
deviceNameSuffix: "-ipvlan"
allowMultipleAllocations: true
capacity:
ipvlans:
value: "64"
requestPolicy:
default: "1"
validValues: ["1"]
supportedNetworkPlugins:
- name: ipvlan
consumePerAllocation: {ipvlans: "1"}
compatibility:
- scope: interface
domain: interface-mode
groups: [ipvlan]The discovery compiler publishes dra.networking/supportedNetworkPlugins using the logical names macvlan and ipvlan. It does not publish networkplugin-links as the capability for both personas.
Sharing an artifact therefore does not change the intended result: multiple macvlan shares can coexist up to the configured quota; ipvlan cannot coexist on that parent until incompatible allocations are released and node cleanup permits reuse. Keep active publication and physical identity stable through the drain rules in the discovery companion.
11. Native Effect Implementation Requirements
11.1 Namespaces are explicit resource handles
Use the companion’s NamespaceHandle identity and availability, including namespace filesystem device/inode and node boot ID. Each process opens and verifies the approved mount path locally. A numeric file descriptor from one process cannot be treated as that descriptor in another gRPC peer.
The coordinator owns retained namespace mounts and their lifetime. Plugin containers must see the approved mount through the deployment contract; they must not guess that /proc/1/ns/net is the host namespace. Cleanup accepts MISSING; connection does not treat missing information as permission to operate on the host.
The Linux effect kit should prefer namespace-scoped handles where practical. Code using thread-local namespace switching must control OS-thread affinity and restore the previous namespace on every path. runtime.LockOSThread is the Go primitive for binding a goroutine to its OS thread; it is not, by itself, namespace restoration or a cross-process namespace handle. A restoration failure must not return a contaminated worker thread to normal work. E7
11.2 Owned effects and snapshots
| Effect family | Capture before mutation | Cleanup must verify |
|---|---|---|
| VF preparation | Physical VF/PF relationship, allowed attributes and binding/config fields actually changed | Original VF still identifiable; no attachments require its prepared state |
| VF move | Prepared location/name, namespace identities, intended destination/name, affected link state | The link is the allocated VF, not a new link reusing its name or ifindex |
| Bond creation | Expected name absence, ownership marker intent, configuration | This operation owns the bond and no required child remains |
| Bond membership | Member identity, original master/state and affected attributes | Only this membership is reversed; restoration does not overwrite another writer |
| VLAN creation | Verified parent, VLAN ID, destination, intended child identity | Owned VLAN child has the expected lineage |
| Macvlan/ipvlan child | Parent entitlement, mode, share identity, namespace and child intent | Delete only the child belonging to the target attachment |
| Shared-parent hold | Actual allocation/quantity, parent contract, preparation reference ID | Release exactly that reference; never infer global allocation absence |
Link flags, MTU, and MAC can change as a consequence of membership operations. Implement those transitions as part of the relevant effect’s checked semantics, not as untracked cleanup guesses. Physical configuration changes by external administrators remain outside this framework’s lock mechanism and must be detected or prohibited by deployment policy.
11.3 Effects cannot hide additional allocations
A generic CreateChild helper needs a specific authorization bound to its root, parent, type, and permitted amount. It must not be a convenience function for arbitrary host link creation.
For shared parents, one DRA share with quantity one covers one child in the initial profile. The SDK verifies the allocation and local prepared entitlement; the compiler verifies the graph’s total effects. Neither should permit extra children because several operations are reachable from the same root.
For VLAN on a bond of exclusive VFs, the initial contract permits the declared layer-2 child on already owned interfaces. It does not imply that every conceivable hardware offload table, VLAN pool, or shared-parent quota is unlimited. Any such constrained resource needs an explicitly supported contract and accounting model.
12. Persistence, Recovery, and Upgrades
12.1 Storage requirements
Select a local transactional backend only after testing its crash/durability behavior. The storage interface must support atomic operation/effect updates, compare-and-set for scope ownership, durable terminal fences, and replayable results. Store original plans and before-images on persistent node storage, not only under a container writable layer or /run.
Use one writer per service database. The shared link process owns its one database and all three partitions. Node-wide physical authority remains with the coordinator; it must not depend on independently writable databases accidentally agreeing on a count.
A store-format version and an effect-format version are separate from a topology config version. An upgrade must retain the readers and effect executors needed to clean active old records. Merely recognizing the old protobuf message does not establish that the new binary can undo the old implementation’s effects.
12.2 Recovery examples
VF response lost after a successful move: InspectOperation finds the durable result and the RPC retry replays it. If the move result was not committed, observe the same VF in the recorded namespaces and reconcile the recorded rename/move substeps. Do not select a different VF.
Bond fails after attaching its first member: undo the failed/uncertain second membership as applicable, detach and restore the first membership, then remove the owned bond. Root VF removal waits for that receipt.
VLAN creation succeeds but output persistence fails: locate the child using stored identity/ownership evidence and expected parent lineage. The name alone is insufficient. Either finish committing the authorized effect or remove it under cleanup intent.
Macvlan claim A is unprepared while claim B still uses the parent: remove A’s child, release A’s local reference, and leave B and the parent untouched. Scheduler allocation status and discovery retention are reconciled separately.
Remove arrives before a delayed Connect retry: persist the removal fence for that same scope. The old connect must not start; an already executing connect is quiesced/reconciled before removal can succeed.
Service restarts with a missing/corrupt journal: NOT_FOUND is not proof of a clean host. Keep affected resources unavailable pending reconciliation. Do not recreate the database and report every old attachment absent.
12.3 Artifact and endpoint upgrades
The shared artifact upgrades VLAN, ipvlan, and macvlan together. Verify all three contracts, state readers, and active-effect recovery before making the new service authoritative. The safe initial upgrade model is controlled handover with one writer, compatible state readers, and retained old artifacts; not overlapping privileged writers against the same resources.
If an incompatible upgrade cannot clean active old state, retain the old service for those records or drain it first. A fully concurrent multi-version service deployment needs an explicit ownership partition and is beyond the initial example.
13. Test the SDK Before Testing Real NICs
13.1 Reusable conformance suite
Every logical registration, including each handler inside the shared service, runs the same suite:
| Test | Required result |
|---|---|
| Same operation ID and payload twice | One effect sequence; identical saved response |
| Same ID with changed payload | Reject before new mutation |
| Retry after terminal removal | Do not replay a connection as currently usable |
| Wrong hook for step kind | Reject prepare/unprepare for bond and VLAN |
| Crash before/after each effect mutation | Reconstruct progress or retain uncertainty without blind duplication |
| Lost reply | Recover saved response or journaled partial operation |
| Cleanup with no returned state | Find original plan by target operation ID |
| Failed dependent cleanup | Do not release its ancestors prematurely |
| Deadline/cancellation | No claim that hardware was rolled back automatically |
| Namespace/name/ifindex reuse | Reject a mismatched identity; do not touch the replacement |
| Old coordinator or service writer still active | Fence/quiesce/reconcile before granting replacement ownership |
| Unknown plugin, effect kind, or state reader | Fail closed with actionable diagnostics |
| Shared service logical dispatch | vlan calls cannot invoke another kind through config |
| Shared-parent reference release | Release one preparation, not all shares on the parent |
The testkit should expose a fake kernel model, a reopenable durable test store, and crash points before/after every persistence and effect boundary. In-memory mocks alone cannot validate crash consistency.
13.2 Plugin-specific tests
For the VF plugin: original-state restoration, prepared-versus-attached snapshots, unexpected VF disappearance, partial namespace move, and no PF-global changes.
For bond: duplicate handles and duplicate physical members, foreign existing master, failure on the second member, downstream VLAN cleanup, and restoration of only the membership-owned changes.
For links: invalid mode/ID, parent in the wrong namespace, foreign child with the same name, one-unit entitlement enforcement, shared-parent MTU mismatch, simultaneous incompatible modes, and deleting one child without deleting the parent or its siblings.
Run cross-service tests too. A per-plugin test cannot establish that a VF service and a link service coordinate conflicting access to the same physical interface.
13.3 An example of the small planning tests
The following illustrates the purpose of the compile/planning harness used for this document. It validates packaging only, not serving or recovery:
package examples
import (
"testing"
"example.com/networkplugins/sdk"
)
// Uses the documentation-only fake Host, not a production server.
func TestPackaging(t *testing.T) {
for _, tc := range []struct {
name string
register func(*sdk.Host) error
count int
}{{"vf", RegisterVF, 1}, {"bond", RegisterBond, 1}, {"links", RegisterLinks, 3}} {
t.Run(tc.name, func(t *testing.T) {
h := &sdk.Host{}
if err := tc.register(h); err != nil {
t.Fatal(err)
}
if len(h.Endpoints) != tc.count {
t.Fatal(h.Endpoints)
}
})
}
}The production conformance suite must test the complete host/runner/backend path, including authentication and durable failure recovery. It cannot stop at verifying that a handler returned four planned effects.
14. Runtime, Deployment, and Security Boundaries
The node coordinator owns the kubelet DRA and NRI adapters. Plugins see prepared claim context and verified sandbox context, not raw runtime callbacks. Keep the coordinator’s required-plugin/container-readiness gate from the allocation design: a connected gRPC endpoint alone is not proof that networking completed for the current sandbox.
Namespaces, plugin socket directories, and persistent state require an explicit host-mount and ownership contract. Bind only the selected local endpoints, restrict access to their directories, and authenticate the intended coordinator/registration identity. Do not give tenants access to plugin sockets or let them select effect kinds, grants, host paths, or plugin artifacts.
Grants limit what the framework authorizes, but these are trusted privileged services, not a security sandbox for malicious plugins. Do not claim that gRPC or signed handles prevent a privileged process from using raw host APIs outside the SDK. Deployment admission, artifact approval, least privilege where feasible, and operational ownership are still required.
A process-level health check and GetInfo are different from readiness for a specific topology revision. The coordinator verifies the complete logical plugin set and pinned contracts, including all derived steps, before preparing new work. Losing readiness must not discard journals, stop cleanup capability, or remove active discovery declarations.
Keep telemetry bounded and attributable: logical plugin, hook, effect kind, duration, retry/recovery reason, and cleanup state. Use claim/sandbox/operation identifiers in structured logs and traces, not unbounded metric labels. Never expose opaque journal contents or mutation credentials in workload-visible status.
15. Implementation Order and Acceptance Gates
| Stage | Deliverable | Gate before proceeding |
|---|---|---|
| 1. Contracts and planning | Generated existing protocol, descriptor bundles, typed call views, deterministic plans, three handler examples | No hardware mutation; invalid graphs/configs/identities rejected in tests |
| 2. Shared SDK host | Identity-bound endpoints, strict envelopes, safe response serialization, inspection, registration | Three processes/five identities dispatch correctly; no endpoint type confusion |
| 3. Durable runner | Transactional store, replay, effect progress, cleanup fences, failure classification | Crash/replay/cleanup suite passes with a fake backend and reopened storage |
| 4. Native common primitives | Namespace handles, identity checks, grants, original-state capture, owned link effects | Linux integration tests verify namespace restoration and conservative ownership behavior |
| 5. VF + bond + VLAN | Kernel VF profile, active-backup bond, layer-2 VLAN | Full four-step topology connects and cleans up through injected failures |
| 6. Shared-parent profiles | Macvlan bridge and ipvlan L2 in the shared link service | One share/one child, incompatible-mode rejection, independent claim cleanup |
| 7. Runtime integration | DRA prepare/unprepare + NRI attachment/readiness/recovery | Affected containers cannot start without the current sandbox’s durable Ready record |
| 8. Expansion | Address/IPAM, LACP, RDMA, VFIO, SDN integrations | Each added contract has authority, accounting, lifecycle, and recovery tests |
Stages are implementation gates, not estimates or claims of completed work. The first meaningful milestone is not “we can create a bond”; it is “a recorded VF/bond/VLAN attachment can fail, restart, and be safely removed without losing ownership.”
Do not duplicate the runner inside each plugin as a shortcut. Do not begin with a broad generic map[string]any plugin that runs arbitrary netlink operations. Make the narrow contracts work, then expand their supported effects deliberately.
16. Validation of This Companion Document
The following checks were performed while producing this file:
- The eight embedded Go blocks were checked for consistency with the planning-only harness. Handler and registration examples were compiled and exercised with six test functions and fourteen named subtests. The split link-plugin blocks belong to one Go package/file; method-only blocks are not separate programs.
- All four embedded YAML blocks, containing three NetworkTopology objects and two DeviceExposurePolicy objects, were parsed with duplicate-key rejection. Local checks verified step kinds, input references, acyclicity, port cardinality, assigned-name uniqueness, selected profile configuration, and matching shared-parent quantities/groups.
- The Markdown front matter, fenced blocks, companion links, and source-reference definitions were checked. The existing companion files were not modified.
These are static and planning-model checks, not production conformance results.
The checks above do not establish API-server admission, Kubernetes scheduling behavior, gRPC authentication/serving, NRI enforcement, persistent-store correctness, namespace behavior, or actual VF/bond/link operations. Those remain implementation acceptance work in Section 15.
The Go snippets use a proposed SDK facade and typed effect builders. Their private test harness deliberately uses inert stand-ins for the SDK host, verification helpers, and effect executors; it performs no privileged operations. Production implementation must replace those stand-ins, not promote their minimal test behavior into the safety contract.
The previous allocation and discovery documents are unchanged. This is an additive SDK companion; newly introduced helper names and packaging choices are not silently asserted to be existing Kubernetes or SDK APIs.
17. Source and Design Notes
Project basis: the latest four-hook allocation design, especially Sections 2–7 and Appendix A, and the aligned discovery design. These establish the lifecycle, typed ports, authority, compatibility, and capacity model. The earlier CNI-binary/prevResult draft is not the basis for this SDK.
The companion versions used for this draft are identified by their content hashes:
dra-chainable-networking-proposal.md— SHA-2566256cc4b2520c2b0f8943f214e7341c57c695ebc087a5cadf01252d6c88e0600.dra-network-device-discovery-design.md— SHA-256561268dda30c0dc31de1c2a0397e5caff1485842dfeb7ceb5c2255fe3138d9e8.
Proposed additions: the local Handler/Plan facade, effect kit, SDK package organization, per-logical sockets in one shared process, and code examples are implementation proposals in this document. They are not supplied automatically by gRPC, Go, Kubernetes, or Linux.
External implementation facts used to delimit those proposals:
- E1: gRPC deadlines — deadlines and server-side work cancellation.
- E2: gRPC cancellation — application handlers must cooperate; cancellation is not side-effect rollback.
- E3: gRPC retry behavior — transport retry is distinct from application effect idempotency.
- E4: Linux Ethernet bonding documentation — modes, monitoring, and external configuration concerns.
- E5: Linux ipvlan documentation — link modes and parent-level mode semantics.
- E6: CNI project’s ipvlan documentation — common-master macvlan/ipvlan incompatibility; cited for that restriction, not adopted as the new plugin ABI.
- E7: Go runtime LockOSThread — OS-thread affinity for code using thread-local operating-system state.