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

40 min read

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 / artifactLogical plugin namesInitial role
networkplugin-vfsriovPrepare and attach an exclusively allocated kernel VF
networkplugin-bondbondCreate a bond from interfaces already attached to the sandbox
networkplugin-linksvlan, ipvlan, macvlanOne 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 designsNewly proposed here
Four mutating NetworkPlugin RPCsA Go handler facade that returns local, serializable effect plans
Separate read-only inspection serviceAn SDK host that implements transport, inspection, replay, and common validation
Named typed ports and allocation provenanceA reusable Linux effect kit for link ownership, moves, membership, and restoration
Coordinator and plugin journalsA common write-ahead runner with explicit effect-level recovery
Logical plugins may share a processThree services; the link service hosts three identity-bound endpoints
Immutable plugin and topology revisionsPer-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 -- secondary0

The 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 nameArtifactInitial stepKindsPreparation outputConnection output
sriovVF service[Allocation]Internal prepared-VF stateinterface: KernelInterface/v1
bondBond service[Operation]Noneinterface: KernelInterface/v1
vlanLink service[Operation]Noneinterface: KernelInterface/v1
macvlanLink service[Allocation]parent: SharedParent/v1interface: KernelInterface/v1
ipvlanLink service[Allocation]parent: SharedParent/v1interface: 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

LayerResponsibilityPlugin author should not duplicate
Control-plane compilerValidate graph, effects budget, descriptors, root selectors, and immutable revisionsDeviceClass generation or allocation selection
Node coordinatorResolve actual claim allocations, authorize the sandbox, retain namespaces, coordinate resource ownership and readinessKubelet/NRI integration and cross-plugin claim ownership
SDK RPC hostAuthenticate calls, validate envelopes, bind plugin identity, enforce phase rules, translate errorsHand-written gRPC dispatch and inconsistent retry handling
SDK operation runnerPersist intent, replay responses, serialize duplicate calls, enforce generations, execute/undo effectsA separate journal and idempotency scheme in every plugin
SDK resource servicesVerify handles and grants, resolve namespaces, validate original ownership, coordinate locksUsing untrusted interface names as authority
Linux effect kitNative VF/link effects, before-images, observation, safe compensationRepeated low-level link creation and cleanup patterns
Logical plugin handlerDeclare its contract and assemble the supported operationsGeneric 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 usage

For 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:

HookVFBondVLANMacvlan / ipvlan
NodePrepareSnapshot and prepare the allocated VFRejectedRejectedHold one authorized shared-parent entitlement
ConnectNetworkMove/connect the prepared VFCreate bond and membershipsCreate VLAN on an authorized sandbox parentCreate one child for the prepared share
RemoveNetworkReturn VF to prepared stateUndo memberships and delete owned bondDelete owned VLANDelete owned child
NodeUnprepareRestore preparation changesRejectedRejectedRelease 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 helperRequired 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; rdmaAccess must 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 behaviorPurpose
CaptureRead identity and before-image; no external mutation
ObserveDetermine whether the intended effect is absent, present as owned, partially applied, or conflicting
ApplyPerform the authorized mutation, using persistent evidence to avoid duplicate effects
RevertCompensate 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 -> Quarantined

Applying 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.

PhaseThis plugin ownsMust remain untouched
PrepareOriginal VF state snapshot, approved per-VF settings, local preparation referenceOther VFs and unrelated PF-wide configuration
ConnectMoving and naming the prepared VF in this sandbox; connection-scoped link stateThe claim’s allocation and preparation lifetime
RemoveReturning the VF to its prepared state after downstream dependencies are goneClaim-scoped preparation and any still-required device access
UnprepareRestoring original preparation changes and releasing this preparationOther 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-bond

Before 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 pluginParent sourceInitial modeOwned effect
vlanparent input from an earlier sandbox stepVLAN ID 1–4094One VLAN on that parent
macvlanThis root’s prepared host-parent entitlementbridgeOne child for the allocated macvlan unit
ipvlanThis root’s prepared host-parent entitlementl2One 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 partitions

These 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).NodeUnprepare

Both 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.

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 remains

10.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 familyCapture before mutationCleanup must verify
VF preparationPhysical VF/PF relationship, allowed attributes and binding/config fields actually changedOriginal VF still identifiable; no attachments require its prepared state
VF movePrepared location/name, namespace identities, intended destination/name, affected link stateThe link is the allocated VF, not a new link reusing its name or ifindex
Bond creationExpected name absence, ownership marker intent, configurationThis operation owns the bond and no required child remains
Bond membershipMember identity, original master/state and affected attributesOnly this membership is reversed; restoration does not overwrite another writer
VLAN creationVerified parent, VLAN ID, destination, intended child identityOwned VLAN child has the expected lineage
Macvlan/ipvlan childParent entitlement, mode, share identity, namespace and child intentDelete only the child belonging to the target attachment
Shared-parent holdActual allocation/quantity, parent contract, preparation reference IDRelease 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:

TestRequired result
Same operation ID and payload twiceOne effect sequence; identical saved response
Same ID with changed payloadReject before new mutation
Retry after terminal removalDo not replay a connection as currently usable
Wrong hook for step kindReject prepare/unprepare for bond and VLAN
Crash before/after each effect mutationReconstruct progress or retain uncertainty without blind duplication
Lost replyRecover saved response or journaled partial operation
Cleanup with no returned stateFind original plan by target operation ID
Failed dependent cleanupDo not release its ancestors prematurely
Deadline/cancellationNo claim that hardware was rolled back automatically
Namespace/name/ifindex reuseReject a mismatched identity; do not touch the replacement
Old coordinator or service writer still activeFence/quiesce/reconcile before granting replacement ownership
Unknown plugin, effect kind, or state readerFail closed with actionable diagnostics
Shared service logical dispatchvlan calls cannot invoke another kind through config
Shared-parent reference releaseRelease 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

StageDeliverableGate before proceeding
1. Contracts and planningGenerated existing protocol, descriptor bundles, typed call views, deterministic plans, three handler examplesNo hardware mutation; invalid graphs/configs/identities rejected in tests
2. Shared SDK hostIdentity-bound endpoints, strict envelopes, safe response serialization, inspection, registrationThree processes/five identities dispatch correctly; no endpoint type confusion
3. Durable runnerTransactional store, replay, effect progress, cleanup fences, failure classificationCrash/replay/cleanup suite passes with a fake backend and reopened storage
4. Native common primitivesNamespace handles, identity checks, grants, original-state capture, owned link effectsLinux integration tests verify namespace restoration and conservative ownership behavior
5. VF + bond + VLANKernel VF profile, active-backup bond, layer-2 VLANFull four-step topology connects and cleans up through injected failures
6. Shared-parent profilesMacvlan bridge and ipvlan L2 in the shared link serviceOne share/one child, incompatible-mode rejection, independent claim cleanup
7. Runtime integrationDRA prepare/unprepare + NRI attachment/readiness/recoveryAffected containers cannot start without the current sandbox’s durable Ready record
8. ExpansionAddress/IPAM, LACP, RDMA, VFIO, SDN integrationsEach 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-256 6256cc4b2520c2b0f8943f214e7341c57c695ebc087a5cadf01252d6c88e0600.
  • dra-network-device-discovery-design.md — SHA-256 561268dda30c0dc31de1c2a0397e5caff1485842dfeb7ceb5c2255fe3138d9e8.

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:

ON THIS PAGE