KASM-8264 Registry based wizard installer

This commit is contained in:
Chris Hunt 2026-03-27 15:10:08 +00:00
parent 502bc2f530
commit d4b7db69f4
No known key found for this signature in database
3 changed files with 98 additions and 5 deletions

View file

@ -24,15 +24,100 @@ const { spawn } = require('node:child_process');
var EULA;
var images;
var currentVersion;
var sourceLocal = false;
var gpuInfo;
var installSettings = {};
var upgradeSettings = {};
// Find the best matching compatibility entry for the current version.
// Exact version match takes precedence over wildcard (e.g. "1.18.x").
function matchVersion(currentVer, compatibilityList) {
if (!compatibilityList || compatibilityList.length === 0) return null;
const parts = currentVer.split('.');
const major = parts[0];
const minor = parts[1] !== undefined ? parts[1] : '0';
// Exact match (most precise)
let match = compatibilityList.find(c => c.version === currentVer);
if (match) return match;
// Major.minor wildcard, e.g. "1.18.x"
match = compatibilityList.find(c => c.version === `${major}.${minor}.x`);
if (match) return match;
return null;
}
// Fetch workspace list from registry and convert to the images YAML structure.
// Falls back to local list.json if the remote is unavailable.
async function fetchWorkspaceList(currentVer, archName) {
let listData;
try {
const response = await fetch('https://registry.kasmweb.com/1.1/list.json');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
listData = await response.json();
} catch (err) {
console.error('Failed to fetch workspace list, falling back to local list.json:', err.message);
let localList = await fsw.readFile('/wizard/list.json', 'utf8');
listData = JSON.parse(localList);
sourceLocal = true;
}
const defaultChannel = listData.default_channel;
const filtered = (listData.workspaces || []).filter(
ws => ws.architecture && ws.architecture.includes(archName)
);
let imageIdx = 1;
const imagesList = [];
for (const ws of filtered) {
const compat = matchVersion(currentVer, ws.compatibility || []);
if (!compat) continue;
// Use default_channel tag if it exists in available_tags, otherwise keep compat image tag
let imageName = compat.image;
const imageBase = imageName.split(':')[0];
if (defaultChannel && compat.available_tags && compat.available_tags.includes(defaultChannel)) {
imageName = imageBase + ':' + defaultChannel;
}
imagesList.push({
categories: ws.categories,
cores: 2.0,
cpu_allocation_method: 'Inherit',
description: ws.description,
docker_registry: ws.docker_registry,
enabled: true,
exec_config: {},
friendly_name: ws.friendly_name,
gpu_count: 0,
hidden: false,
image_id: '${uuid:image_id:' + imageIdx + '}',
image_src: 'https://registry.kasmweb.com/1.1/icons/' + ws.image_src,
image_type: 'Container',
launch_config: {},
memory: 2768000000,
name: imageName,
notes: ws.notes || null,
run_config: {},
uncompressed_size_mb: compat.uncompressed_size_mb,
zone_id: null
});
imageIdx++;
}
return { alembic_version: 'e3900d8a4fee', images: imagesList };
}
// Grab installer variables
async function installerBlobs() {
EULA = await fsw.readFile('/kasm_release/licenses/LICENSE.txt', 'utf8');
let imagesText = await fsw.readFile('/wizard/default_images_' + arch + '.yaml', 'utf8');
images = yaml.load(imagesText);
currentVersion = fs.readFileSync('/version.txt', 'utf8').replace(/(\r\n|\n|\r)/gm,'');
try {
currentVersion = fs.readFileSync('/version.txt', 'utf8').replace(/(\r\n|\n|\r)/gm,'');
} catch (err) {
currentVersion = '1.18.1!';
}
images = await fetchWorkspaceList(currentVersion, arch);
let gpuData = [];
let gpuCmd = spawn('/gpuinfo.sh');
gpuCmd.stdout.on('data', function(data) {
@ -192,6 +277,7 @@ io.on('connection', async function (socket) {
dashinfo['localVersion'] = 'Unknown';
}
dashinfo['currentVersion'] = currentVersion;
dashinfo['sourceLocal'] = sourceLocal;
dashinfo['gpuInfo'] = gpuInfo;
dashinfo['containers'] = containers;
dashinfo['cpu'] = await si.cpu();
@ -201,7 +287,7 @@ io.on('connection', async function (socket) {
socket.emit('renderdash', [dashinfo, images]);
// Render installer
} else {
socket.emit('renderinstall', [EULA, images, gpuInfo]);
socket.emit('renderinstall', [EULA, images, gpuInfo, currentVersion, sourceLocal]);
}
}
// Disable wizard

1
list.json Normal file

File diff suppressed because one or more lines are too long

View file

@ -88,6 +88,10 @@ function titleChange(value) {
$('#title').text(value);
}
function versionChange(ver, local) {
document.title = 'Kasm Wizard ' + ver + (local ? ' (local)' : '');
}
// Render landing as installer page
function renderInstall(data) {
showContainer();
@ -95,6 +99,7 @@ function renderInstall(data) {
EULA = data[0];
images = data[1];
gpus = data[2];
versionChange(data[3], data[4]);
let EULADiv = $('<div>', {id: 'EULA'}).text(EULA);
$('#container').append(EULADiv);
let EULAButton = $('<button>', {id: 'EULAButton', onclick: 'pickSettings()', class: 'btn btn-default btn-ghost'}).text('Accept and continue');
@ -107,6 +112,7 @@ async function renderDash(data) {
titleChange('Dashboard');
let info = data[0];
images = data[1];
versionChange(info.currentVersion, info.sourceLocal);
// Store GPU info
$('body').data('gpuInfo', info.gpuInfo);
// Upgrade button if needed
@ -319,7 +325,7 @@ async function pickImages(upgrade) {
title: image.description,
onclick: 'selectImage(\'' + image.friendly_name.replace(new RegExp(' ', 'g'), '_').replace('.', '-') + '\')'
}).append(imageName).css('filter', 'grayscale(100%)')
let thumb = $('<img>', {class: 'thumb', src: 'public/' + image.image_src});
let thumb = $('<img>', {class: 'thumb', src: image.image_src, loading: 'lazy'});
imageDiv.append(thumb);
$('#images').append(imageDiv);
}