@@ -42,16 +42,15 @@ use tracing::{debug, error, info, warn};
4242
4343use super :: { Context , ReconcilerError } ;
4444use crate :: constants:: {
45- ALLOWED_BOOTSTRAP_API_GROUPS , ALLOWED_INFRASTRUCTURE_API_GROUPS , API_VERSION_FULL ,
46- CAPI_CLUSTER_NAME_LABEL , CAPI_GROUP , CAPI_MACHINE_API_VERSION , CAPI_MACHINE_API_VERSION_FULL ,
47- CAPI_RESOURCE_MACHINES , CONDITION_STATUS_TRUE , CONDITION_TYPE_READY , DEFAULT_INSTANCE_ID ,
48- ENV_OPERATOR_INSTANCE_ID , ERROR_REQUEUE_SECS , FINALIZER_CLEANUP_TIMEOUT_SECS ,
49- FINALIZER_SCHEDULED_MACHINE , MAX_BACKOFF_SECS , MAX_CLUSTER_NAME_LEN , MAX_DURATION_SECS ,
50- MAX_KILL_IF_COMMANDS_COUNT , MAX_KILL_IF_COMMAND_LEN , MAX_RECONCILE_RETRIES , PHASE_ACTIVE ,
51- PHASE_ERROR , PHASE_INACTIVE , PHASE_SHUTTING_DOWN , PHASE_TERMINATED ,
52- POD_EVICTION_GRACE_PERIOD_SECS , RBAC_VERB_CREATE , REASON_GRACE_PERIOD , REASON_KILL_SWITCH ,
53- REASON_RECONCILE_SUCCEEDED , RESERVED_LABEL_PREFIXES , SCHEDULED_MACHINE_LABEL ,
54- TIMER_REQUEUE_SECS ,
45+ capi_machine_api_version, ALLOWED_BOOTSTRAP_API_GROUPS , ALLOWED_INFRASTRUCTURE_API_GROUPS ,
46+ API_VERSION_FULL , CAPI_CLUSTER_NAME_LABEL , CAPI_GROUP , CAPI_RESOURCE_MACHINES ,
47+ CONDITION_STATUS_TRUE , CONDITION_TYPE_READY , DEFAULT_INSTANCE_ID , ENV_OPERATOR_INSTANCE_ID ,
48+ ERROR_REQUEUE_SECS , FINALIZER_CLEANUP_TIMEOUT_SECS , FINALIZER_SCHEDULED_MACHINE ,
49+ MAX_BACKOFF_SECS , MAX_CLUSTER_NAME_LEN , MAX_DURATION_SECS , MAX_KILL_IF_COMMANDS_COUNT ,
50+ MAX_KILL_IF_COMMAND_LEN , MAX_RECONCILE_RETRIES , PHASE_ACTIVE , PHASE_ERROR , PHASE_INACTIVE ,
51+ PHASE_SHUTTING_DOWN , PHASE_TERMINATED , POD_EVICTION_GRACE_PERIOD_SECS , RBAC_VERB_CREATE ,
52+ REASON_GRACE_PERIOD , REASON_KILL_SWITCH , REASON_RECONCILE_SUCCEEDED , RESERVED_LABEL_PREFIXES ,
53+ SCHEDULED_MACHINE_LABEL , TIMER_REQUEUE_SECS ,
5554} ;
5655use crate :: crd:: { Condition , NodeRef , ScheduledMachine , ScheduledMachineStatus } ;
5756use crate :: metrics:: {
@@ -1113,6 +1112,23 @@ pub fn validate_schedule_ref(
11131112// CAPI Resource Creation
11141113// ============================================================================
11151114
1115+ /// Derive the CAPI `Machine` API version (`v1beta1` / `v1beta2`) for a
1116+ /// `ScheduledMachine` from the version component of its bootstrapSpec `apiVersion`
1117+ /// (passthrough — the Machine shares the one CAPI contract version the user
1118+ /// declared for bootstrap + infra). Falls back to the runtime-resolved *served*
1119+ /// version ([`capi_machine_api_version`]) when bootstrap carries no `/<version>`
1120+ /// segment. The Machine group/kind (`cluster.x-k8s.io`/`Machine`) are fixed by the
1121+ /// CAPI contract; only the version is data-driven, never hardcoded for production.
1122+ fn machine_api_version_for ( bootstrap_api_version : Option < & str > ) -> String {
1123+ bootstrap_api_version
1124+ . and_then ( |bv| bv. rsplit_once ( '/' ) . map ( |( _, ver) | ver) )
1125+ . filter ( |ver| !ver. is_empty ( ) )
1126+ . map_or_else (
1127+ || capi_machine_api_version ( ) . to_string ( ) ,
1128+ |ver| ver. to_string ( ) ,
1129+ )
1130+ }
1131+
11161132/// Add machine to cluster by creating bootstrap, infrastructure, and Machine resources
11171133///
11181134/// This function:
@@ -1166,6 +1182,12 @@ pub async fn add_machine_to_cluster(
11661182 "bootstrapSpec missing required field 'apiVersion'" . to_string ( ) ,
11671183 )
11681184 } ) ?;
1185+ // The created CAPI Machine shares the contract version the user declared on
1186+ // bootstrapSpec (passthrough); see machine_api_version_for.
1187+ let machine_api_version_full = format ! (
1188+ "{CAPI_GROUP}/{}" ,
1189+ machine_api_version_for( Some ( bootstrap_api_version) )
1190+ ) ;
11691191 let bootstrap_kind = resource. spec . bootstrap_spec . kind ( ) . ok_or_else ( || {
11701192 ReconcilerError :: InvalidConfig ( "bootstrapSpec missing required field 'kind'" . to_string ( ) )
11711193 } ) ?;
@@ -1245,7 +1267,7 @@ pub async fn add_machine_to_cluster(
12451267 ensure_can_create (
12461268 client,
12471269 namespace,
1248- CAPI_MACHINE_API_VERSION_FULL ,
1270+ & machine_api_version_full ,
12491271 "Machine" ,
12501272 "machine" ,
12511273 )
@@ -1370,7 +1392,7 @@ pub async fn add_machine_to_cluster(
13701392 }
13711393
13721394 let machine_obj = json ! ( {
1373- "apiVersion" : CAPI_MACHINE_API_VERSION_FULL ,
1395+ "apiVersion" : machine_api_version_full . as_str ( ) ,
13741396 "kind" : "Machine" ,
13751397 "metadata" : {
13761398 "name" : name,
@@ -1401,7 +1423,7 @@ pub async fn add_machine_to_cluster(
14011423 create_dynamic_resource (
14021424 client,
14031425 namespace,
1404- CAPI_MACHINE_API_VERSION_FULL ,
1426+ & machine_api_version_full ,
14051427 "Machine" ,
14061428 machine_obj,
14071429 )
@@ -1625,9 +1647,13 @@ pub async fn remove_machine_from_cluster(
16251647 "Deleting CAPI resources"
16261648 ) ;
16271649
1628- // 1. Delete the Machine resource first (this triggers CAPI cleanup)
1650+ // 1. Delete the Machine resource first (this triggers CAPI cleanup). Use the
1651+ // CAPI contract version the user declared on bootstrapSpec (passthrough,
1652+ // matching create); fall back to the runtime-discovered served version when
1653+ // it has none. CAPI conversion bridges either way.
1654+ let machine_api_version = machine_api_version_for ( resource. spec . bootstrap_spec . api_version ( ) ) ;
16291655 let ar = kube:: api:: ApiResource :: from_gvk_with_plural (
1630- & kube:: api:: GroupVersionKind :: gvk ( CAPI_GROUP , CAPI_MACHINE_API_VERSION , "Machine" ) ,
1656+ & kube:: api:: GroupVersionKind :: gvk ( CAPI_GROUP , & machine_api_version , "Machine" ) ,
16311657 CAPI_RESOURCE_MACHINES ,
16321658 ) ;
16331659 let machines: Api < kube:: core:: DynamicObject > =
@@ -1696,9 +1722,21 @@ pub fn extract_machine_refs(
16961722 . get ( "status" )
16971723 . and_then ( |s| s. get ( "nodeRef" ) )
16981724 . and_then ( |nr| {
1699- let api_version = nr. get ( "apiVersion" ) . and_then ( serde_json:: Value :: as_str) ?;
1700- let kind = nr. get ( "kind" ) . and_then ( serde_json:: Value :: as_str) ?;
1725+ // `name` is the only field guaranteed across CAPI versions: v1beta1's
1726+ // nodeRef is a corev1 ObjectReference (apiVersion/kind/uid present),
1727+ // but v1beta2 simplified it to a `MachineNodeReference` carrying just
1728+ // `name` (it is always a core/v1 Node). Require only `name`; default
1729+ // the rest so v1beta2 nodeRef enrichment (node taints, reclaim-agent)
1730+ // still fires.
17011731 let name = nr. get ( "name" ) . and_then ( serde_json:: Value :: as_str) ?;
1732+ let api_version = nr
1733+ . get ( "apiVersion" )
1734+ . and_then ( serde_json:: Value :: as_str)
1735+ . unwrap_or ( "v1" ) ;
1736+ let kind = nr
1737+ . get ( "kind" )
1738+ . and_then ( serde_json:: Value :: as_str)
1739+ . unwrap_or ( "Node" ) ;
17021740 let uid = nr
17031741 . get ( "uid" )
17041742 . and_then ( serde_json:: Value :: as_str)
@@ -1727,7 +1765,7 @@ pub async fn fetch_capi_machine(
17271765 machine_name : & str ,
17281766) -> Result < Option < kube:: core:: DynamicObject > , ReconcilerError > {
17291767 let ar = kube:: api:: ApiResource :: from_gvk_with_plural (
1730- & kube:: api:: GroupVersionKind :: gvk ( CAPI_GROUP , CAPI_MACHINE_API_VERSION , "Machine" ) ,
1768+ & kube:: api:: GroupVersionKind :: gvk ( CAPI_GROUP , capi_machine_api_version ( ) , "Machine" ) ,
17311769 CAPI_RESOURCE_MACHINES ,
17321770 ) ;
17331771 let machines: Api < kube:: core:: DynamicObject > =
0 commit comments