Files
kubeinvaders/html/js/utils.js
2026-05-02 22:31:31 +02:00

725 lines
23 KiB
JavaScript

/*
* Copyright 2024 KubeInvaders Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* Utility Functions */
function loadKubeconfigFile(event) {
var file = event.target.files[0];
if (!file) return;
var filenameSpan = document.getElementById('kubeconfig_filename');
var statusDiv = document.getElementById('kubeconfig-load-status');
var successDiv = document.getElementById('kubeconfig-load-success');
var errorDiv = document.getElementById('kubeconfig-load-error');
if (filenameSpan) filenameSpan.textContent = file.name;
if (statusDiv) statusDiv.style.display = 'none';
if (successDiv) { successDiv.style.display = 'none'; successDiv.textContent = ''; }
if (errorDiv) { errorDiv.style.display = 'none'; errorDiv.textContent = ''; }
var reader = new FileReader();
reader.onload = function(e) {
var kubeconfig;
try {
kubeconfig = jsyaml.load(e.target.result);
} catch (err) {
showKubeconfigStatus('Failed to parse KUBECONFIG: ' + err.message, true);
return;
}
if (!kubeconfig || typeof kubeconfig !== 'object') {
showKubeconfigStatus('Invalid KUBECONFIG file.', true);
return;
}
var currentContextName = kubeconfig['current-context'];
var contexts = kubeconfig.contexts || [];
var contextEntry = contexts.find(function(c) { return c.name === currentContextName; }) || contexts[0];
if (!contextEntry) {
showKubeconfigStatus('No context found in KUBECONFIG.', true);
return;
}
var ctx = contextEntry.context || {};
var clusterName = ctx.cluster;
var userName = ctx.user;
var namespace = ctx.namespace || '';
var clusters = kubeconfig.clusters || [];
var clusterEntry = clusters.find(function(c) { return c.name === clusterName; });
var clusterData = clusterEntry ? (clusterEntry.cluster || {}) : {};
var users = kubeconfig.users || [];
var userEntry = users.find(function(u) { return u.name === userName; });
var userData = userEntry ? (userEntry.user || {}) : {};
var server = clusterData.server || '';
var caCertB64 = clusterData['certificate-authority-data'] || '';
var caCertFilePath = clusterData['certificate-authority'] || '';
var token = userData.token || userData['token-data'] || '';
var usesClientCert = !token && (
userData['client-certificate'] || userData['client-certificate-data'] ||
userData['exec']
);
var caCert = '';
if (caCertB64) {
try {
caCert = atob(caCertB64);
} catch (_) {
caCert = caCertB64;
}
}
var endpointInput = document.getElementById('k8s_api_endpoint');
var tokenInput = document.getElementById('k8s_token');
var caCertInput = document.getElementById('k8s_ca_cert');
var namespacesInput = document.getElementById('k8s_namespaces');
if (endpointInput && server) endpointInput.value = server;
if (tokenInput && token) { tokenInput.value = token; localStorage.setItem('k8s_token', token); }
if (caCertInput) caCertInput.value = caCert;
if (namespacesInput && namespace && !namespacesInput.value) namespacesInput.value = namespace;
if (server) localStorage.setItem('k8s_api_endpoint', server);
if (caCert) localStorage.setItem('k8s_ca_cert', caCert);
if (usesClientCert) {
var ns = namespace || 'default';
var cmds = [
'kubectl create serviceaccount kubeinvaders -n ' + ns,
'kubectl create clusterrolebinding kubeinvaders \\',
' --clusterrole=cluster-admin \\',
' --serviceaccount=' + ns + ':kubeinvaders',
'kubectl create token kubeinvaders -n ' + ns
];
var caCertWarning = (!caCert && caCertFilePath)
? '<br><br><strong>CA certificate:</strong> your kubeconfig references a local file (<code>' +
escapeHtml(caCertFilePath) + '</code>) that cannot be read by the browser. ' +
'Paste its contents manually into the CA Certificate field above.'
: '';
var statusDiv = document.getElementById('kubeconfig-load-status');
var errorDiv = document.getElementById('kubeconfig-load-error');
var successDiv = document.getElementById('kubeconfig-load-success');
if (statusDiv) statusDiv.style.display = 'block';
if (successDiv) successDiv.style.display = 'none';
if (errorDiv) {
errorDiv.style.display = 'block';
errorDiv.innerHTML =
'Endpoint loaded (context: <strong>' + contextEntry.name + '</strong>), but no token found.<br>' +
'This context uses client-certificate auth, which is not supported directly.<br>' +
'We suggest running these commands to create a <strong>cluster-admin</strong> service account token:<br><br>' +
'<code style="display:block; background:#f0f0f0; color:#222; padding:8px 10px; border-radius:4px; white-space:pre-wrap; overflow-wrap:break-word; font-size:12px;">' +
cmds.map(function(c) { return escapeHtml(c); }).join('\n') +
'</code>' +
'<button type="button" onclick="copyKubeconfigCommands(this)" ' +
'data-commands="' + escapeAttr(cmds.join('\n')) + '" ' +
'style="margin-top:8px; font-size:12px; padding:3px 10px;">Copy commands</button>' +
caCertWarning;
}
return;
}
var loaded = [];
if (server) loaded.push('endpoint');
if (token) loaded.push('token');
if (caCert) loaded.push('CA cert');
var contextLabel = contextEntry.name ? ' (context: ' + contextEntry.name + ')' : '';
var msg = 'Loaded ' + loaded.join(', ') + contextLabel + '.';
if (!caCert && caCertFilePath) {
msg += '\nCA certificate references a local file (' + caCertFilePath + ') — paste it manually into the CA Certificate field.';
}
showKubeconfigStatus(msg, false);
};
reader.readAsText(file);
}
function showKubeconfigStatus(message, isError) {
var statusDiv = document.getElementById('kubeconfig-load-status');
var successDiv = document.getElementById('kubeconfig-load-success');
var errorDiv = document.getElementById('kubeconfig-load-error');
if (!statusDiv) return;
statusDiv.style.display = 'block';
if (isError) {
if (errorDiv) { errorDiv.style.display = 'block'; errorDiv.textContent = message; }
if (successDiv) successDiv.style.display = 'none';
} else {
if (successDiv) { successDiv.style.display = 'block'; successDiv.textContent = message; }
if (errorDiv) errorDiv.style.display = 'none';
}
}
function escapeHtml(str) {
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function escapeAttr(str) {
return str.replace(/&/g, '&amp;').replace(/"/g, '&quot;');
}
function copyKubeconfigCommands(btn) {
var commands = btn.getAttribute('data-commands');
navigator.clipboard.writeText(commands).then(function() {
var orig = btn.textContent;
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = orig; }, 2000);
}).catch(function() {
var ta = document.createElement('textarea');
ta.value = commands;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
var orig = btn.textContent;
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = orig; }, 2000);
});
}
function parseNamespacesInput(input) {
if (!input) {
return [];
}
return input
.split(',')
.map(function (ns) {
return ns.trim();
})
.filter(function (ns) {
return ns !== '';
});
}
function normalizeK8sEndpoint(rawEndpoint) {
if (!rawEndpoint) {
return null;
}
var endpoint = rawEndpoint.trim();
if (endpoint === '') {
return null;
}
if (!/^https?:\/\//i.test(endpoint)) {
endpoint = 'https://' + endpoint;
}
var parsedUrl;
try {
parsedUrl = new URL(endpoint);
} catch (error) {
return null;
}
if (!parsedUrl.hostname) {
return null;
}
// Keep protocol, host, port and optional base path, but remove trailing slash.
return (parsedUrl.origin + parsedUrl.pathname).replace(/\/+$/, '');
}
function getK8sTargetUrl() {
var endpointInput = document.getElementById('k8s_api_endpoint');
var storedEndpoint = localStorage.getItem('k8s_api_endpoint') || '';
var rawEndpoint = endpointInput && endpointInput.value.trim() !== ''
? endpointInput.value.trim()
: storedEndpoint;
var normalizedEndpoint = normalizeK8sEndpoint(rawEndpoint);
if (!normalizedEndpoint) {
throw new Error('Invalid Kubernetes API endpoint. Use a full endpoint like https://localhost:6443 or http://kubernetes.default.svc');
}
if (endpointInput) {
endpointInput.value = normalizedEndpoint;
}
return normalizedEndpoint;
}
function buildNetworkErrorMessage(targetUrl, action) {
var message = action + ' failed: network error while reaching ' + targetUrl + '/healthz.';
if (/^https:\/\/(localhost|127\.0\.0\.1|\[::1\])(?::\d+)?(\/|$)/i.test(targetUrl)) {
return message + ' Possible cause: TLS certificate is not trusted (ERR_CERT_AUTHORITY_INVALID). Browsers cannot ignore invalid TLS certificates for XHR requests.';
}
return message + ' Possible causes: DNS error (ERR_NAME_NOT_RESOLVED), TLS certificate issue, or unreachable endpoint.';
}
function getK8sCaCert() {
var caCertInput = document.getElementById('k8s_ca_cert');
if (!caCertInput) {
return '';
}
return caCertInput.value || '';
}
function getK8sToken() {
var tokenInput = document.getElementById('k8s_token');
var inputVal = tokenInput ? tokenInput.value : '';
var storageVal = localStorage.getItem('k8s_token') || '';
var result = inputVal || storageVal;
console.log('[K8S-TOKEN] inputElement exists=' + !!tokenInput + ', inputVal.length=' + inputVal.length + ', storageVal.length=' + storageVal.length + ', result.length=' + result.length);
return result;
}
function getStoredK8sConnectionConfig() {
var endpointInput = document.getElementById('k8s_api_endpoint');
var inputEndpoint = endpointInput ? endpointInput.value.trim() : '';
var storedEndpoint = (localStorage.getItem('k8s_api_endpoint') || '').trim();
var normalizedInputEndpoint = normalizeK8sEndpoint(inputEndpoint);
var normalizedStoredEndpoint = normalizeK8sEndpoint(storedEndpoint);
return {
target: normalizedInputEndpoint || normalizedStoredEndpoint || '',
token: getK8sToken().trim(),
caCert: getK8sCaCert()
};
}
function appendK8sTargetParam(url) {
var cfg = getStoredK8sConnectionConfig();
if (!cfg.target) {
return url;
}
var separator = url.indexOf('?') === -1 ? '?' : '&';
return url + separator + 'target=' + encodeURIComponent(cfg.target);
}
function applyK8sConnectionHeaders(xhr) {
var cfg = getStoredK8sConnectionConfig();
console.log('[K8S-HEADERS] target=' + (cfg.target || '<empty>') + ', token.length=' + (cfg.token ? cfg.token.length : 0) + ', caCert.length=' + (cfg.caCert ? cfg.caCert.length : 0));
if (cfg.target) {
xhr.setRequestHeader('X-K8S-Target', cfg.target);
}
xhr.setRequestHeader('X-K8S-Token', cfg.token || '');
if (cfg.caCert) {
try {
xhr.setRequestHeader('X-K8S-CA-CERT-B64', btoa(cfg.caCert));
} catch (error) {
console.warn('[K8S-CONNECTION] Unable to encode CA cert header', error);
}
}
}
function requestBackendHealthz(targetUrl, action, caCert) {
return new Promise(function(resolve, reject) {
var oReq = new XMLHttpRequest();
oReq.timeout = 5000;
oReq.onreadystatechange = function() {
if (this.readyState === XMLHttpRequest.DONE) {
var payload = null;
try {
payload = this.responseText ? JSON.parse(this.responseText) : null;
} catch (error) {
payload = null;
}
if (this.status === 200 && (!payload || payload.ok === true)) {
resolve(payload || true);
} else {
var errorDetails = '';
if (payload && payload.error) {
errorDetails = ' (' + payload.error + ')';
} else if (payload && payload.body) {
errorDetails = ' (' + payload.body + ')';
} else if (this.responseText) {
errorDetails = ' (' + this.responseText + ')';
}
reject(new Error(action + ' failed with status: ' + this.status + errorDetails));
}
}
};
oReq.ontimeout = function() {
reject(new Error(action + ' timeout'));
};
oReq.onerror = function() {
reject(new Error(buildNetworkErrorMessage(targetUrl, action)));
};
oReq.open('POST', '/kube/healthz', true);
oReq.setRequestHeader('Content-Type', 'application/json');
oReq.send(JSON.stringify({
target: targetUrl,
ca_cert: caCert || ''
}));
});
}
var connectionStatusHideTimers = {};
function clearConnectionStatus(prefix) {
prefix = prefix || 'k8s-connection';
var statusDiv = document.getElementById(prefix + '-status');
var successDiv = document.getElementById(prefix + '-success');
var errorDiv = document.getElementById(prefix + '-error');
var errorMsgSpan = document.getElementById(prefix + '-error-msg');
if (connectionStatusHideTimers[prefix]) {
clearTimeout(connectionStatusHideTimers[prefix]);
connectionStatusHideTimers[prefix] = null;
}
if (statusDiv) statusDiv.style.display = 'none';
if (successDiv) successDiv.style.display = 'none';
if (errorDiv) errorDiv.style.display = 'none';
if (errorMsgSpan) errorMsgSpan.textContent = '';
}
function showConnectionStatus(message, isError, prefix, autoHideDelay) {
prefix = prefix || 'k8s-connection';
var statusDiv = document.getElementById(prefix + '-status');
var successDiv = document.getElementById(prefix + '-success');
var errorDiv = document.getElementById(prefix + '-error');
var errorMsgSpan = document.getElementById(prefix + '-error-msg');
if (!statusDiv) {
console.log('[K8S-CONNECTION] ' + message);
return;
}
statusDiv.style.display = 'block';
if (successDiv) successDiv.style.display = 'none';
if (errorDiv) errorDiv.style.display = 'none';
if (connectionStatusHideTimers[prefix]) {
clearTimeout(connectionStatusHideTimers[prefix]);
connectionStatusHideTimers[prefix] = null;
}
if (isError) {
if (errorDiv) errorDiv.style.display = 'block';
if (errorMsgSpan) errorMsgSpan.textContent = message;
} else {
if (successDiv) successDiv.style.display = 'block';
}
if (autoHideDelay && autoHideDelay > 0) {
connectionStatusHideTimers[prefix] = setTimeout(function () {
clearConnectionStatus(prefix);
}, autoHideDelay);
}
}
function testK8sConnectionRequest() {
return new Promise((resolve, reject) => {
var targetUrl;
var caCert = getK8sCaCert();
try {
targetUrl = getK8sTargetUrl();
} catch (error) {
reject(error);
return;
}
requestBackendHealthz(targetUrl, 'Connection test', caCert)
.then(function () {
resolve(true);
})
.catch(function (error) {
reject(error);
});
});
}
function saveK8sConnectionRequest() {
return new Promise((resolve, reject) => {
const namespacesInput = document.getElementById('k8s_namespaces');
const namespacesRaw = namespacesInput ? namespacesInput.value.trim() : '';
const caCert = getK8sCaCert();
let targetUrl;
try {
targetUrl = getK8sTargetUrl();
} catch (error) {
reject(error);
return;
}
// Persist everything immediately so token/endpoint survive page reloads
// even if the healthz probe fails.
localStorage.setItem('k8s_api_endpoint', targetUrl);
localStorage.setItem('k8s_namespaces', namespacesRaw);
localStorage.setItem('k8s_ca_cert', caCert);
var tokenVal = getK8sToken();
localStorage.setItem('k8s_token', tokenVal);
configured_namespaces = parseNamespacesInput(namespacesRaw);
requestBackendHealthz(targetUrl, 'Save', caCert)
.then(function () {
resolve(true);
})
.catch(function (error) {
reject(error);
});
});
}
function testK8sConnection() {
clearConnectionStatus('k8s-connection');
return testK8sConnectionRequest()
.then(function () {
showConnectionStatus('Connection test succeeded.', false, 'k8s-connection', 3000);
return true;
})
.catch(function (error) {
showConnectionStatus(error.message, true, 'k8s-connection', 3000);
return false;
});
}
function saveK8sConnection() {
clearConnectionStatus('k8s-save');
return saveK8sConnectionRequest()
.then(function () {
if (typeof setSystemSettings === 'function') {
setSystemSettings();
}
if (typeof getEndpoint === 'function') {
getEndpoint();
}
if (typeof getNamespaces === 'function') {
getNamespaces();
}
showConnectionStatus('', false, 'k8s-save', 3000);
showDemoDeployBlock();
return true;
})
.catch(function (error) {
showConnectionStatus(error.message, true, 'k8s-save', 3000);
return false;
});
}
function showDemoDeployBlock() {
var block = document.getElementById('demo-deploy-block');
if (block) {
block.style.display = 'block';
}
}
function deployDemoResources() {
var btn = document.getElementById('demo-deploy-btn');
var statusDiv = document.getElementById('demo-deploy-status');
var successDiv = document.getElementById('demo-deploy-success');
var errorDiv = document.getElementById('demo-deploy-error');
if (btn) {
btn.disabled = true;
btn.textContent = 'Deploying...';
}
var url = appendK8sTargetParam('/kube/demo/deploy');
var oReq = new XMLHttpRequest();
oReq.open('POST', url, true);
applyK8sConnectionHeaders(oReq);
oReq.timeout = 30000;
oReq.onreadystatechange = function () {
if (this.readyState !== XMLHttpRequest.DONE) return;
if (btn) {
btn.disabled = false;
btn.textContent = 'Deploy ns-1 & ns-2 (10 pods each)';
}
statusDiv.style.display = 'block';
var payload = null;
try { payload = JSON.parse(this.responseText); } catch (e) {}
if (this.status === 200 || this.status === 207) {
var msg = (payload && payload.message) ? payload.message : 'Deployed successfully.';
if (payload && payload.errors && payload.errors.length > 0) {
errorDiv.style.display = 'block';
successDiv.style.display = 'none';
errorDiv.textContent = 'Partial failure: ' + payload.errors.join('; ');
} else {
successDiv.style.display = 'block';
errorDiv.style.display = 'none';
successDiv.textContent = '✅ ' + msg;
}
} else {
var errMsg = (payload && payload.error) ? payload.error : this.responseText;
errorDiv.style.display = 'block';
successDiv.style.display = 'none';
errorDiv.textContent = '❌ Deploy failed: ' + errMsg;
}
};
oReq.send();
}
document.addEventListener("DOMContentLoaded", function() {
// Migrate old localStorage key name used in earlier versions
var legacyK8sUrl = localStorage.getItem("k8s_url");
if (legacyK8sUrl && !localStorage.getItem("k8s_api_endpoint")) {
localStorage.setItem("k8s_api_endpoint", legacyK8sUrl);
localStorage.removeItem("k8s_url");
}
var endpointInput = document.getElementById("k8s_api_endpoint");
var namespacesInput = document.getElementById("k8s_namespaces");
var caCertInput = document.getElementById("k8s_ca_cert");
var tokenInput = document.getElementById("k8s_token");
var storedK8sUrl = localStorage.getItem("k8s_api_endpoint");
var storedNamespaces = localStorage.getItem("k8s_namespaces");
var storedCaCert = localStorage.getItem("k8s_ca_cert");
var storedToken = localStorage.getItem("k8s_token");
if (endpointInput) {
endpointInput.value = storedK8sUrl || '';
}
if (namespacesInput) {
namespacesInput.value = storedNamespaces || "";
}
if (caCertInput) {
caCertInput.value = storedCaCert || "";
}
if (tokenInput && storedToken) {
tokenInput.value = storedToken;
}
// Auto-persist token whenever the user types in the field
if (tokenInput) {
tokenInput.addEventListener('input', function () {
localStorage.setItem('k8s_token', tokenInput.value);
});
}
configured_namespaces = parseNamespacesInput(storedNamespaces || "");
if (storedK8sUrl && storedToken) {
showDemoDeployBlock();
}
setTimeout(function() {
var splashScreen = document.getElementById("splash-screen");
var mainGameDiv = document.getElementById("main-game-div");
if (splashScreen) {
splashScreen.style.display = "none";
}
if (mainGameDiv) {
mainGameDiv.style.display = "block";
}
}, 2000);
});
function IsJsonString(str) {
try {
JSON.parse(str);
} catch (e) {
return false;
}
return true;
}
function contains(a, obj) {
for (var i = 0; i < a.length; i++) {
if (a[i] === obj) {
return true;
}
}
return false;
}
function getRandomInt(max) {
return Math.floor(Math.random() * max);
}
function rand_id() {
return getRandomInt(9999);
}
function formattedToday() {
const today = new Date();
return today
}
function convertStringToArrayWithSeparator(str, separator) {
return String(str).split(separator);
}
function demo_mode_alert() {
alert("This is a demo mode installed into the k8s cluster of platformengineering.it, some features are disabled.");
}
function is_demo_mode() {
return demo_mode;
}
function sanitizeStringToURLFriendly(str) {
return str.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase();
}
function kubePingModalSwitch() {
var oReq = new XMLHttpRequest();
oReq.onreadystatechange = function () {
if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {
console.log("[K-INV STARTUP] kubeping status is: |" + this.responseText.trim() + "|");
if (this.responseText.trim() == "Key not found" || is_demo_mode()) {
setModalState(true)
showKubePingModal()
}
}
};;
oReq.open("GET", k8s_url + "/chaos/redis/get?key=kubeping", true);
oReq.send();
}
function setKubePingStatusPing(value) {
var oReq = new XMLHttpRequest();
if (value == 1) {
oReq.open("POST", k8s_url + "/chaos/redis/set?key=kubeping&msg=" + sanitizeStringToURLFriendly(document.getElementById("kubePingJsonTextArea").value), true);
}
else {
oReq.open("POST", k8s_url + "/chaos/redis/set?key=kubeping", true);
}
oReq.onreadystatechange = function () {
if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {
kubeping_sent = true;
}
};;
oReq.setRequestHeader("Content-Type", "application/text");
oReq.send(String(value));
closeKubePingModal();
}