Skip to content

Commit daf2d71

Browse files
committed
Added support for TCPRoute.
1 parent 0b60cbb commit daf2d71

5 files changed

Lines changed: 123 additions & 3 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ futures = "0.3.31"
1111
gateway-api = "0.19.0"
1212
k8s-openapi = { version = "0.26.0", features = ["v1_30"] }
1313
kube = { version = "^2", features = ["client", "config", "runtime"] }
14+
regex = "1.12.2"
1415
serde = "^1"
1516
serde_json = "^1"
1617
tempfile = "3.23.0"

src/args.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,8 @@ pub struct I2GArgs {
1818
// This is ueful for deleting all HTTP or TCPRoute objects when an Ingress is deleted
1919
#[arg(long, env = "I2G_LINK_TO_INGRESS", default_value_t = true)]
2020
pub link_to_ingress: bool,
21+
22+
/// Whether to use experimental gateway-api resources like TCPRoutes.
23+
#[arg(long, env = "I2G_EXPERIMENTAL", default_value_t = false)]
24+
pub experimental: bool,
2125
}

src/main.rs

Lines changed: 104 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ use std::{sync::Arc, time::Duration};
22

33
use futures::StreamExt;
44
use gateway_api::{
5+
apis::experimental::tcproutes::{
6+
TCPRoute, TCPRouteParentRefs, TCPRouteRules, TCPRouteRulesBackendRefs, TCPRouteSpec,
7+
},
58
gateways,
69
httproutes::{
710
HTTPRoute, HTTPRouteParentRefs, HTTPRouteRules, HTTPRouteRulesBackendRefs,
@@ -11,7 +14,7 @@ use gateway_api::{
1114
};
1215
use k8s_openapi::api::{
1316
core::v1::Service,
14-
networking::v1::{Ingress, ServiceBackendPort},
17+
networking::v1::{Ingress, IngressServiceBackend, ServiceBackendPort},
1518
};
1619
use kube::{Api, Resource, ResourceExt, api::PatchParams, runtime::controller::Action};
1720

@@ -63,7 +66,7 @@ async fn create_http_route(
6366
http: &k8s_openapi::api::networking::v1::HTTPIngressRuleValue,
6467
hostname: &str,
6568
) -> anyhow::Result<HTTPRoute> {
66-
let safe_hostname = hostname.replace('.', "-");
69+
let safe_hostname = utils::sanitize_hostname(hostname);
6770
let gw_group = <gateways::Gateway as kube::Resource>::group(&());
6871
let gw_kind = <gateways::Gateway as kube::Resource>::kind(&());
6972

@@ -156,6 +159,68 @@ async fn create_http_route(
156159
))
157160
}
158161

162+
async fn create_tcp_route(
163+
ctx: Arc<ctx::Context>,
164+
namespace: &str,
165+
svc: &IngressServiceBackend,
166+
hostname: &str,
167+
) -> anyhow::Result<TCPRoute> {
168+
let safe_hostname = utils::sanitize_hostname(hostname);
169+
let gw_group = <gateways::Gateway as kube::Resource>::group(&());
170+
let gw_kind = <gateways::Gateway as kube::Resource>::kind(&());
171+
172+
let Some(svc_port) = &svc.port else {
173+
tracing::warn!("Skipping backend without service port");
174+
return Err(anyhow::anyhow!("Backend doesn't have port").into());
175+
};
176+
177+
let Some(svc_port_number) = get_svc_port_number(
178+
Api::namespaced(ctx.client.clone(), namespace),
179+
&svc.name,
180+
svc_port,
181+
)
182+
.await
183+
else {
184+
tracing::warn!(
185+
"skipping backend with unresolvable service port for service {}",
186+
&svc.name
187+
);
188+
return Err(
189+
anyhow::anyhow!(format!("Couldn't resolve port for a service {}", &svc.name)).into(),
190+
);
191+
};
192+
Ok(TCPRoute::new(
193+
&format!("{safe_hostname}-tcp"),
194+
TCPRouteSpec {
195+
use_default_gateways: None,
196+
rules: [TCPRouteRules {
197+
name: None,
198+
backend_refs: [TCPRouteRulesBackendRefs {
199+
name: svc.name.clone(),
200+
port: Some(svc_port_number),
201+
kind: None,
202+
group: None,
203+
namespace: None,
204+
weight: None,
205+
}]
206+
.to_vec(),
207+
}]
208+
.to_vec(),
209+
parent_refs: Some(
210+
[TCPRouteParentRefs {
211+
group: Some(gw_group.to_string()),
212+
kind: Some(gw_kind.to_string()),
213+
name: ctx.args.default_gateway_name.clone(),
214+
namespace: Some(ctx.args.default_gateway_namespace.clone()),
215+
port: Some(80),
216+
section_name: None,
217+
}]
218+
.to_vec(),
219+
),
220+
},
221+
))
222+
}
223+
159224
#[tracing::instrument(skip(ingress, ctx), fields(ingress = ingress.name_any(), namespace = ingress.namespace()), err)]
160225
pub async fn reconcile(ingress: Arc<Ingress>, ctx: Arc<ctx::Context>) -> I2GResult<Action> {
161226
tracing::info!("Reconciling Ingress");
@@ -170,12 +235,14 @@ pub async fn reconcile(ingress: Arc<Ingress>, ctx: Arc<ctx::Context>) -> I2GResu
170235
let ingress_namespace = ingress
171236
.namespace()
172237
.ok_or_else(|| anyhow::anyhow!("Ingress doesn't have a namespace"))?;
238+
let default_backend = ingress_spec.default_backend.as_ref();
173239

174240
for rule in ingress_rules {
175241
let Some(host) = &rule.host else {
176242
tracing::warn!("Skipping rule without host");
177243
continue;
178244
};
245+
179246
if let Some(http) = &rule.http {
180247
let Ok(mut route) =
181248
create_http_route(ctx.clone(), &ingress_namespace, &http, &host).await
@@ -198,8 +265,42 @@ pub async fn reconcile(ingress: Arc<Ingress>, ctx: Arc<ctx::Context>) -> I2GResu
198265
)
199266
.await?;
200267
} else {
268+
if !ctx.args.experimental {
269+
tracing::warn!(
270+
"Skipping rule non-http rule. In order to migrate it to TCPRoute, please add --experimental flag to i2g-operator."
271+
);
272+
}
201273
// In case if rule.http is None
202-
unimplemented!("Only HTTP Ingress rules are supported for now");
274+
let Some(backend) = default_backend else {
275+
tracing::warn!("Skipping non-HTTP Ingress rule without default backend");
276+
continue;
277+
};
278+
let Some(backend_svc) = &backend.service else {
279+
tracing::warn!("defaultBackend doesn't have a service, skipping.");
280+
continue;
281+
};
282+
283+
let Ok(mut route) =
284+
create_tcp_route(ctx.clone(), &ingress_namespace, backend_svc, &host).await
285+
else {
286+
tracing::warn!("Failed to create TCPRoute for host {}", host);
287+
continue;
288+
};
289+
290+
if ctx.args.link_to_ingress {
291+
route.meta_mut().add_owner(ingress.as_ref());
292+
}
293+
294+
Api::<TCPRoute>::namespaced(ctx.client.clone(), &ingress_namespace)
295+
.patch(
296+
&route.name_any(),
297+
&PatchParams {
298+
field_manager: Some("ingress-to-gateway-controller".to_string()),
299+
..PatchParams::default()
300+
},
301+
&kube::api::Patch::Apply(route),
302+
)
303+
.await?;
203304
}
204305
}
205306

src/utils.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,16 @@ impl ObjectMetaI2GExt for ObjectMeta {
3333
self.owner_references = Some(owners);
3434
}
3535
}
36+
37+
pub fn sanitize_hostname(hostname: &str) -> String {
38+
let re = regex::Regex::new("[^a-zA-Z0-9]+").unwrap();
39+
let sanitized_str = re.replace_all(hostname, "-");
40+
let res = sanitized_str
41+
.trim()
42+
.trim_start_matches("-")
43+
.trim_end_matches("-");
44+
if res.is_empty() || res == "*" {
45+
return "all-hosts".to_string();
46+
}
47+
res.to_string()
48+
}

0 commit comments

Comments
 (0)