Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
2b0b298
metalman: separate netboot configuration axes
jveski Jul 22, 2026
f1e2996
metalman: add durable netboot session APIs
jveski Jul 22, 2026
07dd22b
metalman: persist immutable netboot sessions
jveski Jul 22, 2026
86b5b34
metalman: split runtime role commands
jveski Jul 22, 2026
ce53503
metalman: decouple edge from cluster runtime
jveski Jul 22, 2026
8f44c29
metalman: serve immutable session artifacts
jveski Jul 22, 2026
5b21048
metalman: scope callbacks to netboot sessions
jveski Jul 22, 2026
95b7b1f
metalman: advertise session capability boot URLs
jveski Jul 22, 2026
97b5520
metalman: resume edge artifact transfers
jveski Jul 22, 2026
dab49d5
metalman: serve DHCP from immutable sessions
jveski Jul 22, 2026
00330ff
metalman: proxy immutable TFTP artifacts
jveski Jul 22, 2026
4be4fe4
metalman: report session TFTP milestones
jveski Jul 22, 2026
f2cddf1
metalman: support independent HTTP boot axes
jveski Jul 22, 2026
9d358cd
metalman: snapshot netboot rendering inputs
jveski Jul 22, 2026
150890e
metalman: render capability-scoped boot artifacts
jveski Jul 22, 2026
1fb8a11
metalman: bind attestation to netboot sessions
jveski Jul 22, 2026
caddde3
metalman: split operator control and server planes
jveski Jul 22, 2026
1d5ad82
metalman: add server availability policy
jveski Jul 22, 2026
2be1148
metalman: reconcile endpoint edge workloads
jveski Jul 22, 2026
d04bb40
metalman: support TLS edge listeners
jveski Jul 22, 2026
c8c6f11
metalman: terminate edge TLS from Secrets
jveski Jul 22, 2026
ebffa62
metalman: publish managed endpoint readiness
jveski Jul 22, 2026
c36cd52
metalman: remove obsolete Site tuning
jveski Jul 22, 2026
3bc1211
kubectl: add netboot bootstrap command
jveski Jul 22, 2026
a84548a
kubectl: prepare bootstrap netboot resources
jveski Jul 22, 2026
b90d48a
kubectl: wait for bootstrap control plane
jveski Jul 22, 2026
1fc2e73
kubectl: constrain bootstrap edge process
jveski Jul 22, 2026
901655d
kubectl: gate bootstrap handoff on node readiness
jveski Jul 22, 2026
a8b67e0
kubectl: reconnect bootstrap server forwarding
jveski Jul 22, 2026
2c886a4
kubectl: rotate bootstrap edge credentials
jveski Jul 22, 2026
266eb16
kubectl: supervise bootstrap edge process
jveski Jul 22, 2026
b30f82b
kubectl: orchestrate netboot bootstrap lifecycle
jveski Jul 22, 2026
f37a47c
net: support synthetic external gateways
jveski Jul 22, 2026
a47b17f
kubectl: prepare bootstrap gateway resources
jveski Jul 22, 2026
68d272b
net: skip CNI for site-less gateways
jveski Jul 22, 2026
66af6e1
net: define external gateway runner options
jveski Jul 22, 2026
c6e1e6d
net: extract reusable node agent
jveski Jul 22, 2026
0e61ea7
kubectl: run opt-in bootstrap gateway
jveski Jul 22, 2026
fd33cc9
metalman: grant server cluster metadata access
jveski Jul 22, 2026
aca1a70
test: migrate metalman smoke to split runtime
jveski Jul 22, 2026
be6f05d
docs: describe resilient metalman netboot
jveski Jul 22, 2026
175cd7f
operator: wait for netboot CRDs
jveski Jul 22, 2026
8c5b392
metalman: satisfy verification checks
jveski Jul 22, 2026
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
72 changes: 54 additions & 18 deletions api/machina/v1alpha3/machine_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,11 @@ type RedfishSpec struct {
PasswordRef SecretKeySelector `json:"passwordRef"`
}

// PXESpec defines PXE boot configuration for a Machine.
// +kubebuilder:validation:XValidation:rule="!(self.transport == 'TFTP' && self.configurationSource == 'Redfish')",message="TFTP transport requires DHCP configuration"
// +kubebuilder:validation:XValidation:rule="self.networkMode != 'Static' || self.configurationSource == 'Redfish'",message="static networking requires Redfish configuration"
// +kubebuilder:validation:XValidation:rule="self.configurationSource != 'Redfish' || has(self.redfish)",message="Redfish configuration requires redfish connection details"

// PXESpec defines network boot configuration for a Machine.
type PXESpec struct {
// Image is an OCI image reference containing the machine disk image.
// The image must contain /disk/disk.img.gz.
Expand Down Expand Up @@ -389,13 +393,24 @@ type PXESpec struct {
// +optional
NetbootPullSecretRef *NamespacedSecretReference `json:"netbootPullSecretRef,omitempty"`

// BootProtocol selects how metalman should trigger network boot for
// repaves. PXE uses DHCP/TFTP bootfile options. HTTP uses Redfish UEFI
// HTTP boot with a URL derived from the netboot image metadata.
// +kubebuilder:validation:Enum=PXE;HTTP
// +kubebuilder:default=PXE
// Transport selects the firmware boot artifact transport.
// +kubebuilder:default=TFTP
// +optional
Transport NetbootTransport `json:"transport,omitempty"`

// ConfigurationSource selects how firmware receives its boot target.
// +kubebuilder:default=DHCP
// +optional
ConfigurationSource NetbootConfigurationSource `json:"configurationSource,omitempty"`

// NetworkMode selects how firmware configures the provisioning network.
// +kubebuilder:default=DHCP
// +optional
BootProtocol string `json:"bootProtocol,omitempty"`
NetworkMode NetbootNetworkMode `json:"networkMode,omitempty"`

// EndpointRef names the NetbootEndpoint that serves this Machine.
// +kubebuilder:validation:MinLength=1
EndpointRef string `json:"endpointRef"`

// DHCPLeases defines static IPv4 provisioning network settings. PXE boot
// uses them as DHCP leases. HTTP boot uses the first entry to configure the
Expand All @@ -420,18 +435,39 @@ type PXESpec struct {
}

const (
// PXEBootProtocolPXE uses DHCP/TFTP PXE boot.
PXEBootProtocolPXE = "PXE"
// PXEBootProtocolHTTP uses Redfish UEFI HTTP boot.
PXEBootProtocolHTTP = "HTTP"
// PXEArchitectureAMD64 is the x86_64 target architecture for PXE boot.
PXEArchitectureAMD64 = "amd64"
// PXEArchitectureARM64 is the aarch64 target architecture for PXE boot.
PXEArchitectureARM64 = "arm64"
// DefaultPXEArchitecture is used when host.netboot.architecture is omitted.
DefaultPXEArchitecture = PXEArchitectureAMD64
// DefaultPXEBootProtocol is used when host.netboot.bootProtocol is omitted.
DefaultPXEBootProtocol = PXEBootProtocolPXE
)

// NetbootTransport is the protocol firmware uses to fetch boot artifacts.
// +kubebuilder:validation:Enum=TFTP;HTTP
type NetbootTransport string

const (
NetbootTransportTFTP NetbootTransport = "TFTP"
NetbootTransportHTTP NetbootTransport = "HTTP"
)

// NetbootConfigurationSource supplies the firmware boot target.
// +kubebuilder:validation:Enum=DHCP;Redfish
type NetbootConfigurationSource string

const (
NetbootConfigurationSourceDHCP NetbootConfigurationSource = "DHCP"
NetbootConfigurationSourceRedfish NetbootConfigurationSource = "Redfish"
)

// NetbootNetworkMode configures the firmware provisioning interface.
// +kubebuilder:validation:Enum=DHCP;Static
type NetbootNetworkMode string

const (
NetbootNetworkModeDHCP NetbootNetworkMode = "DHCP"
NetbootNetworkModeStatic NetbootNetworkMode = "Static"
)

// TargetArchitecture returns the effective PXE target architecture.
Expand All @@ -443,13 +479,13 @@ func (p *PXESpec) TargetArchitecture() string {
return p.Architecture
}

// TargetBootProtocol returns the effective network boot protocol.
func (p *PXESpec) TargetBootProtocol() string {
if p == nil || p.BootProtocol == "" {
return DefaultPXEBootProtocol
// TargetTransport returns the effective firmware boot transport.
func (p *PXESpec) TargetTransport() NetbootTransport {
if p == nil || p.Transport == "" {
return NetbootTransportTFTP
}

return p.BootProtocol
return p.Transport
}

// CloudInitSpec defines cloud-init customization for PXE-booted machines.
Expand Down
27 changes: 27 additions & 0 deletions api/machina/v1alpha3/machine_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,33 @@ func TestMachineProviderOwnershipSchema(t *testing.T) {
})
}

func TestMachineNetbootSchema(t *testing.T) {
t.Parallel()

data, err := os.ReadFile("../../../deploy/machina/crd/unbounded-cloud.io_machines.yaml")
if err != nil {
t.Fatalf("read Machine CRD: %v", err)
}

var crd apiextensionsv1.CustomResourceDefinition
if err := yaml.Unmarshal(data, &crd); err != nil {
t.Fatalf("parse Machine CRD: %v", err)
}

netbootSchema := crd.Spec.Versions[0].Schema.OpenAPIV3Schema.
Properties["spec"].Properties["host"].Properties["netboot"]

for _, field := range []string{"transport", "configurationSource", "networkMode", "endpointRef"} {
if _, ok := netbootSchema.Properties[field]; !ok {
t.Errorf("netboot schema is missing %q", field)
}
}

if _, ok := netbootSchema.Properties["bootProtocol"]; ok {
t.Error("netboot schema still exposes bootProtocol")
}
}

func assertSchemaValidations(t *testing.T, schema apiextensionsv1.JSONSchemaProps, want map[string]string) {
t.Helper()

Expand Down
5 changes: 5 additions & 0 deletions api/machina/v1alpha3/machineoperation_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,11 @@ type ProviderOperationStatus struct {
// one MachineOperation target. It is persisted before provider execution so
// retries do not silently adopt later desired-state changes.
type MachineOperationTargetInput struct {
// NetbootSessionRef identifies the immutable provisioning session assigned
// to this HostReplace target.
// +optional
NetbootSessionRef *NetbootSessionReference `json:"netbootSessionRef,omitempty"`

// ProviderRef identifies the exact provider-owned resource referenced by
// host.external.machineRef when the operation target was initialized.
// +optional
Expand Down
132 changes: 132 additions & 0 deletions api/machina/v1alpha3/netboot_validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

package v1alpha3

import (
"os"
"testing"

apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"sigs.k8s.io/yaml"
)

func TestNetbootEndpointSchema(t *testing.T) {
t.Parallel()

crd := readCRD(t, "../../../deploy/machina/crd/unbounded-cloud.io_netbootendpoints.yaml")
if crd.Spec.Scope != apiextensionsv1.ClusterScoped {
t.Errorf("scope = %q, want %q", crd.Spec.Scope, apiextensionsv1.ClusterScoped)
}

spec := crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["spec"]
for _, field := range []string{"siteRef", "type", "externalURL", "tls", "managedL2", "http"} {
if _, ok := spec.Properties[field]; !ok {
t.Errorf("endpoint spec is missing %q", field)
}
}

assertSchemaValidations(t, spec, map[string]string{
"self.tls.trust != 'Public' || (self.externalURL.startsWith('https://') && self.tls.mode != 'Disabled')": "public endpoints require HTTPS",
"self.type == 'ManagedL2' ? has(self.managedL2) : !has(self.managedL2)": "managedL2 configuration must be set only for ManagedL2 endpoints",
"self.type == 'HTTP' ? has(self.http) : !has(self.http)": "http configuration must be set only for HTTP endpoints",
})
assertSchemaValidations(t, spec.Properties["tls"], map[string]string{
"self.mode == 'Secret' ? has(self.secretRef) : !has(self.secretRef)": "secretRef must be set only when TLS mode is Secret",
})

status := crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["status"]
for _, field := range []string{"observedGeneration", "claim", "conditions"} {
if _, ok := status.Properties[field]; !ok {
t.Errorf("endpoint status is missing %q", field)
}
}
}

func TestNetbootSessionSchema(t *testing.T) {
t.Parallel()

crd := readCRD(t, "../../../deploy/machina/crd/unbounded-cloud.io_netbootsessions.yaml")
if crd.Spec.Scope != apiextensionsv1.ClusterScoped {
t.Errorf("scope = %q, want %q", crd.Spec.Scope, apiextensionsv1.ClusterScoped)
}

spec := crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["spec"]
for _, field := range []string{"machine", "operation", "endpoint", "boot", "provisioning", "artifacts", "expiresAt"} {
if _, ok := spec.Properties[field]; !ok {
t.Errorf("session spec is missing %q", field)
}
}

assertSchemaValidations(t, spec, map[string]string{
"self == oldSelf": "netboot session spec is immutable",
})
boot := spec.Properties["boot"]

firmware, ok := boot.Properties["firmwareArtifact"]
if !ok {
t.Fatal("session boot snapshot is missing firmwareArtifact")
}

if firmware.MinLength == nil || *firmware.MinLength != 1 {
t.Error("session boot firmwareArtifact must be non-empty")
}

provisioning := spec.Properties["provisioning"]
for _, field := range []string{"cluster", "kubernetes", "agent", "providerLabels", "userData"} {
if _, ok := provisioning.Properties[field]; !ok {
t.Errorf("session provisioning snapshot is missing %q", field)
}
}

artifactSource := spec.Properties["artifacts"].Properties["files"].Items.Schema.Properties["source"]
requireEnumValue(t, artifactSource.Enum, "Session")

status := crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["status"]
for _, field := range []string{"phase", "conditions"} {
if _, ok := status.Properties[field]; !ok {
t.Errorf("session status is missing %q", field)
}
}
}

func requireEnumValue(t *testing.T, values []apiextensionsv1.JSON, want string) {
t.Helper()

for _, value := range values {
var got string
if err := yaml.Unmarshal(value.Raw, &got); err == nil && got == want {
return
}
}

t.Errorf("enum does not contain %q", want)
}

func TestMachineOperationTargetInputHasNetbootSessionRef(t *testing.T) {
t.Parallel()

crd := readCRD(t, "../../../deploy/machina/crd/unbounded-cloud.io_machineoperations.yaml")

input := crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["status"].
Properties["targets"].Items.Schema.Properties["input"]
if _, ok := input.Properties["netbootSessionRef"]; !ok {
t.Error("operation target input is missing netbootSessionRef")
}
}

func readCRD(t *testing.T, path string) apiextensionsv1.CustomResourceDefinition {
t.Helper()

data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read CRD: %v", err)
}

var crd apiextensionsv1.CustomResourceDefinition
if err := yaml.Unmarshal(data, &crd); err != nil {
t.Fatalf("parse CRD: %v", err)
}

return crd
}
Loading
Loading