diff --git a/Dockerfile b/Dockerfile index 6f62f9f..92bd502 100644 --- a/Dockerfile +++ b/Dockerfile @@ -61,6 +61,7 @@ COPY scripts/chaos-containers.lua /usr/local/openresty/nginx/conf/kubeinvaders/c COPY scripts/programming_mode.lua /usr/local/openresty/nginx/conf/kubeinvaders/programming_mode.lua COPY scripts/config_kubeinv.lua /usr/local/openresty/lualib/config_kubeinv.lua COPY scripts/data/codenames.txt /usr/local/openresty/nginx/conf/kubeinvaders/data/codenames.txt +COPY scripts/demo_deploy.lua /usr/local/openresty/nginx/conf/kubeinvaders/demo_deploy.lua # Copy Python helpers COPY scripts/programming_mode /opt/programming_mode/ diff --git a/html/index.html b/html/index.html index 4df6f59..ad32b26 100644 --- a/html/index.html +++ b/html/index.html @@ -12,6 +12,7 @@ + @@ -549,6 +550,20 @@ EOF
' +
+ escapeHtml(caCertFilePath) + ') 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: ' + contextEntry.name + '), but no token found.' +
+ cmds.map(function(c) { return escapeHtml(c); }).join('\n') +
+ '' +
+ '' +
+ 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, '&').replace(//g, '>');
+}
+
+function escapeAttr(str) {
+ return str.replace(/&/g, '&').replace(/"/g, '"');
+}
+
+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 [];
@@ -336,6 +513,7 @@ function saveK8sConnection() {
}
showConnectionStatus('', false, 'k8s-save', 3000);
+ showDemoDeployBlock();
return true;
})
.catch(function (error) {
@@ -344,6 +522,64 @@ function saveK8sConnection() {
});
}
+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");
@@ -386,6 +622,10 @@ document.addEventListener("DOMContentLoaded", function() {
configured_namespaces = parseNamespacesInput(storedNamespaces || "");
+ if (storedK8sUrl && storedToken) {
+ showDemoDeployBlock();
+ }
+
setTimeout(function() {
var splashScreen = document.getElementById("splash-screen");
var mainGameDiv = document.getElementById("main-game-div");
diff --git a/nginx/KubeInvaders.conf b/nginx/KubeInvaders.conf
index 3199b68..a136c61 100644
--- a/nginx/KubeInvaders.conf
+++ b/nginx/KubeInvaders.conf
@@ -62,6 +62,10 @@ server {
sub_filter selected_env_vars_placeholder $selected_env_vars;
}
+ location /kube/demo/deploy {
+ access_by_lua_file "/usr/local/openresty/nginx/conf/kubeinvaders/demo_deploy.lua";
+ }
+
location /kube/ingresses {
access_by_lua_file "/usr/local/openresty/nginx/conf/kubeinvaders/ingress.lua";
}
diff --git a/scripts/demo_deploy.lua b/scripts/demo_deploy.lua
new file mode 100644
index 0000000..39b5e1e
--- /dev/null
+++ b/scripts/demo_deploy.lua
@@ -0,0 +1,147 @@
+local https = require "ssl.https"
+local ltn12 = require "ltn12"
+local json = require "lunajson"
+
+local arg = ngx.req.get_uri_args()
+local req_headers = ngx.req.get_headers()
+
+ngx.header['Content-Type'] = 'application/json'
+ngx.header['Access-Control-Allow-Origin'] = '*'
+ngx.header['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS'
+ngx.header['Access-Control-Allow-Headers'] = 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,X-K8S-Target,X-K8S-Token,X-K8S-CA-CERT-B64'
+ngx.header['Access-Control-Expose-Headers'] = 'Content-Length,Content-Range'
+
+if ngx.var.request_method == "OPTIONS" then
+ ngx.status = 204
+ return
+end
+
+local k8s_url = ""
+local kube_host = os.getenv("KUBERNETES_SERVICE_HOST")
+local kube_port = os.getenv("KUBERNETES_SERVICE_PORT_HTTPS")
+local endpoint = os.getenv("ENDPOINT")
+
+if kube_host and kube_host ~= "" then
+ local port_suffix = kube_port and kube_port ~= "" and (":" .. kube_port) or ""
+ k8s_url = "https://" .. kube_host .. port_suffix
+else
+ k8s_url = endpoint or ""
+end
+
+local target = arg['target'] or req_headers["x-k8s-target"] or req_headers["X-K8S-Target"]
+if target and target ~= "" then
+ if not string.match(target, "^https?://") then
+ target = "https://" .. target
+ end
+ k8s_url = string.gsub(target, "/+$", "")
+end
+
+if k8s_url == "" then
+ ngx.status = 500
+ ngx.say(json.encode({ok = false, error = "Missing Kubernetes endpoint"}))
+ return
+end
+
+if not string.match(k8s_url, "^https?://") then
+ k8s_url = "https://" .. k8s_url
+end
+k8s_url = string.gsub(k8s_url, "/+$", "")
+
+local header_token = req_headers["x-k8s-token"] or req_headers["X-K8S-Token"]
+local token = header_token and header_token ~= "" and header_token or tostring(os.getenv("TOKEN") or "")
+if token == "" then
+ local f = io.open("/var/run/secrets/kubernetes.io/serviceaccount/token", "r")
+ if f then
+ token = f:read("*a") or ""
+ token = token:gsub("%s+$", "")
+ f:close()
+ end
+end
+
+if token == "" then
+ ngx.status = 500
+ ngx.say(json.encode({ok = false, error = "Missing Kubernetes API token"}))
+ return
+end
+
+local ca_cert_b64 = req_headers["x-k8s-ca-cert-b64"] or req_headers["X-K8S-CA-CERT-B64"]
+local ca_cert = nil
+if ca_cert_b64 and ca_cert_b64 ~= "" then
+ ca_cert = ngx.decode_base64(ca_cert_b64)
+end
+
+local disable_tls_env = string.lower(tostring(os.getenv("DISABLE_TLS") or "false"))
+local disable_tls = disable_tls_env == "true" or disable_tls_env == "1" or disable_tls_env == "yes"
+
+local ca_file_path = "/tmp/kubeinv-demo-ca.crt"
+if not disable_tls and ca_cert and ca_cert ~= "" then
+ local ca_file = io.open(ca_file_path, "w")
+ if ca_file then
+ ca_file:write(ca_cert)
+ ca_file:close()
+ end
+end
+
+local function k8s_request(url, method, body)
+ local resp = {}
+ local headers = {
+ ["Accept"] = "application/json",
+ ["Content-Type"] = "application/json",
+ ["Authorization"] = "Bearer " .. token,
+ }
+ local opts = {
+ url = url,
+ headers = headers,
+ method = method,
+ verify = disable_tls and "none" or "peer",
+ sink = ltn12.sink.table(resp),
+ }
+ if body and body ~= "" then
+ headers["Content-Length"] = tostring(#body)
+ opts.source = ltn12.source.string(body)
+ end
+ if not disable_tls and ca_cert and ca_cert ~= "" then
+ opts.cafile = ca_file_path
+ end
+ local ok, status_code, _, _ = https.request(opts)
+ return ok, status_code, table.concat(resp)
+end
+
+local ns_body_fmt = '{"apiVersion":"v1","kind":"Namespace","metadata":{"name":"%s","labels":{"app":"kubeinvaders-demo"}}}'
+local deploy_body_fmt = '{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"name":"kubeinvaders-demo","namespace":"%s","labels":{"app":"kubeinvaders-demo"}},"spec":{"replicas":10,"selector":{"matchLabels":{"app":"kubeinvaders-demo"}},"template":{"metadata":{"labels":{"app":"kubeinvaders-demo"}},"spec":{"containers":[{"name":"nginx","image":"nginx:alpine","resources":{"requests":{"memory":"32Mi","cpu":"10m"},"limits":{"memory":"64Mi","cpu":"50m"}}}]}}}}'
+
+local created = {}
+local errors = {}
+
+for _, ns in ipairs({"ns-1", "ns-2"}) do
+ local ns_body = string.format(ns_body_fmt, ns)
+ local ok, status, _ = k8s_request(k8s_url .. "/api/v1/namespaces", "POST", ns_body)
+
+ if not ok then
+ table.insert(errors, "namespace " .. ns .. ": request failed")
+ elseif status ~= 200 and status ~= 201 and status ~= 409 then
+ table.insert(errors, "namespace " .. ns .. ": HTTP " .. tostring(status))
+ else
+ local deploy_body = string.format(deploy_body_fmt, ns)
+ local dok, dstatus, _ = k8s_request(
+ k8s_url .. "/apis/apps/v1/namespaces/" .. ns .. "/deployments",
+ "POST",
+ deploy_body
+ )
+ if not dok then
+ table.insert(errors, "deployment in " .. ns .. ": request failed")
+ elseif dstatus ~= 200 and dstatus ~= 201 and dstatus ~= 409 then
+ table.insert(errors, "deployment in " .. ns .. ": HTTP " .. tostring(dstatus))
+ else
+ table.insert(created, ns)
+ end
+ end
+end
+
+if #errors == 0 then
+ ngx.status = 200
+ ngx.say(json.encode({ok = true, created = created, message = "Deployed ns-1 and ns-2 with 10 nginx pods each"}))
+else
+ ngx.status = 207
+ ngx.say(json.encode({ok = #created > 0, created = created, errors = errors}))
+end