Skip to content

Commit a99aba9

Browse files
committed
Handle targetPort services, resolve to clusterIP of service instead of DNS name
1 parent 459ffdf commit a99aba9

File tree

2 files changed

+44
-44
lines changed

2 files changed

+44
-44
lines changed

src/deployment.rs

Lines changed: 41 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -15,41 +15,47 @@ use k8s_openapi::{
1515
},
1616
apimachinery::pkg::apis::meta::v1::LabelSelector,
1717
};
18-
use kube::{api::ResourceExt, core::ObjectMeta, error::ErrorResponse, Resource};
18+
use kube::{core::ObjectMeta, error::ErrorResponse, Resource};
1919
use tracing::{info, instrument, trace};
2020

2121
const CHISEL_IMAGE: &str = "jpillora/chisel";
2222

23-
/// The function takes a ServicePort struct and returns a string representation of the port number and
24-
/// protocol (if specified).
23+
/// The function takes a ServicePort struct and returns a string representation of the target port
24+
/// and protocol (if specified).
2525
///
2626
/// Arguments:
2727
///
28-
/// * `svcport`: `svcport` is a variable of type `ServicePort`, which is likely a struct or enum that
29-
/// represents a service port in a network application. The function `convert_service_port` takes this
30-
/// `svcport` as input and returns a string representation of the port number and protocol (if
31-
/// specified).
28+
/// * `svcport`: `svcport` is a variable of type `ServicePort`, which represents a service port in
29+
/// Kubernetes. The function extracts the target port (what pods listen on) for use in chisel tunnels.
3230
///
3331
/// Returns:
3432
///
35-
/// a string that represents the service port. The string contains the port number and, if applicable,
36-
/// the protocol (TCP or UDP) in the format "port/protocol".
37-
fn convert_service_port(svcport: ServicePort) -> String {
38-
let mut port = String::new();
39-
40-
// get port number
41-
port.push_str(&svcport.port.to_string());
42-
43-
if let Some(protocol) = svcport.protocol {
44-
match protocol.as_str() {
45-
// todo: we probably want to imply none by default
46-
"TCP" => port.push_str("/tcp"),
47-
"UDP" => port.push_str("/udp"),
48-
_ => (),
49-
};
33+
/// a string that represents the target port with protocol suffix. If a numeric target_port is specified,
34+
/// it is used; otherwise falls back to the service port. Named target ports (strings) fall back to
35+
/// the service port since they cannot be resolved without pod container port information.
36+
fn get_target_port(svcport: &ServicePort) -> i32 {
37+
use k8s_openapi::apimachinery::pkg::util::intstr::IntOrString;
38+
39+
// Use numeric target_port if specified, otherwise fall back to the service port.
40+
// Named ports (strings like "web", "http") cannot be resolved here since we'd need
41+
// to look up the Pod's container ports, so we fall back to service port.
42+
match &svcport.target_port {
43+
Some(IntOrString::Int(p)) => *p,
44+
Some(IntOrString::String(_)) => svcport.port, // Can't resolve named ports
45+
None => svcport.port,
5046
}
47+
}
5148

52-
port
49+
fn get_protocol_suffix(svcport: &ServicePort) -> &'static str {
50+
svcport
51+
.protocol
52+
.as_ref()
53+
.map(|p| match p.as_str() {
54+
"TCP" => "/tcp",
55+
"UDP" => "/udp",
56+
_ => "",
57+
})
58+
.unwrap_or("")
5359
}
5460

5561
/// This function generates a remote argument string using an ExitNode's host and port information.
@@ -98,28 +104,20 @@ pub fn generate_remote_arg(node: &ExitNode) -> String {
98104
/// a `Result` containing a `Vec` of `String`s. The `Vec` contains arguments for a tunnel, which are
99105
/// generated based on the input `Service`.
100106
pub fn generate_tunnel_args(svc: &Service) -> Result<Vec<String>, ReconcileError> {
101-
// We can unwrap safely since Service is guaranteed to have a name
102-
let service_name = svc.metadata.name.clone().unwrap();
103-
// We can unwrap safely since Service is namespaced scoped
104-
let service_namespace = svc.namespace().unwrap();
105-
106-
// this feels kind of janky, will need to refactor this later
107-
108-
// check if there's a custom IP set
109-
// let target_ip = svc
110-
// .spec
111-
// .as_ref()
112-
// .map(|spec| spec.load_balancer_ip.clone())
113-
// .flatten()
114-
// .unwrap_or_else(|| "R".to_string());
115-
116107
let proxy_protocol = svc.metadata.annotations.as_ref().and_then(|annotations| {
117108
annotations
118109
.get(EXIT_NODE_PROXY_PROTOCOL_ANNOTATION)
119110
.map(String::as_ref)
120111
}) == Some("true");
121112
let target_ip = if proxy_protocol { "RP" } else { "R" };
122113

114+
// Use ClusterIP directly instead of DNS name for more reliable routing
115+
let cluster_ip = svc
116+
.spec
117+
.as_ref()
118+
.and_then(|spec| spec.cluster_ip.as_ref())
119+
.ok_or(ReconcileError::NoClusterIP)?;
120+
123121
// We can unwrap safely since Service is guaranteed to have a spec
124122
let ports = svc
125123
.spec
@@ -130,13 +128,12 @@ pub fn generate_tunnel_args(svc: &Service) -> Result<Vec<String>, ReconcileError
130128
.ok_or(ReconcileError::NoPortsSet)?
131129
.iter()
132130
.map(|p| {
131+
// The target port is what we expose externally and what the backend listens on
132+
let target_port = get_target_port(p);
133+
let protocol = get_protocol_suffix(p);
133134
format!(
134-
"{}:{}:{}.{}:{}",
135-
target_ip,
136-
p.port,
137-
service_name,
138-
service_namespace,
139-
convert_service_port(p.clone())
135+
"{}:{}:{}:{}{}",
136+
target_ip, target_port, cluster_ip, target_port, protocol
140137
)
141138
})
142139
.collect();

src/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ pub enum ReconcileError {
1414
#[error("There are no ports set on this LoadBalancer")]
1515
NoPortsSet,
1616

17+
#[error("The service does not have a ClusterIP assigned")]
18+
NoClusterIP,
19+
1720
#[error("The provided cloud provisioner was not found in the cluster")]
1821
CloudProvisionerNotFound,
1922
#[error("The secret keys for the cloud provisioner were not found in the cluster")]

0 commit comments

Comments
 (0)