diff --git a/README.md b/README.md index 4075d36..1376e5c 100644 --- a/README.md +++ b/README.md @@ -17,20 +17,15 @@ Here are the slides (https://www.slideshare.net/EugenioMarzo/kubeinvaders-chaos- # Table of Contents 1. [Description](#Description) -2. [Installation - Helm with ClusterIP Service + Nginx Ingress](#Installation-default) -2. [Installation - Helm with NodePort Service](#Installation-nodeport) -2. [Installation - Using Podman or Docker](#Installation-podman) 3. [Usage](#Usage) 4. [URL Monitoring During Chaos Session](#URL-Monitoring-During-Chaos-Session) 5. [Persistence](#Persistence) 6. [Generic Troubleshooting & Known Problems](#Generic-Troubleshooting-And-Known-Problems) 7. [Troubleshooting Unknown Namespace](#Troubleshooting-Unknown-Namespace) 8. [Metrics](#Metrics) -9. [Security](#Security) -10. [Roadmap](#Roadmap) -11. [Community](#Community) -12. [Community blogs and videos](#Community-blogs-and-videos) -13. [License](#License) +9. [Community](#Community) +10. [Community blogs and videos](#Community-blogs-and-videos) +11. [License](#License) ## Description @@ -38,212 +33,29 @@ Inspired by the classic Space Invaders game, Kubeinvaders offers a playful and e ## Installation-default -If you need a lab kubernetes cluster you can use this setup via Make and Minikube. Follow [this readme](./minikube-setup/README.md) +**Helm installation is currently not supported.** -[![Artifact HUB](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/kubeinvaders)](https://artifacthub.io/packages/search?repo=kubeinvaders) +The easiest way to run KubeInvaders is directly with Podman or Docker. + +Run with Podman: ```bash -helm repo add kubeinvaders https://lucky-sideburn.github.io/helm-charts/ -helm repo update - -kubectl create namespace kubeinvaders - -# With ingress and TLS enabled -helm install --set-string config.target_namespace="namespace1\,namespace2" --set ingress.enabled=true --set ingress.hostName=kubeinvaders.local --set deployment.image.tag=latest -n kubeinvaders kubeinvaders kubeinvaders/kubeinvaders --set ingress.tls_enabled=true - -# With ingress enabled but TLS disabled (in case you have a reverse-proxy that does TLS termination and nginx controller in http) -helm install --set-string config.target_namespace="namespace1\,namespace2" --set ingress.enabled=true --set ingress.hostName=kubeinvaders.local --set deployment.image.tag=latest -n kubeinvaders kubeinvaders kubeinvaders/kubeinvaders/ --set ingress.tls_enabled=false - +podman run -p 8080:8080 docker.io/luckysideburn/kubeinvaders:latest ``` -### Example for K3S +Run with Docker: ```bash -curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="--disable traefik" sh -s - - -cat >/tmp/ingress-nginx.yaml <deployment.yaml </32 -``` - -## Roadmap - -Roadmap: Chaos Engineering Platform Enhancement -Phase 1: Authentication and Authorization - - Implement robust user authentication: - Allow for both local and external authentication (e.g., LDAP, OAuth) - Securely store user credentials - Introduce role-based access control (RBAC): - Define granular permissions based on user roles (e.g., admin, engineer, viewer) - Enforce authorization at the resource level (namespaces, experiments, etc.) - -Phase 2: Analytics and Reporting - - Develop namespace-specific statistics: - Track the frequency of chaos engineering sessions per namespace - Visualize trends and patterns over time - Create comprehensive reports: - Generate customizable reports for management - Include metrics on experiment coverage, success rates, and failure rates - Export reporting data: - Allow for data export in various formats (e.g., CSV, JSON, PDF) - -Phase 3: API Development - - Expose platform functionality via a RESTful API: - Enable integration with other tools and systems - Support CRUD operations for core entities (experiments, scenarios, etc.) - -Phase 4: UI Enhancements - - Improve user experience: - Redesign the UI for better usability and aesthetics - Optimize performance and responsiveness - -Phase 5: LLM Integration for Experiment Creation - - Integrate an LLM: Develop an interface that allows users to describe experiments in natural language. - Translate to code: Utilize the LLM to translate natural language descriptions into executable code. - Validate and optimize: Implement mechanisms to validate and optimize the code generated by the LLM. - ## Community Please reach out for news, bugs, feature requests, and other issues via: diff --git a/html/index.html b/html/index.html index 51239aa..d707976 100644 --- a/html/index.html +++ b/html/index.html @@ -808,7 +808,7 @@ k8s_jobs:
- +
diff --git a/html/js/chaos_report.js b/html/js/chaos_report.js index 6a1a7df..3b24d4d 100644 --- a/html/js/chaos_report.js +++ b/html/js/chaos_report.js @@ -18,11 +18,17 @@ function setChaosReportURL(select) { var selectedValue = select.options[select.selectedIndex].value; + if (selectedValue === "No Ingress found") { + return; + } document.getElementById("chaosReportCheckSiteURL").value = selectedValue; } function addElementToSelect(selectId, elementValue) { var select = document.getElementById(selectId); + if (!select || !elementValue) { + return; + } var option = document.createElement("option"); option.text = elementValue; option.value = elementValue; @@ -30,17 +36,38 @@ function addElementToSelect(selectId, elementValue) { } function parseIngressListJSON(ingressList) { - var hostOfIngress = convertStringToArrayWithSeparator(ingressList, ",") - - if (hostOfIngress.length > 0) { - document.getElementById("chaosReportCheckSiteURL").value = hostOfIngress[0]; + var select = document.getElementById("ingressHostList"); + if (!select) { + return; } - for (i in hostOfIngress) { - if (hostOfIngress[i] != "No Ingress found") { - addElementToSelect("ingressHostList", hostOfIngress[i]); - } + select.innerHTML = ""; + + var hostOfIngress = []; + if (Array.isArray(ingressList)) { + hostOfIngress = ingressList; + } else if (typeof ingressList === "string") { + hostOfIngress = convertStringToArrayWithSeparator(ingressList, ","); } + + hostOfIngress = hostOfIngress + .map(function (host) { + return String(host || "").trim(); + }) + .filter(function (host) { + return host !== "" && host !== "No Ingress found"; + }); + + if (hostOfIngress.length === 0) { + addElementToSelect("ingressHostList", "No Ingress found"); + return; + } + + for (var i = 0; i < hostOfIngress.length; i++) { + addElementToSelect("ingressHostList", hostOfIngress[i]); + } + + document.getElementById("chaosReportCheckSiteURL").value = hostOfIngress[0]; } function resizeCharts() { @@ -56,12 +83,42 @@ function resizeCharts() { } function getIngressLists() { + if (!namespace && configured_namespaces && configured_namespaces.length > 0) { + namespace = configured_namespaces[0]; + } + + if (!namespace) { + $('#alert_placeholder').replaceWith(alert_div + 'Set at least one namespace before configuring ingress checks.
'); + return; + } + var oReq = new XMLHttpRequest(); - oReq.onreadystatechange = function () { - if (this.readyState === XMLHttpRequest.DONE && this.status === 200) { - parseIngressListJSON(JSON.parse(this.responseText)); + oReq.onload = function () { + if (this.status !== 200) { + $('#alert_placeholder').replaceWith(alert_div + 'Ingress lookup failed with status ' + this.status + ' on namespace ' + namespace + '.
'); + parseIngressListJSON([]); + return; } - };; + + var ingressData = []; + try { + ingressData = JSON.parse(this.responseText); + } catch (e) { + ingressData = []; + } + + parseIngressListJSON(ingressData); + + if (!Array.isArray(ingressData) || ingressData.length === 0) { + $('#alert_placeholder').replaceWith(alert_div + 'No ingress hosts found in namespace ' + namespace + '. You can type a URL manually.'); + } + }; + + oReq.onerror = function () { + $('#alert_placeholder').replaceWith(alert_div + 'Ingress lookup failed due to network or CORS error. You can type a URL manually.'); + parseIngressListJSON([]); + }; + var ingressUrl = appendK8sTargetParam(k8s_url + "/kube/ingresses?namespace=" + namespace); oReq.open("GET", ingressUrl, true); applyK8sConnectionHeaders(oReq); @@ -94,7 +151,7 @@ function chaosReportHttpEndpointAdd() {
-
diff --git a/html/js/kubeinvaders.js b/html/js/kubeinvaders.js index 11a25cc..b68810f 100644 --- a/html/js/kubeinvaders.js +++ b/html/js/kubeinvaders.js @@ -161,9 +161,14 @@ function setCodeNameToTextInput(elementId) { function getMetrics() { var oReq = new XMLHttpRequest(); oReq.onload = function () { + if (this.status !== 200) { + console.warn('[METRICS] /metrics returned status ' + this.status); + return; + } var lines = this.responseText.split('\n'); for (var i = 0;i < lines.length;i++){ - metric = lines[i].split(' '); + var metric = lines[i].split(' '); + if (!metric[0] || metric[0] === '') continue; if (metric[0] == "chaos_node_jobs_total") { $('#chaos_jobs_total').text(metric[1]); @@ -189,7 +194,7 @@ function getMetrics() { $('#pods_match_regex').text(metric[1]); } else if (metric[0].match(chaos_job_regex)) { - metrics_split = metric[0].split(":"); + var metrics_split = metric[0].split(":"); chaos_jobs_status.set(metrics_split[1] + ":" + metrics_split[2] + ":" + metrics_split[3], metric[1]); } else if (metric[0] == "current_chaos_job_pod") { @@ -197,7 +202,10 @@ function getMetrics() { $('#current_chaos_job_pod').text(metric[1]); } } - };; + }; + oReq.onerror = function () { + console.error('[METRICS] XHR error fetching /metrics'); + }; oReq.open("GET", k8s_url + "/metrics"); oReq.send(); } @@ -205,16 +213,21 @@ function getMetrics() { function getChaosJobsPodsPhase() { var oReq = new XMLHttpRequest(); oReq.onload = function () { + if (this.status !== 200) return; var lines = this.responseText.split('\n'); for (var i = 0;i < lines.length;i++){ - metric = lines[i].split(' '); + var metric = lines[i].split(' '); + if (!metric[0] || metric[0] === '') continue; if (metric[0].match(chaos_job_regex)) { - metrics_split = metric[0].split(":"); + var metrics_split = metric[0].split(":"); chaos_jobs_status.set(metrics_split[1] + ":" + metrics_split[2] + ":" + metrics_split[3], metric[1]); } } - };; + }; + oReq.onerror = function () { + console.error('[METRICS] XHR error fetching /chaos_jobs_pod_phase'); + }; oReq.open("GET", k8s_url + "/chaos_jobs_pod_phase"); oReq.send(); } @@ -461,8 +474,9 @@ function getPods() { let new_pods = jsonData.items; // Pod might just be killed in game, but not terminated in k8s yet. + // Only keep "killed" visual if K8s hasn't reported it as fully ready again. for (i=0; i alien.name == new_pods[i].name && alien.status == "killed")) { + if (new_pods[i].status !== "ready" && aliens.some((alien) => alien.name == new_pods[i].name && alien.status == "killed")) { new_pods[i].status = "killed"; } } diff --git a/nginx/KubeInvaders.conf b/nginx/KubeInvaders.conf index 3cb3abe..5157763 100644 --- a/nginx/KubeInvaders.conf +++ b/nginx/KubeInvaders.conf @@ -332,27 +332,45 @@ server { } location /metrics { - default_type text/html; + default_type text/plain; content_by_lua_block { + 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' + ngx.header['Access-Control-Expose-Headers'] = 'Content-Length,Content-Range' + local redis = require "resty.redis" local red = redis:new() local okredis, errredis = red:connect("unix:/tmp/redis.sock") - for i, res in ipairs(red:keys("*total*")) do - if string.find(res, "chaos_node_jobs_total_on") then - local node = string.gsub(res, "chaos_node_jobs_total_on_", "") - local metric = "chaos_jobs_node_count{node=\"".. node .."\"}" - ngx.say(metric .. " " .. red:get(res)) + if not okredis then + ngx.log(ngx.ERR, "[metrics] Redis connection failed: " .. tostring(errredis)) + ngx.status = 500 + ngx.say("Redis connection failed") + return + end - elseif string.find(res, "deleted_pods_total_on") then - local namespace = string.gsub(res, "deleted_pods_total_on_", "") - local metric = "deleted_namespace_pods_count{namespace=\"".. namespace .."\"}" - ngx.say(metric .. " " .. red:get(res)) + local total_keys = red:keys("*total*") + if total_keys and type(total_keys) == "table" then + for i, res in ipairs(total_keys) do + if string.find(res, "chaos_node_jobs_total_on") then + local node = string.gsub(res, "chaos_node_jobs_total_on_", "") + local metric = "chaos_jobs_node_count{node=\"".. node .."\"}" + ngx.say(metric .. " " .. red:get(res)) + + elseif string.find(res, "deleted_pods_total_on") then + local namespace = string.gsub(res, "deleted_pods_total_on_", "") + local metric = "deleted_namespace_pods_count{namespace=\"".. namespace .."\"}" + ngx.say(metric .. " " .. red:get(res)) + end end end - for i, res in ipairs(red:keys("pods_match_regex:*")) do - ngx.say(res .. " " .. red:get(res)) + local regex_keys = red:keys("pods_match_regex:*") + if regex_keys and type(regex_keys) == "table" then + for i, res in ipairs(regex_keys) do + ngx.say(res .. " " .. red:get(res)) + end end local metrics = { @@ -377,20 +395,39 @@ server { end end - for i, res in ipairs(red:keys("chaos_jobs_status*")) do - ngx.say(res .. " " .. red:get(res)) + local status_keys = red:keys("chaos_jobs_status*") + if status_keys and type(status_keys) == "table" then + for i, res in ipairs(status_keys) do + ngx.say(res .. " " .. red:get(res)) + end end } } location /chaos_jobs_pod_phase { - default_type text/html; + default_type text/plain; content_by_lua_block { + 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' + ngx.header['Access-Control-Expose-Headers'] = 'Content-Length,Content-Range' + local redis = require "resty.redis" local red = redis:new() local okredis, errredis = red:connect("unix:/tmp/redis.sock") - for i, res in ipairs(red:keys("chaos_jobs_pod_phase*")) do - ngx.say(res .. " " .. red:get(res)) + + if not okredis then + ngx.log(ngx.ERR, "[chaos_jobs_pod_phase] Redis connection failed: " .. tostring(errredis)) + ngx.status = 500 + ngx.say("Redis connection failed") + return + end + + local phase_keys = red:keys("chaos_jobs_pod_phase*") + if phase_keys and type(phase_keys) == "table" then + for i, res in ipairs(phase_keys) do + ngx.say(res .. " " .. red:get(res)) + end end } } @@ -740,10 +777,6 @@ server { local lyaml = require "lyaml" local json = require 'lunajson' - math.randomseed(os.clock()*100000000000) - local rand = math.random(999, 9999) - local file_name = "/tmp/chaosprogram" .. rand - ngx.header['Access-Control-Allow-Origin'] = '*' ngx.header['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS' @@ -751,29 +784,8 @@ server { ngx.header['Access-Control-Expose-Headers'] = 'Content-Length,Content-Range'; ngx.req.read_body() local data = ngx.req.get_body_data() - ngx.log(ngx.INFO, "[PROGRAMMING_MODE] Chaos program payload sent from client: " .. data) - ngx.log(ngx.INFO, "[PROGRAMMING_MODE] Write temp file: " .. file_name) - - local yamlfile = io.open(file_name, "w") - yamlfile:write(data) - yamlfile:close() - - ngx.log(ngx.INFO, "[PROGRAMMING_MODE] Checking if " .. file_name .. " is a valid YAML file") - local handle = io.popen("python3 -c 'import yaml, sys; print(yaml.safe_load(sys.stdin))' < " .. file_name .. " ; echo $? > " .. file_name .. ".check") - local result = handle:read("*a") - - handle = io.popen("cat " .. file_name .. ".check") - result = handle:read("*a") - ngx.log(ngx.INFO, "[PROGRAMMING_MODE] Exit code for checking YAML syntax of " .. file_name .. " is " .. result) - - if result == "0\n" then - ngx.log(ngx.INFO, "[PROGRAMMING_MODE] YAML Syntax is OK") - else - ngx.log(ngx.INFO, "[PROGRAMMING_MODE] YAML Syntax is NOT OK") - handle = io.popen("rm -f " .. file_name .. ".check") - handle:read("*a") - ngx.status = 400 - ngx.say("Invalid YAML Chaos Program") + if data then + ngx.log(ngx.INFO, "[PROGRAMMING_MODE] Chaos program payload sent from client: " .. data) end if data == nil then @@ -781,8 +793,12 @@ server { ngx.status = 400 ngx.say(error) else - - local yaml_data = lyaml.load(data) + local parse_ok, yaml_data = pcall(lyaml.load, data) + if not parse_ok or type(yaml_data) ~= "table" then + ngx.status = 400 + ngx.say("Invalid YAML Chaos Program") + return + end if not key_exists(yaml_data, "k8s_jobs") then error = "[PROGRAMMING_MODE] Chaos program does not contain 'jobs' key." @@ -805,7 +821,6 @@ server { ngx.say(error) else - os.remove(file_name) local response = json.encode(yaml_data) ngx.log(ngx.INFO, response) ngx.status = 200 diff --git a/scripts/chaos-node.lua b/scripts/chaos-node.lua index 60c9844..e6ac672 100644 --- a/scripts/chaos-node.lua +++ b/scripts/chaos-node.lua @@ -58,7 +58,21 @@ end k8s_url = string.gsub(k8s_url, "/+$", "") -local token = req_headers["x-k8s-token"] or req_headers["X-K8S-Token"] or tostring(os.getenv("TOKEN") or "") +local header_token = req_headers["x-k8s-token"] or req_headers["X-K8S-Token"] +local token = "" +if header_token and header_token ~= "" then + token = header_token +else + token = tostring(os.getenv("TOKEN") or "") +end +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("Missing Kubernetes API token configuration.") diff --git a/scripts/ingress.lua b/scripts/ingress.lua index 5571791..beeba98 100644 --- a/scripts/ingress.lua +++ b/scripts/ingress.lua @@ -37,6 +37,15 @@ else k8s_url = endpoint or "" end +local req_headers = ngx.req.get_headers() +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("Missing Kubernetes endpoint configuration. Set KUBERNETES_SERVICE_HOST or ENDPOINT.") @@ -49,16 +58,24 @@ end k8s_url = string.gsub(k8s_url, "/+$", "") -local req_headers = ngx.req.get_headers() -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, "/+$", "") +local header_token = req_headers["x-k8s-token"] or req_headers["X-K8S-Token"] +local token = "" + +if header_token and header_token ~= "" then + token = header_token +else + token = tostring(os.getenv("TOKEN") or "") +end + +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 -local token = req_headers["x-k8s-token"] or req_headers["X-K8S-Token"] or tostring(os.getenv("TOKEN") or "") if token == "" then ngx.status = 500 ngx.say("Missing Kubernetes API token configuration.") diff --git a/scripts/node.lua b/scripts/node.lua index d18b1c6..2187954 100644 --- a/scripts/node.lua +++ b/scripts/node.lua @@ -23,7 +23,21 @@ local disable_tls = disable_tls_env == "true" or disable_tls_env == "1" or disab local arg = ngx.req.get_uri_args() local req_headers = ngx.req.get_headers() local target = arg['target'] or req_headers["x-k8s-target"] or req_headers["X-K8S-Target"] -local token = req_headers["x-k8s-token"] or req_headers["X-K8S-Token"] or tostring(os.getenv("TOKEN") or "") +local header_token = req_headers["x-k8s-token"] or req_headers["X-K8S-Token"] +local token = "" +if header_token and header_token ~= "" then + token = header_token +else + token = tostring(os.getenv("TOKEN") or "") +end +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 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 diff --git a/scripts/pod.lua b/scripts/pod.lua index 2e716d4..9e269a7 100644 --- a/scripts/pod.lua +++ b/scripts/pod.lua @@ -4,29 +4,19 @@ local json = require 'lunajson' local redis = require "resty.redis" local incr = 0 -ngx.log(ngx.INFO, "[pod.lua] === Request started ===") - 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") -ngx.log(ngx.INFO, "[pod.lua] ENV KUBERNETES_SERVICE_HOST=" .. tostring(kube_host)) -ngx.log(ngx.INFO, "[pod.lua] ENV KUBERNETES_SERVICE_PORT_HTTPS=" .. tostring(kube_port)) -ngx.log(ngx.INFO, "[pod.lua] ENV ENDPOINT=" .. tostring(endpoint)) -ngx.log(ngx.INFO, "[pod.lua] ENV TOKEN present=" .. tostring(os.getenv("TOKEN") ~= nil and os.getenv("TOKEN") ~= "")) -ngx.log(ngx.INFO, "[pod.lua] ENV DISABLE_TLS=" .. tostring(os.getenv("DISABLE_TLS"))) - if kube_host and kube_host ~= "" then local port_suffix = "" if kube_port and kube_port ~= "" then port_suffix = ":" .. kube_port end k8s_url = "https://" .. kube_host .. port_suffix - ngx.log(ngx.INFO, "[pod.lua] k8s_url from KUBERNETES_SERVICE_HOST=" .. k8s_url) else k8s_url = endpoint or "" - ngx.log(ngx.INFO, "[pod.lua] k8s_url from ENDPOINT=" .. k8s_url) end local token = tostring(os.getenv("TOKEN") or "") if token == "" then @@ -35,10 +25,8 @@ if token == "" then token = f:read("*a") or "" token = token:gsub("%s+$", "") f:close() - ngx.log(ngx.INFO, "[pod.lua] Token loaded from SA file, length=" .. tostring(#token)) end end -ngx.log(ngx.INFO, "[pod.lua] Initial token length=" .. tostring(#token)) 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" @@ -49,24 +37,12 @@ local header_token = req_headers["x-k8s-token"] or req_headers["X-K8S-Token"] local ca_cert_b64 = req_headers["x-k8s-ca-cert-b64"] or req_headers["X-K8S-CA-CERT-B64"] local ca_cert = nil -ngx.log(ngx.INFO, "[pod.lua] Query arg target=" .. tostring(arg['target'])) -ngx.log(ngx.INFO, "[pod.lua] Header x-k8s-target=" .. tostring(req_headers["x-k8s-target"])) -ngx.log(ngx.INFO, "[pod.lua] Resolved target=" .. tostring(target)) -ngx.log(ngx.INFO, "[pod.lua] Header x-k8s-token present=" .. tostring(header_token ~= nil and header_token ~= "")) -ngx.log(ngx.INFO, "[pod.lua] Header x-k8s-ca-cert-b64 present=" .. tostring(ca_cert_b64 ~= nil and ca_cert_b64 ~= "")) -ngx.log(ngx.INFO, "[pod.lua] Query arg namespace=" .. tostring(arg['namespace'])) -ngx.log(ngx.INFO, "[pod.lua] Query arg action=" .. tostring(arg['action'])) - if ca_cert_b64 and ca_cert_b64 ~= "" then ca_cert = ngx.decode_base64(ca_cert_b64) - ngx.log(ngx.INFO, "[pod.lua] CA cert decoded, length=" .. tostring(#ca_cert)) end if header_token and header_token ~= "" then token = header_token - ngx.log(ngx.INFO, "[pod.lua] Token overridden from header, new length=" .. tostring(#token)) -else - ngx.log(ngx.INFO, "[pod.lua] No token override from header, keeping env token (length=" .. tostring(#token) .. ")") end local namespace = arg['namespace'] @@ -82,13 +58,8 @@ if target and target ~= "" then target = "https://" .. target end k8s_url = string.gsub(target, "/+$", "") - ngx.log(ngx.INFO, "[pod.lua] k8s_url overridden from target=" .. k8s_url) end -ngx.log(ngx.INFO, "[pod.lua] Final k8s_url=" .. k8s_url) -ngx.log(ngx.INFO, "[pod.lua] Final token length=" .. tostring(#token)) -ngx.log(ngx.INFO, "[pod.lua] disable_tls=" .. tostring(disable_tls)) - if k8s_url == "" then ngx.log(ngx.ERR, "[pod.lua] FAIL: k8s_url is empty") ngx.status = 500 @@ -126,21 +97,17 @@ if action == "delete" then local okredis, errredis = red:connect("unix:/tmp/redis.sock") if okredis then - ngx.log(ngx.INFO, "Connection to Redis is ok") else - ngx.log(ngx.INFO, "Connection to Redis is not ok") - ngx.log(ngx.INFO, errredis) + ngx.log(ngx.ERR, "[pod.lua] Redis connection failed: " .. tostring(errredis)) end local res, err = red:get("deleted_pods_total") if res == ngx.null then ngx.say(err) - ngx.log(ngx.INFO, "deleted_pods_total is not present on Redis. Creating it..") red:set("deleted_pods_total", 1) else incr = res + 1 - ngx.log(ngx.INFO, "deleted_pods_total is present on Redis. Incrementing it..") red:set("deleted_pods_total", incr) end @@ -157,13 +124,11 @@ end if action == "list" then url = k8s_url.. "/api/v1/namespaces/" .. namespace .. "/pods" - ngx.log(ngx.INFO, "[pod.lua] Action=list, URL=" .. url) elseif action == "delete" then local pod_name = arg['pod_name'] url = k8s_url.. "/api/v1/namespaces/" .. namespace .. "/pods/" .. pod_name method = "DELETE" - ngx.log(ngx.INFO, "[pod.lua] Action=delete, pod=" .. tostring(pod_name) .. ", URL=" .. url) else ngx.log(ngx.ERR, "[pod.lua] FAIL: invalid action=" .. tostring(action)) @@ -194,55 +159,55 @@ if not disable_tls and ca_cert and ca_cert ~= "" then ca_file:write(ca_cert) ca_file:close() request_opts.cafile = ca_file_path - ngx.log(ngx.INFO, "[pod.lua] Custom CA cert written to " .. ca_file_path) else ngx.log(ngx.ERR, "[pod.lua] Failed to write CA cert to " .. ca_file_path) end end -ngx.log(ngx.INFO, "[pod.lua] Sending " .. method .. " request to " .. url .. " verify=" .. tostring(request_opts.verify) .. " cafile=" .. tostring(request_opts.cafile)) local ok, statusCode, headers, statusText = https.request(request_opts) -ngx.log(ngx.INFO, "[pod.lua] Response: ok=" .. tostring(ok) .. " statusCode=" .. tostring(statusCode) .. " statusText=" .. tostring(statusText)) -ngx.log(ngx.INFO, "[pod.lua] Response body length=" .. tostring(#table.concat(resp))) if action == "list" then local i = 1 local j = 0 pods["items"] = {} local resp_body = table.concat(resp) - ngx.log(ngx.INFO, "[pod.lua] Decoding JSON response for list action, body preview=" .. string.sub(resp_body, 1, 200)) local decode_ok, decode_err = pcall(function() decoded = json.decode(resp_body) end) if not decode_ok then ngx.log(ngx.ERR, "[pod.lua] JSON decode failed: " .. tostring(decode_err)) ngx.say("{\"items\": []}") return end - ngx.log(ngx.INFO, "[pod.lua] Decoded kind=" .. tostring(decoded["kind"]) .. " items count=" .. tostring(decoded["items"] and #decoded["items"] or "nil")) if decoded["kind"] == "PodList" then for k2,v2 in ipairs(decoded["items"]) do - if v2["status"]["phase"] == "Running" and v2["metadata"]["labels"]["chaos-controller"] ~= "kubeinvaders" then - -- ngx.log(ngx.INFO, "found pod " .. v2["metadata"]["name"]) - local status = "pending" - for _, c in ipairs(v2["status"]["conditions"]) do - if c["type"] == "ContainersReady" and c["status"] == "True" then - status = "ready" - break + local metadata = v2["metadata"] or {} + local labels = metadata["labels"] or {} + local pod_name = metadata["name"] or "" + local pod_status = v2["status"] or {} + local phase = pod_status["phase"] or "" + + if phase == "Running" and labels["chaos-controller"] ~= "kubeinvaders" then + local status = "ready" + local conditions = pod_status["conditions"] + if type(conditions) == "table" then + for _, c in ipairs(conditions) do + if (c["type"] == "ContainersReady" or c["type"] == "Ready") and c["status"] == "False" then + status = "pending" + break + end end end - pods["items"][i] = { name = v2["metadata"]["name"], status = status } + pods["items"][i] = { name = pod_name, status = status } i = i + 1 pods_not_found = false; - elseif v2["status"]["phase"] == "ContainerCreating" and v2["metadata"]["labels"]["chaos-controller"] ~= "kubeinvaders" then - -- ngx.log(ngx.INFO, "found pod " .. v2["metadata"]["name"]) - pods["items"][i] = { name = v2["metadata"]["name"], status = "pending" } + elseif phase == "ContainerCreating" and labels["chaos-controller"] ~= "kubeinvaders" then + pods["items"][i] = { name = pod_name, status = "pending" } i = i + 1 pods_not_found = false; - elseif v2["status"]["phase"] == "Terminating" and v2["metadata"]["labels"]["chaos-controller"] ~= "kubeinvaders" then - -- ngx.log(ngx.INFO, "found pod " .. v2["metadata"]["name"]) - pods["items"][i] = { name = v2["metadata"]["name"], status = "killed" } + elseif phase == "Terminating" and labels["chaos-controller"] ~= "kubeinvaders" then + pods["items"][i] = { name = pod_name, status = "killed" } i = i + 1 pods_not_found = false; - elseif v2["status"]["phase"] ~= "Running" and v2["status"]["phase"] ~= "Completed" and v2["status"]["phase"] ~= "Succeeded" then + elseif phase ~= "Running" and phase ~= "Completed" and phase ~= "Succeeded" then j = j + 1 end end @@ -285,18 +250,13 @@ if action == "list" then end if pods_not_found then - ngx.log(ngx.INFO, "[pod.lua] No pods found in namespace " .. namespace) ngx.say("{\"items\": []}") else local encoded = json.encode(pods) - ngx.log(ngx.INFO, "[pod.lua] Returning " .. tostring(i - 1) .. " pods for namespace " .. namespace) ngx.say(encoded) end elseif action == "delete" then local delete_resp = table.concat(resp) - ngx.log(ngx.INFO, "[pod.lua] Delete response: " .. string.sub(delete_resp, 1, 300)) ngx.say(delete_resp) end - -ngx.log(ngx.INFO, "[pod.lua] === Request finished ===") diff --git a/scripts/programming_mode.lua b/scripts/programming_mode.lua index d8ae931..77a7985 100644 --- a/scripts/programming_mode.lua +++ b/scripts/programming_mode.lua @@ -8,9 +8,24 @@ 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") +local arg = ngx.req.get_uri_args() local req_headers = ngx.req.get_headers() local target = arg["target"] or req_headers["x-k8s-target"] or req_headers["X-K8S-Target"] -local token = req_headers["x-k8s-token"] or req_headers["X-K8S-Token"] or tostring(os.getenv("TOKEN") or "") +local header_token = req_headers["x-k8s-token"] or req_headers["X-K8S-Token"] +local token = "" +if header_token and header_token ~= "" then + token = header_token +else + token = tostring(os.getenv("TOKEN") or "") +end +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 local ca_cert_b64 = req_headers["x-k8s-ca-cert-b64"] or req_headers["X-K8S-CA-CERT-B64"] if kube_host and kube_host ~= "" then @@ -62,7 +77,6 @@ ngx.header['Access-Control-Allow-Headers'] = 'DNT,User-Agent,X-Requested-With,If ngx.header['Access-Control-Expose-Headers'] = 'Content-Length,Content-Range'; ngx.req.read_body() -local arg = ngx.req.get_uri_args() local body_data = ngx.req.get_body_data() ngx.log(ngx.INFO, "[PROGRAMMING_MODE] Payload sent by client: " .. body_data) diff --git a/scripts/vm.lua b/scripts/vm.lua index b54b8ac..65c9842 100644 --- a/scripts/vm.lua +++ b/scripts/vm.lua @@ -41,7 +41,21 @@ end k8s_url = string.gsub(k8s_url, "/+$", "") -local token = req_headers["x-k8s-token"] or req_headers["X-K8S-Token"] or tostring(os.getenv("TOKEN") or "") +local header_token = req_headers["x-k8s-token"] or req_headers["X-K8S-Token"] +local token = "" +if header_token and header_token ~= "" then + token = header_token +else + token = tostring(os.getenv("TOKEN") or "") +end +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("Missing Kubernetes API token configuration.") diff --git a/scripts/vm_reboot.lua b/scripts/vm_reboot.lua index e7f0e12..8dacb64 100644 --- a/scripts/vm_reboot.lua +++ b/scripts/vm_reboot.lua @@ -41,7 +41,21 @@ end k8s_url = string.gsub(k8s_url, "/+$", "") -local token = req_headers["x-k8s-token"] or req_headers["X-K8S-Token"] or tostring(os.getenv("TOKEN") or "") +local header_token = req_headers["x-k8s-token"] or req_headers["X-K8S-Token"] +local token = "" +if header_token and header_token ~= "" then + token = header_token +else + token = tostring(os.getenv("TOKEN") or "") +end +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("Missing Kubernetes API token configuration.")