Skip to content
This repository was archived by the owner on Sep 8, 2025. It is now read-only.

Commit 95ed451

Browse files
authored
feat(controlplane): gateway resource cleanup (#443)
use a finalizer to clean up created resources fixes #440 Signed-off-by: Harald Gutmann <harald@gutmann.one>
1 parent 56379f5 commit 95ed451

File tree

4 files changed

+158
-37
lines changed

4 files changed

+158
-37
lines changed

controlplane/src/consts.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ pub const BLIXT_DATAPLANE_COMPONENT_LABEL: &str = "dataplane";
1111
// The finalizer used for Blixt dataplane cleanup.
1212
pub const DATAPLANE_FINALIZER: &str = "blixt.gateway.networking.k8s.io/dataplane";
1313

14+
// The finalizer used for Blixt controlplane cleanup.
15+
pub const CONTROLPLANE_FINALIZER: &str = "blixt.gateway.networking.k8s.io/controlplane";
16+
1417
// Controller name for the Blixt GatewayClass.
1518
pub const GATEWAY_CLASS_CONTROLLER_NAME: &str = "gateway.networking.k8s.io/blixt";
1619

controlplane/src/gateway_controller.rs

Lines changed: 69 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,6 @@ use std::{
2020
time::{Duration, Instant},
2121
};
2222

23-
use crate::{
24-
consts::{GATEWAY_CLASS_CONTROLLER_NAME, GATEWAY_SERVICE_LABEL},
25-
*,
26-
};
27-
use gateway_utils::*;
28-
use route_utils::set_condition;
29-
3023
use chrono::Utc;
3124
use futures::StreamExt;
3225
use gateway_api::apis::standard::gateways::{Gateway, GatewayStatus};
@@ -39,33 +32,31 @@ use k8s_openapi::apimachinery::pkg::apis::meta::v1 as metav1;
3932
use kube::{
4033
Resource, ResourceExt,
4134
api::{Api, ListParams, Patch, PatchParams},
42-
runtime::{Controller, controller::Action, watcher::Config},
35+
runtime::{
36+
Controller, controller::Action, finalizer, finalizer::Error as FinalizerError,
37+
finalizer::Event, watcher::Config,
38+
},
4339
};
4440
use tracing::{debug, error, info, warn};
4541

42+
use crate::{
43+
Context, Error, NamespaceName, Result,
44+
consts::{CONTROLPLANE_FINALIZER, GATEWAY_CLASS_CONTROLLER_NAME, GATEWAY_SERVICE_LABEL},
45+
gateway_utils::{
46+
create_endpoint_if_not_exists, create_loadbalancer_service, delete_endpoint,
47+
delete_loadbalancer_service, get_accepted_condition, get_ingress_ip_len, get_service_key,
48+
patch_status, set_gateway_status_addresses, set_listener_status,
49+
update_service_for_gateway,
50+
},
51+
gatewayclass_utils,
52+
route_utils::set_condition,
53+
};
54+
4655
pub async fn reconcile(gateway: Arc<Gateway>, ctx: Arc<Context>) -> Result<Action> {
4756
let start = Instant::now();
4857
let client = ctx.client.clone();
4958

50-
let name = gateway
51-
.metadata
52-
.name
53-
.clone()
54-
.ok_or(Error::InvalidConfigError("invalid name".to_string()))?;
55-
56-
let ns = gateway
57-
.metadata
58-
.namespace
59-
.clone()
60-
.ok_or(Error::InvalidConfigError("invalid namespace".to_string()))?;
61-
62-
let gateway_api: Api<Gateway> = Api::namespaced(client.clone(), &ns);
63-
let mut gw = Gateway {
64-
metadata: gateway.metadata.clone(),
65-
spec: gateway.spec.clone(),
66-
status: gateway.status.clone(),
67-
};
68-
59+
// check gateway_class
6960
let gateway_class_api = Api::<GatewayClass>::all(client.clone());
7061
let gateway_class = gateway_class_api
7162
.get(gateway.spec.gateway_class_name.as_str())
@@ -90,6 +81,47 @@ pub async fn reconcile(gateway: Arc<Gateway>, ctx: Arc<Context>) -> Result<Actio
9081
return Ok(Action::await_change());
9182
}
9283

84+
let gateway_id = gateway.metadata.namespaced_name()?;
85+
let gateway_api: Api<Gateway> = Api::namespaced(ctx.client.clone(), &gateway_id.namespace);
86+
finalizer(
87+
&gateway_api,
88+
CONTROLPLANE_FINALIZER,
89+
gateway,
90+
|event| async {
91+
match event {
92+
Event::Apply(gateway) => configure_gateway(gateway, ctx.clone(), start).await,
93+
Event::Cleanup(gateway) => cleanup_gateway(gateway, &ctx).await,
94+
}
95+
},
96+
)
97+
.await
98+
.map_err(|e| match e {
99+
FinalizerError::ApplyFailed(e) | FinalizerError::CleanupFailed(e) => e,
100+
FinalizerError::AddFinalizer(e) | FinalizerError::RemoveFinalizer(e) => e.into(),
101+
FinalizerError::UnnamedObject => Error::MissingResourceName,
102+
FinalizerError::InvalidFinalizer => {
103+
Error::InvalidConfigError("InvalidFinalizer".to_string())
104+
}
105+
})
106+
}
107+
108+
pub async fn configure_gateway(
109+
gateway: Arc<Gateway>,
110+
ctx: Arc<Context>,
111+
start: Instant,
112+
) -> Result<Action> {
113+
let gateway_id = gateway.metadata.namespaced_name()?;
114+
let ns = gateway_id.namespace.clone();
115+
let name = gateway_id.name.clone();
116+
let client = ctx.client.clone();
117+
118+
let gateway_api: Api<Gateway> = Api::namespaced(ctx.client.clone(), &gateway_id.namespace);
119+
let mut gw = Gateway {
120+
metadata: gateway.metadata.clone(),
121+
spec: gateway.spec.clone(),
122+
status: gateway.status.clone(),
123+
};
124+
93125
set_listener_status(&mut gw)?;
94126
let accepted_cond = get_accepted_condition(&gw);
95127
set_condition(&mut gw, accepted_cond.clone());
@@ -154,7 +186,7 @@ pub async fn reconcile(gateway: Arc<Gateway>, ctx: Arc<Context>) -> Result<Actio
154186
}
155187
} else {
156188
info!("creating loadbalancer service");
157-
service = create_svc_for_gateway(ctx.clone(), gateway.as_ref()).await?;
189+
service = create_loadbalancer_service(ctx.clone(), gateway.as_ref()).await?;
158190
}
159191

160192
// invalid_lb_condition is a Condition that signifies that the Loadbalancer service is invalid.
@@ -220,6 +252,15 @@ pub async fn reconcile(gateway: Arc<Gateway>, ctx: Arc<Context>) -> Result<Actio
220252
Ok(Action::requeue(Duration::from_secs(60)))
221253
}
222254

255+
async fn cleanup_gateway(gateway: Arc<Gateway>, ctx: &Context) -> Result<Action> {
256+
let gateway_id = gateway.metadata.namespaced_name()?;
257+
258+
delete_endpoint(ctx.client.clone(), &gateway_id).await?;
259+
delete_loadbalancer_service(ctx.client.clone(), &gateway_id).await?;
260+
261+
Ok(Action::await_change())
262+
}
263+
223264
pub async fn controller(ctx: Context) -> Result<()> {
224265
let gateway = Api::<Gateway>::all(ctx.client.clone());
225266
gateway

controlplane/src/gateway_utils.rs

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ use k8s_openapi::api::core::v1::{
4848
use k8s_openapi::apimachinery::pkg::apis::meta::v1 as metav1;
4949

5050
use chrono::Utc;
51+
use kube::api::DeleteParams;
5152
use serde_json::json;
5253
use tracing::*;
5354

@@ -149,6 +150,19 @@ pub async fn create_endpoint_if_not_exists(
149150
Ok(())
150151
}
151152

153+
// Deletes the Endpoint for the provided Gateway.
154+
pub(crate) async fn delete_endpoint(k8s_client: Client, gateway_id: &NamespacedName) -> Result<()> {
155+
let endpoint_name = format!("service-for-gateway-{}", gateway_id.name.clone());
156+
info!("Deleting Endpoint {endpoint_name} for gateway {gateway_id}.");
157+
158+
let endpoints_api: Api<Endpoints> = Api::namespaced(k8s_client.clone(), &gateway_id.namespace);
159+
endpoints_api
160+
.delete(&endpoint_name, &DeleteParams::default())
161+
.await
162+
.map_err(Error::KubeError)
163+
.map(|_| ())
164+
}
165+
152166
// Returns true if the provided error is a not found error.
153167
pub fn check_if_not_found_err(error: kube::Error) -> bool {
154168
if let kube::Error::Api(response) = error
@@ -170,11 +184,11 @@ pub fn get_ingress_ip_len(svc_status: &ServiceStatus) -> usize {
170184
}
171185

172186
// Creates a LoadBalancer Service for the provided Gateway.
173-
pub async fn create_svc_for_gateway(ctx: Arc<Context>, gateway: &Gateway) -> Result<Service> {
187+
pub async fn create_loadbalancer_service(ctx: Arc<Context>, gateway: &Gateway) -> Result<Service> {
174188
let mut svc_meta = ObjectMeta::default();
175-
let ns = gateway.namespace().unwrap_or("default".to_string());
176-
svc_meta.namespace = Some(ns.clone());
177-
svc_meta.generate_name = Some(format!("service-for-gateway-{}-", gateway.name_any()));
189+
let gateway_id = gateway.metadata.namespaced_name()?;
190+
svc_meta.namespace = Some(gateway_id.namespace.clone());
191+
svc_meta.name = Some(format!("service-for-gateway-{}", gateway_id.name));
178192

179193
let mut labels = BTreeMap::new();
180194
labels.insert(GATEWAY_SERVICE_LABEL.to_string(), gateway.name_any());
@@ -187,7 +201,7 @@ pub async fn create_svc_for_gateway(ctx: Arc<Context>, gateway: &Gateway) -> Res
187201
};
188202
update_service_for_gateway(gateway, &mut svc)?;
189203

190-
let svc_api: Api<Service> = Api::namespaced(ctx.client.clone(), ns.as_str());
204+
let svc_api: Api<Service> = Api::namespaced(ctx.client.clone(), &gateway_id.namespace);
191205
let service = svc_api
192206
.create(&PostParams::default(), &svc)
193207
.await
@@ -196,6 +210,22 @@ pub async fn create_svc_for_gateway(ctx: Arc<Context>, gateway: &Gateway) -> Res
196210
Ok(service)
197211
}
198212

213+
// Deletes a LoadBalancer Service for the provided Gateway.
214+
pub(crate) async fn delete_loadbalancer_service(
215+
k8s_client: Client,
216+
gateway_id: &NamespacedName,
217+
) -> Result<()> {
218+
let service_name = format!("service-for-gateway-{}", &gateway_id.name);
219+
info!("Deleting Service {service_name} of type LoadBalancer for Gateway {gateway_id}",);
220+
221+
let service_api: Api<Service> = Api::namespaced(k8s_client, &gateway_id.namespace);
222+
service_api
223+
.delete(&service_name, &DeleteParams::default())
224+
.await
225+
.map_err(Error::KubeError)
226+
.map(|_| ())
227+
}
228+
199229
// Updates the provided Service to match the desired state according to the provided Gateway.
200230
// Returns true if Service was modified.
201231
pub fn update_service_for_gateway(gateway: &Gateway, svc: &mut Service) -> Result<bool> {

controlplane/src/lib.rs

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,17 @@ mod tcproute_controller;
2525
mod traits;
2626
mod udproute_controller;
2727

28+
use std::fmt::{Debug, Display, Formatter};
29+
30+
use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta;
31+
use kube::Client;
32+
use thiserror::Error;
33+
2834
pub use gateway_controller::controller as gateway_controller;
2935
pub use gatewayclass_controller::controller as gatewayclass_controller;
3036
pub use tcproute_controller::controller as tcproute_controller;
3137
pub use udproute_controller::controller as udproute_controller;
3238

33-
use kube::Client;
34-
use thiserror::Error;
35-
3639
// Context for our reconciler
3740
#[derive(Clone)]
3841
pub struct Context {
@@ -43,7 +46,7 @@ pub struct Context {
4346
#[derive(Error, Debug)]
4447
pub enum Error {
4548
#[error("kube error: {0}")]
46-
KubeError(#[source] kube::Error),
49+
KubeError(#[from] kube::Error),
4750
#[error("invalid configuration: `{0}`")]
4851
InvalidConfigError(String),
4952
#[error("error reconciling loadbalancer service: `{0}`")]
@@ -52,11 +55,55 @@ pub enum Error {
5255
CRDNotFoundError(#[source] kube::Error),
5356
#[error("dataplane error: {0}")]
5457
DataplaneError(String),
58+
#[error("missing resource namespace")]
59+
MissingResourceNamespace,
60+
#[error("missing resource name")]
61+
MissingResourceName,
5562
}
5663

5764
pub type Result<T, E = Error> = std::result::Result<T, E>;
5865

66+
#[derive(Clone, Hash, Eq, PartialEq)]
5967
pub struct NamespacedName {
6068
pub name: String,
6169
pub namespace: String,
6270
}
71+
72+
impl Display for NamespacedName {
73+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
74+
f.write_str(self.namespace.as_str())?;
75+
f.write_str("/")?;
76+
f.write_str(self.name.as_str())
77+
}
78+
}
79+
80+
impl Debug for NamespacedName {
81+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
82+
Display::fmt(self, f)
83+
}
84+
}
85+
86+
pub trait NamespaceName {
87+
fn namespace(&self) -> std::result::Result<&str, Error>;
88+
fn name(&self) -> std::result::Result<&str, Error>;
89+
fn namespaced_name(&self) -> std::result::Result<NamespacedName, Error>;
90+
}
91+
92+
impl NamespaceName for ObjectMeta {
93+
fn namespace(&self) -> std::result::Result<&str, Error> {
94+
self.namespace
95+
.as_deref()
96+
.ok_or(Error::MissingResourceNamespace)
97+
}
98+
99+
fn name(&self) -> std::result::Result<&str, Error> {
100+
self.name.as_deref().ok_or(Error::MissingResourceName)
101+
}
102+
103+
fn namespaced_name(&self) -> std::result::Result<NamespacedName, Error> {
104+
Ok(NamespacedName {
105+
name: self.name()?.to_string(),
106+
namespace: self.namespace()?.to_string(),
107+
})
108+
}
109+
}

0 commit comments

Comments
 (0)