import * as vscode from "vscode" ; import { Kubectl } from "./kubectl" ; import { kubeChannel } from "./kubeChannel" ; import { sleep } from "./sleep" ; import { ObjectMeta , KubernetesCollection , DataResource , Namespace , Pod , KubernetesResource , CRD , KeyValuePairs } from './kuberesources.objectmodel' ; import { failed , Errorable } from "./errorable" ; import { ExecResult } from "./binutilplusplus" ; import { shellMessage } from "./shell" ; import { ResourceKind } from "./kuberesources" ; export interface KubectlContext { readonly clusterName : string ; readonly contextName : string ; readonly userName : string ; readonly active : boolean ; } interface Kubeconfig { readonly apiVersion : string ; readonly 'current-context' : string ; readonly clusters : { readonly name : string ; readonly cluster : { readonly server : string ; readonly 'certificate-authority' ? : string ; readonly 'certificate-authority-data' ? : string ; } ; } [ ] | undefined ; readonly contexts : { readonly name : string ; readonly context : { readonly cluster : string ; readonly user : string ; readonly namespace ? : string ; } ; } [ ] | undefined ; readonly users : { readonly name : string ; readonly user : { } ; } [ ] | undefined ; } export interface KubernetesObject { readonly name : string ; readonly metadata : ObjectMeta ; } export interface NamespaceInfo extends KubernetesObject { readonly active : boolean ; } export interface HasSelector extends KubernetesObject { readonly selector : object ; } export interface PodInfo extends KubernetesObject { readonly namespace : string ; readonly nodeName : string ; readonly status : string ; } export interface ClusterConfig { readonly server : string ; readonly certificateAuthority : string | undefined ; } export interface DataHolder { readonly metadata : ObjectMeta ; readonly data : any ; } export interface ConfigReadOptions { readonly silent ? : boolean ; } async function getKubeconfig ( kubectl , options ) { const config = await kubectl . readJSON < any > ( "config view -o json" ) ; if ( ExecResult . failed ( config ) ) { if ( options . silent ) { console . log ( ExecResult . failureMessage ( config , { } ) ) ; } else { kubectl . reportFailure ( config , { } ) ; } return null ; } return config . result ; } export async function getCurrentClusterConfig ( kubectl , options ) { const kubeConfig = await getKubeconfig ( kubectl , options ) ; if ( ! kubeConfig || ! kubeConfig . clusters || ! kubeConfig . contexts ) { return undefined ; } const contextConfig = kubeConfig . contexts . find ( ( context ) => context . name === kubeConfig [ "current-context" ] ) ! ; const clusterConfig = kubeConfig . clusters . find ( ( cluster ) => cluster . name === contextConfig . context . cluster ) ! ; return { server : clusterConfig . cluster . server , certificateAuthority : clusterConfig . cluster [ "certificate-authority" ] } ; } export async function getContexts ( kubectl , options ) { const kubectlConfig = await getKubeconfig ( kubectl , options ) ; if ( ! kubectlConfig ) { return [ ] ; } const currentContext = kubectlConfig [ "current-context" ] ; const contexts = kubectlConfig . contexts || [ ] ; return contexts . map ( ( c ) => { return { clusterName : c . context . cluster , contextName : c . name , userName : c . context . user , active : c . name === currentContext } ; } ) ; } export async function getCurrentContext ( kubectl , options ) { const contexts = await getContexts ( kubectl , options ) ; return contexts . find ( ( c ) => c . active ) ; } export async function deleteCluster ( kubectl , context ) { const deleteClusterResult = await kubectl . invokeCommandWithFeedback ( `config delete-cluster ${ context . clusterName }` , "Deleting cluster..." ) ; if ( ExecResult . failed ( deleteClusterResult ) ) { const whatFailed = `Failed to remove the underlying cluster for context ${ context . clusterName } from the kubeconfig` ; kubeChannel . showOutput ( ExecResult . failureMessage ( deleteClusterResult , { whatFailed } ) , `Delete ${ context . contextName }` ) ; if ( deleteClusterResult . resultKind === 'exec-bin-not-found' ) { kubectl . promptInstallDependencies ( deleteClusterResult , `Can't delete ${ context . contextName }): kubectl not found` ) ; return false ; } vscode . window . showWarningMessage ( `Failed to remove the underlying cluster for context ${ context . contextName }. See Output window for more details.` ) ; } const deleteUserResult = await kubectl . invokeCommandWithFeedback ( `config unset users.${ context . userName }` , "Deleting user..." ) ; if ( ExecResult . failed ( deleteUserResult ) ) { const whatFailed = `Failed to remove the underlying user for context ${ context . contextName } from the kubeconfig` ; kubeChannel . showOutput ( ExecResult . failureMessage ( deleteUserResult , { whatFailed } ) ) ; vscode . window . showWarningMessage ( `Failed to remove the underlying user for context ${ context . contextName }. See Output window for more details.` ) ; } const deleteContextResult = await kubectl . invokeCommandWithFeedback ( `config delete-context ${ context . contextName }` , "Deleting context..." ) ; if ( ExecResult . failed ( deleteContextResult ) ) { const whatFailed = `Failed to delete the specified cluster's context ${ context . contextName } from the kubeconfig` ; kubeChannel . showOutput ( ExecResult . failureMessage ( deleteContextResult , { whatFailed } ) ) ; vscode . window . showErrorMessage ( `Delete ${ context . contextName } failed. See Output window for more details.` ) ; return false ; } vscode . window . showInformationMessage ( `Deleted context '${ context . contextName }' and associated data from the kubeconfig.` ) ; return true ; } export async function getAsDataResource ( resourceId , kind , kubectl ) { const currentNS = await currentNamespace ( kubectl ) ; const dataResource = await kubectl . asJson < DataResource > ( `get ${ kind } ${ resourceId } -o json --namespace=${ currentNS }` ) ; if ( failed ( dataResource ) ) { vscode . window . showErrorMessage ( dataResource . error [ 0 ] ) ; return { data : { } , kind , metadata : { name : resourceId , namespace : currentNS } } ; } return dataResource . result ; } export async function getAsDataResources ( resource , kubectl ) { const currentNS = await currentNamespace ( kubectl ) ; const resources = await kubectl . asLines ( `get ${ resource } --show-labels --namespace=${ currentNS }` ) ; if ( failed ( resources ) ) { vscode . window . showErrorMessage ( resources . error [ 0 ] ) ; return [ ] ; } const resourcesAsDataResource = resources . result . map ( ( s ) => { const bits = s . split ( /\s+/ ) ; const labels = bits [ bits . length - 1 ] ; const pairs = labels . split ( '&' ) ; const labelsAsObj = pairs . reduce ( ( obj , data ) => { const [ k , v ] = data . split ( '=' ) ; obj [ k ] = v ; return obj ; } , { } ) ; return { data : { } , kind : resource , metadata : { name : bits [ 0 ] , namespace : currentNS , labels : labelsAsObj } } ; } ) ; return resourcesAsDataResource ; } export async function getGlobalResources ( kubectl , resource ) { const rsrcs = await kubectl . asJson < KubernetesCollection < KubernetesResource > > ( `get ${ resource } -o json` ) ; if ( failed ( rsrcs ) ) { vscode . window . showErrorMessage ( rsrcs . error [ 0 ] ) ; return [ ] ; } return rsrcs . result . items . map ( ( item ) => { return { metadata : item . metadata , kind : resource } ; } ) ; } export async function getCRDTypes ( kubectl ) { const crdTypes = await kubectl . asJson < KubernetesCollection < any > > ( `get crd -o json` ) ; if ( failed ( crdTypes ) ) { vscode . window . showErrorMessage ( crdTypes . error [ 0 ] ) ; return [ ] ; } return crdTypes . result . items . map ( ( item ) => { return { metadata : item . metadata , kind : item . spec . names . kind , spec : item . spec } ; } ) ; } export async function getNamespaces ( kubectl ) { const ns = await kubectl . asJson < KubernetesCollection < Namespace > > ( "get namespaces -o json" ) ; if ( failed ( ns ) ) { vscode . window . showErrorMessage ( ns . error [ 0 ] ) ; return [ ] ; } const currentNS = await currentNamespace ( kubectl ) ; return ns . result . items . map ( ( item ) => { return { name : item . metadata . name , metadata : item . metadata , active : item . metadata . name === currentNS } ; } ) ; } export async function getResourceWithSelector ( resource , kubectl ) { const currentNS = await currentNamespace ( kubectl ) ; const shellResult = await kubectl . asJson < KubernetesCollection < any > > ( `get ${ resource } -o json --namespace=${ currentNS }` ) ; if ( failed ( shellResult ) ) { vscode . window . showErrorMessage ( shellResult . error [ 0 ] ) ; return [ ] ; } return shellResult . result . items . map ( ( item ) => { return { name : item . metadata . name , metadata : item . metadata , selector : item . spec . selector } ; } ) ; } export async function getPods ( kubectl , selector , namespace = null ) { const ns = namespace || await currentNamespace ( kubectl ) ; let nsFlag = `--namespace=${ ns }` ; if ( ns === 'all' ) { nsFlag = '--all-namespaces' ; } const labels = Array . of < string > ( ) ; let matchLabelObj = selector ; if ( selector && selector . matchLabels ) { matchLabelObj = selector . matchLabels ; } if ( matchLabelObj ) { Object . keys ( matchLabelObj ) . forEach ( ( key ) => { labels . push ( `${ key }=${ matchLabelObj [ key ] }` ) ; } ) ; } let labelStr = "" ; if ( labels . length > 0 ) { labelStr = "--selector=" + labels . join ( "," ) ; } const pods = await kubectl . readTable ( `get pods -o wide ${ nsFlag } ${ labelStr }` ) ; if ( ExecResult . failed ( pods ) ) { kubectl . reportFailure ( pods , { } ) ; return [ ] ; } return pods . result . map ( ( item ) => { return { name : item . name , namespace : item . namespace || ns , nodeName : item . node , status : item . status , metadata : { name : item . name , namespace : item . namespace || ns } , } ; } ) ; } export async function currentNamespace ( kubectl ) { const kubectlConfig = await getKubeconfig ( kubectl , { silent : false } ) ; if ( ! kubectlConfig ) { return "" ; } const ctxName = kubectlConfig [ "current-context" ] ; const currentContext = ( kubectlConfig . contexts || [ ] ) . find ( ( ctx ) => ctx . name === ctxName ) ; if ( ! currentContext ) { return "" ; } return currentContext . context . namespace || "default" ; } export async function currentNamespaceArg ( kubectl ) { const ns = await currentNamespace ( kubectl ) ; if ( ns . length === 0 ) { return '' ; } return `--namespace ${ ns }` ; } export const onDidChangeNamespaceEmitter = new vscode . EventEmitter < string > ( ) ; export async function switchNamespace ( kubectl , namespace ) { const er = await kubectl . invokeCommand ( "config current-context" ) ; if ( ExecResult . failed ( er ) ) { kubeChannel . showOutput ( ExecResult . failureMessage ( er , { whatFailed : 'Cannot get the current context' } ) , `Switch namespace ${ namespace }` ) ; vscode . window . showErrorMessage ( "Switch namespace failed. See Output window for more details." ) ; return false ; } const updateResult = await kubectl . invokeCommandWithFeedback ( `config set-context ${ er . stdout . trim ( ) } --namespace="${ namespace }"` , "Switching namespace..." ) ; if ( ExecResult . failed ( updateResult ) ) { kubeChannel . showOutput ( ExecResult . failureMessage ( updateResult , { whatFailed : `Failed to switch the namespace` } ) , `Switch namespace ${ namespace }` ) ; if ( updateResult . resultKind === 'exec-bin-not-found' ) { kubectl . promptInstallDependencies ( updateResult , `Switch namespace failed: kubectl not found` ) ; } else { vscode . window . showErrorMessage ( "Switch namespace failed. See Output window for more details." ) ; } return false ; } onDidChangeNamespaceEmitter . fire ( namespace ) ; return true ; } param kubectl param deploymentName param image param exposedPorts param env return export async function runAsDeployment ( kubectl , deploymentName , image , exposedPorts , env , debugArgs ? ) { const imageName = image . split ( ":" ) [ 0 ] ; const imagePrefix = imageName . substring ( 0 , imageName . lastIndexOf ( "/" ) + 1 ) ; if ( ! deploymentName ) { const baseName = imageName . substring ( imageName . lastIndexOf ( "/" ) + 1 ) ; deploymentName = `${ baseName }-${ Date . now ( ) }` ; } const runCmd = [ "run" , deploymentName , `--image=${ image }` , imagePrefix ? "" : "--image-pull-policy=Never" , ... exposedPorts . map ( ( port ) => `--port=${ port }` ) , ... Object . keys ( env || { } ) . map ( ( key ) => `--env="${ key }=${ env [ key ] }"` ) , debugArgs ? debugArgs : "" ] ; const runResult = await kubectl . invokeCommand ( runCmd . join ( " " ) ) ; if ( ExecResult . failed ( runResult ) ) { throw new Error ( ExecResult . failureMessage ( runResult , { whatFailed : `Failed to run the image "${ image }" on Kubernetes` } ) ) ; } return deploymentName ; } param kubectl param labelQuery return export async function findPodsByLabel ( kubectl , labelQuery ) { const getResult = await kubectl . asJson < KubernetesCollection < Pod > > ( `get pods -o json -l ${ labelQuery }` ) ; if ( failed ( getResult ) ) { throw new Error ( 'Kubectl command failed: ' + getResult . error [ 0 ] ) ; } return getResult . result ; } param kubectl param podName export async function waitForRunningPod ( kubectl , podName ) { while ( true ) { const er = await kubectl . invokeCommand ( `get pod/${ podName } --no-headers` ) ; if ( ExecResult . failed ( er ) ) { throw new Error ( ExecResult . failureMessage ( er , { whatFailed : 'Failed to get pod status' } ) ) ; } const status = er . stdout . split ( /\s+/ ) [ 2 ] ; kubeChannel . showOutput ( `pod/${ podName } status: ${ status }` ) ; if ( status === "Running" ) { return ; } else if ( ! isTransientPodState ( status ) ) { const logsResult = await kubectl . invokeCommand ( `logs pod/${ podName }` ) ; kubeChannel . showOutput ( `Failed to start the pod "${ podName }". Its status is "${ status }".
                Pod logs:\n${ shellMessage ( logsResult , "Unable to retrieve logs" ) }` ) ; throw new Error ( `Failed to start the pod "${ podName }". Its status is "${ status }".` ) ; } await sleep ( 1000 ) ; } } function isTransientPodState ( status ) { return status === "ContainerCreating" || status === "Pending" || status === "Succeeded" ; } param kubectl param resourceId return export async function getResourceAsJson < T extends KubernetesResource | KubernetesCollection < KubernetesResource > > ( kubectl , resourceId , resourceNamespace ? , silent ? ) { const nsarg = resourceNamespace ? `--namespace ${ resourceNamespace }` : '' ; const shellResult = await kubectl . asJson < T > ( `get ${ resourceId } ${ nsarg } -o json` ) ; if ( failed ( shellResult ) ) { if ( ! silent ) { vscode . window . showErrorMessage ( shellResult . error [ 0 ] ) ; } return undefined ; } return shellResult . result ; } export async function createResourceFromUri ( uri , kubectl ) { await changeResourceFromUri ( uri , kubectl , 'create' , 'creating' , 'created' ) ; } export async function deleteResourceFromUri ( uri , kubectl ) { const result = await vscode . window . showWarningMessage ( 'Are you sure you want to delete this resource?' , 'Delete' , 'Cancel' ) ; if ( result === 'Delete' ) { await changeResourceFromUri ( uri , kubectl , 'delete' , 'deleting' , 'deleted' ) ; } } export async function applyResourceFromUri ( uri , kubectl ) { await changeResourceFromUri ( uri , kubectl , 'apply' , 'applying' , 'applied' ) ; } async function changeResourceFromUri ( uri , kubectl , command , verbParticiple , verbPast ) { if ( uri . scheme !== 'file' ) { vscode . window . showErrorMessage ( `${ uri . toString ( ) } is not a file path.` ) ; return ; } const path = vscode . workspace . asRelativePath ( uri ) ; const result = await kubectl . invokeCommand ( `${ command } -f "${ path }"` ) ; if ( ExecResult . failed ( result ) ) { kubectl . reportFailure ( result , { whatFailed : `Error ${ verbParticiple } resource` } ) ; } else { vscode . window . showInformationMessage ( `Resource ${ path } ${ verbPast }.` ) ; } } export async function namespaceResources ( kubectl , ns ) { const arresult = await kubectl . readTable ( 'api-resources -o wide' ) ; if ( ExecResult . failed ( arresult ) ) { return { succeeded : false , error : [ ExecResult . failureMessage ( arresult , { } ) ] } ; } const resourceKinds = arresult . result . filter ( ( r ) => r . namespaced === 'true' ) . filter ( ( r ) => r . verbs . includes ( 'list' ) ) . map ( ( r ) => r . name ) ; const resourceKindsList = resourceKinds . join ( ',' ) ; const getresult = await kubectl . invokeCommand ( `get ${ resourceKindsList } -o name --namespace ${ ns } --ignore-not-found` ) ; return ExecResult . tryMap < string [ ] > ( getresult , ( text ) => text . split ( '\n' ) . map ( ( s ) => s . trim ( ) ) . filter ( ( s ) => s . length > 0 ) ) ; } export async function allResourceKinds ( kubectl , verbs ) { const arresult = await kubectl . readTable ( 'api-resources -o wide' ) ; if ( ExecResult . failed ( arresult ) ) { return { succeeded : false , error : [ ExecResult . failureMessage ( arresult , { } ) ] } ; } const resourceKinds = arresult . result . filter ( ( r ) => verbs . every ( ( verb ) => r . verbs . includes ( verb ) ) ) . map ( ( r ) => new ResourceKind ( r . name , r . name , r . kind , r . name ) ) ; return { succeeded : true , result : resourceKinds } ; } export async function getResourceVersion ( kubectl , resource ) { const documentation = await kubectl . asLines ( ` explain ${ resource }` ) ; if ( failed ( documentation ) ) { return undefined ; } const rgx = new RegExp ( '(?<=VERSION:\\s*)(\\S)+.*' ) ; for ( const line of documentation . result ) { const match = line . match ( rgx ) ; if ( match ) { return match [ 0 ] ; } } return undefined ; }