11import { readableStreamFromReader , TextLineStream } from '../deps.ts' ;
2- import { RestClient , RequestOptions , JSONValue } from '../lib/contract.ts' ;
2+ import { RestClient , RequestOptions , JSONValue , KubernetesTunnel } from '../lib/contract.ts' ;
33import { JsonParsingTransformer } from '../lib/stream-transformers.ts' ;
44
55const isVerbose = Deno . args . includes ( '--verbose' ) ;
@@ -32,6 +32,7 @@ export class KubectlRawRestClient implements RestClient {
3232 bodyRaw ?: Uint8Array ;
3333 bodyJson ?: JSONValue ;
3434 bodyStream ?: ReadableStream < Uint8Array > ;
35+ bodyPassthru ?: boolean ;
3536 } ) {
3637
3738 const hasReqBody = opts . bodyJson !== undefined || ! ! opts . bodyRaw || ! ! opts . bodyStream ;
@@ -41,14 +42,13 @@ export class KubectlRawRestClient implements RestClient {
4142 '--context' , this . contextName ,
4243 ] : [ ] ;
4344
44- const kubectl = new Deno . Command ( 'kubectl' , {
45+ const p = new Deno . Command ( 'kubectl' , {
4546 args : [ ...ctxArgs , ...args ] ,
46- stdin : hasReqBody ? 'piped' : 'null' ,
47+ stdin : ( hasReqBody || opts . bodyPassthru ) ? 'piped' : 'null' ,
4748 stdout : 'piped' ,
4849 stderr : 'inherit' ,
4950 signal : opts . abortSignal ,
50- } ) ;
51- const p = kubectl . spawn ( ) ;
51+ } ) . spawn ( ) ;
5252
5353 if ( hasReqBody ) {
5454 if ( opts . bodyStream ) {
@@ -82,6 +82,21 @@ export class KubectlRawRestClient implements RestClient {
8282
8383 if ( opts . abortSignal ?. aborted ) throw new Error ( `Given AbortSignal is already aborted` ) ;
8484
85+ if ( opts . expectTunnel ) {
86+ if ( opts . expectTunnel . includes ( 'v4.channel.k8s.io' ) ) {
87+ // We can implement PodExec with `kubectl exec`, for this specific route:
88+ const match = new URLPattern ( {
89+ pathname : '/api/:version/namespaces/:namespace/pods/:podName/exec' ,
90+ } ) . exec ( { pathname : opts . path } ) ;
91+ if ( match ) {
92+ const { namespace, podName } = match . pathname . groups ;
93+ return await this . emulateExecTunnel ( namespace ! , podName ! , opts . querystring ?? new URLSearchParams ( ) , opts . abortSignal ) ;
94+ }
95+ }
96+ if ( opts . expectTunnel ) throw new Error (
97+ `That socket-based API (via ${ opts . expectTunnel [ 0 ] } ) is not implemented by this client.` ) ;
98+ }
99+
85100 let path = opts . path || '/' ;
86101 const query = opts . querystring ?. toString ( ) ?? '' ;
87102 if ( query ) {
@@ -91,9 +106,6 @@ export class KubectlRawRestClient implements RestClient {
91106 const hasReqBody = opts . bodyJson !== undefined || ! ! opts . bodyRaw || ! ! opts . bodyStream ;
92107 isVerbose && console . error ( opts . method , path , hasReqBody ? '(w/ body)' : '' ) ;
93108
94- if ( opts . expectTunnel ) throw new Error (
95- `Channel-based APIs are not currently implemented by this client.` ) ;
96-
97109 let rawArgs = [ command , ...( hasReqBody ? [ '-f' , '-' ] : [ ] ) , "--raw" , path ] ;
98110
99111 if ( command === 'patch' ) {
@@ -139,6 +151,68 @@ export class KubectlRawRestClient implements RestClient {
139151 }
140152 }
141153
154+ private async emulateExecTunnel ( namespace : string , podName : string , querystring : URLSearchParams , signal ?: AbortSignal ) : Promise < KubernetesTunnel > {
155+ const wantsStdin = querystring . get ( 'stdin' ) == '1' ;
156+ const wantsTty = querystring . get ( 'tty' ) == '1' ;
157+ const wantsContainer = querystring . get ( 'container' ) ;
158+
159+ // upstream feature request: https://github.com/denoland/deno/issues/3994
160+ if ( wantsTty ) throw new Error (
161+ `This Kubernetes client (${ this . constructor . name } does not support opening TTYs. Try a Kubeconfig-based client if you need TTY.` ) ;
162+
163+ const [ p , status ] = await this . runKubectl ( [
164+ 'exec' ,
165+ ...( querystring . get ( 'stdin' ) == '1' ? [ '--stdin' ] : [ ] ) ,
166+ ...( querystring . get ( 'tty' ) == '1' ? [ '--tty' ] : [ ] ) ,
167+ ...( namespace ? [ '-n' , namespace ] : [ ] ) ,
168+ `--quiet` ,
169+ podName ,
170+ ...( wantsContainer ? [ '-c' , wantsContainer ] : [ ] ) ,
171+ `--` , // disable non-positional arguments after here, for safety
172+ ...querystring . getAll ( 'command' ) ,
173+ ] , {
174+ abortSignal : signal ,
175+ bodyPassthru : true , // lets us use the raw streams
176+ } ) ;
177+
178+ return {
179+ transportProtocol : 'Opaque' ,
180+ subProtocol : 'v4.channel.k8s.io' ,
181+ ready : ( ) => Promise . resolve ( ) , // we don't actually know!
182+ stop : ( ) => Promise . resolve ( p . kill ( ) ) ,
183+ getChannel : ( opts ) => {
184+ if ( opts . streamIndex == 0 && wantsStdin ) {
185+ return Promise . resolve ( { writable : p . stdin } as any ) ;
186+ }
187+ if ( opts . streamIndex == 1 ) {
188+ return Promise . resolve ( { readable : p . stdout } as any ) ;
189+ }
190+ if ( opts . streamIndex == 2 ) {
191+ // We don't pipe stderr, but we don't block it either
192+ // Just provide a dummy stream for compatibility
193+ const readable = new ReadableStream ( {
194+ start ( ctlr ) { ctlr . close ( ) } ,
195+ } ) ;
196+ return Promise . resolve ( { readable } as any ) ;
197+ }
198+ if ( opts . streamIndex == 3 ) {
199+ // Invent a JSON stream and give a limited ExecStatus
200+ const readable = new ReadableStream ( {
201+ async start ( ctlr ) {
202+ const stat = await status ;
203+ ctlr . enqueue ( new TextEncoder ( ) . encode ( JSON . stringify ( {
204+ status : stat . success ? 'Success' : 'Failure' ,
205+ message : `kubectl exited with ${ stat . code } ` ,
206+ } ) ) ) ;
207+ ctlr . close ( ) ;
208+ } ,
209+ } ) ;
210+ return Promise . resolve ( { readable } as any ) ;
211+ }
212+ throw new Error ( `BUG: Unmocked stream ${ opts . streamIndex } in kubectl client!` ) ;
213+ } ,
214+ } ;
215+ }
142216}
143217
144218// `kubectl patch` doesn't have --raw so we convert the HTTP request into a non-raw `kubectl patch` command
0 commit comments