diff --git a/release-notes/unreleased/170-applyset-label-propagation.md b/release-notes/unreleased/170-applyset-label-propagation.md new file mode 100644 index 0000000..88eda25 --- /dev/null +++ b/release-notes/unreleased/170-applyset-label-propagation.md @@ -0,0 +1,32 @@ +# Release Notes for Issue #170: Do not propagate ApplySet bookkeeping labels + +## Bug Fix + +### What Changed + +`RestateDeployment` no longer copies labels in the +`applyset.kubernetes.io/` namespace onto its operator-owned Services, Knative +Configurations, or Knative Routes. Other user labels continue to propagate. + +### Why This Matters + +kubectl uses `applyset.kubernetes.io/part-of` to decide which resources belong +to an ApplySet. Copying that label made operator-owned child resources look +like direct members of the ApplySet that contained the `RestateDeployment`. +A later `kubectl apply --prune --applyset=...` could consequently delete a +versioned Service before Restate had drained the corresponding deployment. + +### Impact on Users + +- Existing `RestateDeployment` resources need no manifest changes. +- The operator removes the bookkeeping labels from children on reconciliation. +- kubectl continues to manage and prune the `RestateDeployment` itself. + +### Migration Guidance + +Upgrade the operator. No CRD or workload configuration changes are required. + +### Related Issues + +- Issue #170: RestateDeployment propagates ApplySet membership to owned + resources diff --git a/src/controllers/restatedeployment/controller.rs b/src/controllers/restatedeployment/controller.rs index cebee63..63a797d 100644 --- a/src/controllers/restatedeployment/controller.rs +++ b/src/controllers/restatedeployment/controller.rs @@ -55,6 +55,17 @@ use super::reconcilers::replicaset::{ pub(super) const RESTATE_DEPLOYMENT_ID_ANNOTATION: &str = "restate.dev/deployment-id"; pub(super) const OWNED_BY_LABEL: &str = "restate.dev/owned-by"; pub(super) const APP_MANAGED_BY_LABEL: &str = "app.kubernetes.io/managed-by"; +const APPLYSET_LABEL_PREFIX: &str = "applyset.kubernetes.io/"; + +/// Copy user labels to an operator-owned child without copying ApplySet +/// bookkeeping. A child that inherits `applyset.kubernetes.io/part-of` becomes +/// an accidental member of the parent's kubectl ApplySet and can be pruned even +/// though it was created and is still needed by this operator. +pub(super) fn propagated_labels(labels: &BTreeMap) -> BTreeMap { + let mut propagated = labels.clone(); + propagated.retain(|key, _| !key.starts_with(APPLYSET_LABEL_PREFIX)); + propagated +} pub(super) struct Context { /// Kubernetes client @@ -391,7 +402,7 @@ impl RestateDeployment { .await?; } - let mut service_labels = self.labels().clone(); + let mut service_labels = propagated_labels(self.labels()); service_labels.insert( APP_MANAGED_BY_LABEL.to_string(), "restate-operator".to_string(), @@ -1546,4 +1557,24 @@ mod tests { let s1_again = latest_version_label_selector(&v1, None).expect("v1 selector again"); assert_eq!(s1, s1_again, "selector should be deterministic"); } + + #[test] + fn propagated_labels_exclude_applyset_bookkeeping() { + let labels = BTreeMap::from([ + ("app.kubernetes.io/name".to_string(), "greeter".to_string()), + ( + "applyset.kubernetes.io/part-of".to_string(), + "applyset-parent-id".to_string(), + ), + ( + "applyset.kubernetes.io/id".to_string(), + "applyset-parent-id".to_string(), + ), + ]); + + assert_eq!( + propagated_labels(&labels), + BTreeMap::from([("app.kubernetes.io/name".to_string(), "greeter".to_string())]) + ); + } } diff --git a/src/controllers/restatedeployment/reconcilers/knative.rs b/src/controllers/restatedeployment/reconcilers/knative.rs index 4ea5993..775d88e 100644 --- a/src/controllers/restatedeployment/reconcilers/knative.rs +++ b/src/controllers/restatedeployment/reconcilers/knative.rs @@ -9,7 +9,7 @@ use tracing::*; use url::Url; use crate::controllers::restatedeployment::controller::{ - Context, RESTATE_DEPLOYMENT_ID_ANNOTATION, + Context, RESTATE_DEPLOYMENT_ID_ANNOTATION, propagated_labels, }; use crate::controllers::restatedeployment::reconcilers::replicaset::generate_pod_template_hash; use crate::resources::knative::{ @@ -338,7 +338,7 @@ fn build_configuration_spec( config_annotations.insert(RESTATE_POD_TEMPLATE_ANNOTATION.to_string(), v.to_string()); } - let mut config_labels = rsd.labels().clone(); + let mut config_labels = propagated_labels(rsd.labels()); config_labels.insert( "app.kubernetes.io/managed-by".to_string(), "restate-operator".to_string(), @@ -516,7 +516,7 @@ fn build_route_spec( route_annotations.insert(RESTATE_DEPLOYMENT_ANNOTATION.to_string(), rsd.name_any()); // Propagate RestateDeployment labels to Route - let mut route_labels = rsd.labels().clone(); + let mut route_labels = propagated_labels(rsd.labels()); route_labels.insert( "app.kubernetes.io/managed-by".to_string(), "restate-operator".to_string(),