@@ -19,14 +19,12 @@ use anyhow::Result;
19
19
use k8s_openapi:: api:: core:: v1:: Service ;
20
20
use kube:: api:: ListParams ;
21
21
use kube:: { Api , Client } ;
22
+ use tokio:: io:: AsyncBufReadExt ;
22
23
23
- use std:: time:: Duration ;
24
-
25
- use mz_ore:: retry:: { self , RetryResult } ;
26
- use tracing:: { info, warn} ;
24
+ use tracing:: info;
27
25
28
26
use crate :: SelfManagedDebugMode ;
29
- #[ derive( Debug , Clone ) ]
27
+ #[ derive( Debug ) ]
30
28
pub struct KubectlPortForwarder {
31
29
pub namespace : String ,
32
30
pub service_name : String ,
@@ -36,89 +34,73 @@ pub struct KubectlPortForwarder {
36
34
pub context : Option < String > ,
37
35
}
38
36
39
- impl KubectlPortForwarder {
40
- /// Port forwards a given k8s service via Kubectl.
41
- /// The process will retry if the port-forwarding fails and
42
- /// will terminate once the port forwarding reaches the max number of retries.
43
- /// We retry since kubectl port-forward is flaky.
44
- pub async fn port_forward ( & self ) {
45
- if let Err ( err) = retry:: Retry :: default ( )
46
- . max_duration ( Duration :: from_secs ( 60 ) )
47
- . retry_async ( |retry_state| {
48
- let k8s_context = self . context . clone ( ) ;
49
- let namespace = self . namespace . clone ( ) ;
50
- let service_name = self . service_name . clone ( ) ;
51
- let local_address = self . local_address . clone ( ) ;
52
- let local_port = self . local_port ;
53
- let target_port = self . target_port ;
54
-
55
- info ! (
56
- "Spawning port forwarding process for {} from ports {}:{} -> {}" ,
57
- service_name, local_address, local_port, target_port
58
- ) ;
37
+ pub struct PortForwardConnection {
38
+ // tokio process that's killed on drop
39
+ pub _port_forward_process : tokio:: process:: Child ,
40
+ }
59
41
60
- async move {
61
- let port_arg_str = format ! ( "{}:{}" , & local_port, & target_port) ;
62
- let service_name_arg_str = format ! ( "services/{}" , & service_name) ;
63
- let mut args = vec ! [
64
- "port-forward" ,
65
- & service_name_arg_str,
66
- & port_arg_str,
67
- "-n" ,
68
- & namespace,
69
- "--address" ,
70
- & local_address,
71
- ] ;
72
-
73
- if let Some ( k8s_context) = & k8s_context {
74
- args. extend ( [ "--context" , k8s_context] ) ;
75
- }
42
+ impl KubectlPortForwarder {
43
+ /// Spawns a port forwarding process that resolves when
44
+ /// the port forward is established.
45
+ pub async fn spawn_port_forward ( & mut self ) -> Result < PortForwardConnection , anyhow:: Error > {
46
+ let port_arg_str = format ! ( "{}:{}" , & self . local_port, & self . target_port) ;
47
+ let service_name_arg_str = format ! ( "services/{}" , & self . service_name) ;
48
+ let mut args = vec ! [
49
+ "port-forward" ,
50
+ & service_name_arg_str,
51
+ & port_arg_str,
52
+ "-n" ,
53
+ & self . namespace,
54
+ "--address" ,
55
+ & self . local_address,
56
+ ] ;
57
+
58
+ if let Some ( k8s_context) = & self . context {
59
+ args. extend ( [ "--context" , k8s_context] ) ;
60
+ }
76
61
77
- match tokio:: process:: Command :: new ( "kubectl" )
78
- . args ( args)
79
- // Silence stdout/stderr
80
- . stdout ( std:: process:: Stdio :: null ( ) )
81
- . stderr ( std:: process:: Stdio :: null ( ) )
82
- . kill_on_drop ( true )
83
- . output ( )
84
- . await
85
- {
86
- Ok ( output) => {
87
- if !output. status . success ( ) {
88
- let retry_err_msg = format ! (
89
- "Failed to port-forward{}: {}" ,
90
- retry_state. next_backoff. map_or_else(
91
- || "" . to_string( ) ,
92
- |d| format!( ", retrying in {:?}" , d)
93
- ) ,
94
- String :: from_utf8_lossy( & output. stderr)
95
- ) ;
96
- warn ! ( "{}" , retry_err_msg) ;
97
-
98
- return RetryResult :: RetryableErr ( anyhow:: anyhow!( retry_err_msg) ) ;
99
- }
100
- }
101
- Err ( err) => {
102
- return RetryResult :: RetryableErr ( anyhow:: anyhow!(
103
- "Failed to port-forward: {}" ,
104
- err
105
- ) ) ;
62
+ let child = tokio:: process:: Command :: new ( "kubectl" )
63
+ . args ( args)
64
+ // Silence stdout
65
+ . stdout ( std:: process:: Stdio :: null ( ) )
66
+ . stderr ( std:: process:: Stdio :: piped ( ) )
67
+ . kill_on_drop ( true )
68
+ . spawn ( ) ;
69
+
70
+ if let Ok ( mut child) = child {
71
+ if let Some ( stderr) = child. stderr . take ( ) {
72
+ let stderr_reader = tokio:: io:: BufReader :: new ( stderr) ;
73
+ // Wait until we know port forwarding is established
74
+ let timeout = tokio:: time:: timeout ( std:: time:: Duration :: from_secs ( 5 ) , async {
75
+ let mut lines = stderr_reader. lines ( ) ;
76
+ while let Ok ( Some ( line) ) = lines. next_line ( ) . await {
77
+ if line. contains ( "Forwarding from" ) {
78
+ break ;
106
79
}
107
80
}
108
- // The kubectl subprocess's future will only resolve on error, thus the
109
- // code here is unreachable. We return RetryResult::Ok to satisfy
110
- // the type checker.
111
- RetryResult :: Ok ( ( ) )
81
+ } )
82
+ . await ;
83
+
84
+ if timeout. is_err ( ) {
85
+ return Err ( anyhow:: anyhow!( "Port forwarding timed out after 5 seconds" ) ) ;
112
86
}
113
- } )
114
- . await
115
- {
116
- warn ! ( "{}" , err) ;
87
+
88
+ info ! (
89
+ "Port forwarding established for {} from ports {}:{} -> {}" ,
90
+ & self . service_name, & self . local_address, & self . local_port, & self . target_port
91
+ ) ;
92
+
93
+ return Ok ( PortForwardConnection {
94
+ _port_forward_process : child,
95
+ } ) ;
96
+ }
117
97
}
98
+ Err ( anyhow:: anyhow!( "Failed to spawn port forwarding process" ) )
118
99
}
119
100
}
120
101
121
- pub async fn create_kubectl_port_forwarder (
102
+ /// Creates a port forwarder for the external pg wire port of balancerd.
103
+ pub async fn create_pg_wire_port_forwarder (
122
104
client : & Client ,
123
105
args : & SelfManagedDebugMode ,
124
106
) -> Result < KubectlPortForwarder , anyhow:: Error > {
0 commit comments