Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 53 additions & 6 deletions api/v1alpha/runtimeclass_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,29 @@ const (
RuntimeClassLifecycleSnapshot RuntimeClassLifecycleOperation = "Snapshot"
)

// RuntimeClassNetworkAttachment is how a guest in the class takes the network
// interface the platform gives it. The platform reads it to wire the instance;
// it is never handed to a runtime.
//
// +kubebuilder:validation:Enum=Netns;Hypervisor;HypervisorDeclared
type RuntimeClassNetworkAttachment string

const (
// RuntimeClassNetworkAttachmentNetns places the interface in the guest's
// network namespace, which is what an ordinary container takes.
RuntimeClassNetworkAttachmentNetns RuntimeClassNetworkAttachment = "Netns"

// RuntimeClassNetworkAttachmentHypervisor hands the interface to a
// hypervisor as a device that the hypervisor finds from what the node
// publishes.
RuntimeClassNetworkAttachmentHypervisor RuntimeClassNetworkAttachment = "Hypervisor"

// RuntimeClassNetworkAttachmentHypervisorDeclared hands the interface to a
// hypervisor as a device and has the platform state that device to the
// hypervisor. A runtime that reads no node state needs it.
RuntimeClassNetworkAttachmentHypervisorDeclared RuntimeClassNetworkAttachment = "HypervisorDeclared"
)

// RuntimeClassIsolation describes what separates a workload in the class from
// other tenants' workloads. Multi-tenant customers report this boundary to
// their own auditors, so the API publishes it and holds it stable across
Expand Down Expand Up @@ -194,12 +217,17 @@ type RuntimeClassLifecycle struct {
// billing dimensions a class is metered on, and customers are billed against
// the catalog. Restating a price here would create a second source of truth.
//
// Provider-specific parameters are deliberately absent as well. Everything here
// is what a customer is promised, and the platform reserves the right to change
// which runtime a provider uses to keep that promise. A provider slot on this
// object would also put runtime configuration one RBAC mistake away from a
// tenant, which is the escape path this design closes. Provider configuration
// stays with the provider's own deployment.
// This object carries the contract published to a customer, plus the minimum
// the platform itself needs to wire an instance of the class. Nothing else
// belongs here.
//
// Opaque provider parameters remain deliberately absent. The platform reserves
// the right to change which runtime a provider uses to keep the published
// promise, and a pass-through slot on this object would put runtime
// configuration one RBAC mistake away from a tenant, which is the escape path
// this design closes. Provider configuration stays with the provider's own
// deployment. A field the platform reads itself, drawn from a closed set of
// values, reaches no runtime and is not such a slot.
type RuntimeClassSpec struct {
// The controller that implements this class. A provider watches for classes
// carrying its own controller name, claims them, and reports through the
Expand Down Expand Up @@ -252,6 +280,25 @@ type RuntimeClassSpec struct {
//
// +kubebuilder:validation:Optional
Lifecycle RuntimeClassLifecycle `json:"lifecycle,omitempty"`

// How a guest in this class takes the network interface the platform gives
// it. The provider that publishes the class states it, because only the
// provider knows what its runtime expects.
//
// Leaving it empty is the right answer for any class whose guests take the
// interface the cell already gives them, and it is what every class
// published today does. An empty value asks the networking layer for
// nothing, so the cell's own setting continues to decide. Stating a value
// overrides that setting for every guest in the class, in every cell, so
// state one only for a class whose runtime cannot use what the cell would
// otherwise give it.
//
// The value is resolved when a deployment is created and then fixed for the
// life of each instance, so correcting it here moves new instances without
// disturbing running ones.
//
// +kubebuilder:validation:Optional
NetworkAttachment RuntimeClassNetworkAttachment `json:"networkAttachment,omitempty"`
}

// Condition types reported on a RuntimeClass.
Expand Down
11 changes: 11 additions & 0 deletions api/v1alpha/workloaddeployment_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,17 @@ type WorkloadDeploymentSpec struct {
//
// +kubebuilder:validation:Optional
Replicas *int32 `json:"replicas,omitempty"`

// How a guest takes the network interface the platform gives it, resolved
// from the runtime class this deployment runs in.
//
// The class catalog is readable only where a deployment is created, so the
// answer is resolved once here and carried to the cell that acts on it. An
// empty value asks the networking layer for nothing and leaves the cell's
// own setting deciding, which is what every deployment does today.
//
// +kubebuilder:validation:Optional
NetworkAttachment RuntimeClassNetworkAttachment `json:"networkAttachment,omitempty"`
}

// WorkloadDeploymentStatus defines the observed state of WorkloadDeployment
Expand Down
22 changes: 22 additions & 0 deletions config/base/crd/bases/compute.datumapis.com_runtimeclasses.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,28 @@ spec:
measure it.
type: string
type: object
networkAttachment:
description: |-
How a guest in this class takes the network interface the platform gives
it. The provider that publishes the class states it, because only the
provider knows what its runtime expects.

Leaving it empty is the right answer for any class whose guests take the
interface the cell already gives them, and it is what every class
published today does. An empty value asks the networking layer for
nothing, so the cell's own setting continues to decide. Stating a value
overrides that setting for every guest in the class, in every cell, so
state one only for a class whose runtime cannot use what the cell would
otherwise give it.

The value is resolved when a deployment is created and then fixed for the
life of each instance, so correcting it here moves new instances without
disturbing running ones.
enum:
- Netns
- Hypervisor
- HypervisorDeclared
type: string
required:
- capabilities
- controllerName
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,20 @@ spec:
required:
- name
type: object
networkAttachment:
description: |-
How a guest takes the network interface the platform gives it, resolved
from the runtime class this deployment runs in.

The class catalog is readable only where a deployment is created, so the
answer is resolved once here and carried to the cell that acts on it. An
empty value asks the networking layer for nothing and leaves the cell's
own setting deciding, which is what every deployment does today.
enum:
- Netns
- Hypervisor
- HypervisorDeclared
type: string
placementName:
description: The placement in the workload which is driving a deployment
type: string
Expand Down
17 changes: 17 additions & 0 deletions docs/enhancements/runtime-classes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,23 @@ one class may run its instances as containers on a host, another may provision a
machine from a cloud provider. The platform's abstraction is the instance, not any
particular realization of it, and this proposal does not narrow that.

**Realization a neighboring system needs is published with the class.** Some classes need
the networking layer to attach an interface differently, because their runtime is told
about the device rather than discovering it. The provider that publishes the class states
that alongside it, so adding a class carries its own answer and stays a single declarative
act. Requiring an operator to edit configuration and roll a manager in every cell before a
new class worked would make the class object documentation rather than an API.

The class object holds the published contract plus the minimum the platform needs to wire
an instance. This field is read by the platform and drawn from a closed set of values, so
it never reaches a runtime; opaque provider parameters remain excluded. Because the
catalog is readable where deployments are created and not in a cell, the answer is
resolved once there and travels with the deployment, which also removes any chance of a
deployment reaching a cell before the class does.

A class that states nothing leaves the cell's own setting deciding, which is what every
class published today does.

**Capability gaps are validated, not silently dropped.** A class will not support
everything the instance API can express — some won't support disk-backed volumes, some
will constrain where images may be pulled from. Today an unsupported feature can be
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ require (
github.com/onsi/gomega v1.42.1
github.com/prometheus/client_golang v1.23.2
github.com/stretchr/testify v1.11.1
go.datum.net/network-services-operator v0.26.5-0.20260911135634-aeaa6ff1ab23
go.datum.net/network-services-operator v0.26.5-0.20260911235442-196ee79e9ebf
// Pinned by pseudo-version to the commit deployed to staging, which is the
// same one network-services-operator pins. The module publishes no tag yet.
go.miloapis.com/locations v0.0.0-20260825185141-507ac2cbd48c
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -337,8 +337,8 @@ github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zU
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
go.datum.net/datumctl v0.17.1-0.20260710003126-296c2fcbbd67 h1:Mhgt688CeTh2hX7ql61ySR3+/YHdZMgYBSFhBW7W2gA=
go.datum.net/datumctl v0.17.1-0.20260710003126-296c2fcbbd67/go.mod h1:6skEjcE7aT8VPf/HVamA/BB6Dc9IISA6c/DdYKhqWNc=
go.datum.net/network-services-operator v0.26.5-0.20260911135634-aeaa6ff1ab23 h1:a6qaqnhT1eLueTvAgkebWmZ+8taFaQwAX7knVEPYEW4=
go.datum.net/network-services-operator v0.26.5-0.20260911135634-aeaa6ff1ab23/go.mod h1:9nuuBWdrkdnIBMaWJsWM3j4CcbKJIF1GDuCmAjdpIHo=
go.datum.net/network-services-operator v0.26.5-0.20260911235442-196ee79e9ebf h1:kShApE3Ghq/wv4G38CFb91jMSWTwD+SiPd84yyHzffk=
go.datum.net/network-services-operator v0.26.5-0.20260911235442-196ee79e9ebf/go.mod h1:9nuuBWdrkdnIBMaWJsWM3j4CcbKJIF1GDuCmAjdpIHo=
go.miloapis.com/locations v0.0.0-20260825185141-507ac2cbd48c h1:+BQirT3wYCgv7H2lEZtAH+dpMiWu9j/Wa+c8s4jJUIA=
go.miloapis.com/locations v0.0.0-20260825185141-507ac2cbd48c/go.mod h1:gzfAfHhSMwl/N68k/uSNYXOKK3IOBJCdXYaBgCE3gdE=
go.miloapis.com/milo v0.32.0 h1:TkNIQu/37d+SEquLJ5+GmdisSl+K2RT7eEC4idg6RIs=
Expand Down
18 changes: 13 additions & 5 deletions internal/controller/networkinterfaceclaim.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,20 @@ func instanceInterfaceName(networkInterface computev1alpha.InstanceNetworkInterf
// is already location scoped. networkInterfaceName is left unset so the claim
// binds the interface of its own name — the retained interface, when there is
// one.
func desiredNetworkInterfaceClaimSpec(networkInterface computev1alpha.InstanceNetworkInterface) networkingv1alpha.NetworkInterfaceClaimSpec {
//
// An empty attachment mode is left off the claim, which leaves the networking
// API's own default in force and the data plane free to attach the interface
// the way the cell already attaches every other one.
func desiredNetworkInterfaceClaimSpec(
networkInterface computev1alpha.InstanceNetworkInterface,
attachmentMode networkingv1alpha.NetworkInterfaceAttachmentMode,
) networkingv1alpha.NetworkInterfaceClaimSpec {
spec := networkingv1alpha.NetworkInterfaceClaimSpec{
Network: networkingv1alpha.LocalNetworkRef{Name: networkInterface.Network.Name},
InterfaceName: instanceInterfaceName(networkInterface),
IPFamilies: append([]networkingv1alpha.IPFamily(nil), networkInterface.IPFamilies...),
ReclaimPolicy: networkInterface.ReclaimPolicy,
Network: networkingv1alpha.LocalNetworkRef{Name: networkInterface.Network.Name},
InterfaceName: instanceInterfaceName(networkInterface),
IPFamilies: append([]networkingv1alpha.IPFamily(nil), networkInterface.IPFamilies...),
ReclaimPolicy: networkInterface.ReclaimPolicy,
AttachmentMode: attachmentMode,
}

for _, address := range networkInterface.Addresses {
Expand Down
9 changes: 7 additions & 2 deletions internal/controller/networkinterfaceclaim_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func TestDesiredNetworkInterfaceClaimSpec(t *testing.T) {
Addresses: []computev1alpha.InstanceNetworkInterfaceAddressRequest{
{Class: claimTestClass},
},
})
}, networkingv1alpha.NetworkInterfaceAttachmentModeHypervisorDeclared)

assert.Equal(t, claimTestNetwork, spec.Network.Name)
assert.Equal(t, "eth1", spec.InterfaceName)
Expand All @@ -114,10 +114,15 @@ func TestDesiredNetworkInterfaceClaimSpec(t *testing.T) {
assert.Empty(t, spec.NetworkInterfaceName,
"the claim must bind the interface of its own name so a retained one is reused")

assert.Equal(t, networkingv1alpha.NetworkInterfaceAttachmentModeHypervisorDeclared,
spec.AttachmentMode)

defaulted := desiredNetworkInterfaceClaimSpec(computev1alpha.InstanceNetworkInterface{
Network: networkingv1alpha.NetworkRef{Name: claimTestNetwork},
})
}, "")
assert.Equal(t, defaultInterfaceName, defaulted.InterfaceName)
assert.Empty(t, defaulted.AttachmentMode,
"a cell that states no mode leaves the networking default in force")
}

// TestNetworkInterfaceClaimSatisfied is the regression guard for the readiness
Expand Down
43 changes: 43 additions & 0 deletions internal/controller/workload_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ import (
mcreconcile "sigs.k8s.io/multicluster-runtime/pkg/reconcile"

computev1alpha "go.datum.net/compute/api/v1alpha"
"go.datum.net/compute/internal/features"
"go.datum.net/compute/internal/locations"
"go.datum.net/compute/pkg/runtimeclass"
networkingv1alpha "go.datum.net/network-services-operator/api/v1alpha"
locationsv1alpha1 "go.miloapis.com/locations/api/v1alpha1"
servicesv1alpha1 "go.miloapis.com/service-catalog/api/v1alpha1"
Expand Down Expand Up @@ -60,6 +62,7 @@ type WorkloadReconciler struct {
}

// +kubebuilder:rbac:groups=compute.datumapis.com,resources=workloads,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=compute.datumapis.com,resources=runtimeclasses,verbs=get;list;watch
// +kubebuilder:rbac:groups=compute.datumapis.com,resources=workloads/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=compute.datumapis.com,resources=workloads/finalizers,verbs=update
// +kubebuilder:rbac:groups=networking.datumapis.com,resources=networks,verbs=get;list;watch
Expand Down Expand Up @@ -471,6 +474,39 @@ func (r *WorkloadReconciler) Finalize(ctx context.Context, obj client.Object) (f
// getDeploymentsForWorkload returns both deployments that are desired to exist
// for a workload, and deployments that have been orphaned and should be
// removed.
// networkAttachment resolves how the workload's guests take a network
// interface, from the runtime class the workload runs in.
//
// The catalog is readable here and nowhere downstream, so the answer travels
// with the deployment rather than being looked up again in a cell. A class the
// catalog does not publish, or one that states nothing, resolves to nothing
// and leaves the cell deciding.
func (r *WorkloadReconciler) networkAttachment(
ctx context.Context,
upstreamClient client.Client,
workload *computev1alpha.Workload,
) (computev1alpha.RuntimeClassNetworkAttachment, error) {
if !features.FeatureGate.Enabled(features.RuntimeClasses) {
return "", nil
}

className := workload.Spec.Template.Spec.Runtime.Class
if className == "" {
return "", nil
}

var classes computev1alpha.RuntimeClassList
if err := upstreamClient.List(ctx, &classes); err != nil {
return "", fmt.Errorf("failed listing runtime classes: %w", err)
}

class := runtimeclass.Catalog(classes.Items).Find(className)
if class == nil {
return "", nil
}
return class.Spec.NetworkAttachment, nil
}

func (r *WorkloadReconciler) getDeploymentsForWorkload(
ctx context.Context,
upstreamClient client.Client,
Expand All @@ -497,6 +533,11 @@ func (r *WorkloadReconciler) getDeploymentsForWorkload(
return nil, nil, err
}

networkAttachment, err := r.networkAttachment(ctx, upstreamClient, workload)
if err != nil {
return nil, nil, err
}

if len(placementLocations) == 0 {
return nil, nil, fmt.Errorf("no locations are registered with the system")
}
Expand Down Expand Up @@ -540,6 +581,8 @@ func (r *WorkloadReconciler) getDeploymentsForWorkload(
Template: workload.Spec.Template,
ScaleSettings: placement.ScaleSettings,
Replicas: new(placement.ScaleSettings.MinReplicas),

NetworkAttachment: networkAttachment,
},
})
}
Expand Down
Loading
Loading