Merge pull request #709 from rancher-sandbox/697-migrate-from-kim-to-nerdctl

697 migrate from kim to nerdctl
This commit is contained in:
Eric Promislow
2021-10-06 13:26:32 -07:00
committed by GitHub
24 changed files with 618 additions and 596 deletions

View File

@@ -9,12 +9,12 @@ Rancher Desktop provides the following features in the form of a desktop applica
- The version of Kubernetes you choose
- Ability to test upgrading Kubernetes to a new version and see how your workloads respond
- Build, push, and pull images (powered by [KIM])
- Run containers, and build, push, and pull images (powered by [nerdctl])
- Expose an application in Kubernetes for local access
All of this is wrapped in an open-source application.
[KIM]: https://github.com/rancher/kim
[nerdctl]: https://github.com/containerd/nerdctl
## Get The App
@@ -38,7 +38,7 @@ https://github.com/rancher-sandbox/rancher-desktop/actions/workflows/package.yam
Rancher Desktop is an Electron application with the primary business logic
written in TypeScript and JavaScript. It leverages several other pieces of
technology to provide the platform elements which include k3s, kim, kubectl,
technology to provide the platform elements which include k3s, kubectl, nerdctl
WSL, qemu, and more. The application wraps numerous pieces of technology to
provide one cohesive application.
@@ -58,9 +58,9 @@ be installed to build the source. On Windows, [Go] is also required.
#### Windows
There are two options for building from source on Windows: with a
[Development VM Setup](#development-vm-setup) or
[Manual Development Environment Setup](#manual-development-environment-setup)
There are two options for building from source on Windows: with a
[Development VM Setup](#development-vm-setup) or
[Manual Development Environment Setup](#manual-development-environment-setup)
with an existing Windows installation.
##### Development VM Setup

View File

@@ -7,7 +7,9 @@ import Electron from 'electron';
import _ from 'lodash';
import mainEvents from '@/main/mainEvents';
import { setupKim } from '@/main/kim';
import { ImageProcessor } from '@/k8s-engine/images/imageProcessor';
import { ImageProcessorName } from '@/k8s-engine/images/imageFactory';
import { setupImageProcessor } from '@/main/imageEvents';
import * as settings from '@/config/settings';
import * as window from '@/window';
import * as K8s from '@/k8s-engine/k8s';
@@ -25,8 +27,12 @@ import buildApplicationMenu from '@/main/mainmenu';
Electron.app.setName('Rancher Desktop');
const console = Logging.background;
// If/when we support more than one image processor this will be a pref with a watcher
// for changes, but it's fine as a constant now.
const ImageProviderName: ImageProcessorName = 'nerdctl';
const k8smanager = newK8sManager();
let imageProcessor: ImageProcessor;
setupPaths();
@@ -121,7 +127,7 @@ async function doFirstRun() {
if (os.platform() === 'darwin') {
await Promise.all([
linkResource('helm', true),
linkResource('kim', true),
linkResource('kim', true), // TODO: Remove when we stop shipping kim
linkResource('kubectl', true),
linkResource('nerdctl', true),
]);
@@ -181,7 +187,7 @@ async function startBackend(cfg: settings.Settings) {
await checkBackendValid();
k8smanager.start(cfg.kubernetes).catch(handleFailure);
setupKim(k8smanager);
imageProcessor = setupImageProcessor(ImageProviderName, k8smanager);
}
Electron.app.on('second-instance', async() => {
@@ -237,6 +243,26 @@ Electron.ipcMain.on('settings-read', (event) => {
event.returnValue = cfg;
});
async function relayImageProcessorNamespaces() {
try {
const namespaces = await imageProcessor.getNamespaces();
const comparator = Intl.Collator(undefined, { sensitivity: 'base' }).compare;
if (!namespaces.includes('default')) {
namespaces.push('default');
}
window.send('images-namespaces', namespaces.sort(comparator));
} catch (err) {
console.log('Error getting image namespaces:', err);
}
}
Electron.ipcMain.on('images-namespaces-read', (event) => {
if ([K8s.State.VM_STARTED, K8s.State.STARTED].includes(k8smanager.state)) {
relayImageProcessorNamespaces().catch();
}
});
// Partial<T> (https://www.typescriptlang.org/docs/handbook/utility-types.html#partialtype)
// only allows missing properties on the top level; if anything is given, then all
// properties of that top-level property must exist. RecursivePartial<T> instead
@@ -255,6 +281,12 @@ function writeSettings(arg: RecursivePartial<settings.Settings>) {
mainEvents.emit('settings-update', cfg);
Electron.ipcMain.emit('k8s-restart-required');
if (imageProcessor && imageProcessor.namespace !== cfg.images.namespace) {
imageProcessor.namespace = cfg.images.namespace;
imageProcessor.refreshImages().catch((err: Error) => {
console.log(`Error refreshing images:`, err);
});
}
}
Electron.ipcMain.handle('settings-write', (event, arg) => {
@@ -319,6 +351,7 @@ Electron.ipcMain.on('k8s-restart', async() => {
try {
switch (k8smanager.state) {
case K8s.State.STOPPED:
case K8s.State.VM_STARTED:
case K8s.State.STARTED:
// Calling start() will restart the backend, possible switching versions
// as a side-effect.
@@ -404,7 +437,7 @@ Electron.ipcMain.on('factory-reset', async() => {
await k8smanager.factoryReset();
if (os.platform() === 'darwin') {
// Unlink binaries
for (const name of ['helm', 'kim', 'kubectl']) {
for (const name of ['helm', 'kim', 'kubectl', 'nerdctl']) {
Electron.ipcMain.emit('install-set', { reply: () => { } }, name, false);
}
}
@@ -429,9 +462,9 @@ Electron.ipcMain.on('troubleshooting/show-logs', async(event) => {
console.error(`Failed to open logs: ${ error }`);
if (browserWindow) {
Electron.dialog.showMessageBox(browserWindow, options);
await Electron.dialog.showMessageBox(browserWindow, options);
} else {
Electron.dialog.showMessageBox(options);
await Electron.dialog.showMessageBox(options);
}
}
});
@@ -531,6 +564,7 @@ function newK8sManager() {
if (!cfg.kubernetes.version) {
writeSettings({ kubernetes: { version: mgr.version } });
}
relayImageProcessorNamespaces().catch();
}
});

View File

@@ -1,19 +1,22 @@
# Images
Rancher Desktop provides the ability to build, push, and pull images via the
[KIM](https://github.com/rancher/kim) project.
[NERDCTL](https://github.com/containerd/nerdctl) project.
Note, `kim` is put into the path automatically. This occurs during the
Note, `nerdctl` is put into the path automatically. This occurs during the
installer on Windows, and upon first run on macOS.
## Using KIM
## Using NERDCTL
You can learn about all of the command options by running `kim -h`. This will
display the help documentation.
You can learn about all of the command options by running `nerdctl -h`. This will
display the help documentation. The command requires Rancher Desktop to be running
for it to work.
KIM has a client side and server side component. The server side part is a
container running in Kubernetes while the client side application runs on
Mac or Windows. Images are stored in the same containerd that Kubernetes uses.
The initial set of images are stored in the same containerd that Kubernetes uses,
and are part of the `k8s.io` namespace. You can also switch to a namespace called
`default` if you wish to build or pull images into a different namespace. Currently
the only way to create other namespaces is to build or pull an image with the
`nerdctl` CLI, using the `--namespace <NAMESPACE_NAME>` option.
## Building Images
@@ -22,7 +25,7 @@ running `kim` from a directory with a `Dockerfile` where the `Dockerfile` is
using a scratch image.
```console
kim build .
nerdctl build .
[+] Building 0.1s (4/4) FINISHED
=> [internal] load build definition from Dockerfile
=> => transferring dockerfile: 31B
@@ -33,8 +36,8 @@ using a scratch image.
=> CACHED [1/1] ADD anvil-app /
```
`kim` has tags for tagging at the same time as building and other options you've
`nerdctl` has tags for tagging at the same time as building and other options you've
come to expect.
If you want to tag an existing image you've built you can use the `kim tag`
If you want to tag an existing image you've built you can use the `nerdctl tag`
command.

View File

@@ -164,10 +164,10 @@ suffix:
# Components & Pages
##############################
images:
title: Images
state:
title: Images
state:
k8sUnready: Waiting for Kubernetes to be ready
kimUnready: Waiting for image manager to be ready
imagesUnready: Waiting for image manager to be ready
unknown: 'Error: Unknown state; please reload.'
close: Close Output to Continue
manager:

View File

@@ -14,13 +14,26 @@
:table-actions="false"
:paging="true"
>
<template #header-left>
<label>Image Namespace:</label>
<select class="select-namespace" :value="selectedNamespace" @change="handleChangeNamespace($event)">
<option v-for="item in imageNamespaces" :key="item" :value="item" :selected="item === selectedNamespace">
{{ item }}
</option>
</select>
</template>
<template #header-middle>
<Checkbox
:value="showAll"
:label="t('images.manager.table.label')"
:disabled="!supportsShowAll"
@input="handleShowAllCheckbox"
/>
</template>
<!-- The SortableTable component puts the Filter box goes in the #header-right slot
Too bad, because it means we can't use a css grid to manage the relative
positions of these three widgets
-->
</SortableTable>
<Card :show-highlight-border="false" :show-actions="false">
@@ -88,11 +101,8 @@
</Card>
</div>
<div v-else>
<h3 v-if="state === 'K8S_UNREADY'">
{{ t('images.state.k8sUnready') }}
</h3>
<h3 v-else-if="state === 'KIM_UNREADY'">
{{ t('images.state.kimUnready') }}
<h3 v-if="state === 'IMAGE_MANAGER_UNREADY'">
{{ t('images.state.imagesUnready') }}
</h3>
<h3 v-else>
{{ t('images.state.unknown') }}
@@ -118,10 +128,18 @@ export default {
type: Array,
required: true,
},
imageNamespaces: {
type: Array,
required: true,
},
selectedNamespace: {
type: String,
default: 'default',
},
state: {
type: String,
default: 'K8S_UNREADY',
validator: value => ['K8S_UNREADY', 'KIM_UNREADY', 'READY'].includes(value),
default: 'IMAGE_MANAGER_UNREADY',
validator: value => ['IMAGE_MANAGER_UNREADY', 'READY'].includes(value),
},
showAll: {
type: Boolean,
@@ -168,7 +186,7 @@ export default {
},
computed: {
filteredImages() {
if (this.showAll) {
if (!this.supportsShowAll || this.showAll) {
return this.images;
}
@@ -241,17 +259,20 @@ export default {
imageManagerProcessFinishedWithFailure() {
return this.imageManagerProcessIsFinished && !this.completionStatus;
},
supportsShowAll() {
return this.selectedNamespace === 'k8s.io';
},
},
mounted() {
this.main = document.getElementsByTagName('main')[0];
ipcRenderer.on('kim-process-cancelled', (event) => {
ipcRenderer.on('images-process-cancelled', (event) => {
this.handleProcessCancelled();
});
ipcRenderer.on('kim-process-ended', (event, status) => {
ipcRenderer.on('images-process-ended', (event, status) => {
this.handleProcessEnd(status);
});
ipcRenderer.on('kim-process-output', (event, data, isStderr) => {
ipcRenderer.on('images-process-output', (event, data, isStderr) => {
this.appendImageManagerOutput(data, isStderr);
});
},
@@ -477,6 +498,9 @@ export default {
handleShowAllCheckbox(value) {
this.$emit('toggledShowAll', value);
},
handleChangeNamespace(event) {
this.$emit('switchNamespace', event.target.value);
}
},
};
</script>
@@ -512,4 +536,8 @@ export default {
.imagesTable::v-deep tr.highlightFade {
animation: highlightFade 1s;
}
.imagesTable::v-deep div.search {
margin-top: 12px;
}
</style>

View File

@@ -29,7 +29,10 @@ const defaultSettings = {
port: 6443,
},
portForwarding: { includeKubernetesServices: false },
images: { showAll: true },
images: {
showAll: true,
namespace: 'k8s.io',
},
telemetry: true,
/** Whether we should check for updates and apply them. */
updater: true,

View File

@@ -0,0 +1,26 @@
import { ImageProcessor } from '@/k8s-engine/images/imageProcessor';
import NerdctlImageProcessor from '@/k8s-engine/images/nerdctlImageProcessor';
import * as K8s from '@/k8s-engine/k8s';
/**
* An or-barred enum of valid string values for the names of supported image processors
*/
export type ImageProcessorName = 'nerdctl'; // | 'kim' has been dropped
/**
* Currently there's only one image processor.
* But at one point, when we transitioned from kim to nerdctl, there were two.
* And there might be new ones in the future, so the only changes are adding the new
* module and three lines to this file (one for the import, two for the switch stmt).
* @param processorName
* @param k8sManager
*/
export function createImageProcessor(processorName: ImageProcessorName, k8sManager: K8s.KubernetesBackend): ImageProcessor {
switch (processorName) {
case 'nerdctl':
return new NerdctlImageProcessor(k8sManager);
default:
throw new Error(`No image processor called ${ processorName }`);
}
}

View File

@@ -0,0 +1,286 @@
import { Buffer } from 'buffer';
import { ChildProcess, spawn } from 'child_process';
import { Console } from 'console';
import { EventEmitter } from 'events';
import os from 'os';
import timers from 'timers';
import * as K8s from '@/k8s-engine/k8s';
import mainEvents from '@/main/mainEvents';
import Logging from '@/utils/logging';
import LimaBackend from '@/k8s-engine/lima';
const REFRESH_INTERVAL = 5 * 1000;
const APP_NAME = 'rancher-desktop';
const console = new Console(Logging.images.stream);
/**
* The fields that cover the results of a finished process.
* Not all fields are set for every process.
*/
export interface childResultType {
stdout: string;
stderr: string;
code: number;
signal?: string;
}
/**
* The fields for display in the images table
*/
export interface imageType {
imageName: string,
tag: string,
imageID: string,
size: string,
}
/**
* Define all methods common to all ImageProcessor subclasses here.
* Abstract methods need to be implemented in concrete subclasses.
*/
export abstract class ImageProcessor extends EventEmitter {
protected k8sManager: K8s.KubernetesBackend|null;
// Sometimes the `images` subcommand repeatedly fires the same error message.
// Instead of logging it every time, keep track of the current error and give a count instead.
private lastErrorMessage = '';
private sameErrorMessageCount = 0;
protected showedStderr = false;
private refreshInterval: ReturnType<typeof timers.setInterval> | null = null;
protected images:imageType[] = [];
protected _isReady = false;
private isK8sReady = false;
private hasImageListeners = false;
private isWatching = false;
_refreshImages: () => Promise<void>;
protected currentNamespace = 'default';
constructor(k8sManager: K8s.KubernetesBackend) {
super();
this.k8sManager = k8sManager;
this._refreshImages = this.refreshImages.bind(this);
this.on('newListener', (event: string | symbol) => {
if (event === 'images-changed' && !this.hasImageListeners) {
this.hasImageListeners = true;
this.updateWatchStatus();
}
});
this.on('removeListener', (event: string | symbol) => {
if (event === 'images-changed' && this.hasImageListeners) {
this.hasImageListeners = this.listeners('images-changed').length > 0;
this.updateWatchStatus();
}
});
mainEvents.on('k8s-check-state', (mgr: K8s.KubernetesBackend) => {
this.isK8sReady = [K8s.State.VM_STARTED, K8s.State.STARTED].includes(mgr.state);
this.updateWatchStatus();
});
}
protected updateWatchStatus() {
const shouldWatch = this.isK8sReady && this.hasImageListeners;
if (this.isWatching === shouldWatch) {
return;
}
if (this.refreshInterval) {
timers.clearInterval(this.refreshInterval);
}
if (shouldWatch) {
this.refreshInterval = timers.setInterval(this._refreshImages, REFRESH_INTERVAL);
timers.setImmediate(this._refreshImages);
}
this.isWatching = shouldWatch;
}
/**
* Are images ready for display in the UI?
*/
get isReady() {
return this._isReady;
}
/**
* Wrapper around the trivy command to scan the specified image.
* @param taggedImageName
*/
async scanImage(taggedImageName: string): Promise<childResultType> {
return await this.runTrivyCommand(['image', '--no-progress', '--format', 'template',
'--template', '@/var/lib/trivy.tpl', taggedImageName]);
}
/**
* This method figures out which command to run for scanning, based on the platform
* and provided args.
* @param args
* @param sendNotifications
*/
async runTrivyCommand(args: string[], sendNotifications = true): Promise<childResultType> {
let child: ChildProcess;
const subcommandName = args[0];
if (os.platform().startsWith('win')) {
args = ['-d', APP_NAME, 'trivy'].concat(args);
child = spawn('wsl', args);
} else if (os.platform().startsWith('darwin') || os.platform().startsWith('linux')) {
const limaBackend = this.k8sManager as LimaBackend;
args = ['trivy'].concat(args);
child = limaBackend.limaSpawn(args);
} else {
throw new Error(`Don't know how to run trivy on platform ${ os.platform() }`);
}
return await this.processChildOutput(child, subcommandName, sendNotifications);
}
/**
* Returns the current list of cached images.
*/
listImages(): imageType[] {
return this.images;
}
/**
* Refreshes the current cache of processed iamges.
*/
async refreshImages() {
try {
const result:childResultType = await this.getImages();
if (result.stderr) {
if (!this.showedStderr) {
console.log(`${ this.processorName } images: ${ result.stderr } `);
this.showedStderr = true;
}
} else {
this.showedStderr = false;
}
this.images = this.parse(result.stdout);
if (!this._isReady) {
this._isReady = true;
this.emit('readiness-changed', true);
}
this.emit('images-changed', this.images);
} catch (err) {
if (!this.showedStderr) {
if (err.stderr && !err.stdout && !err.signal) {
console.log(err.stderr);
} else {
console.log(err);
}
}
this.showedStderr = true;
if ('code' in err && this._isReady) {
this._isReady = false;
this.emit('readiness-changed', false);
}
}
}
protected parse(data: string): imageType[] {
const results = data.trimEnd().split(/\r?\n/).slice(1).map((line) => {
const [imageName, tag, imageID, size] = line.split(/\s+/);
return {
imageName, tag, imageID, size
};
});
return results;
}
/**
* Takes the `childProcess` returned by a command like `child_process.spawn` and processes the
* output streams and exit code and signal.
*
* @param child
* @param subcommandName - used for error messages only
* @param sendNotifications
*/
async processChildOutput(child: ChildProcess, subcommandName: string, sendNotifications: boolean): Promise<childResultType> {
const result = { stdout: '', stderr: '' };
return await new Promise((resolve, reject) => {
child.stdout?.on('data', (data: Buffer) => {
const dataString = data.toString();
if (sendNotifications) {
this.emit('images-process-output', dataString, false);
}
result.stdout += dataString;
});
child.stderr?.on('data', (data: Buffer) => {
let dataString = data.toString();
if (this.processorName === 'nerdctl' && subcommandName === 'images') {
/**
* `nerdctl images` issues some dubious error messages
* (see https://github.com/containerd/nerdctl/issues/353 , logged 2021-09-10)
* Pull them out for now
*/
dataString = dataString
.replace(/time=".+?"\s+level=.+?\s+msg="failed to compute image\(s\) size"\s*/g, '')
.replace(/time=".+?"\s+level=.+?\s+msg="unparsable image name.*?sha256:[0-9a-fA-F]{64}.*?\\""\s*/g, '');
if (!dataString) {
return;
}
}
result.stderr += dataString;
if (sendNotifications) {
this.emit('images-process-output', dataString, true);
}
});
child.on('exit', (code, signal) => {
if (result.stderr) {
const timeLessMessage = result.stderr.replace(/\btime=".*?"/g, '');
if (this.lastErrorMessage !== timeLessMessage) {
this.lastErrorMessage = timeLessMessage;
this.sameErrorMessageCount = 1;
console.log(result.stderr.replace(/(?!<\r)\n/g, '\r\n'));
} else {
const m = /(Error: .*)/.exec(this.lastErrorMessage);
this.sameErrorMessageCount += 1;
console.log(`${ this.processorName } ${ subcommandName }: ${ m ? m[1] : 'same error message' } #${ this.sameErrorMessageCount }\r`);
}
}
if (code === 0) {
resolve({ ...result, code });
} else if (signal) {
reject({
...result, code: -1, signal
});
} else {
reject({ ...result, code });
}
});
});
}
get namespace() {
return this.currentNamespace;
}
set namespace(value: string) {
this.currentNamespace = value;
}
/* Subclass-specific method definitions here: */
protected abstract get processorName(): string;
abstract getNamespaces(): Promise<Array<string>>;
abstract buildImage(dirPart: string, filePart: string, taggedImageName: string): Promise<childResultType>;
abstract deleteImage(imageID: string): Promise<childResultType>;
abstract pullImage(taggedImageName: string): Promise<childResultType>;
abstract pushImage(taggedImageName: string): Promise<childResultType>;
abstract getImages(): Promise<childResultType>;
}

View File

@@ -0,0 +1,111 @@
import { spawn } from 'child_process';
import { Console } from 'console';
import path from 'path';
import Logging from '@/utils/logging';
import resources from '@/resources';
import * as imageProcessor from '@/k8s-engine/images/imageProcessor';
import * as childProcess from '@/utils/childProcess';
const console = new Console(Logging.images.stream);
export default class NerdctlImageProcessor extends imageProcessor.ImageProcessor {
protected get processorName() {
return 'nerdctl';
}
protected async runImagesCommand(args: string[], sendNotifications = true): Promise<imageProcessor.childResultType> {
const subcommandName = args[0];
const namespacedArgs = ['--namespace', this.currentNamespace].concat(args);
return await this.processChildOutput(spawn(resources.executable('nerdctl'), namespacedArgs), subcommandName, sendNotifications);
}
async buildImage(dirPart: string, filePart: string, taggedImageName: string): Promise<imageProcessor.childResultType> {
const args = ['build',
'--file', path.join(dirPart, filePart),
'--tag', taggedImageName,
dirPart];
return await this.runImagesCommand(args);
}
async deleteImage(imageID: string): Promise<imageProcessor.childResultType> {
return await this.runImagesCommand(['rmi', imageID]);
}
async pullImage(taggedImageName: string): Promise<imageProcessor.childResultType> {
return await this.runImagesCommand(['pull', taggedImageName, '--debug']);
}
async pushImage(taggedImageName: string): Promise<imageProcessor.childResultType> {
return await this.runImagesCommand(['push', taggedImageName]);
}
async getImages(): Promise<imageProcessor.childResultType> {
return await this.runImagesCommand(
['images', '--format', '{{json .}}'],
false);
}
async scanImage(taggedImageName: string): Promise<imageProcessor.childResultType> {
return await this.runTrivyCommand(['image', '--no-progress', '--format', 'template',
'--template', '@/var/lib/trivy.tpl', taggedImageName]);
}
async getNamespaces(): Promise<Array<string>> {
const { stdout, stderr } = await childProcess.spawnFile(resources.executable('nerdctl'),
['namespace', 'list', '--quiet'],
{ stdio: ['inherit', 'pipe', 'pipe'] });
if (stderr) {
console.log(`Error getting namespaces: ${ stderr }`, stderr);
}
return stdout.trim().split(/\r?\n/).map(line => line.trim()).sort();
}
/**
* Sample output (line-oriented JSON output, as opposed to one JSON document):
*
* {"CreatedAt":"2021-10-05 22:04:12 +0000 UTC","CreatedSince":"20 hours ago","ID":"171689e43026","Repository":"","Tag":"","Size":"119.2 MiB"}
* {"CreatedAt":"2021-10-05 22:04:20 +0000 UTC","CreatedSince":"20 hours ago","ID":"55fe4b211a51","Repository":"rancher/kim","Tag":"v0.1.0-beta.6","Size":"46.2 MiB"}
* ...
*/
parse(data: string): imageProcessor.imageType[] {
const images: Array<imageProcessor.imageType> = [];
const records = data.split(/\r?\n/)
.filter(line => line.trim().length > 0)
.map((line) => {
try {
return JSON.parse(line);
} catch (err) {
console.log(`Error json-parsing line [${ line }]:`, err);
return null;
}
})
.filter(record => record);
for (const record of records) {
if (['', 'sha256'].includes(record.Repository)) {
continue;
}
images.push({
imageName: record.Repository,
tag: record.Tag,
imageID: record.ID,
size: record.Size
});
}
return images.sort(imageComparator);
}
}
function imageComparator(a: imageProcessor.imageType, b: imageProcessor.imageType): number {
return a.imageName.localeCompare(b.imageName) ||
a.tag.localeCompare(b.tag) ||
a.imageID.localeCompare(b.imageID);
}

View File

@@ -10,6 +10,7 @@ export { KubeClient as Client, ServiceEntry } from './client';
export enum State {
STOPPED = 0, // The engine is not running.
STARTING, // The engine is attempting to start.
VM_STARTED, // The VM is up, but k8s hasn't started yet
STARTED, // The engine is started; the dashboard is not yet ready.
STOPPING, // The engine is attempting to stop.
ERROR, // There is an error and we cannot recover automatically.

View File

@@ -1,482 +0,0 @@
import { Buffer } from 'buffer';
import { ChildProcess, spawn } from 'child_process';
import { EventEmitter } from 'events';
import net from 'net';
import os from 'os';
import path from 'path';
import timers from 'timers';
import tls from 'tls';
import util from 'util';
import * as k8s from '@kubernetes/client-node';
import * as childProcess from '@/utils/childProcess';
import * as K8s from '@/k8s-engine/k8s';
import mainEvents from '@/main/mainEvents';
import Logging from '@/utils/logging';
import resources from '@/resources';
import LimaBackend from '@/k8s-engine/lima';
const REFRESH_INTERVAL = 5 * 1000;
const APP_NAME = 'rancher-desktop';
const KUBE_CONTEXT = 'rancher-desktop';
const console = Logging.kim;
function defined<T>(input: T | undefined | null): input is T {
return typeof input !== 'undefined' && input !== null;
}
interface childResultType {
stdout: string;
stderr: string;
code: number;
signal?: string;
}
interface imageType {
imageName: string,
tag: string,
imageID: string,
size: string,
}
interface Kim extends EventEmitter {
/**
* Emitted when the images are different. Note that we will only refresh the
* image list when listeners are registered for this event.
*/
on(event: 'images-changed', listener: (images: imageType[]) => void): this;
/**
* Emitted when command output is received.
*/
on(event: 'kim-process-output', listener: (data: string, isStderr: boolean) => void): this;
/**
* Emitted when the Kim backend readiness has changed.
*/
on(event: 'readiness-changed', listener: (isReady: boolean) => void): this;
// Inherited, for internal handling.
on(event: 'newListener', listener: (eventName: string | symbol, listener: (...args: any[]) => void) => void): this;
on(event: 'removeListener', listener: (eventName: string | symbol, listener: (...args: any[]) => void) => void): this;
}
class Kim extends EventEmitter {
private showedStderr = false;
private refreshInterval: ReturnType<typeof timers.setInterval> | null = null;
// During startup `kim images` repeatedly fires the same error message. Instead,
// keep track of the current error and give a count instead.
private lastErrorMessage = '';
private sameErrorMessageCount = 0;
private images: imageType[] = [];
private _isReady = false;
private isK8sReady = false;
private hasImageListeners = false;
private isWatching = false;
private k8sManager: K8s.KubernetesBackend|null;
constructor(k8sManager: K8s.KubernetesBackend) {
super();
this.k8sManager = k8sManager;
this._refreshImages = this.refreshImages.bind(this);
this.on('newListener', (event: string | symbol) => {
if (event === 'images-changed' && !this.hasImageListeners) {
this.hasImageListeners = true;
this.updateWatchStatus();
}
});
this.on('removeListener', (event: string | symbol) => {
if (event === 'images-changed' && this.hasImageListeners) {
this.hasImageListeners = this.listeners('images-changed').length > 0;
this.updateWatchStatus();
}
});
mainEvents.on('k8s-check-state', async(mgr: K8s.KubernetesBackend) => {
this.isK8sReady = mgr.state === K8s.State.STARTED;
this.updateWatchStatus();
if (this.isK8sReady) {
let endpoint: string | undefined;
// XXX temporary hack: use a fixed address for kim endpoint
if (mgr.backend === 'lima') {
endpoint = '127.0.0.1';
}
const needsForce = !(await this.isInstallValid(mgr, endpoint));
this.install(mgr, needsForce, endpoint);
}
});
}
protected updateWatchStatus() {
const shouldWatch = this.isK8sReady && this.hasImageListeners;
if (this.isWatching === shouldWatch) {
return;
}
if (this.refreshInterval) {
timers.clearInterval(this.refreshInterval);
}
if (shouldWatch) {
this.refreshInterval = timers.setInterval(this._refreshImages, REFRESH_INTERVAL);
timers.setImmediate(this._refreshImages);
}
this.isWatching = shouldWatch;
}
get isReady() {
return this._isReady;
}
protected async runKimCommand(args: string[], sendNotifications = true): Promise<childResultType> {
// Insert options needed for all calls to kim.
const finalArgs = ['--context', KUBE_CONTEXT].concat(args);
return await this.processChildOutput(spawn(resources.executable('kim'), finalArgs), args[0], sendNotifications);
}
async runTrivyCommand(args: string[], sendNotifications = true): Promise<childResultType> {
let child: ChildProcess;
const subcommandName = args[0];
if (os.platform().startsWith('win')) {
args = ['-d', APP_NAME, 'trivy'].concat(args);
child = spawn('wsl', args);
} else if (os.platform().startsWith('darwin') || os.platform().startsWith('linux')) {
const limaBackend = this.k8sManager as LimaBackend;
args = ['trivy'].concat(args);
child = limaBackend.limaSpawn(args);
} else {
throw new Error(`Don't know how to run trivy on platform ${ os.platform() }`);
}
return await this.processChildOutput(child, subcommandName, sendNotifications);
}
async processChildOutput(child: ChildProcess, subcommandName: string, sendNotifications: boolean): Promise<childResultType> {
const result = { stdout: '', stderr: '' };
return await new Promise((resolve, reject) => {
child.stdout?.on('data', (data: Buffer) => {
const dataString = data.toString();
if (sendNotifications) {
this.emit('kim-process-output', dataString, false);
}
result.stdout += dataString;
});
child.stderr?.on('data', (data: Buffer) => {
const dataString = data.toString();
result.stderr += dataString;
if (sendNotifications) {
this.emit('kim-process-output', dataString, true);
}
});
child.on('exit', (code, signal) => {
if (result.stderr) {
const timeLessMessage = result.stderr.replace(/\btime=".*?"/g, '');
if (this.lastErrorMessage !== timeLessMessage) {
this.lastErrorMessage = timeLessMessage;
this.sameErrorMessageCount = 1;
console.log(result.stderr.replace(/(?!<\r)\n/g, '\r\n'));
} else {
const m = /(Error: .*)/.exec(this.lastErrorMessage);
this.sameErrorMessageCount += 1;
console.log(`kim ${ subcommandName }: ${ m ? m[1] : 'same error message' } #${ this.sameErrorMessageCount }\r`);
}
}
if (code === 0) {
resolve({ ...result, code });
} else if (signal) {
reject({
...result, code: -1, signal
});
} else {
reject({ ...result, code });
}
});
});
}
/**
* Determine if the Kim service needs to be reinstalled.
*/
protected async isInstallValid(mgr: K8s.KubernetesBackend, endpoint?: string): Promise<boolean> {
const host = await mgr.ipAddress;
if (!host) {
return false;
}
const client = new k8s.KubeConfig();
client.loadFromDefault();
client.setCurrentContext(KUBE_CONTEXT);
const api = client.makeApiClient(k8s.CoreV1Api);
// Remove any stale pods; do this first, as we may end up having an invalid
// configuration but with stale pods. Note that `kim builder install --force`
// will _not_ fix any stale pods. We need to wait for the node IP to be
// correct first, though, to ensure that we don't end up with a recreated
// pod with the stale address.
await this.waitForNodeIP(api, host);
await this.removeStalePods(api);
const wantedEndpoint = endpoint || host;
// Check if the endpoint has the correct address
try {
const { body: endpointBody } = await api.readNamespacedEndpoints('builder', 'kube-image');
const subset = endpointBody.subsets?.find(subset => subset.ports?.some(port => port.name === 'kim'));
if (!(subset?.addresses || []).some(address => address.ip === wantedEndpoint)) {
console.log('Existing kim install invalid: incorrect endpoint address.');
return false;
}
} catch (ex) {
if (ex.statusCode === 404) {
console.log('Existing kim install invalid: missing endpoint');
return false;
}
console.error('Error looking for endpoints:', ex);
throw ex;
}
// Check if the certificate has the correct address
const { body: secretBody } = await api.readNamespacedSecret('kim-tls-server', 'kube-image');
const encodedCert = (secretBody.data || {})['tls.crt'];
// If we don't have a cert, that's fine — kim will fix it.
if (encodedCert) {
const cert = Buffer.from(encodedCert, 'base64');
const secureContext = tls.createSecureContext({ cert });
const socket = new tls.TLSSocket(new net.Socket(), { secureContext });
const parsedCert = socket.getCertificate();
console.log(parsedCert);
if (parsedCert && 'subjectaltname' in parsedCert) {
const { subjectaltname } = parsedCert;
const names = subjectaltname.split(',').map(s => s.trim());
const acceptable = [`IP Address:${ wantedEndpoint }`, `DNS:${ wantedEndpoint }`];
if (!names.some(name => acceptable.includes(name))) {
console.log(`Existing kim install invalid: incorrect certificate (${ subjectaltname } does not contain ${ wantedEndpoint }).`);
return false;
}
}
}
return true;
}
/**
* Wait for the Kubernetes node to have the expected IP address.
*
* When the (single-node) cluster initially starts up, the node (internal)
* address can take a while to be updated.
* @param api API to communicate with Kubernetes.
* @param hostAddr The expected node address.
*/
protected async waitForNodeIP(api: k8s.CoreV1Api, hostAddr: string) {
console.log(`Waiting for Kubernetes node IP to become ${ hostAddr }...`);
while (true) {
const { body: nodeList } = await api.listNode();
const addresses = nodeList.items
.flatMap(node => node.status?.addresses)
.filter(defined)
.filter(address => address.type === 'InternalIP')
.flatMap(address => address.address);
if (addresses.includes(hostAddr)) {
break;
}
await util.promisify(setTimeout)(1_000);
}
}
/**
* When we start the cluster, we may have leftover pods from the builder
* daemonset that have stale addresses. They will not work correctly (not
* listening on the new address), but their existence will prevent a new,
* correct pod from being created.
*
* @param api API to communicate with Kubernetes.
* @param hostAddr The expected node address.
*/
protected async removeStalePods(api: k8s.CoreV1Api) {
const { body: nodeList } = await api.listNode();
const addresses = nodeList.items
.flatMap(node => node.status?.addresses)
.filter(defined)
.filter(address => address.type === 'InternalIP')
.flatMap(address => address.address);
const { body: podList } = await api.listNamespacedPod(
'kube-image', undefined, undefined, undefined, undefined,
'app.kubernetes.io/name=kim,app.kubernetes.io/component=builder');
for (const pod of podList.items) {
const { namespace, name } = pod.metadata || {};
if (!namespace || !name) {
continue;
}
const currentAddress = pod.status?.podIP;
if (currentAddress && !addresses.includes(currentAddress)) {
console.log(`Deleting stale builder pod ${ namespace }:${ name } - pod IP ${ currentAddress } not in ${ addresses }`);
api.deleteNamespacedPod(name, namespace);
} else {
console.log(`Keeping builder pod ${ namespace }:${ name } - pod IP ${ currentAddress } in ${ addresses }`);
}
}
}
/**
* Install the kim backend if required; this returns when the backend is ready.
* @param force If true, force a reinstall of the backend.
*/
async install(backend: K8s.KubernetesBackend, force = false, address?: string) {
if (!force && await backend.isServiceReady('kube-image', 'builder')) {
console.log('Skipping kim reinstall: service is ready, and without --force');
return;
}
const startTime = Date.now();
const maxWaitTime = 120_000;
const waitTime = 3_000;
const args = ['builder', 'install'];
if (force) {
args.push('--force');
}
if (address) {
args.push('--endpoint-addr', address);
}
console.log(`Installing kim: kim ${ args.join(' ') }`);
try {
await childProcess.spawnFile(
resources.executable('kim'),
args,
{ stdio: console, windowsHide: true });
while (true) {
const currentTime = Date.now();
if ((currentTime - startTime) > maxWaitTime) {
console.log(`Waited more than ${ maxWaitTime / 1000 } secs, it might start up later`);
break;
}
if (await backend.isServiceReady('kube-image', 'builder')) {
break;
}
await util.promisify(setTimeout)(waitTime);
}
} catch (e) {
console.error(`Failed to restart the kim builder: ${ e.message }.`);
console.error('The images page will probably be empty');
}
}
async buildImage(dirPart: string, filePart: string, taggedImageName: string): Promise<childResultType> {
const args = ['build'];
args.push('--file');
args.push(path.join(dirPart, filePart));
args.push('--tag');
args.push(taggedImageName);
args.push(dirPart);
return await this.runKimCommand(args);
}
async deleteImage(imageID: string): Promise<childResultType> {
return await this.runKimCommand(['rmi', imageID]);
}
async pullImage(taggedImageName: string): Promise<childResultType> {
return await this.runKimCommand(['pull', taggedImageName, '--debug']);
}
async pushImage(taggedImageName: string): Promise<childResultType> {
return await this.runKimCommand(['push', taggedImageName, '--debug']);
}
async getImages(): Promise<childResultType> {
return await this.runKimCommand(['images', '--all'], false);
}
async scanImage(taggedImageName: string): Promise<childResultType> {
return await this.runTrivyCommand(['image', '--no-progress', '--format', 'template',
'--template', '@/var/lib/trivy.tpl', taggedImageName]);
}
parse(data: string): imageType[] {
const results = data.trimEnd().split(/\r?\n/).slice(1).map((line) => {
const [imageName, tag, imageID, size] = line.split(/\s+/);
return {
imageName, tag, imageID, size
};
});
return results;
}
listImages(): imageType[] {
return this.images;
}
async refreshImages() {
try {
const result: childResultType = await this.getImages();
if (result.stderr) {
if (!this.showedStderr) {
console.log(`kim images: ${ result.stderr } `);
this.showedStderr = true;
}
} else {
this.showedStderr = false;
}
this.images = this.parse(result.stdout);
if (!this._isReady) {
this._isReady = true;
this.emit('readiness-changed', true);
}
this.emit('images-changed', this.images);
} catch (err) {
if (!this.showedStderr) {
if (err.stderr && !err.stdout && !err.signal) {
console.log(err.stderr);
} else {
console.log(err);
}
}
this.showedStderr = true;
if ('code' in err && this._isReady) {
this._isReady = false;
this.emit('readiness-changed', false);
}
}
}
_refreshImages: () => Promise<void>;
}
export default Kim;

View File

@@ -30,13 +30,6 @@ import K3sHelper, { ShortVersion } from './k3sHelper';
import ProgressTracker from './progressTracker';
import * as K8s from './k8s';
// Helpers for setting progress
enum Progress {
INDETERMINATE = '<indeterminate>',
DONE = '<done>',
EMPTY = '<empty>',
}
/**
* Enumeration for tracking what operation the backend is undergoing.
*/
@@ -753,6 +746,7 @@ export default class LimaBackend extends events.EventEmitter implements K8s.Kube
console.debug('/etc/rancher/k3s/k3s.yaml is ready.');
}
);
this.setState(K8s.State.VM_STARTED);
await this.progressTracker.action(
'Updating kubeconfig',
50,

View File

@@ -6,6 +6,7 @@ import resources from '@/resources';
import PathConflictManager from '@/main/pathConflictManager';
import * as window from '@/window';
// TODO: Remove 'kim' when we stop shipping kim
const INTEGRATIONS = ['helm', 'kim', 'kubectl', 'nerdctl'];
const console = Logging.background;
const PUBLIC_LINK_DIR = '/usr/local/bin';

View File

@@ -1,4 +1,4 @@
// Kuberentes backend for Windows, based on WSL2 + k3s
// Kubernetes backend for Windows, based on WSL2 + k3s
import crypto from 'crypto';
import events from 'events';

View File

@@ -1,27 +1,33 @@
/**
* This module contains code for handling kim (images).
* This module contains code for handling image-processor events (nerdctl, kim).
*/
import path from 'path';
import util from 'util';
import Electron from 'electron';
import Kim from '@/k8s-engine/kim';
import { ImageProcessor } from '@/k8s-engine/images/imageProcessor';
import { createImageProcessor, ImageProcessorName } from '@/k8s-engine/images/imageFactory';
import Logging from '@/utils/logging';
import * as window from '@/window';
import * as K8s from '@/k8s-engine/k8s';
const console = Logging.kim;
let imageManager: Kim;
let imageManager: ImageProcessor;
let lastBuildDirectory = '';
let mountCount = 0;
export function setupKim(k8sManager: K8s.KubernetesBackend) {
imageManager = imageManager ?? new Kim(k8sManager);
/**
* Map image-related events to the associated image processor's methods
* @param imageProcessorName
* @param k8sManager
*/
interface KimImage {
export function setupImageProcessor(imageProcessorName: ImageProcessorName, k8sManager: K8s.KubernetesBackend): ImageProcessor {
imageManager = imageManager ?? createImageProcessor(imageProcessorName, k8sManager);
interface ImageContents {
imageName: string,
tag: string,
imageID: string,
@@ -30,11 +36,11 @@ export function setupKim(k8sManager: K8s.KubernetesBackend) {
imageManager.on('readiness-changed', (state: boolean) => {
window.send('images-check-state', state);
});
imageManager.on('kim-process-output', (data: string, isStderr: boolean) => {
window.send('kim-process-output', data, isStderr);
imageManager.on('images-process-output', (data: string, isStderr: boolean) => {
window.send('images-process-output', data, isStderr);
});
function onImagesChanged(images: KimImage[]) {
function onImagesChanged(images: ImageContents[]) {
window.send('images-changed', images);
}
Electron.ipcMain.handle('images-mounted', (_, mounted) => {
@@ -50,30 +56,15 @@ export function setupKim(k8sManager: K8s.KubernetesBackend) {
Electron.ipcMain.on('do-image-deletion', async(event, imageName, imageID) => {
try {
const maxNumAttempts = 2;
// On macOS a second attempt is needed to actually delete the image.
// Probably due to a timing issue on the server part of kim, but not determined why.
// Leave this in for windows in case it can happen there too.
let i = 0;
for (i = 0; i < maxNumAttempts; i++) {
await imageManager.deleteImage(imageID);
await imageManager.refreshImages();
if (!imageManager.listImages().some(image => image.imageID === imageID)) {
break;
}
await util.promisify(setTimeout)(500);
}
if (i === maxNumAttempts) {
console.log(`Failed to delete ${ imageID } in ${ maxNumAttempts } tries`);
}
event.reply('kim-process-ended', 0);
await imageManager.deleteImage(imageID);
await imageManager.refreshImages();
event.reply('images-process-ended', 0);
} catch (err) {
await Electron.dialog.showMessageBox({
message: `Error trying to delete image ${ imageName } (${ imageID }):\n\n ${ err.stderr } `,
type: 'error'
});
event.reply('kim-process-ended', 1);
event.reply('images-process-ended', 1);
}
});
@@ -90,13 +81,13 @@ export function setupKim(k8sManager: K8s.KubernetesBackend) {
const results = Electron.dialog.showOpenDialogSync(options);
if (results === undefined) {
event.reply('kim-process-cancelled');
event.reply('images-process-cancelled');
return;
}
if (results.length !== 1) {
console.log(`Expecting exactly one result, got ${ results.join(', ') }`);
event.reply('kim-process-cancelled');
event.reply('images-process-cancelled');
return;
}
@@ -114,7 +105,7 @@ export function setupKim(k8sManager: K8s.KubernetesBackend) {
type: 'error'
});
}
event.reply('kim-process-ended', code);
event.reply('images-process-ended', code);
});
Electron.ipcMain.on('do-image-pull', async(event, imageName) => {
@@ -134,7 +125,7 @@ export function setupKim(k8sManager: K8s.KubernetesBackend) {
type: 'error'
});
}
event.reply('kim-process-ended', code);
event.reply('images-process-ended', code);
});
Electron.ipcMain.on('do-image-scan', async(event, imageName) => {
@@ -154,7 +145,7 @@ export function setupKim(k8sManager: K8s.KubernetesBackend) {
type: 'error'
});
}
event.reply('kim-process-ended', code);
event.reply('images-process-ended', code);
});
Electron.ipcMain.on('do-image-push', async(event, imageName, imageID, tag) => {
@@ -170,10 +161,12 @@ export function setupKim(k8sManager: K8s.KubernetesBackend) {
type: 'error'
});
}
event.reply('kim-process-ended', code);
event.reply('images-process-ended', code);
});
Electron.ipcMain.handle('images-check-state', () => {
return imageManager.isReady;
});
return imageManager;
}

View File

@@ -174,11 +174,12 @@ export class Tray {
protected updateMenu() {
const labels = {
[State.STOPPED]: 'Kubernetes is stopped',
[State.STARTING]: 'Kubernetes is starting',
[State.STARTED]: 'Kubernetes is running',
[State.STOPPING]: 'Kubernetes is shutting down',
[State.ERROR]: 'Kubernetes has encountered an error',
[State.STOPPED]: 'Kubernetes is stopped',
[State.STARTING]: 'Kubernetes is starting',
[State.VM_STARTED]: 'VM is ready',
[State.STARTED]: 'Kubernetes is running',
[State.STOPPING]: 'Kubernetes is shutting down',
[State.ERROR]: 'Kubernetes has encountered an error',
};
let icon = resources.get('icons/kubernetes-icon-black.png');

View File

@@ -3,9 +3,12 @@
<Images
class="content"
:images="images"
:image-namespaces="imageNamespaces"
:state="state"
:show-all="settings.images.showAll"
:selected-namespace="settings.images.namespace"
@toggledShowAll="onShowAllImagesChanged"
@switchNamespace="onChangeNamespace"
/>
</div>
</template>
@@ -19,20 +22,21 @@ export default {
components: { Images },
data() {
return {
settings: ipcRenderer.sendSync('settings-read'),
k8sState: ipcRenderer.sendSync('k8s-state'),
kimState: false,
images: [],
settings: ipcRenderer.sendSync('settings-read'),
k8sState: ipcRenderer.sendSync('k8s-state'),
imageManagerState: false,
images: [],
imageNamespaces: [],
};
},
computed: {
state() {
if (this.k8sState !== K8s.State.STARTED) {
return 'K8S_UNREADY';
if (![K8s.State.VM_STARTED, K8s.State.STARTED].includes(this.k8sState)) {
return 'IMAGE_MANAGER_UNREADY';
}
return this.kimState ? 'READY' : 'KIM_UNREADY';
return this.imageManagerState ? 'READY' : 'IMAGE_MANAGER_UNREADY';
}
},
@@ -43,12 +47,18 @@ export default {
);
ipcRenderer.on('images-changed', (event, images) => {
this.$data.images = images;
if (this.imageNamespaces.length === 0) {
// This happens if the user clicked on the Images panel before data was ready,
// so no namespaces were available when it initially asked for them.
// When the data is ready, images are pushed in, but namespaces aren't.
ipcRenderer.send('images-namespaces-read');
}
});
ipcRenderer.on('k8s-check-state', (event, state) => {
this.$data.k8sState = state;
});
ipcRenderer.on('images-check-state', (event, state) => {
this.kimState = state;
this.imageManagerState = state;
});
ipcRenderer.on('settings-update', (event, settings) => {
// TODO: put in a status bar
@@ -58,8 +68,12 @@ export default {
this.$data.images = await ipcRenderer.invoke('images-mounted', true);
})();
(async() => {
this.$data.kimState = await ipcRenderer.invoke('images-check-state');
this.$data.imageManagerState = await ipcRenderer.invoke('images-check-state');
})();
ipcRenderer.on('images-namespaces', (event, namespaces) => {
this.$data.imageNamespaces = namespaces;
});
ipcRenderer.send('images-namespaces-read');
},
beforeDestroy() {
@@ -73,6 +87,12 @@ export default {
{ images: { showAll: value } } );
}
},
onChangeNamespace(value) {
if (value !== this.settings.images.namespace) {
ipcRenderer.invoke('settings-write',
{ images: { namespace: value } } );
}
}
}
};

View File

@@ -38,13 +38,14 @@ interface IpcMainEvents {
'update-state': () => void;
// #endregion
// #region main/kim
// #region main/imageEvents
'confirm-do-image-deletion': (imageName: string, imageID: string) => void;
'do-image-build': (taggedImageName: string) => void;
'do-image-pull': (imageName: string) => void;
'do-image-scan': (imageName: string) => void;
'do-image-push': (imageName: string, imageID: string, tag: string) => void;
'do-image-deletion': (imageName: string, imageID: string) => void;
'images-namespaces-read': () => void;
// #endregion
// #region firstrun
@@ -67,7 +68,7 @@ interface IpcMainInvokeEvents {
'get-app-version': () => string;
'show-message-box': (options: Electron.MessageBoxOptions) => Promise<Electron.MessageBoxReturnValue>;
// #region main/kim
// #region main/imageEvents
'images-mounted': (mounted: boolean) => {imageName: string, tag: string, imageID: string, size: string}[];
'images-check-state': () => boolean;
// #endregion
@@ -90,11 +91,12 @@ interface IpcRendererEvents {
'service-changed': (services: import('@/k8s-engine/k8s').ServiceEntry[]) => void;
// #region Images
'kim-process-cancelled': () => void;
'kim-process-ended': (exitCode: number) => void;
'kim-process-output': (data: string, isStdErr: boolean) => void;
'images-process-cancelled': () => void;
'images-process-ended': (exitCode: number) => void;
'images-process-output': (data: string, isStdErr: boolean) => void;
'images-changed': (images: {imageName: string, tag: string, imageID: string, size: string}[]) => void;
'images-check-state': (state: boolean) => void;
'images-namespaces': (namespaces: string[]) => void;
// #endregion
}

View File

@@ -1,14 +1,14 @@
import KimNonBuildOutputCuller from '~/utils/processOutputInterpreters/kim-non-build-output';
import KimBuildOutputCuller from '~/utils/processOutputInterpreters/kim-build-output';
import TrivyScanImageOutputCuller from '~/utils/processOutputInterpreters/trivy-image-output';
import ImageNonBuildOutputCuller from '@/utils/processOutputInterpreters/image-non-build-output';
import ImageBuildOutputCuller from '@/utils/processOutputInterpreters/image-build-output';
import TrivyScanImageOutputCuller from '@/utils/processOutputInterpreters/trivy-image-output';
const cullersByName = {
build: KimBuildOutputCuller,
build: ImageBuildOutputCuller,
'trivy-image': TrivyScanImageOutputCuller
};
export default function getImageOutputCuller(command) {
const klass = cullersByName[command] || KimNonBuildOutputCuller;
const klass = cullersByName[command] || ImageNonBuildOutputCuller;
return new klass();
}

View File

@@ -5,6 +5,7 @@ import semver from 'semver';
import * as childProcess from '@/utils/childProcess';
import resources from '@/resources';
// TODO: Remove all references to kim once we stop shipping it
const flags: Record<string, string> = {
helm: 'version',
kim: '-v',

View File

@@ -1,13 +1,13 @@
import fs from 'fs';
import path from 'path';
import KimBuildOutputCuller from '@/utils/processOutputInterpreters/kim-build-output';
import ImageBuildOutputCuller from '@/utils/processOutputInterpreters/image-build-output';
describe('kim build output', () => {
describe('image build output', () => {
it('returns the raw text back', () => {
const buildOutputPath = path.join('./src/utils/processOutputInterpreters/__tests__/assets', 'build.txt');
const data = fs.readFileSync(buildOutputPath).toString();
const culler = new KimBuildOutputCuller();
const culler = new ImageBuildOutputCuller();
culler.addData(data);
expect(culler.getProcessedData()).toBe(data.replace(/\r/g, ''));

View File

@@ -1,15 +1,15 @@
import fs from 'fs';
import path from 'path';
import KimNonBuildOutputCuller from '@/utils/processOutputInterpreters/kim-non-build-output';
import ImageNonBuildOutputCuller from '@/utils/processOutputInterpreters/image-non-build-output';
describe('simple kim output', () => {
describe('simple image output', () => {
describe('push', () => {
it('culls by SHA', () => {
const fname = path.join('./src/utils/processOutputInterpreters/__tests__/assets', 'push.txt');
const data = fs.readFileSync(fname).toString();
const lines = data.split(/(\r?\n)/);
const culler = new KimNonBuildOutputCuller();
const culler = new ImageNonBuildOutputCuller();
expect(lines.length).toBeGreaterThan(6);
culler.addData(lines.slice(0, 24).join(''));
@@ -131,7 +131,7 @@ describe('simple kim output', () => {
const fname = path.join('./src/utils/processOutputInterpreters/__tests__/assets', 'pull.txt');
const data = fs.readFileSync(fname).toString();
const lines = data.split(/(\r?\n)/);
const culler = new KimNonBuildOutputCuller();
const culler = new ImageNonBuildOutputCuller();
expect(lines.length).toBeGreaterThan(6);
culler.addData(lines.slice(0, 16).join(''));

View File

@@ -1,6 +1,6 @@
const LineSplitter = /\r?\n/;
export default class KimBuildOutputCuller {
export default class ImageBuildOutputCuller {
constructor() {
this.lines = [];
}

View File

@@ -2,7 +2,7 @@ const LineSplitter = /\r?\n/;
const ShaLineMatcher = /^[-\w]+-sha256:(\w+):\s*\w+\s*\|.*?\|/;
const SummaryLineMatcher = /^elapsed:.*total:/;
export default class KimNonBuildOutputCuller {
export default class ImageNonBuildOutputCuller {
constructor() {
this.buffering = true;
this.lines = [];