'use strict' ; import { Kubectl } from '../../kubectl' ; import { host } from '../../host' ; import { findAllPods , tryFindKindNameFromEditor , FindPodsResult } from '../../extension' ; import { QuickPickOptions } from 'vscode' ; import * as portFinder from 'portfinder' ; import { succeeded } from '../../errorable' ; import * as kubectlUtils from '../../kubectlUtils' ; import * as kuberesources from '../../kuberesources' ; import * as kubernetes from '@kubernetes/client-node' ; import { ClusterExplorerResourceNode } from '../clusterexplorer/node' ; import { ExecResult } from '../../binutilplusplus' ; const PORT_FORWARD_TERMINAL = 'kubectl port-forward' ; const MAX_PORT_COUNT = 65535 ; export interface PortMapping { readonly localPort ? : number ; readonly targetPort : number ; } interface ExtractedPort { readonly name : string ; readonly port : number ; } interface ValidationResult { readonly valid : boolean ; readonly error ? : string ; } interface PodFromDocument { readonly succeeded : true ; readonly pod : string ; readonly fromOpenDocument : true ; readonly namespace ? : string ; } enum PortSpecifier { Required , AllowEmpty } type PortForwardFindPodsResult = PodFromDocument | FindPodsResult ; function isFindResultFromDocument ( obj ) { return ( obj as PodFromDocument ) . fromOpenDocument ; } param explorerNode export async function portForwardKubernetes ( kubectl , explorerNode ? ) { if ( explorerNode ) { const resourceNode = explorerNode as ClusterExplorerResourceNode ; const kind = resourceNode . kind . apiName || 'pods' ; const resourceName = resourceNode . name ; const namespace = resourceNode . namespace || await kubectlUtils . currentNamespace ( kubectl ) ; return await promptAndForwardPort ( kubectl , kind , resourceName , namespace ) ; } else { let portForwardablePods ; try { portForwardablePods = await findPortForwardablePods ( ) ; } catch ( e ) { host . showErrorMessage ( "Error while fetching pods for port-forward" ) ; throw e ; } if ( ! portForwardablePods . succeeded ) { host . showInformationMessage ( "Error while fetching pods for port-forward" ) ; } if ( isFindResultFromDocument ( portForwardablePods ) ) { const podSelection = portForwardablePods . pod ; return await promptAndForwardPort ( kubectl , 'pods' , podSelection , portForwardablePods . namespace ) ; } let podSelection ; const pods = portForwardablePods . pods ; try { const podNames = pods . map ( ( podObj ) => podObj . metadata . name ) ; podSelection = await host . showQuickPick ( podNames , { placeHolder : "Select a pod to port-forward to" } ) ; } catch ( e ) { host . showErrorMessage ( "Error while selecting pod for port-forward" ) ; throw e ; } if ( ! podSelection ) { host . showErrorMessage ( "Error while selecting pod for port-forward" ) ; return ; } const namespace = await kubectlUtils . currentNamespace ( kubectl ) ; await promptAndForwardPort ( kubectl , 'pods' , podSelection , namespace ) ; } } /**
 * Prompts the user on what port to port-forward to, and sets up the forwarding
 * if a valid input was provided.
 */ async function promptAndForwardPort ( kubectl , kind , resourceName , namespace ? ) { const portMapping = await promptForPort ( kubectl , kind , resourceName , namespace ) ; if ( portMapping . length !== 0 ) { portForwardToResource ( kubectl , kind , resourceName , portMapping , namespace ) ; } } /**
 * Given a array of containers, extract the ports to suggest to the user
 * for port forwarding.
 */ function extractContainerPorts ( containers ) { const ports = Array . of < ExtractedPort > ( ) ; containers . forEach ( ( container ) => { if ( container . ports ) { const containerPorts = container . ports . map ( ( { name , containerPort } ) => ( { name : name || `port-${ containerPort }` , port : containerPort } ) ) ; ports . push ( ... containerPorts ) ; } } ) ; return ports ; } /**
 * Given a JSON representation of a Pod, extract the ports to suggest to the user
 * for port forwarding.
 */ function extractPodPorts ( podJson ) { const pod = JSON . parse ( podJson ) as kubernetes . V1Pod ; const containers = pod . spec ? pod . spec . containers : [ ] ; return extractContainerPorts ( containers ) ; } /**
 * Given a JSON representation of a Service, extract the ports to suggest to the user
 * for port forwarding.
 */ function extractServicePorts ( serviceJson ) { const service = JSON . parse ( serviceJson ) as kubernetes . V1Service ; const k8sPorts = service . spec ? ( service . spec . ports || [ ] ) : [ ] ; return k8sPorts . map ( ( k8sport ) => ( { name : k8sport . name || `port-${ k8sport . port }` , port : k8sport . port , } ) ) ; } /**
 * Given a JSON representation of a Deployment, extract the ports to suggest to the user
 * for port forwarding.
 */ function extractDeploymentPorts ( podJson ) { const deployment = JSON . parse ( podJson ) as kubernetes . V1Deployment ; const spec = deployment . spec ? deployment . spec . template . spec : undefined ; const containers = spec ? spec . containers : [ ] ; return extractContainerPorts ( containers ) ; } returns async function promptForPort ( kubectl , kind , resourceName , namespace ? ) { let portString ; let extractedPorts = Array . of < ExtractedPort > ( ) ; if ( resourceName && kubectl ) { const ns = namespace || 'default' ; try { const result = await kubectl . invokeCommand ( `get ${ kind } ${ resourceName } --namespace ${ ns } -o json` ) ; if ( ExecResult . failed ( result ) ) { console . log ( ExecResult . failureMessage ( result , { whatFailed : 'Error getting ports' } ) ) ; } else if ( kind === kuberesources . allKinds . pod . apiName ) { extractedPorts = extractPodPorts ( result . stdout ) ; } else if ( kind === kuberesources . allKinds . service . apiName ) { extractedPorts = extractServicePorts ( result . stdout ) ; } else if ( kind === kuberesources . allKinds . deployment . apiName ) { extractedPorts = extractDeploymentPorts ( result . stdout ) ; } } catch ( err ) { console . log ( err ) ; } } let defaultValue = undefined ; if ( extractedPorts . length > 0 ) { const portPairs = extractedPorts . map ( ( { name , port } ) => `${ port }:${ name || port }` ) ; defaultValue = portPairs . join ( ' ' ) ; } try { portString = await host . showInputBox ( < QuickPickOptions > { placeHolder : 'ex: 8888:5000 8889:5001' , value : defaultValue , prompt : `Port mappings in the format LOCAL:REMOTE. Separate multiple port mappings with spaces.` , validateInput : ( portMapping ) => { const validatedPortMapping = validatePortMapping ( portMapping , extractedPorts ) ; if ( validatedPortMapping && validatedPortMapping . error ) { return validatedPortMapping . error ; } return undefined ; } } ) ; } catch ( e ) { host . showErrorMessage ( "Could not validate on input port" ) ; } if ( ! portString ) { return [ ] ; } return buildPortMapping ( portString , extractedPorts ) ; } param portMapping param validPorts returns function validatePortMapping ( portMapping , validPorts = [ ] ) { const portPairs = portMapping . split ( ' ' ) ; const validationResults = portPairs . map ( ( pair ) => validatePortPair ( validPorts , pair ) ) ; return validationResults . find ( ( result ) => ! result . valid ) ; } param validPorts param portPair returns function validatePortPair ( validPorts , portPair ) { const splitMapping = portPair . split ( ':' ) ; if ( ! portPair . includes ( ':' ) && isPortValid ( validPorts , portPair , PortSpecifier . Required ) ) { return { valid : true } ; } if ( splitMapping . length > 2 ) { return { valid : false , error : `Invalid port mapping: ${ portPair }` } ; } const localPort = splitMapping [ 0 ] ; const targetPort = splitMapping [ 1 ] ; if ( isPortValid ( validPorts , localPort , PortSpecifier . AllowEmpty ) && isPortValid ( validPorts , targetPort , PortSpecifier . Required ) ) { return { valid : true } ; } return { valid : false , error : `Invalid ports. Please enter a valid port mapping ie: 8888:5000 or 5000. Valid port range:  1 – ${ MAX_PORT_COUNT }` } ; } param validPorts param port param portSpecifier returns function isPortValid ( validPorts , port , portSpecifier ) { if ( validPorts . map ( ( { name } ) => name ) . includes ( port ) ) { return true ; } if ( portSpecifier === PortSpecifier . AllowEmpty && [ '' , '0' ] . includes ( port ) ) { return true ; } return 0 < Number ( port ) && Number ( port ) <= MAX_PORT_COUNT ; } param portString param namedPorts returns export function buildPortMapping ( portString , namedPorts = [ ] ) { const portPairs = portString . split ( ' ' ) ; return portPairs . map ( ( pair ) => buildPortPair ( namedPorts , pair ) ) ; } param validPorts param portString returns function buildPortPair ( validPorts , portPair ) { if ( ! portPair . includes ( ':' ) ) { return { targetPort : buildPort ( validPorts , portPair ) , localPort : undefined } ; } const splitString = portPair . split ( ':' ) ; const localPort = splitString [ 0 ] ; const targetPort = splitString [ 1 ] ; return { localPort : buildNullablePort ( validPorts , localPort ) , targetPort : buildPort ( validPorts , targetPort ) } ; } param validPorts param portString returns function buildNullablePort ( validPorts , port ) { if ( [ '' , '0' ] . includes ( port ) ) { return undefined ; } return buildPort ( validPorts , port ) ; } param validPorts param portString returns function buildPort ( validPorts , port ) { const validPort = validPorts . find ( ( { name } ) => name === port ) ; if ( validPort ) { return validPort . port ; } return Number ( port ) ; } /**
 * Returns one or all available port-forwardable pods
 * Checks the open document and returns an object describing the Pod, if it can find one
 */ async function findPortForwardablePods ( ) { const kindFromEditor = tryFindKindNameFromEditor ( ) ; if ( succeeded ( kindFromEditor ) ) { if ( kindFromEditor . result . kind !== 'pods' && kindFromEditor . result . kind !== 'pod' ) { return await findAllPods ( ) ; } return { succeeded : true , pod : kindFromEditor . result . resourceName , namespace : kindFromEditor . result . namespace , fromOpenDocument : true } ; } return await findAllPods ( ) as PortForwardFindPodsResult ; } param podName param portMapping param namespace returns export async function portForwardToPod ( kubectl , podName , portMapping , namespace ? ) { return portForwardToResource ( kubectl , 'pods' , podName , portMapping , namespace ) ; } export async function portForwardToService ( kubectl , serviceName , portMapping , namespace ? ) { return portForwardToResource ( kubectl , 'services' , serviceName , portMapping , namespace ) ; } export async function portForwardToDeployment ( kubectl , name , portMapping , namespace ? ) { return portForwardToResource ( kubectl , 'deployments' , name , portMapping , namespace ) ; } async function portForwardToResource ( kubectl , kind , name , portMapping , namespace ? ) { const usedPortMappings = await Promise . all ( portMapping . map ( buildUsablePortPair ) ) ; usedPortMappings . forEach ( ( usedPortPair ) => { host . showInformationMessage ( `Forwarding from 127.0.0.1:${ usedPortPair . localPort } -> ${ kind }/${ name }:${ usedPortPair . targetPort }` ) ; } ) ; const usedNamespace = namespace || 'default' ; const portPairStrings = usedPortMappings . map ( ( usedPortPair ) => `${ usedPortPair . localPort }:${ usedPortPair . targetPort }` ) ; kubectl . invokeInNewTerminal ( `port-forward ${ kind }/${ name } ${ portPairStrings . join ( ' ' ) } -n ${ usedNamespace }` , PORT_FORWARD_TERMINAL ) ; return usedPortMappings . choose ( ( usedPortPair ) => usedPortPair . localPort ) ; } param portPair returns async function buildUsablePortPair ( portPair ) { const localPort = portPair . localPort ; const targetPort = portPair . targetPort ; let usedPort = localPort ; if ( ! localPort ) { usedPort = await portFinder . getPortPromise ( { port : 10000 } as portFinder . PortFinderOptions ) ; } return { targetPort : targetPort , localPort : usedPort } ; }