-
Notifications
You must be signed in to change notification settings - Fork 52
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[feat] Implement editor agnostic kubeconfig injection
Signed-off-by: Josh Pinkney <[email protected]>
- Loading branch information
Showing
9 changed files
with
325 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
/* | ||
* Copyright (c) 2018-2021 Red Hat, Inc. | ||
* This program and the accompanying materials are made | ||
* available under the terms of the Eclipse Public License 2.0 | ||
* which is available at https://www.eclipse.org/legal/epl-2.0/ | ||
* | ||
* SPDX-License-Identifier: EPL-2.0 | ||
* | ||
* Contributors: | ||
* Red Hat, Inc. - initial API and implementation | ||
*/ | ||
|
||
import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'; | ||
import { baseApiPath } from '../constants/config'; | ||
import { getDevWorkspaceClient } from './helper'; | ||
import { getSchema } from '../services/helpers'; | ||
import { restParams } from '../typings/models'; | ||
|
||
const tags = ['kubeconfig']; | ||
|
||
export function registerKubeConfigApi(server: FastifyInstance) { | ||
server.post( | ||
`${baseApiPath}/namespace/:namespace/devworkspaceId/:devworkspaceId/kubeconfig`, | ||
getSchema({ tags }), | ||
async function (request: FastifyRequest, reply: FastifyReply) { | ||
try { | ||
const { kubeConfigApi } = await getDevWorkspaceClient(request); | ||
const { namespace, devworkspaceId } = request.params as restParams.INamespacedPodParam; | ||
await kubeConfigApi.injectKubeConfig(namespace, devworkspaceId); | ||
reply.code(204); | ||
return reply.send(); | ||
} catch (e) { | ||
return reply.send(e); | ||
} | ||
}, | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
192 changes: 192 additions & 0 deletions
192
packages/dashboard-backend/src/devworkspace-client/services/api/kubeConfigApi.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
/* | ||
* Copyright (c) 2018-2021 Red Hat, Inc. | ||
* This program and the accompanying materials are made | ||
* available under the terms of the Eclipse Public License 2.0 | ||
* which is available at https://www.eclipse.org/legal/epl-2.0/ | ||
* | ||
* SPDX-License-Identifier: EPL-2.0 | ||
* | ||
* Contributors: | ||
* Red Hat, Inc. - initial API and implementation | ||
*/ | ||
|
||
import * as k8s from '@kubernetes/client-node'; | ||
import { IKubeConfigApi } from '../../types'; | ||
import { StdStream } from '../helpers/stream'; | ||
|
||
export class KubeConfigAPI implements IKubeConfigApi { | ||
private readonly execAPI: k8s.Exec; | ||
private readonly corev1API: k8s.CoreV1Api; | ||
private readonly kubeConfig: string; | ||
|
||
constructor(kc: k8s.KubeConfig) { | ||
this.execAPI = new k8s.Exec(kc); | ||
this.corev1API = kc.makeApiClient(k8s.CoreV1Api); | ||
this.kubeConfig = kc.exportConfig(); | ||
} | ||
|
||
/** | ||
* Inject the kubeconfig into all containers with the given name in the namespace | ||
* @param namespace The namespace where the pod lives | ||
* @param devworkspaceId The id of the devworkspace | ||
*/ | ||
async injectKubeConfig(namespace: string, devworkspaceId: string): Promise<void> { | ||
const currentPod = await this.getPodByDevWorkspaceId(namespace, devworkspaceId); | ||
const podName = currentPod.metadata?.name || ''; | ||
const currentPodContainers = currentPod.spec?.containers || []; | ||
|
||
for (const container of currentPodContainers) { | ||
const containerName = container.name; | ||
|
||
// find the directory where we should create the kubeconfig | ||
const kubeConfigDirectory = await this.resolveDirectory(podName, namespace, containerName); | ||
if (kubeConfigDirectory === '') { | ||
console.log( | ||
`Could not find appropriate kubeconfig directory for ${namespace}/${podName}/${containerName}`, | ||
); | ||
continue; | ||
} | ||
|
||
// then create the directory if it doesn't exist | ||
await this.exec(podName, namespace, containerName, [ | ||
'sh', | ||
'-c', | ||
`mkdir -p ${kubeConfigDirectory}`, | ||
]); | ||
|
||
// if -f ${kubeConfigDirectory}/config is not found then sync kubeconfig to the container | ||
await this.exec(podName, namespace, containerName, [ | ||
'sh', | ||
'-c', | ||
`[ -f ${kubeConfigDirectory}/config ] || echo '${this.kubeConfig}' > ${kubeConfigDirectory}/config`, | ||
]); | ||
} | ||
} | ||
|
||
/** | ||
* Given a namespace, find a pod that has the label controller.devfile.io/devworkspace_id=${devworkspaceId} | ||
* @param namespace The namespace to look in | ||
* @param devworkspaceId The id of the devworkspace | ||
* @returns The containers for the first pod with given devworkspaceId | ||
*/ | ||
private async getPodByDevWorkspaceId( | ||
namespace: string, | ||
devworkspaceId: string, | ||
): Promise<k8s.V1Pod> { | ||
try { | ||
const resp = await this.corev1API.listNamespacedPod( | ||
namespace, | ||
undefined, | ||
false, | ||
undefined, | ||
undefined, | ||
`controller.devfile.io/devworkspace_id=${devworkspaceId}`, | ||
); | ||
if (resp.body.items.length === 0) { | ||
throw new Error( | ||
`Could not find requested devworkspace with id ${devworkspaceId} in ${namespace}`, | ||
); | ||
} | ||
return resp.body.items[0]; | ||
} catch (e: any) { | ||
throw new Error(`Error occured when attempting to retrieve pod. ${e.message}`); | ||
} | ||
} | ||
|
||
/** | ||
* Resolve the directory where the kubeconfig is going to live. First it looks for the $KUBECONFIG env variable if | ||
* that is found then use that. If that is not found then the default directory is $HOME/.kube | ||
* @param name The name of the pod | ||
* @param namespace The namespace where the pod lives | ||
* @param containerName The name of the container to resolve the directory for | ||
* @returns A promise of the directory where the kubeconfig is going to live | ||
*/ | ||
private async resolveDirectory( | ||
name: string, | ||
namespace: string, | ||
containerName: string, | ||
): Promise<string> { | ||
// attempt to resolve the kubeconfig env variable | ||
const kubeConfigEnvResolver = await this.exec(name, namespace, containerName, [ | ||
'sh', | ||
'-c', | ||
'echo $KUBECONFIG', | ||
]); | ||
|
||
if (kubeConfigEnvResolver.stdOut !== '') { | ||
return kubeConfigEnvResolver.stdOut.replace(new RegExp('/config$'), ''); | ||
} | ||
|
||
// attempt to resolve the home directory | ||
const homeEnvResolution = await this.exec(name, namespace, containerName, [ | ||
'sh', | ||
'-c', | ||
'echo $HOME', | ||
]); | ||
|
||
if (homeEnvResolution.stdOut !== '' && homeEnvResolution.stdOut !== '/') { | ||
if (homeEnvResolution.stdOut.substr(-1) === '/') { | ||
return homeEnvResolution.stdOut + '.kube'; | ||
} else { | ||
return homeEnvResolution.stdOut + '/.kube'; | ||
} | ||
} | ||
return ''; | ||
} | ||
|
||
/** | ||
* Execute the given command inside of a given container in a pod with a name and namespace and return the | ||
* stdout and stderr responses | ||
* @param name The name of the pod | ||
* @param namespace The namespace where the pod lives | ||
* @param containerName The name of the container | ||
* @param command The command to return | ||
* @returns The strings containing the stdout and stderr of running the command in the container | ||
*/ | ||
private async exec( | ||
name: string, | ||
namespace: string, | ||
containerName: string, | ||
command: string[], | ||
): Promise<{ stdOut: string }> { | ||
try { | ||
const stdOutStream = new StdStream(); | ||
|
||
// Wait until the exec request is done and reject if the final status is a failure, otherwise | ||
// everything went OK and stdOutStream contains the response | ||
await new Promise((resolve, reject) => { | ||
this.execAPI.exec( | ||
namespace, | ||
name, | ||
containerName, | ||
command, | ||
stdOutStream, | ||
null, | ||
null, | ||
true, | ||
status => { | ||
if (status.status === 'Failure') { | ||
reject(status); | ||
} else { | ||
resolve(status); | ||
} | ||
}, | ||
); | ||
}); | ||
return { | ||
stdOut: stdOutStream.chunks, | ||
}; | ||
} catch (e: any) { | ||
// swallow this error message and log it out instead because not all containers have a shell that you can use to inject | ||
// and will fail by default | ||
console.log( | ||
`Failed trying to run command: ${command.join( | ||
' ', | ||
)} in ${namespace}/${name}/${containerName} with message: ${e.message}`, | ||
); | ||
return { | ||
stdOut: '', | ||
}; | ||
} | ||
} | ||
} |
31 changes: 31 additions & 0 deletions
31
packages/dashboard-backend/src/devworkspace-client/services/helpers/stream.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
/* | ||
* Copyright (c) 2018-2021 Red Hat, Inc. | ||
* This program and the accompanying materials are made | ||
* available under the terms of the Eclipse Public License 2.0 | ||
* which is available at https://www.eclipse.org/legal/epl-2.0/ | ||
* | ||
* SPDX-License-Identifier: EPL-2.0 | ||
* | ||
* Contributors: | ||
* Red Hat, Inc. - initial API and implementation | ||
*/ | ||
|
||
import { Writable } from 'stream'; | ||
|
||
export class StdStream extends Writable { | ||
private _chunks; | ||
|
||
constructor() { | ||
super(); | ||
this._chunks = ''; | ||
} | ||
|
||
_write(chunk: string, encoding: BufferEncoding, done: (error?: Error | null) => void): void { | ||
this._chunks += chunk.toString(); | ||
done(); | ||
} | ||
|
||
get chunks() { | ||
return this._chunks.trim(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.