@@ -102,8 +102,8 @@ const ANNOTATION_SANDBOX_RUNTIME_MAIN_PROCESS_SPEC: &str =
102102const ANNOTATION_SANDBOX_RUNTIME_LOG_LEVEL : & str = "openshell.ai/sandbox-runtime-log-level" ;
103103const ANNOTATION_SANDBOX_RUNTIME_NETWORK_POLICY_UID : & str =
104104 "openshell.ai/sandbox-runtime-network-policy-uid" ;
105- const ANNOTATION_SANDBOX_RUNTIME_NETWORK_POLICY_VERSION : & str =
106- "openshell.ai/sandbox-runtime-network-policy-version " ;
105+ const ANNOTATION_SANDBOX_RUNTIME_NETWORK_POLICY_GENERATION : & str =
106+ "openshell.ai/sandbox-runtime-network-policy-generation " ;
107107
108108#[ derive( Clone , Copy , Debug , PartialEq , Eq ) ]
109109enum SandboxRuntimeBootstrapPhase {
@@ -2480,6 +2480,9 @@ impl KubernetesComputeDriver {
24802480 let fence_uid = fence. metadata . uid . ok_or_else ( || {
24812481 KubernetesDriverError :: Message ( "workload NetworkPolicy has no UID" . to_string ( ) )
24822482 } ) ?;
2483+ let fence_generation = fence. metadata . generation . ok_or_else ( || {
2484+ KubernetesDriverError :: Message ( "workload NetworkPolicy has no generation" . to_string ( ) )
2485+ } ) ?;
24832486 let fence_resource_version = fence. metadata . resource_version . ok_or_else ( || {
24842487 KubernetesDriverError :: Message (
24852488 "workload NetworkPolicy has no resourceVersion" . to_string ( ) ,
@@ -2529,7 +2532,7 @@ impl KubernetesComputeDriver {
25292532 ANNOTATION_SANDBOX_RUNTIME_WORKLOAD_UID : workload_pod_uid. clone( ) ,
25302533 ANNOTATION_SANDBOX_RUNTIME_SUPERVISOR_UID : supervisor_uid. clone( ) ,
25312534 ANNOTATION_SANDBOX_RUNTIME_NETWORK_POLICY_UID : fence_uid. clone( ) ,
2532- ANNOTATION_SANDBOX_RUNTIME_NETWORK_POLICY_VERSION : fence_resource_version . clone ( ) ,
2535+ ANNOTATION_SANDBOX_RUNTIME_NETWORK_POLICY_GENERATION : fence_generation . to_string ( ) ,
25332536 }
25342537 }
25352538 } )
@@ -2767,6 +2770,9 @@ impl KubernetesComputeDriver {
27672770 let fence_uid = fence. metadata . uid . ok_or_else ( || {
27682771 KubernetesDriverError :: Message ( "workload NetworkPolicy has no UID" . to_string ( ) )
27692772 } ) ?;
2773+ let fence_generation = fence. metadata . generation . ok_or_else ( || {
2774+ KubernetesDriverError :: Message ( "workload NetworkPolicy has no generation" . to_string ( ) )
2775+ } ) ?;
27702776 let fence_resource_version = fence. metadata . resource_version . ok_or_else ( || {
27712777 KubernetesDriverError :: Message (
27722778 "workload NetworkPolicy has no resourceVersion" . to_string ( ) ,
@@ -2808,7 +2814,7 @@ impl KubernetesComputeDriver {
28082814 ANNOTATION_SANDBOX_RUNTIME_WORKLOAD_UID : workload_pod_uid. clone( ) ,
28092815 ANNOTATION_SANDBOX_RUNTIME_SUPERVISOR_UID : supervisor_uid,
28102816 ANNOTATION_SANDBOX_RUNTIME_NETWORK_POLICY_UID : fence_uid. clone( ) ,
2811- ANNOTATION_SANDBOX_RUNTIME_NETWORK_POLICY_VERSION : fence_resource_version . clone ( ) ,
2817+ ANNOTATION_SANDBOX_RUNTIME_NETWORK_POLICY_GENERATION : fence_generation . to_string ( ) ,
28122818 }
28132819 }
28142820 } )
@@ -5489,9 +5495,12 @@ async fn sandbox_from_object_with_sandbox_runtime_readiness(
54895495 ) ) ;
54905496 let ( dependencies, workload_generation, supervisor_generation) =
54915497 tokio:: join!( dependencies, workload_generation, supervisor_generation) ;
5492- if dependencies != SandboxRuntimeControlAvailability :: Available
5493- || workload_generation != SandboxRuntimeControlAvailability :: Available
5494- || supervisor_generation != SandboxRuntimeControlAvailability :: Available
5498+ // A transient API read failure is not evidence that the running
5499+ // boundary disappeared. Preserve the CR's published readiness for
5500+ // Unknown and let periodic reconciliation retry.
5501+ if dependencies == SandboxRuntimeControlAvailability :: Unavailable
5502+ || workload_generation == SandboxRuntimeControlAvailability :: Unavailable
5503+ || supervisor_generation == SandboxRuntimeControlAvailability :: Unavailable
54955504 {
54965505 mark_sandbox_runtime_control_unavailable ( & mut sandbox) ;
54975506 }
@@ -6730,6 +6739,22 @@ fn required_sandbox_annotation(
67306739 } )
67316740}
67326741
6742+ fn condition_observes_current_generation (
6743+ object : & DynamicObject ,
6744+ condition : & serde_json:: Value ,
6745+ ) -> bool {
6746+ match (
6747+ object. metadata . generation ,
6748+ condition
6749+ . get ( "observedGeneration" )
6750+ . and_then ( serde_json:: Value :: as_i64) ,
6751+ ) {
6752+ ( Some ( generation) , Some ( observed_generation) ) => generation == observed_generation,
6753+ // Older Agent Sandbox API versions can omit observedGeneration.
6754+ _ => true ,
6755+ }
6756+ }
6757+
67336758fn status_from_object ( obj : & DynamicObject ) -> Option < SandboxStatus > {
67346759 let status = obj. data . get ( "status" ) ?;
67356760 let status_obj = status. as_object ( ) ?;
@@ -6740,6 +6765,7 @@ fn status_from_object(obj: &DynamicObject) -> Option<SandboxStatus> {
67406765 . map ( |items| {
67416766 items
67426767 . iter ( )
6768+ . filter ( |condition| condition_observes_current_generation ( obj, condition) )
67436769 . filter_map ( condition_from_value)
67446770 . collect :: < Vec < _ > > ( )
67456771 } )
@@ -6877,10 +6903,10 @@ fn sandbox_runtime_namespace_fence_generation_matches(
68776903 == annotations
68786904 . get ( ANNOTATION_SANDBOX_RUNTIME_NETWORK_POLICY_UID )
68796905 . map ( String :: as_str)
6880- && policy . metadata . resource_version . as_deref ( )
6881- == annotations
6882- . get ( ANNOTATION_SANDBOX_RUNTIME_NETWORK_POLICY_VERSION )
6883- . map ( String :: as_str )
6906+ && annotations
6907+ . get ( ANNOTATION_SANDBOX_RUNTIME_NETWORK_POLICY_GENERATION )
6908+ . and_then ( |generation| generation . parse :: < i64 > ( ) . ok ( ) )
6909+ == policy . metadata . generation
68846910}
68856911
68866912fn kubernetes_sandbox_has_stopped_condition ( obj : & DynamicObject ) -> bool {
@@ -6890,8 +6916,9 @@ fn kubernetes_sandbox_has_stopped_condition(obj: &DynamicObject) -> bool {
68906916 . and_then ( serde_json:: Value :: as_array)
68916917 . is_some_and ( |conditions| {
68926918 conditions. iter ( ) . any ( |condition| {
6893- condition. get ( "type" ) . and_then ( serde_json:: Value :: as_str)
6894- == Some ( SANDBOX_SUSPENDED_CONDITION )
6919+ condition_observes_current_generation ( obj, condition)
6920+ && condition. get ( "type" ) . and_then ( serde_json:: Value :: as_str)
6921+ == Some ( SANDBOX_SUSPENDED_CONDITION )
68956922 && condition
68966923 . get ( "status" )
68976924 . and_then ( serde_json:: Value :: as_str)
@@ -6920,8 +6947,9 @@ fn kubernetes_sandbox_stop_failure(obj: &DynamicObject) -> Option<String> {
69206947 . as_array ( ) ?
69216948 . iter ( )
69226949 . find_map ( |condition| {
6923- let is_terminal = condition. get ( "type" ) . and_then ( serde_json:: Value :: as_str)
6924- == Some ( SANDBOX_SUSPENDED_CONDITION )
6950+ let is_terminal = condition_observes_current_generation ( obj, condition)
6951+ && condition. get ( "type" ) . and_then ( serde_json:: Value :: as_str)
6952+ == Some ( SANDBOX_SUSPENDED_CONDITION )
69256953 && condition
69266954 . get ( "status" )
69276955 . and_then ( serde_json:: Value :: as_str)
@@ -7074,7 +7102,7 @@ fn sandbox_runtime_suspension_completion_patch(resource_version: &str) -> serde_
70747102 ANNOTATION_SANDBOX_RUNTIME_WORKLOAD_UID : serde_json:: Value :: Null ,
70757103 ANNOTATION_SANDBOX_RUNTIME_SUPERVISOR_UID : serde_json:: Value :: Null ,
70767104 ANNOTATION_SANDBOX_RUNTIME_NETWORK_POLICY_UID : serde_json:: Value :: Null ,
7077- ANNOTATION_SANDBOX_RUNTIME_NETWORK_POLICY_VERSION : serde_json:: Value :: Null ,
7105+ ANNOTATION_SANDBOX_RUNTIME_NETWORK_POLICY_GENERATION : serde_json:: Value :: Null ,
70787106 ANNOTATION_SANDBOX_RUNTIME_READINESS : "unavailable" ,
70797107 } ,
70807108 }
@@ -8240,6 +8268,58 @@ mod tests {
82408268 ) ) ;
82418269 }
82428270
8271+ fn sandbox_runtime_fence_pair_for_test ( ) -> ( NetworkPolicy , DynamicObject ) {
8272+ let policy = NetworkPolicy {
8273+ metadata : ObjectMeta {
8274+ uid : Some ( "policy-uid" . to_string ( ) ) ,
8275+ generation : Some ( 7 ) ,
8276+ resource_version : Some ( "200" . to_string ( ) ) ,
8277+ ..Default :: default ( )
8278+ } ,
8279+ ..Default :: default ( )
8280+ } ;
8281+ let resource = ApiResource :: from_gvk ( & GroupVersionKind :: gvk (
8282+ SANDBOX_GROUP ,
8283+ SANDBOX_VERSION_V1BETA1 ,
8284+ SANDBOX_KIND ,
8285+ ) ) ;
8286+ let mut sandbox = DynamicObject :: new ( "sandbox" , & resource) ;
8287+ sandbox. metadata . annotations = Some ( BTreeMap :: from ( [
8288+ (
8289+ ANNOTATION_SANDBOX_RUNTIME_NETWORK_POLICY_UID . to_string ( ) ,
8290+ "policy-uid" . to_string ( ) ,
8291+ ) ,
8292+ (
8293+ ANNOTATION_SANDBOX_RUNTIME_NETWORK_POLICY_GENERATION . to_string ( ) ,
8294+ "7" . to_string ( ) ,
8295+ ) ,
8296+ ] ) ) ;
8297+ ( policy, sandbox)
8298+ }
8299+
8300+ #[ test]
8301+ fn sandbox_runtime_fence_reconciliation_uses_generation_not_resource_version ( ) {
8302+ let ( mut policy, sandbox) = sandbox_runtime_fence_pair_for_test ( ) ;
8303+
8304+ assert ! (
8305+ sandbox_runtime_namespace_fence_generation_matches( & policy, & sandbox) ,
8306+ "metadata-only writes must not invalidate an unchanged fence"
8307+ ) ;
8308+
8309+ policy. metadata . generation = Some ( 8 ) ;
8310+ assert ! (
8311+ !sandbox_runtime_namespace_fence_generation_matches( & policy, & sandbox) ,
8312+ "a policy spec change must invalidate the recorded fence generation"
8313+ ) ;
8314+
8315+ policy. metadata . generation = Some ( 7 ) ;
8316+ policy. metadata . uid = Some ( "replacement-policy-uid" . to_string ( ) ) ;
8317+ assert ! (
8318+ !sandbox_runtime_namespace_fence_generation_matches( & policy, & sandbox) ,
8319+ "a replacement policy must invalidate the recorded fence identity"
8320+ ) ;
8321+ }
8322+
82438323 #[ test]
82448324 fn sandbox_runtime_bootstrap_marker_and_age_gate_rollback ( ) {
82458325 let started = Duration :: from_hours ( 490_896 ) ;
@@ -8660,6 +8740,40 @@ mod tests {
86608740 assert ! ( kubernetes_sandbox_has_stopped_condition( & sandbox) ) ;
86618741 }
86628742
8743+ #[ test]
8744+ fn stale_generation_conditions_do_not_change_current_status ( ) {
8745+ let resource = ApiResource :: from_gvk ( & GroupVersionKind :: gvk (
8746+ SANDBOX_GROUP ,
8747+ SANDBOX_VERSION_V1BETA1 ,
8748+ SANDBOX_KIND ,
8749+ ) ) ;
8750+ let mut sandbox = DynamicObject :: new ( "sandbox" , & resource) ;
8751+ sandbox. metadata . generation = Some ( 2 ) ;
8752+ sandbox. data = serde_json:: json!( {
8753+ "status" : {
8754+ "conditions" : [
8755+ {
8756+ "type" : "Suspended" ,
8757+ "status" : "True" ,
8758+ "reason" : "PodTerminated" ,
8759+ "observedGeneration" : 1
8760+ } ,
8761+ {
8762+ "type" : "Ready" ,
8763+ "status" : "True" ,
8764+ "reason" : "DependenciesReady" ,
8765+ "observedGeneration" : 2
8766+ }
8767+ ]
8768+ }
8769+ } ) ;
8770+
8771+ assert ! ( !kubernetes_sandbox_has_stopped_condition( & sandbox) ) ;
8772+ let status = status_from_object ( & sandbox) . expect ( "sandbox status" ) ;
8773+ assert_eq ! ( status. conditions. len( ) , 1 ) ;
8774+ assert_eq ! ( status. conditions[ 0 ] . r#type, "Ready" ) ;
8775+ }
8776+
86638777 #[ test]
86648778 fn beta_stop_requires_suspended_condition_and_deleted_pod ( ) {
86658779 let resource = ApiResource :: from_gvk ( & GroupVersionKind :: gvk (
@@ -11314,7 +11428,7 @@ mod tests {
1131411428 ANNOTATION_SANDBOX_RUNTIME_WORKLOAD_UID ,
1131511429 ANNOTATION_SANDBOX_RUNTIME_SUPERVISOR_UID ,
1131611430 ANNOTATION_SANDBOX_RUNTIME_NETWORK_POLICY_UID ,
11317- ANNOTATION_SANDBOX_RUNTIME_NETWORK_POLICY_VERSION ,
11431+ ANNOTATION_SANDBOX_RUNTIME_NETWORK_POLICY_GENERATION ,
1131811432 ] {
1131911433 assert_eq ! (
1132011434 complete[ "metadata" ] [ "annotations" ] [ annotation] ,
0 commit comments