@@ -2,15 +2,18 @@ import WebSocket from 'isomorphic-ws';
22import querystring from 'node:querystring' ;
33import stream from 'node:stream' ;
44
5+ import { AppsV1Api , CoreV1Api , V1Pod } from './gen/index.js' ;
56import { KubeConfig } from './config.js' ;
67import { WebSocketHandler , WebSocketInterface } from './web-socket-handler.js' ;
78
89export class PortForward {
10+ private readonly config : KubeConfig ;
911 private readonly handler : WebSocketInterface ;
1012 private readonly disconnectOnErr : boolean ;
1113
1214 // handler is a parameter really only for injecting for testing.
1315 constructor ( config : KubeConfig , disconnectOnErr ?: boolean , handler ?: WebSocketInterface ) {
16+ this . config = config ;
1417 this . handler = handler || new WebSocketHandler ( config ) ;
1518 this . disconnectOnErr = disconnectOnErr === undefined ? true : disconnectOnErr ;
1619 }
@@ -70,4 +73,128 @@ export class PortForward {
7073
7174 return WebSocketHandler . restartableHandleStandardInput ( createWebSocket , input , 0 , retryCount ) ;
7275 }
76+
77+ /**
78+ * Port forward to a service by resolving to the first ready pod selected by the service's selector.
79+ *
80+ * @param namespace - The namespace of the service
81+ * @param serviceName - The name of the service
82+ * @param targetPorts - The target ports to forward to
83+ * @param output - The writable stream for output
84+ * @param err - The writable stream for error output (can be null)
85+ * @param input - The readable stream for input
86+ * @param retryCount - The number of times to retry the connection
87+ * @throws Will throw an error if the service is not found or has no ready pods
88+ */
89+ public async portForwardService (
90+ namespace : string ,
91+ serviceName : string ,
92+ targetPorts : number [ ] ,
93+ output : stream . Writable ,
94+ err : stream . Writable | null ,
95+ input : stream . Readable ,
96+ retryCount : number = 0 ,
97+ ) : Promise < WebSocket . WebSocket | ( ( ) => WebSocket . WebSocket | null ) > {
98+ const coreApi = this . config . makeApiClient ( CoreV1Api ) ;
99+ const service = await coreApi . readNamespacedService ( { name : serviceName , namespace } ) ;
100+
101+ if ( ! service . spec ?. selector || Object . keys ( service . spec . selector ) . length === 0 ) {
102+ throw new Error ( `Service ${ namespace } /${ serviceName } has no selector defined` ) ;
103+ }
104+
105+ const labelSelector = this . buildLabelSelector ( service . spec . selector ) ;
106+ const pod = await this . getFirstReadyPod ( namespace , labelSelector ) ;
107+
108+ return this . portForward ( namespace , pod . metadata ! . name ! , targetPorts , output , err , input , retryCount ) ;
109+ }
110+
111+ /**
112+ * Port forward to a deployment by resolving to the first ready pod selected by the deployment's selector.
113+ *
114+ * @param namespace - The namespace of the deployment
115+ * @param deploymentName - The name of the deployment
116+ * @param targetPorts - The target ports to forward to
117+ * @param output - The writable stream for output
118+ * @param err - The writable stream for error output (can be null)
119+ * @param input - The readable stream for input
120+ * @param retryCount - The number of times to retry the connection
121+ * @throws Will throw an error if the deployment is not found or has no ready pods
122+ */
123+ public async portForwardDeployment (
124+ namespace : string ,
125+ deploymentName : string ,
126+ targetPorts : number [ ] ,
127+ output : stream . Writable ,
128+ err : stream . Writable | null ,
129+ input : stream . Readable ,
130+ retryCount : number = 0 ,
131+ ) : Promise < WebSocket . WebSocket | ( ( ) => WebSocket . WebSocket | null ) > {
132+ const appsApi = this . config . makeApiClient ( AppsV1Api ) ;
133+ const deployment = await appsApi . readNamespacedDeployment ( { name : deploymentName , namespace } ) ;
134+
135+ if (
136+ ! deployment . spec ?. selector ?. matchLabels ||
137+ Object . keys ( deployment . spec . selector . matchLabels ) . length === 0
138+ ) {
139+ throw new Error ( `Deployment ${ namespace } /${ deploymentName } has no selector defined` ) ;
140+ }
141+
142+ const labelSelector = this . buildLabelSelector ( deployment . spec . selector . matchLabels ) ;
143+ const pod = await this . getFirstReadyPod ( namespace , labelSelector ) ;
144+
145+ return this . portForward ( namespace , pod . metadata ! . name ! , targetPorts , output , err , input , retryCount ) ;
146+ }
147+
148+ /**
149+ * Get the first ready pod matching the label selector.
150+ *
151+ * @param namespace - The namespace to query
152+ * @param labelSelector - The label selector to filter pods
153+ * @returns The first ready pod
154+ * @throws Will throw an error if no ready pods are found
155+ */
156+ private async getFirstReadyPod ( namespace : string , labelSelector : string ) : Promise < V1Pod > {
157+ const coreApi = this . config . makeApiClient ( CoreV1Api ) ;
158+ const podList = await coreApi . listNamespacedPod ( { namespace, labelSelector } ) ;
159+
160+ if ( ! podList . items || podList . items . length === 0 ) {
161+ throw new Error ( `No pods found with selector "${ labelSelector } " in namespace ${ namespace } ` ) ;
162+ }
163+
164+ // Find the first pod with Ready status
165+ for ( const pod of podList . items ) {
166+ if ( this . isPodReady ( pod ) ) {
167+ return pod ;
168+ }
169+ }
170+
171+ throw new Error ( `No ready pods found with selector "${ labelSelector } " in namespace ${ namespace } ` ) ;
172+ }
173+
174+ /**
175+ * Check if a pod is ready by looking at its status conditions.
176+ *
177+ * @param pod - The pod to check
178+ * @returns True if the pod has a Ready condition with status True
179+ */
180+ private isPodReady ( pod : V1Pod ) : boolean {
181+ if ( ! pod . status ?. conditions ) {
182+ return false ;
183+ }
184+ return pod . status . conditions . some (
185+ ( condition ) => condition . type === 'Ready' && condition . status === 'True' ,
186+ ) ;
187+ }
188+
189+ /**
190+ * Build a Kubernetes label selector string from a label object.
191+ *
192+ * @param labels - An object of label key-value pairs
193+ * @returns A Kubernetes label selector string
194+ */
195+ private buildLabelSelector ( labels : { [ key : string ] : string } ) : string {
196+ return Object . entries ( labels )
197+ . map ( ( [ key , value ] ) => `${ key } =${ value } ` )
198+ . join ( ',' ) ;
199+ }
73200}
0 commit comments