import { Terminal , Disposable } from 'vscode' ; import { ChildProcess , spawn as spawnChildProcess } from "child_process" ; import { Host , host , LongRunningUIOptions } from './host' ; import { FS } from './fs' ; import { Shell , ShellResult } from './shell' ; import * as binutil from './binutil' ; import { Errorable } from './errorable' ; import { parseLineOutput } from './outputUtils' ; import * as compatibility from './components/kubectl/compatibility' ; import { getToolPath , affectsUs , getUseWsl , KubectlVersioning } from './components/config/config' ; import { ensureSuitableKubectl } from './components/kubectl/autoversion' ; import { invokeForResult , ExternalBinary , FindBinaryStatus , ExecResult , ExecSucceeded , discardFailureInteractive , logText , parseJSON , findBinary , showErrorMessageWithInstallPrompt , parseTable , ExecBinNotFound , invokeTracking , FailedExecResult , ParsedExecResult , parseLinedText , BackgroundExecResult , invokeBackground , RunningProcess } from './binutilplusplus' ; import { updateYAMLSchema } from './yaml-support/yaml-schema' ; import { Dictionary } from './utils/dictionary' ; const KUBECTL_OUTPUT_COLUMN_SEPARATOR = /\s\s+/g ; export interface Kubectl { legacyInvokeAsync ( command , stdin ? ) : Promise < ShellResult | undefined > ; legacySpawnAsChild ( command ) : Promise < ChildProcess | undefined > ; invokeInNewTerminal ( command , terminalName , onClose ? , pipeTo ? ) : Promise < Disposable > ; invokeInSharedTerminal ( command ) : Promise < void > ; runAsTerminal ( command , terminalName ) : Promise < void > ; asLines ( command ) : Promise < Errorable < string [ ] > > ; asJson < T > ( command ) : Promise < Errorable < T > > ; ensurePresent ( options ) : Promise < boolean > ; invokeCommand ( command , stdin ? ) : Promise < ExecResult > ; invokeCommandThen < T > ( command , fn ) : Promise < T > ; observeCommand ( args ) : Promise < RunningProcess > ; spawnCommand ( args ) : Promise < BackgroundExecResult > ; invokeCommandWithFeedback ( command , uiOptions ) : Promise < ExecResult > ; invokeCommandWithFeedbackThen < T > ( command , uiOptions , fn ) : Promise < T > ; reportResult ( execResult , options ) : Promise < ExecSucceeded | undefined > ; reportFailure ( execResult , options ) : Promise < void > ; promptInstallDependencies ( execResult , message ) : Promise < void > ; parseJSON < T > ( execResult ) : Errorable < T > ; parseTable ( execResult ) : Errorable < Dictionary < string > [ ] > ; readJSON < T > ( command ) : Promise < ParsedExecResult < T > > ; readTable ( command ) : Promise < ParsedExecResult < Dictionary < string > [ ] > > ; } export interface ReportResultOptions { readonly whatFailed ? : string ; readonly updateSchemasOnSuccess ? : boolean ; } export type EnsurePresentOptions = { readonly silent ? : false ; readonly warningIfNotPresent : string ; } | { readonly silent : true ; } ; interface Context { readonly host : Host ; readonly fs : FS ; readonly shell : Shell ; readonly pathfinder : ( ( ) => Promise < string > ) | undefined ; binFound : boolean ; binPath : string ; readonly binary : ExternalBinary ; status : FindBinaryStatus | undefined ; } const KUBECTL_BINARY = { binBaseName : 'kubectl' , configKeyName : 'kubectl' , displayName : 'Kubectl' , offersInstall : true , } ; let checkedCompatibility = false ; class KubectlImpl implements Kubectl { constructor ( host , fs , shell , pathfinder , kubectlFound ) { this . context = { host : host , fs : fs , shell : shell , pathfinder : pathfinder , binFound : kubectlFound , binPath : 'kubectl' , binary : KUBECTL_BINARY , status : undefined } ; } private readonly context : Context ; private sharedTerminal : Terminal | null = null ; checkPresent ( errorMessageMode ) { return checkPresent ( this . context , errorMessageMode ) ; } legacyInvokeAsync ( command , stdin ? , callback ? ) { return invokeAsync ( this . context , command , stdin , callback ) ; } legacySpawnAsChild ( command ) { return spawnAsChild ( this . context , command ) ; } async invokeInNewTerminal ( command , terminalName , onClose ? , pipeTo ? ) { const terminal = this . context . host . createTerminal ( terminalName ) ; const disposable = onClose ? this . context . host . onDidCloseTerminal ( onClose ) : new Disposable ( ( ) => { } ) ; await invokeInTerminal ( this . context , command , pipeTo , terminal ) ; return disposable ; } invokeInSharedTerminal ( command ) { const terminal = this . getSharedTerminal ( ) ; return invokeInTerminal ( this . context , command , undefined , terminal ) ; } runAsTerminal ( command , terminalName ) { return runAsTerminal ( this . context , command , terminalName ) ; } asLines ( command ) { return asLines ( this . context , command ) ; } fromLines ( command ) { return fromLines ( this . context , command ) ; } asJson < T > ( command ) { return asJson ( this . context , command ) ; } private getSharedTerminal ( ) { if ( ! this . sharedTerminal ) { this . sharedTerminal = this . context . host . createTerminal ( 'kubectl' ) ; const disposable = this . context . host . onDidCloseTerminal ( ( terminal ) => { if ( terminal === this . sharedTerminal ) { this . sharedTerminal = null ; disposable . dispose ( ) ; } } ) ; this . context . host . onDidChangeConfiguration ( ( change ) => { if ( affectsUs ( change ) && this . sharedTerminal ) { this . sharedTerminal . dispose ( ) ; } } ) ; } return this . sharedTerminal ; } async checkPossibleIncompatibility ( afterError ) { if ( checkedCompatibility ) { return ; } if ( afterError . resultKind === 'exec-bin-not-found' ) { return ; } checkedCompatibility = true ; const kubectl = this ; async function kubectlLoadJSON ( cmd ) { const json = await kubectl . readJSON < compatibility . Version > ( cmd ) ; return ExecResult . asErrorable ( json , { } ) ; } const compat = await compatibility . check ( kubectlLoadJSON ) ; if ( ! compatibility . isGuaranteedCompatible ( compat ) && compat . didCheck ) { const versionAlert = `kubectl version ${ compat . clientVersion } may be incompatible with cluster Kubernetes version ${ compat . serverVersion }` ; this . context . host . showWarningMessage ( versionAlert ) ; } } async ensurePresent ( options ) { if ( this . context . pathfinder ) { return true ; } const status = await findBinary ( this . context ) ; if ( status . found ) { return true ; } if ( ! options . silent ) { showErrorMessageWithInstallPrompt ( this . context , status , options . warningIfNotPresent ) ; } return false ; } async invokeCommand ( command , stdin ? ) { return await invokeForResult ( this . context , command , stdin ) ; } async invokeCommandThen < T > ( command , fn ) { const er = await this . invokeCommand ( command ) ; const result = fn ( er ) ; return result ; } async observeCommand ( args ) { return await invokeTracking ( this . context , args ) ; } async spawnCommand ( args ) { return await invokeBackground ( this . context , args ) ; } async invokeCommandWithFeedback ( command , uiOptions ) { return await this . context . host . longRunning ( uiOptions , ( ) => this . invokeCommand ( command ) ) ; } async invokeCommandWithFeedbackThen < T > ( command , uiOptions , fn ) { const er = await this . context . host . longRunning ( uiOptions , ( ) => this . invokeCommand ( command ) ) ; const result = fn ( er ) ; return result ; } async reportResult ( execResult , options ) { const discardFailureOptions = { whatFailed : options . whatFailed } ; const success = await discardFailureInteractive ( this . context , execResult , discardFailureOptions ) ; if ( success ) { if ( options . updateSchemasOnSuccess ) { updateYAMLSchema ( ) ; } host . showInformationMessage ( success . stdout ) ; } else { console . log ( logText ( this . context , execResult ) ) ; this . checkPossibleIncompatibility ( execResult ) ; } return success ; } async reportFailure ( execResult , options ) { const discardFailureOptions = { whatFailed : options . whatFailed } ; await discardFailureInteractive ( this . context , execResult , discardFailureOptions ) ; console . log ( logText ( this . context , execResult ) ) ; this . checkPossibleIncompatibility ( execResult ) ; } async promptInstallDependencies ( execResult , message ) { await showErrorMessageWithInstallPrompt ( this . context , execResult . findResult , message ) ; } parseJSON < T > ( execResult ) { return parseJSON < T > ( execResult ) ; } parseTable ( execResult ) { return parseTable ( execResult , KUBECTL_OUTPUT_COLUMN_SEPARATOR ) ; } async readJSON < T > ( command ) { const er = await this . invokeCommand ( command ) ; return ExecResult . map ( er , ( s ) => JSON . parse ( s ) as T ) ; } async readTable ( command ) { const er = await this . invokeCommand ( command ) ; return ExecResult . map ( er , ( s ) => parseLinedText ( s , KUBECTL_OUTPUT_COLUMN_SEPARATOR ) ) ; } } export function create ( versioning , host , fs , shell ) { if ( versioning === KubectlVersioning . Infer ) { return createAutoVersioned ( host , fs , shell ) ; } return createSingleVersion ( host , fs , shell ) ; } function createSingleVersion ( host , fs , shell ) { return new KubectlImpl ( host , fs , shell , undefined , false ) ; } function createAutoVersioned ( host , fs , shell ) { const bootstrapper = createSingleVersion ( host , fs , shell ) ; const pathfinder = async ( ) => ( await ensureSuitableKubectl ( bootstrapper , shell , host ) ) || 'kubectl' ; return new KubectlImpl ( host , fs , shell , pathfinder , false ) ; } export function createOnBinary ( host , fs , shell , bin ) { const pathfinder = async ( ) => bin ; return new KubectlImpl ( host , fs , shell , pathfinder , false ) ; } export enum CheckPresentMessageMode { Command , Silent } async function checkPresent ( context , errorMessageMode ) { if ( context . binFound || context . pathfinder ) { return true ; } return await checkForKubectlInternal ( context , errorMessageMode ) ; } async function checkForKubectlInternal ( context , errorMessageMode ) { const binName = 'kubectl' ; const bin = getToolPath ( context . host , context . shell , binName ) ; const contextMessage = getCheckKubectlContextMessage ( errorMessageMode ) ; const inferFailedMessage = `Could not find "${ binName }" binary.${ contextMessage }` ; const configuredFileMissingMessage = `${ bin } does not exist! ${ contextMessage }` ; return await binutil . checkForBinary ( context , bin , binName , inferFailedMessage , configuredFileMissingMessage , errorMessageMode !== CheckPresentMessageMode . Silent ) ; } function getCheckKubectlContextMessage ( errorMessageMode ) { if ( errorMessageMode === CheckPresentMessageMode . Command ) { return ' Cannot execute command.' ; } return '' ; } async function invokeAsync ( context , command , stdin ? , callback ? ) { if ( await checkPresent ( context , CheckPresentMessageMode . Command ) ) { const bin = await baseKubectlPath ( context ) ; const cmd = `${ bin } ${ command }` ; let sr ; if ( stdin ) { sr = await context . shell . exec ( cmd , stdin ) ; } else { sr = await context . shell . execStreaming ( cmd , callback ) ; } if ( sr && sr . code !== 0 ) { checkPossibleIncompatibilityLegacy ( context ) ; } return sr ; } else { return { code : - 1 , stdout : '' , stderr : '' } ; } } async function checkPossibleIncompatibilityLegacy ( context ) { if ( checkedCompatibility ) { return ; } checkedCompatibility = true ; const compat = await compatibility . check ( ( cmd ) => asJson < compatibility . Version > ( context , cmd ) ) ; if ( ! compatibility . isGuaranteedCompatible ( compat ) && compat . didCheck ) { const versionAlert = `kubectl version ${ compat . clientVersion } may be incompatible with cluster Kubernetes version ${ compat . serverVersion }` ; context . host . showWarningMessage ( versionAlert ) ; } } async function spawnAsChild ( context , command ) { if ( await checkPresent ( context , CheckPresentMessageMode . Command ) ) { return spawnChildProcess ( await path ( context ) , command , context . shell . execOpts ( ) ) ; } return undefined ; } async function invokeInTerminal ( context , command , pipeTo , terminal ) { if ( await checkPresent ( context , CheckPresentMessageMode . Command ) ) { const kubectlCommand = `kubectl ${ command }` ; const fullCommand = pipeTo ? `${ kubectlCommand } | ${ pipeTo }` : kubectlCommand ; terminal . sendText ( fullCommand ) ; terminal . show ( ) ; } } async function runAsTerminal ( context , command , terminalName ) { if ( await checkPresent ( context , CheckPresentMessageMode . Command ) ) { let execPath = await path ( context ) ; const cmd = command ; if ( getUseWsl ( ) ) { cmd . unshift ( execPath ) ; execPath = 'wsl.exe' ; } const term = context . host . createTerminal ( terminalName , execPath , cmd ) ; term . show ( ) ; } } async function unquotedBaseKubectlPath ( context ) { if ( context . pathfinder ) { return await context . pathfinder ( ) ; } let bin = getToolPath ( context . host , context . shell , 'kubectl' ) ; if ( ! bin ) { bin = 'kubectl' ; } return bin ; } async function baseKubectlPath ( context ) { let bin = await unquotedBaseKubectlPath ( context ) ; if ( bin && bin . includes ( ' ' ) ) { bin = `"${ bin }"` ; } return bin ; } async function asLines ( context , command ) { const shellResult = await invokeAsync ( context , command ) ; if ( ! shellResult ) { return { succeeded : false , error : [ `Unable to run command (${ command })` ] } ; } if ( shellResult . code === 0 ) { let lines = shellResult . stdout . split ( '\n' ) ; lines . shift ( ) ; lines = lines . filter ( ( l ) => l . length > 0 ) ; return { succeeded : true , result : lines } ; } return { succeeded : false , error : [ shellResult . stderr ] } ; } async function fromLines ( context , command ) { const shellResult = await invokeAsync ( context , command ) ; if ( ! shellResult ) { return { succeeded : false , error : [ `Unable to run command (${ command })` ] } ; } if ( shellResult . code === 0 ) { let lines = shellResult . stdout . split ( '\n' ) ; lines = lines . filter ( ( l ) => l . length > 0 ) ; const parsedOutput = parseLineOutput ( lines , KUBECTL_OUTPUT_COLUMN_SEPARATOR ) ; return { succeeded : true , result : parsedOutput } ; } return { succeeded : false , error : [ shellResult . stderr ] } ; } async function asJson < T > ( context , command ) { const shellResult = await invokeAsync ( context , command ) ; if ( ! shellResult ) { return { succeeded : false , error : [ `Unable to run command (${ command })` ] } ; } if ( shellResult . code === 0 ) { return { succeeded : true , result : JSON . parse ( shellResult . stdout . trim ( ) ) as T } ; } return { succeeded : false , error : [ shellResult . stderr ] } ; } async function path ( context ) { const bin = await baseKubectlPath ( context ) ; return binutil . execPath ( context . shell , bin ) ; }