# A service-mesh workshop — Kubernetes and Istio in an afternoon, with a cluster to break

Recipe No. 30, Labs and workshops. From The know.sh Cookbook: https://know.sh/cookbook/mesh-workshop

Every mesh workshop loses twenty minutes to a laptop that will not start a cluster. Plan for it. Your handout sets the afternoon out as a table and each exercise’s commands as numbered steps, with a simulator at the top, so the attendee with the broken laptop still splits traffic, injects a fault and turns on strict mTLS.

- For: a platform engineer running a half-day Istio workshop
- You get: a handout with every command as a step, and a mesh simulator in it
- Time: an evening, then two on a lab cluster
- Made with: Shelf, Document, Your AI assistant, Elements, Public link, Quiz

## How it works

1. **Draft the handout from your outline.** Give your assistant your outline and the Istio version you teach. Ask for a document per exercise with the same five sections, every command taken from the documentation, with a link.
2. **Run every command on a lab cluster.** Create a throwaway cluster with kind, install Istio’s demo profile and deploy Bookinfo. Work through every exercise as written, and fix every command that fails.
3. **Set the commands out as steps.** Ask for a steps element in each *Commands* section, one command per step with what a correct result looks like. Then write *On a real cluster* yourself.
4. **Put a simulator at the top.** Ask for a widget element: a terminal, a Bookinfo topology and five exercises that tick themselves off. Check it against your lab cluster, and list where they differ.

## Try this prompt

Your assistant, connected to know.sh (Claude, ChatGPT or a local model):

> Using know.sh, add a widget element to the top of my Mesh workshop handout: a terminal that accepts kubectl get, label, rollout restart and apply, and istioctl analyze, against a pretend cluster with Bookinfo in the shop namespace; a topology with the reviews weights drawn on it; a button that sends 100 requests; and five exercises that tick themselves off. Say in the caption what it leaves out.

Your assistant, connected to know.sh (Claude, ChatGPT or a local model):

> Using know.sh, read Exercise 3 on my Mesh workshop shelf and rewrite its Commands section as a steps element: one command per step, in order, from checking the sidecars to reading back the weights, each with a sentence on what a correct result looks like. Keep my On a real cluster paragraph exactly as it is.

## Elements in the specimens

Written as your assistant writes them through the know.sh MCP server. Each reads as plain words until you turn elements on (Account → Elements).

Widget, specimen 1:

```element widget
{
  "title": "Kubernetes and Istio simulator",
  "caption": "Interactive terminal and traffic topology simulator for Kubernetes and Istio mesh diagnostics.",
  "height": 680,
  "html": "<div class=\"kubectl-sim\" tabindex=\"0\" aria-label=\"Kubernetes and Istio cluster simulator\">\n  <header class=\"bar\">\n    <div class=\"bar-title\">\n      <span class=\"cluster-ctx\">Cluster <b>mesh-lab</b></span>\n      <span class=\"ns-ctx\">Namespace <b id=\"activeNs\">shop</b></span>\n    </div>\n    <div class=\"bar-actions\">\n      <button type=\"button\" id=\"btnLoad\">Load (100 reqs)</button>\n      <button type=\"button\" id=\"btnReset\">Reset</button>\n      <button type=\"button\" id=\"btnHelp\">Help</button>\n    </div>\n  </header>\n\n  <div class=\"sim-body\">\n    <section class=\"term-pane\" aria-label=\"Terminal\">\n      <div class=\"term-log\" id=\"termLog\" role=\"log\" aria-live=\"polite\"></div>\n      <div class=\"term-form\" id=\"termForm\">\n        <label for=\"termInput\" class=\"term-prompt\">$</label>\n        <input\n          id=\"termInput\"\n          class=\"term-input\"\n          type=\"text\"\n          spellcheck=\"false\"\n          autocapitalize=\"off\"\n          autocomplete=\"off\"\n          autocorrect=\"off\"\n          aria-label=\"kubectl command input\"\n        />\n      </div>\n    </section>\n\n    <aside class=\"side-pane\">\n      <section class=\"panel exercises-panel\">\n        <h3 class=\"panel-title\">Guided exercises</h3>\n        <ol class=\"exercise-list\" id=\"exerciseList\">\n          <li data-step=\"1\"><span class=\"check\" id=\"chk1\">&#9675;</span> <span class=\"step-text\">Enable injection on namespace shop</span></li>\n          <li data-step=\"2\"><span class=\"check\" id=\"chk2\">&#9675;</span> <span class=\"step-text\">Restart reviews to inject sidecars (2/2)</span></li>\n          <li data-step=\"3\"><span class=\"check\" id=\"chk3\">&#9675;</span> <span class=\"step-text\">Split reviews traffic 50/50 (v2/v3)</span></li>\n          <li data-step=\"4\"><span class=\"check\" id=\"chk4\">&#9675;</span> <span class=\"step-text\">Add 2 s delay to ratings (20% reqs)</span></li>\n          <li data-step=\"5\"><span class=\"check\" id=\"chk5\">&#9675;</span> <span class=\"step-text\">Turn on strict mTLS</span></li>\n        </ol>\n        <div class=\"hint-bar\">\n          <button type=\"button\" id=\"btnHint\">Hint</button>\n          <span class=\"hint-text\" id=\"hintOutput\">Type hint or click for guidance.</span>\n        </div>\n      </section>\n\n      <section class=\"panel mesh-panel\">\n        <div class=\"panel-header\">\n          <h3 class=\"panel-title\">Traffic topology</h3>\n          <span class=\"mtls-status\" id=\"mtlsStatus\">mTLS permissive</span>\n        </div>\n        <svg class=\"mesh-svg\" id=\"meshSvg\" viewBox=\"0 0 380 180\" role=\"img\" aria-label=\"Bookinfo service topology diagram\"></svg>\n        <div class=\"traffic-stats\" id=\"trafficStats\">\n          <span class=\"stat\">Total: <b id=\"statTotal\">0</b></span>\n          <span class=\"stat\">v1: <b id=\"statV1\">34%</b></span>\n          <span class=\"stat\">v2: <b id=\"statV2\">33%</b></span>\n          <span class=\"stat\">v3: <b id=\"statV3\">33%</b></span>\n          <span class=\"stat\">Ratings: <b id=\"statRatings\">0</b></span>\n          <span class=\"stat\">Latency: <b id=\"statLatency\">12 ms</b></span>\n        </div>\n      </section>\n    </aside>\n  </div>\n</div>",
  "css": ".kubectl-sim {\n  position: relative;\n  outline: none;\n  font-family: var(--sans);\n  color: var(--ink);\n  background: var(--paper);\n}\n.kubectl-sim:focus-visible {\n  outline: 1px solid var(--ink);\n  outline-offset: 2px;\n}\n.bar {\n  display: flex;\n  flex-wrap: wrap;\n  align-items: center;\n  justify-content: space-between;\n  gap: 12px;\n  padding: 8px 0;\n  border-bottom: 1px solid var(--rule);\n  background: var(--paper);\n  font: 400 13px/1.3 var(--sans);\n  color: var(--ink-2);\n}\n.bar b {\n  font-weight: 500;\n  color: var(--ink);\n}\n.bar-title {\n  display: flex;\n  align-items: center;\n  gap: 16px;\n}\n.bar-actions {\n  display: flex;\n  gap: 8px;\n}\n.bar-actions button, .hint-bar button {\n  min-height: 28px;\n  padding: 4px 10px;\n  font: 500 13px/1 var(--sans);\n  background: var(--paper);\n  color: var(--ink);\n  border: 1px solid var(--ink);\n  border-radius: 0;\n  cursor: pointer;\n}\n.bar-actions button:hover, .hint-bar button:hover,\n.bar-actions button:focus-visible, .hint-bar button:focus-visible {\n  background: var(--wash);\n}\n.sim-body {\n  display: flex;\n  flex-direction: column;\n  gap: 16px;\n  margin-top: 12px;\n}\n@media (min-width: 860px) {\n  .sim-body {\n    flex-direction: row;\n    align-items: flex-start;\n  }\n  .term-pane {\n    flex: 1 1 56%;\n  }\n  .side-pane {\n    flex: 1 1 44%;\n  }\n}\n.term-pane {\n  display: flex;\n  flex-direction: column;\n  background: var(--paper);\n  border: 1px solid var(--rule);\n}\n.term-log {\n  flex: 1;\n  max-height: 380px;\n  min-height: 240px;\n  overflow-y: auto;\n  overflow-x: hidden;\n  padding: 10px 12px;\n  font-family: var(--mono);\n  font-size: 12px;\n  line-height: 1.45;\n  white-space: pre-wrap;\n  overflow-wrap: anywhere;\n  word-break: break-word;\n}\n.term-log .cmd-line {\n  font-weight: 600;\n  color: var(--ink);\n  margin-top: 6px;\n  white-space: pre-wrap;\n  overflow-wrap: anywhere;\n  word-break: break-word;\n}\n.term-log .cmd-out {\n  color: var(--ink-2);\n  margin-bottom: 6px;\n  white-space: pre-wrap;\n  overflow-wrap: anywhere;\n  word-break: break-word;\n}\n.term-log .cmd-err {\n  color: var(--ink);\n  font-style: italic;\n  margin-bottom: 6px;\n  white-space: pre-wrap;\n  overflow-wrap: anywhere;\n  word-break: break-word;\n}\n.term-form {\n  display: flex;\n  align-items: center;\n  gap: 6px;\n  padding: 6px 12px;\n  border-top: 1px solid var(--rule);\n  background: var(--paper);\n}\n.term-prompt {\n  font-family: var(--mono);\n  font-size: 13px;\n  font-weight: 600;\n  color: var(--ink);\n  user-select: none;\n}\n.term-input {\n  flex: 1;\n  font-family: var(--mono);\n  font-size: 12.5px;\n  color: var(--ink);\n  background: transparent;\n  border: none;\n  outline: none;\n  min-height: 28px;\n}\n.side-pane {\n  display: flex;\n  flex-direction: column;\n  gap: 16px;\n}\n.panel {\n  padding-bottom: 14px;\n  border-bottom: 1px solid var(--rule);\n}\n.panel:last-child {\n  border-bottom: none;\n}\n.panel-header {\n  display: flex;\n  justify-content: space-between;\n  align-items: baseline;\n  margin-bottom: 8px;\n}\n.panel-title {\n  margin: 0 0 8px;\n  font: 600 13px/1.3 var(--sans);\n  color: var(--ink);\n}\n.exercise-list {\n  list-style: none;\n  padding: 0;\n  margin: 0;\n}\n.exercise-list li {\n  display: flex;\n  align-items: flex-start;\n  gap: 8px;\n  padding: 3px 0;\n  font: 400 13px/1.3 var(--sans);\n  color: var(--ink-2);\n}\n.exercise-list .check {\n  font-family: var(--mono);\n  font-size: 13px;\n  font-weight: 600;\n  line-height: 1;\n  color: var(--ink-2);\n}\n.exercise-list li.done {\n  color: var(--ink);\n  font-weight: 500;\n}\n.exercise-list li.done .check {\n  color: var(--ink);\n}\n.hint-bar {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  margin-top: 8px;\n  padding-top: 8px;\n  border-top: 1px solid var(--rule);\n}\n.hint-text {\n  font: italic 400 12px/1.3 var(--serif);\n  color: var(--ink-2);\n  flex: 1;\n}\n.mtls-status {\n  font: 400 12px/1.3 var(--sans);\n  color: var(--ink-2);\n}\n.mtls-status.strict {\n  font-weight: 600;\n  color: var(--ink);\n}\n.mesh-svg {\n  display: block;\n  width: 100%;\n  height: auto;\n  max-height: 190px;\n  background: var(--paper);\n}\n.traffic-stats {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 4px 12px;\n  margin-top: 8px;\n  font: 400 12px/1.3 var(--sans);\n  color: var(--ink-2);\n}\n.traffic-stats b {\n  font-weight: 600;\n  color: var(--ink);\n  font-variant-numeric: tabular-nums;\n}\n@media (pointer: coarse) {\n  .bar-actions button, .hint-bar button {\n    min-height: 44px;\n    padding: 8px 14px;\n  }\n  .term-input {\n    min-height: 44px;\n  }\n}",
  "js": "(function () {\n\nvar BUILTIN_MANIFESTS = {\n  'destination-rule.yaml': 'apiVersion: networking.istio.io/v1alpha3\\nkind: DestinationRule\\nmetadata:\\n  name: reviews\\n  namespace: shop\\nspec:\\n  host: reviews\\n  subsets:\\n  - name: v1\\n    labels:\\n      version: v1\\n  - name: v2\\n    labels:\\n      version: v2\\n  - name: v3\\n    labels:\\n      version: v3',\n  'reviews-v2-v3-split.yaml': 'apiVersion: networking.istio.io/v1alpha3\\nkind: VirtualService\\nmetadata:\\n  name: reviews\\n  namespace: shop\\nspec:\\n  hosts:\\n  - reviews\\n  http:\\n  - route:\\n    - destination:\\n        host: reviews\\n        subset: v2\\n      weight: 50\\n    - destination:\\n        host: reviews\\n        subset: v3\\n      weight: 50',\n  'fault-delay.yaml': 'apiVersion: networking.istio.io/v1alpha3\\nkind: VirtualService\\nmetadata:\\n  name: ratings\\n  namespace: shop\\nspec:\\n  hosts:\\n  - ratings\\n  http:\\n  - fault:\\n      delay:\\n        percentage:\\n          value: 20\\n        fixedDelay: 2s\\n    route:\\n    - destination:\\n        host: ratings\\n        subset: v1',\n  'mtls-strict.yaml': 'apiVersion: security.istio.io/v1beta1\\nkind: PeerAuthentication\\nmetadata:\\n  name: default\\n  namespace: shop\\nspec:\\n  mtls:\\n    mode: STRICT'\n};\n\nvar RESOURCE_KINDS = {\n  deploy: 'deployment', deployment: 'deployment', deployments: 'deployment',\n  'deploy.apps': 'deployment', 'deployment.apps': 'deployment', 'deployments.apps': 'deployment',\n  po: 'pod', pod: 'pod', pods: 'pod',\n  svc: 'service', service: 'service', services: 'service',\n  ns: 'namespace', namespace: 'namespace', namespaces: 'namespace',\n  rs: 'replicaset', replicaset: 'replicaset', replicasets: 'replicaset', 'replicasets.apps': 'replicaset',\n  no: 'node', node: 'node', nodes: 'node',\n  vs: 'virtualservice', virtualservice: 'virtualservice', virtualservices: 'virtualservice',\n  'virtualservice.networking.istio.io': 'virtualservice', 'virtualservices.networking.istio.io': 'virtualservice',\n  dr: 'destinationrule', destinationrule: 'destinationrule', destinationrules: 'destinationrule',\n  'destinationrule.networking.istio.io': 'destinationrule', 'destinationrules.networking.istio.io': 'destinationrule',\n  pa: 'peerauthentication', peerauthentication: 'peerauthentication', peerauthentications: 'peerauthentication',\n  'peerauthentication.security.istio.io': 'peerauthentication', 'peerauthentications.security.istio.io': 'peerauthentication',\n  gw: 'gateway', gateway: 'gateway', gateways: 'gateway',\n  'gateway.networking.istio.io': 'gateway', 'gateways.networking.istio.io': 'gateway'\n};\n\nvar ERROR_TYPE_NAMES = {\n  deployment: 'deployments.apps', pod: 'pods', service: 'services', namespace: 'namespaces',\n  replicaset: 'replicasets.apps', node: 'nodes', virtualservice: 'virtualservices.networking.istio.io',\n  destinationrule: 'destinationrules.networking.istio.io', peerauthentication: 'peerauthentications.security.istio.io',\n  gateway: 'gateways.networking.istio.io'\n};\n\nfunction formatAge(seconds) {\n  if (seconds < 60) return Math.max(0, Math.floor(seconds)) + 's';\n  if (seconds < 3600) return Math.floor(seconds / 60) + 'm';\n  if (seconds < 86400) return Math.floor(seconds / 3600) + 'h';\n  return Math.floor(seconds / 86400) + 'd';\n}\n\nfunction formatColumns(headers, rows) {\n  if (rows.length === 0) return headers.join('   ');\n  var colCount = headers.length;\n  var colWidths = headers.map(function (h) { return h.length; });\n  for (var r = 0; r < rows.length; r++) {\n    for (var c = 0; c < colCount; c++) {\n      var cell = rows[r][c] || '';\n      if (cell.length > colWidths[c]) colWidths[c] = cell.length;\n    }\n  }\n  function renderRow(cells) {\n    return cells.map(function (cell, idx) {\n      if (idx === colCount - 1) return cell;\n      var w = colWidths[idx] || 0;\n      var str = cell || '';\n      while (str.length < w + 3) str += ' ';\n      return str;\n    }).join('');\n  }\n  return [renderRow(headers)].concat(rows.map(renderRow)).join('\\n');\n}\n\nfunction createInitialCluster() {\n  var baseAge = 2520;\n  function mkDep(name, app, ver, ns, img, cont) {\n    ns = ns || \"shop\";\n    return {\n      name: name, namespace: ns, replicas: 1, readyReplicas: 1, updatedReplicas: 1, availableReplicas: 1,\n      image: img || (\"docker.io/istio/examples-bookinfo-\" + name + \":1.19.0\"),\n      containers: cont || [app], labels: { app: app, version: ver }, selector: { app: app, version: ver },\n      sidecarInjected: false, ageSeconds: baseAge\n    };\n  }\n  function mkPod(name, depName, ip, node, app, ver, ns, cont) {\n    ns = ns || \"shop\";\n    return {\n      name: name, namespace: ns, ready: \"1/1\", status: \"Running\", restarts: 0, ageSeconds: baseAge,\n      ip: ip, node: node, labels: { app: app, version: ver }, sidecar: false, deploymentName: depName,\n      containerNames: cont || [app]\n    };\n  }\n  function mkSvc(name, ip, ns, type, ports) {\n    ns = ns || \"shop\";\n    return { name: name, namespace: ns, type: type || \"ClusterIP\", clusterIP: ip, externalIP: \"<none>\", ports: ports || \"9080/TCP\", selector: { app: name }, ageSeconds: baseAge };\n  }\n  function mkRs(name, dep, ns) {\n    return { name: name, namespace: ns || \"shop\", desired: 1, current: 1, ready: 1, deploymentName: dep, ageSeconds: baseAge };\n  }\n\n  return {\n    currentNamespace: \"shop\",\n    namespaces: {\n      \"default\": { name: \"default\", status: \"Active\", labels: { \"kubernetes.io/metadata.name\": \"default\" }, ageSeconds: baseAge },\n      \"istio-system\": { name: \"istio-system\", status: \"Active\", labels: { \"kubernetes.io/metadata.name\": \"istio-system\" }, ageSeconds: baseAge },\n      \"shop\": { name: \"shop\", status: \"Active\", labels: { \"kubernetes.io/metadata.name\": \"shop\" }, ageSeconds: baseAge }\n    },\n    nodes: [\n      { name: \"node-1\", status: \"Ready\", roles: \"control-plane,worker\", ageSeconds: baseAge, version: \"v1.28.2\" },\n      { name: \"node-2\", status: \"Ready\", roles: \"worker\", ageSeconds: baseAge, version: \"v1.28.2\" }\n    ],\n    deployments: {\n      \"details-v1\": mkDep(\"details-v1\", \"details\", \"v1\"),\n      \"productpage-v1\": mkDep(\"productpage-v1\", \"productpage\", \"v1\"),\n      \"ratings-v1\": mkDep(\"ratings-v1\", \"ratings\", \"v1\"),\n      \"reviews-v1\": mkDep(\"reviews-v1\", \"reviews\", \"v1\"),\n      \"reviews-v2\": mkDep(\"reviews-v2\", \"reviews\", \"v2\"),\n      \"reviews-v3\": mkDep(\"reviews-v3\", \"reviews\", \"v3\"),\n      \"istiod\": mkDep(\"istiod\", \"istiod\", \"pilot\", \"istio-system\", \"docker.io/istio/pilot:1.22.0\", [\"discovery\"]),\n      \"istio-ingressgateway\": mkDep(\"istio-ingressgateway\", \"istio-ingressgateway\", \"ingressgateway\", \"istio-system\", \"docker.io/istio/proxyv2:1.22.0\", [\"istio-proxy\"])\n    },\n    services: {\n      details: mkSvc(\"details\", \"10.96.10.2\"),\n      productpage: mkSvc(\"productpage\", \"10.96.10.1\"),\n      ratings: mkSvc(\"ratings\", \"10.96.10.3\"),\n      reviews: mkSvc(\"reviews\", \"10.96.10.4\"),\n      istiod: mkSvc(\"istiod\", \"10.96.0.10\", \"istio-system\", \"ClusterIP\", \"15010/TCP,15012/TCP,443/TCP\"),\n      \"istio-ingressgateway\": { name: \"istio-ingressgateway\", namespace: \"istio-system\", type: \"LoadBalancer\", clusterIP: \"10.96.0.20\", externalIP: \"192.168.1.100\", ports: \"80:30080/TCP,443:30443/TCP\", selector: { app: \"istio-ingressgateway\" }, ageSeconds: baseAge }\n    },\n    replicaSets: {\n      \"details-v1-66b6955995\": mkRs(\"details-v1-66b6955995\", \"details-v1\"),\n      \"productpage-v1-564d4686f\": mkRs(\"productpage-v1-564d4686f\", \"productpage-v1\"),\n      \"ratings-v1-6484c4d8bb\": mkRs(\"ratings-v1-6484c4d8bb\", \"ratings-v1\"),\n      \"reviews-v1-55b668b89\": mkRs(\"reviews-v1-55b668b89\", \"reviews-v1\"),\n      \"reviews-v2-68c5b5dd7b\": mkRs(\"reviews-v2-68c5b5dd7b\", \"reviews-v2\"),\n      \"reviews-v3-74b888b5dc\": mkRs(\"reviews-v3-74b888b5dc\", \"reviews-v3\"),\n      \"istiod-75f8f5c98d\": mkRs(\"istiod-75f8f5c98d\", \"istiod\", \"istio-system\"),\n      \"istio-ingressgateway-6d8fc9f4\": mkRs(\"istio-ingressgateway-6d8fc9f4\", \"istio-ingressgateway\", \"istio-system\")\n    },\n    pods: [\n      mkPod(\"details-v1-66b6955995-m8q2x\", \"details-v1\", \"10.244.1.12\", \"node-2\", \"details\", \"v1\"),\n      mkPod(\"productpage-v1-564d4686f-9zk4b\", \"productpage-v1\", \"10.244.0.18\", \"node-1\", \"productpage\", \"v1\"),\n      mkPod(\"ratings-v1-6484c4d8bb-s8w5k\", \"ratings-v1\", \"10.244.1.14\", \"node-2\", \"ratings\", \"v1\"),\n      mkPod(\"reviews-v1-55b668b89-h6dfk\", \"reviews-v1\", \"10.244.0.22\", \"node-1\", \"reviews\", \"v1\"),\n      mkPod(\"reviews-v2-68c5b5dd7b-p7m2s\", \"reviews-v2\", \"10.244.1.25\", \"node-2\", \"reviews\", \"v2\"),\n      mkPod(\"reviews-v3-74b888b5dc-d4j8w\", \"reviews-v3\", \"10.244.0.31\", \"node-1\", \"reviews\", \"v3\"),\n      mkPod(\"istiod-75f8f5c98d-j2k9l\", \"istiod\", \"10.244.0.5\", \"node-1\", \"istiod\", \"pilot\", \"istio-system\", [\"discovery\"]),\n      mkPod(\"istio-ingressgateway-6d8fc9f4-x9q7s\", \"istio-ingressgateway\", \"10.244.1.8\", \"node-2\", \"istio-ingressgateway\", \"ingressgateway\", \"istio-system\", [\"istio-proxy\"])\n    ],\n    virtualServices: {},\n    destinationRules: {},\n    peerAuthentications: {},\n    gateways: {\n      \"bookinfo-gateway\": { name: \"bookinfo-gateway\", namespace: \"shop\", hosts: [\"*\"], port: 80, ageSeconds: baseAge }\n    },\n    traffic: {\n      total: 0, productpage: 0, details: 0, reviewsV1: 0, reviewsV2: 0, reviewsV3: 0, ratings: 0,\n      lastSplitV1: 34, lastSplitV2: 33, lastSplitV3: 33, avgLatencyMs: 12\n    },\n    elapsedSeconds: 0\n  };\n}\nfunction parseCommand(line) {\n  var trimmed = line.trim();\n  var execParts = trimmed.split(/\\s+--\\s+/);\n  var mainPart = execParts[0] || '';\n  var execExtra = execParts.length > 1 ? execParts.slice(1).join(' -- ').trim().split(/\\s+/) : undefined;\n\n  var rawTokens = mainPart.split(/\\s+/).filter(Boolean);\n  if (rawTokens.length === 0) {\n    return { raw: trimmed, tool: '', subCmd: '', args: [], flags: {}, execExtra: execExtra };\n  }\n\n  var tool = rawTokens[0];\n  var flags = {};\n  var positional = [];\n\n  for (var i = 1; i < rawTokens.length; i++) {\n    var token = rawTokens[i];\n    if (token.indexOf('--') === 0) {\n      var eqIdx = token.indexOf('=');\n      if (eqIdx !== -1) {\n        flags[token.slice(2, eqIdx)] = token.slice(eqIdx + 1);\n      } else {\n        var k = token.slice(2);\n        var next = rawTokens[i + 1];\n        if (next && next.indexOf('-') !== 0) {\n          flags[k] = next;\n          i++;\n        } else {\n          flags[k] = true;\n        }\n      }\n    } else if (token.indexOf('-') === 0) {\n      var eqIdx2 = token.indexOf('=');\n      if (eqIdx2 !== -1) {\n        flags[token.slice(1, eqIdx2)] = token.slice(eqIdx2 + 1);\n      } else {\n        var k2 = token.slice(1);\n        if (k2 === 'A') {\n          flags['A'] = true;\n        } else {\n          var next2 = rawTokens[i + 1];\n          if (next2 && next2.indexOf('-') !== 0) {\n            flags[k2] = next2;\n            i++;\n          } else {\n            flags[k2] = true;\n          }\n        }\n      }\n    } else {\n      positional.push(token);\n    }\n  }\n\n  return { raw: trimmed, tool: tool, subCmd: positional[0] || '', args: positional.slice(1), flags: flags, execExtra: execExtra };\n}\n\nfunction resolveNamespace(state, flags) {\n  if (flags['A'] || flags['all-namespaces']) return null;\n  var ns = flags['n'] || flags['namespace'];\n  if (ns && typeof ns = 'string') return ns;\n  return state.currentNamespace;\n}\n\nfunction parseResourceTargets(args) {\n  if (!args || args.length = 0) return { error: 'you must specify a resource' };\n\n  if (args[0].indexOf(',') ! -1 && args[0].indexOf('/') = -1) {\n    var types = args[0].split(',');\n    var multiTargets = [];\n    for (var m = 0; m < types.length; m++) {\n      var rawM = types[m].toLowerCase();\n      var kM = RESOURCE_KINDS[rawM];\n      if (!kM) return { errorType: types[m] };\n      multiTargets.push({ kind: kM, name: null, rawType: rawM });\n    }\n    return { targets: multiTargets };\n  }\n\n  if (args[0].indexOf('/') !== -1) {\n    var targets = [];\n    for (var i = 0; i < args.length; i++) {\n      var slashIdx = args[i].indexOf('/');\n      if (slashIdx === -1) {\n        var spelling = args[i].toLowerCase();\n        var k = RESOURCE_KINDS[spelling];\n        if (!k) return { errorType: args[i] };\n        targets.push({ kind: k, name: null, rawType: spelling });\n      } else {\n        var rawT = args[i].slice(0, slashIdx).toLowerCase();\n        var name = args[i].slice(slashIdx + 1);\n        var k2 = RESOURCE_KINDS[rawT];\n        if (!k2) return { errorType: rawT };\n        targets.push({ kind: k2, name: name, rawType: rawT });\n      }\n    }\n    return { targets: targets };\n  }\n\n  var rawType = args[0].toLowerCase();\n  var kind = RESOURCE_KINDS[rawType];\n  if (!kind) return { errorType: args[0] };\n\n  var names = args.slice(1);\n  if (names.length === 0) return { targets: [{ kind: kind, name: null, rawType: rawType }] };\n\n  var resTargets = [];\n  for (var j = 0; j < names.length; j++) {\n    var n = names[j];\n    if (n.indexOf('/') !== -1) n = n.slice(n.indexOf('/') + 1);\n    resTargets.push({ kind: kind, name: n, rawType: rawType });\n  }\n  return { targets: resTargets };\n}\n\nfunction checkExercises(state) {\n  var step1 = Boolean(state.namespaces['shop'] && state.namespaces['shop'].labels['istio-injection'] === 'enabled');\n  var reviewsPods = state.pods.filter(function (p) { return p.namespace = 'shop' && p.deploymentName.indexOf('reviews-') = 0; });\n  var step2 = Boolean(step1 && reviewsPods.length >= 3 && reviewsPods.every(function (p) { return p.sidecar && p.ready = '2/2' && p.status = 'Running'; }));\n\n  var hasDr = Boolean(state.destinationRules['reviews'] && state.destinationRules['reviews'].subsets);\n  var reviewsVs = state.virtualServices['reviews'];\n  var hasSplit = Boolean(reviewsVs && reviewsVs.routes && reviewsVs.routes.some(function (r) { return r.subset = 'v2' && r.weight = 50; }) && reviewsVs.routes.some(function (r) { return r.subset = 'v3' && r.weight = 50; }));\n  var step3 = Boolean(hasDr && hasSplit);\n\n  var ratingsVs = state.virtualServices['ratings'];\n  var step4 = Boolean(ratingsVs && ratingsVs.fault && ratingsVs.fault.delaySeconds = 2 && ratingsVs.fault.delayPercentage = 20);\n\n  var defaultPa = state.peerAuthentications['default'];\n  var step5 = Boolean(defaultPa && defaultPa.namespace = 'shop' && defaultPa.mode = 'STRICT');\n\n  return [step1, step2, step3, step4, step5];\n}\n\nvar SERVICE_GRAPH = [\n  { from: 'ingress', to: 'productpage' },\n  { from: 'productpage', to: 'details' },\n  { from: 'productpage', to: 'reviews-v1' },\n  { from: 'productpage', to: 'reviews-v2' },\n  { from: 'productpage', to: 'reviews-v3' },\n  { from: 'reviews-v2', to: 'ratings' },\n  { from: 'reviews-v3', to: 'ratings' }\n];\n\nfunction getReviewsWeights(state) {\n  var rVs = state.virtualServices && state.virtualServices['reviews'];\n  if (rVs && rVs.routes && rVs.routes.length > 0) {\n    var rMap = {};\n    for (var i = 0; i < rVs.routes.length; i++) rMap[rVs.routes[i].subset] = rVs.routes[i].weight;\n    return {\n      v1: rMap['v1'] ! undefined ? rMap['v1'] : 0,\n      v2: rMap['v2'] ! undefined ? rMap['v2'] : 0,\n      v3: rMap['v3'] !== undefined ? rMap['v3'] : 0\n    };\n  }\n  return { v1: 34, v2: 33, v3: 33 };\n}\n\nfunction computeTopology(state) {\n  var weights = getReviewsWeights(state);\n  var isMtlsStrict = Boolean(state.peerAuthentications && state.peerAuthentications['default'] && state.peerAuthentications['default'].mode === 'STRICT');\n  var lockGlyph = isMtlsStrict ? ' 🔒' : '';\n  var faultActive = Boolean(state.virtualServices && state.virtualServices['ratings'] && state.virtualServices['ratings'].fault);\n\n  var nodes = [\n    { id: 'ingress', title: 'Ingress', sub: ':80', x: 10, y: 75, width: 54, height: 26 },\n    { id: 'productpage', title: 'productpage', sub: 'v1' + lockGlyph, x: 94, y: 74, width: 80, height: 28 },\n    { id: 'details', title: 'details-v1', sub: 'v1' + lockGlyph, x: 226, y: 10, width: 72, height: 26 },\n    { id: 'reviews-v1', title: 'reviews-v1', sub: 'v1' + lockGlyph, x: 226, y: 52, width: 72, height: 26 },\n    { id: 'reviews-v2', title: 'reviews-v2', sub: 'v2' + lockGlyph, x: 226, y: 94, width: 72, height: 26 },\n    { id: 'reviews-v3', title: 'reviews-v3', sub: 'v3' + lockGlyph, x: 226, y: 136, width: 72, height: 26 },\n    { id: 'ratings', title: 'ratings-v1', sub: 'v1' + (faultActive ? ' (2s delay)' : '') + lockGlyph, x: 308, y: 114, width: 66, height: 28 }\n  ];\n\n  var edges = [\n    { from: 'ingress', to: 'productpage', d: 'M 64 88 L 94 88' },\n    { from: 'productpage', to: 'details', d: 'M 174 80 L 186 23 L 226 23' },\n    { from: 'productpage', to: 'reviews-v1', d: 'M 174 84 L 186 65 L 226 65', weight: weights.v1, labelBox: { x: 191, y: 59, width: 24, height: 12, text: weights.v1 + '%' } },\n    { from: 'productpage', to: 'reviews-v2', d: 'M 174 88 L 186 107 L 226 107', weight: weights.v2, labelBox: { x: 191, y: 101, width: 24, height: 12, text: weights.v2 + '%' } },\n    { from: 'productpage', to: 'reviews-v3', d: 'M 174 92 L 186 149 L 226 149', weight: weights.v3, labelBox: { x: 191, y: 143, width: 24, height: 12, text: weights.v3 + '%' } },\n    { from: 'reviews-v2', to: 'ratings', d: 'M 298 107 L 308 122' },\n    { from: 'reviews-v3', to: 'ratings', d: 'M 298 149 L 308 134' }\n  ];\n\n  return {\n    viewBox: { width: 380, height: 180 },\n    nodes: nodes,\n    edges: edges\n  };\n}\n\nfunction simulateTraffic(state, count) {\n  if (typeof count !== 'number') count = 100;\n  state.traffic.total += count;\n\n  var weights = getReviewsWeights(state);\n  var splitV1 = weights.v1, splitV2 = weights.v2, splitV3 = weights.v3;\n\n  state.traffic.lastSplitV1 = splitV1;\n  state.traffic.lastSplitV2 = splitV2;\n  state.traffic.lastSplitV3 = splitV3;\n\n  state.traffic.productpage += count;\n  state.traffic.details += count;\n  state.traffic.reviewsV1 += Math.round(count * (splitV1 / 100));\n  state.traffic.reviewsV2 += Math.round(count * (splitV2 / 100));\n  state.traffic.reviewsV3 += Math.round(count * (splitV3 / 100));\n  state.traffic.ratings += Math.round(count * ((splitV2 + splitV3) / 100));\n\n  var ratingsVs = state.virtualServices['ratings'];\n  if (ratingsVs && ratingsVs.fault) {\n    var faultDelay = ratingsVs.fault.delaySeconds || 2;\n    var faultPct = ratingsVs.fault.delayPercentage || 20;\n    state.traffic.avgLatencyMs = Math.round(12 + (faultDelay * 1000 * (faultPct / 100)));\n  } else {\n    state.traffic.avgLatencyMs = 12;\n  }\n\n  var lines = [\n    'Simulated ' + count + ' incoming requests to productpage:9080/productpage',\n    '  productpage: ' + state.traffic.productpage + ' total',\n    '  details:     ' + state.traffic.details + ' total',\n    '  reviews v1:  ' + splitV1 + '% (' + state.traffic.reviewsV1 + ')',\n    '  reviews v2:  ' + splitV2 + '% (' + state.traffic.reviewsV2 + ')',\n    '  reviews v3:  ' + splitV3 + '% (' + state.traffic.reviewsV3 + ')',\n    '  ratings:     ' + state.traffic.ratings + ' total (avg latency ' + state.traffic.avgLatencyMs + 'ms)'\n  ];\n\n  return { output: lines.join('\\n'), counts: state.traffic };\n}\n\nfunction executeRolloutDep(state, dep, action) {\n  if (action = 'status') return { line: 'deployment \"' + dep.name + '\" successfully rolled out' };\n  if (action = 'restart') {\n    var isNsInjected = Boolean(state.namespaces[dep.namespace] && state.namespaces[dep.namespace].labels['istio-injection'] === 'enabled');\n    dep.sidecarInjected = isNsInjected;\n    state.pods = state.pods.filter(function (pod) { return pod.deploymentName !== dep.name; });\n    for (var i = 0; i < dep.replicas; i++) {\n      var randSuffix = Math.random().toString(36).slice(2, 7);\n      state.pods.push({\n        name: dep.name + '-rollout-' + randSuffix, namespace: dep.namespace,\n        ready: isNsInjected ? '2/2' : '1/1', status: 'Running', restarts: 0, ageSeconds: 1,\n        ip: '10.244.0.' + (20 + Math.floor(Math.random() * 40)), node: i % 2 = 0 ? 'node-1' : 'node-2',\n        labels: Object.assign({}, dep.labels), sidecar: isNsInjected, deploymentName: dep.name,\n        containerNames: isNsInjected ? dep.containers.concat(['istio-proxy']) : dep.containers.slice()\n      });\n    }\n    return { line: 'deployment.apps/' + dep.name + ' restarted' };\n  }\n  if (action = 'undo') {\n    if (dep.previousImage) { dep.image = dep.previousImage; dep.previousImage = undefined; }\n    for (var p = 0; p < state.pods.length; p++) {\n      if (state.pods[p].deploymentName === dep.name) {\n        state.pods[p].status = 'Running';\n        state.pods[p].ready = state.pods[p].sidecar ? '2/2' : '1/1';\n      }\n    }\n    return { line: 'deployment.apps/' + dep.name + ' rolled back' };\n  }\n}\n\nfunction handleKubectlRollout(state, p) {\n  var action = p.args[0];\n  if (action ! 'status' && action ! 'restart' && action !== 'undo') {\n    return { output: 'error: rollout action \"' + action + '\" not recognized. Supported: status, restart, undo', state: state, error: true };\n  }\n\n  var targetArgs = p.args.slice(1);\n  if (targetArgs.length === 0) return { output: 'error: you must specify the resource to ' + action, state: state, error: true };\n\n  var parsed = parseResourceTargets(targetArgs);\n  if (parsed.errorType) return { output: 'error: the server doesn\\'t have a resource type \"' + parsed.errorType + '\"', state: state, error: true };\n\n  var targets = parsed.targets || [];\n  var ns = resolveNamespace(state, p.flags) || state.currentNamespace || 'shop';\n  var outputs = [];\n\n  for (var i = 0; i < targets.length; i++) {\n    var t = targets[i];\n    if (t.kind ! 'deployment') {\n      return { output: 'error: cannot ' + action + ' ' + t.rawType + ': the server doesn\\'t have a resource type \"' + t.rawType + '\"', state: state, error: true };\n    }\n\n    if (t.name = null) {\n      var nsDeps = Object.values(state.deployments).filter(function (d) { return d.namespace = ns; });\n      if (nsDeps.length = 0) outputs.push('No resources found in ' + ns + ' namespace.');\n      else {\n        for (var d = 0; d < nsDeps.length; d++) outputs.push(executeRolloutDep(state, nsDeps[d], action).line);\n      }\n    } else {\n      var dep = state.deployments[t.name] || Object.values(state.deployments).find(function (d) { return d.namespace = ns && d.name = t.name; });\n      if (!dep) return { output: 'Error from server (NotFound): deployments.apps \"' + t.name + '\" not found', state: state, error: true };\n      outputs.push(executeRolloutDep(state, dep, action).line);\n    }\n  }\n\n  return { output: outputs.join('\\n'), state: state };\n}\n\nfunction handleKubectlScale(state, p) {\n  var rawReplicas = p.flags['replicas'];\n  if (rawReplicas === undefined) return { output: 'error: --replicas is required', state: state, error: true };\n  var replicas = parseInt(String(rawReplicas), 10);\n  if (isNaN(replicas) || replicas < 0) return { output: 'error: invalid replicas count: ' + rawReplicas, state: state, error: true };\n  if (p.args.length === 0) return { output: 'error: you must specify a resource to scale', state: state, error: true };\n\n  var parsed = parseResourceTargets(p.args);\n  if (parsed.errorType) return { output: 'error: the server doesn\\'t have a resource type \"' + parsed.errorType + '\"', state: state, error: true };\n\n  var targets = parsed.targets || [];\n  var ns = resolveNamespace(state, p.flags) || state.currentNamespace || 'shop';\n  var outputs = [];\n\n  for (var i = 0; i < targets.length; i++) {\n    var t = targets[i];\n    if (t.kind !== 'deployment') {\n      return { output: 'error: cannot scale ' + t.rawType + ': the server doesn\\'t have a resource type \"' + t.rawType + '\"', state: state, error: true };\n    }\n    if (!t.name) return { output: 'error: you must specify a deployment name to scale', state: state, error: true };\n    var dep = state.deployments[t.name] || Object.values(state.deployments).find(function (d) { return d.namespace = ns && d.name = t.name; });\n    if (!dep) return { output: 'Error from server (NotFound): deployments.apps \"' + t.name + '\" not found', state: state, error: true };\n\n    var oldReplicas = dep.replicas;\n    dep.replicas = replicas;\n    dep.readyReplicas = replicas;\n    dep.availableReplicas = replicas;\n    dep.updatedReplicas = replicas;\n\n    if (replicas > oldReplicas) {\n      var isNsInjected = Boolean(state.namespaces[dep.namespace] && state.namespaces[dep.namespace].labels['istio-injection'] === 'enabled');\n      for (var r = oldReplicas; r < replicas; r++) {\n        var randSuffix = Math.random().toString(36).slice(2, 7);\n        state.pods.push({\n          name: dep.name + '-scale-' + randSuffix, namespace: dep.namespace,\n          ready: isNsInjected && dep.sidecarInjected ? '2/2' : '1/1', status: 'Running', restarts: 0, ageSeconds: 1,\n          ip: '10.244.1.' + (30 + Math.floor(Math.random() * 40)), node: r % 2 === 0 ? 'node-1' : 'node-2',\n          labels: Object.assign({}, dep.labels), sidecar: isNsInjected && dep.sidecarInjected, deploymentName: dep.name,\n          containerNames: isNsInjected && dep.sidecarInjected ? dep.containers.concat(['istio-proxy']) : dep.containers.slice()\n        });\n      }\n    } else if (replicas < oldReplicas) {\n      var toRemove = oldReplicas - replicas;\n      var removedCount = 0;\n      state.pods = state.pods.filter(function (pod) {\n        if (pod.deploymentName === dep.name && removedCount < toRemove) {\n          removedCount++;\n          return false;\n        }\n        return true;\n      });\n    }\n\n    outputs.push('deployment.apps/' + dep.name + ' scaled');\n  }\n\n  return { output: outputs.join('\\n'), state: state };\n}\n\nfunction handleKubectlSet(state, p) {\n  var sub = p.args[0];\n  if (sub !== 'image') return { output: 'error: set \"' + sub + '\" not supported. Use \"kubectl set image deploy/<name> <container>=<image>\"', state: state, error: true };\n\n  var rest = p.args.slice(1);\n  if (rest.length === 0) return { output: 'error: you must specify a resource and container=image spec', state: state, error: true };\n\n  var specIdx = -1;\n  for (var i = 0; i < rest.length; i++) {\n    if (rest[i].indexOf('=') !== -1) { specIdx = i; break; }\n  }\n  if (specIdx === -1) return { output: 'error: you must specify a container image in format <container>=<image>', state: state, error: true };\n\n  var targetArgs = rest.slice(0, specIdx);\n  var spec = rest[specIdx];\n  var specParts = spec.split('=');\n  var containerName = specParts[0];\n  var newImage = specParts[1];\n\n  var parsed = parseResourceTargets(targetArgs);\n  if (parsed.errorType) return { output: 'error: the server doesn\\'t have a resource type \"' + parsed.errorType + '\"', state: state, error: true };\n\n  var targets = parsed.targets || [];\n  if (targets.length === 0 || !targets[0].name) return { output: 'error: you must specify a deployment name', state: state, error: true };\n\n  var target = targets[0];\n  if (target.kind !== 'deployment') return { output: 'error: cannot set image on ' + target.rawType + ': the server doesn\\'t have a resource type \"' + target.rawType + '\"', state: state, error: true };\n\n  var ns = resolveNamespace(state, p.flags) || state.currentNamespace || 'shop';\n  var dep = state.deployments[target.name] || Object.values(state.deployments).find(function (d) { return d.namespace = ns && d.name = target.name; });\n  if (!dep) return { output: 'Error from server (NotFound): deployments.apps \"' + target.name + '\" not found', state: state, error: true };\n\n  dep.previousImage = dep.image;\n  dep.image = newImage;\n\n  var isBroken = newImage.indexOf('bad') ! -1 || newImage.indexOf('invalid') ! -1 || newImage.indexOf('fail') !== -1;\n  for (var podIdx = 0; podIdx < state.pods.length; podIdx++) {\n    var pod = state.pods[podIdx];\n    if (pod.deploymentName === dep.name) {\n      if (isBroken) {\n        pod.status = 'ImagePullBackOff';\n        pod.ready = pod.sidecar ? '0/2' : '0/1';\n      } else {\n        pod.status = 'Running';\n        pod.ready = pod.sidecar ? '2/2' : '1/1';\n      }\n    }\n  }\n\n  return { output: 'deployment.apps/' + dep.name + ' image updated', state: state };\n}\n\nfunction handleKubectlDelete(state, p) {\n  var file = p.flags['f'] || p.flags['filename'];\n  if (file) {\n    var manifestName = String(file);\n    if (!BUILTIN_MANIFESTS[manifestName]) return { output: 'error: the path \"' + manifestName + '\" does not exist', state: state, error: true };\n    if (manifestName = 'destination-rule.yaml') { delete state.destinationRules['reviews']; return { output: 'destinationrule.networking.istio.io \"reviews\" deleted', state: state }; }\n    if (manifestName = 'reviews-v2-v3-split.yaml') { delete state.virtualServices['reviews']; return { output: 'virtualservice.networking.istio.io \"reviews\" deleted', state: state }; }\n    if (manifestName = 'fault-delay.yaml') { if (state.virtualServices['ratings']) delete state.virtualServices['ratings'].fault; return { output: 'virtualservice.networking.istio.io \"ratings\" deleted', state: state }; }\n    if (manifestName = 'mtls-strict.yaml') { delete state.peerAuthentications['default']; return { output: 'peerauthentication.security.istio.io \"default\" deleted', state: state }; }\n  }\n\n  if (p.args.length === 0) return { output: 'error: you must specify a resource to delete', state: state, error: true };\n\n  var parsed = parseResourceTargets(p.args);\n  if (parsed.errorType) return { output: 'error: the server doesn\\'t have a resource type \"' + parsed.errorType + '\"', state: state, error: true };\n\n  var targets = parsed.targets || [];\n  var ns = resolveNamespace(state, p.flags) || state.currentNamespace || 'shop';\n  var outputs = [];\n\n  for (var i = 0; i < targets.length; i++) {\n    var t = targets[i];\n    if (!t.name) return { output: 'error: you must specify a resource name to delete', state: state, error: true };\n\n    if (t.kind === 'pod') {\n      var podIdx = state.pods.findIndex(function (pod) { return (pod.namespace = ns || ns = null) && pod.name = t.name; });\n      if (podIdx = -1) return { output: 'Error from server (NotFound): pods \"' + t.name + '\" not found', state: state, error: true };\n      var oldPod = state.pods[podIdx];\n      state.pods.splice(podIdx, 1);\n      var depObj = state.deployments[oldPod.deploymentName];\n      if (depObj) {\n        var isNsInjected = Boolean(state.namespaces[oldPod.namespace] && state.namespaces[oldPod.namespace].labels['istio-injection'] === 'enabled');\n        var sidecar = Boolean(isNsInjected && depObj.sidecarInjected);\n        var randSuffix = Math.random().toString(36).slice(2, 7);\n        state.pods.push({\n          name: oldPod.deploymentName + '-repl-' + randSuffix, namespace: oldPod.namespace,\n          ready: sidecar ? '2/2' : '1/1', status: 'Running', restarts: 0, ageSeconds: 1,\n          ip: oldPod.ip, node: oldPod.node, labels: Object.assign({}, oldPod.labels), sidecar: sidecar,\n          deploymentName: oldPod.deploymentName, containerNames: sidecar ? depObj.containers.concat(['istio-proxy']) : depObj.containers.slice()\n        });\n      }\n      outputs.push('pod \"' + t.name + '\" deleted');\n    } else if (t.kind === 'deployment') {\n      var dep = state.deployments[t.name];\n      if (!dep) return { output: 'Error from server (NotFound): deployments.apps \"' + t.name + '\" not found', state: state, error: true };\n      delete state.deployments[t.name];\n      state.pods = state.pods.filter(function (p) { return p.deploymentName !== t.name; });\n      outputs.push('deployment.apps \"' + t.name + '\" deleted');\n    } else {\n      var mapKey = { virtualservice: 'virtualServices', destinationrule: 'destinationRules', peerauthentication: 'peerAuthentications', service: 'services' }[t.kind];\n      if (mapKey) {\n        if (!state[mapKey][t.name]) return { output: 'Error from server (NotFound): ' + ERROR_TYPE_NAMES[t.kind] + ' \"' + t.name + '\" not found', state: state, error: true };\n        delete state[mapKey][t.name];\n        outputs.push(ERROR_TYPE_NAMES[t.kind] + ' \"' + t.name + '\" deleted');\n      } else {\n        return { output: 'error: cannot delete ' + t.rawType + ': unsupported resource type', state: state, error: true };\n      }\n    }\n  }\n\n  return { output: outputs.join('\\n'), state: state };\n}\n\nfunction handleKubectlLabel(state, p) {\n  var specIdx = -1;\n  for (var i = 0; i < p.args.length; i++) {\n    if (p.args[i].indexOf('=') !== -1) { specIdx = i; break; }\n  }\n  if (specIdx === -1) return { output: 'error: you must specify a label in key=value format', state: state, error: true };\n\n  var targetArgs = p.args.slice(0, specIdx);\n  var spec = p.args[specIdx];\n  var eqIdx = spec.indexOf('=');\n  var key = spec.slice(0, eqIdx);\n  var val = spec.slice(eqIdx + 1);\n\n  var parsed = parseResourceTargets(targetArgs);\n  if (parsed.errorType) return { output: 'error: the server doesn\\'t have a resource type \"' + parsed.errorType + '\"', state: state, error: true };\n\n  var targets = parsed.targets || [];\n  if (targets.length === 0 || !targets[0].name) return { output: 'error: you must specify a resource name to label', state: state, error: true };\n\n  var target = targets[0];\n  if (target.kind === 'namespace') {\n    var nsObj = state.namespaces[target.name];\n    if (!nsObj) return { output: 'Error from server (NotFound): namespaces \"' + target.name + '\" not found', state: state, error: true };\n    nsObj.labels[key] = val;\n    return { output: 'namespace/' + target.name + ' labeled', state: state };\n  }\n\n  if (target.kind === 'pod') {\n    var podObj = state.pods.find(function (pod) { return pod.name === target.name; });\n    if (!podObj) return { output: 'Error from server (NotFound): pods \"' + target.name + '\" not found', state: state, error: true };\n    podObj.labels[key] = val;\n    return { output: 'pod/' + target.name + ' labeled', state: state };\n  }\n\n  return { output: 'error: cannot label ' + target.rawType + ': unsupported resource type', state: state, error: true };\n}\n\nfunction handleKubectlApply(state, p) {\n  var file = p.flags['f'] || p.flags['filename'] || p.args[0];\n  if (!file) return { output: 'error: must specify one of -f and -k', state: state, error: true };\n  var filename = String(file);\n  if (!BUILTIN_MANIFESTS[filename]) return { output: 'error: the path \"' + filename + '\" does not exist', state: state, error: true };\n\n  if (filename === 'destination-rule.yaml') {\n    state.destinationRules['reviews'] = {\n      name: 'reviews', namespace: 'shop', host: 'reviews',\n      subsets: [{ name: 'v1', labels: { version: 'v1' } }, { name: 'v2', labels: { version: 'v2' } }, { name: 'v3', labels: { version: 'v3' } }],\n      ageSeconds: 1\n    };\n    return { output: 'destinationrule.networking.istio.io/reviews created', state: state };\n  }\n\n  if (filename === 'reviews-v2-v3-split.yaml') {\n    state.virtualServices['reviews'] = {\n      name: 'reviews', namespace: 'shop', hosts: ['reviews'],\n      routes: [{ subset: 'v2', weight: 50 }, { subset: 'v3', weight: 50 }],\n      ageSeconds: 1\n    };\n    return { output: 'virtualservice.networking.istio.io/reviews configured', state: state };\n  }\n\n  if (filename === 'fault-delay.yaml') {\n    if (!state.virtualServices['ratings']) {\n      state.virtualServices['ratings'] = { name: 'ratings', namespace: 'shop', hosts: ['ratings'], routes: [{ subset: 'v1', weight: 100 }], ageSeconds: 1 };\n    }\n    state.virtualServices['ratings'].fault = { delayPercentage: 20, delaySeconds: 2 };\n    return { output: 'virtualservice.networking.istio.io/ratings configured', state: state };\n  }\n\n  if (filename === 'mtls-strict.yaml') {\n    state.peerAuthentications['default'] = { name: 'default', namespace: 'shop', mode: 'STRICT', ageSeconds: 1 };\n    return { output: 'peerauthentication.security.istio.io/default created', state: state };\n  }\n\n  return { output: 'applied ' + filename, state: state };\n}\n\nfunction handleKubectlLogs(state, p) {\n  if (p.args.length === 0) return { output: 'error: you must specify a pod to get logs from', state: state, error: true };\n  var parsed = parseResourceTargets(p.args);\n  if (parsed.errorType) return { output: 'error: the server doesn\\'t have a resource type \"' + parsed.errorType + '\"', state: state, error: true };\n  var targets = parsed.targets || [];\n  var podName = targets[0] && targets[0].name ? targets[0].name : p.args[0];\n  if (podName.indexOf('/') !== -1) podName = podName.slice(podName.indexOf('/') + 1);\n\n  var ns = resolveNamespace(state, p.flags) || state.currentNamespace || 'shop';\n  var pod = state.pods.find(function (item) { return (item.namespace = ns || ns = null) && item.name === podName; });\n  if (!pod) return { output: 'Error from server (NotFound): pods \"' + podName + '\" not found', state: state, error: true };\n\n  var cName = p.flags['c'] || p.flags['container'] || pod.containerNames[0];\n  if (cName = 'istio-proxy') {\n    return {\n      output: [\n        '2026-09-26T10:00:00.123456Z info  Envoy proxy initialized',\n        '2026-09-26T10:00:00.234567Z info  Pilot discovery connected: 10.96.0.10:15012',\n        '2026-09-26T10:00:00.345678Z info  CDS: route clusters synchronized',\n        '2026-09-26T10:00:00.456789Z info  mTLS: connection established using SPIFFE identity'\n      ].join('\\n'),\n      state: state\n    };\n  }\n\n  return {\n    output: [\n      '[Server] Listening on port 9080',\n      '[Route] GET /reviews/0 HTTP/1.1 200 OK',\n      '[Route] GET /ratings/0 HTTP/1.1 200 OK (latency: 14ms)'\n    ].join('\\n'),\n    state: state\n  };\n}\n\nfunction handleKubectlExec(state, p) {\n  if (p.args.length = 0) return { output: 'error: you must specify a pod to exec into', state: state, error: true };\n  var podTarget = p.args[0];\n  if (podTarget.indexOf('/') !== -1) podTarget = podTarget.slice(podTarget.indexOf('/') + 1);\n\n  var ns = resolveNamespace(state, p.flags) || state.currentNamespace || 'shop';\n  var pod = state.pods.find(function (item) { return (item.namespace = ns || ns = null) && item.name === podTarget; });\n  if (!pod) return { output: 'Error from server (NotFound): pods \"' + podTarget + '\" not found', state: state, error: true };\n\n  var rVs = state.virtualServices['reviews'];\n  var stars = '(no stars)';\n  if (rVs && rVs.routes && rVs.routes.some(function (r) { return r.subset === 'v2'; })) {\n    stars = '★★★★☆ (black stars from ratings)';\n  } else if (rVs && rVs.routes && rVs.routes.some(function (r) { return r.subset === 'v3'; })) {\n    stars = '★★★★★ (red stars from ratings)';\n  }\n\n  return {\n    output: [\n      'HTTP/1.1 200 OK', 'content-type: text/html; charset=utf-8', 'content-length: 1824', '',\n      '<!DOCTYPE html><html><title>Simple Bookstore App</title><body>',\n      '<h1>Product: The Comedy of Errors</h1>', '<p>Reviews: ' + stars + '</p>', '</body></html>'\n    ].join('\\n'),\n    state: state\n  };\n}\n\nfunction handleKubectlDescribe(state, p) {\n  if (p.args.length === 0) return { output: 'error: you must specify a resource to describe', state: state, error: true };\n  var parsed = parseResourceTargets(p.args);\n  if (parsed.errorType) return { output: 'error: the server doesn\\'t have a resource type \"' + parsed.errorType + '\"', state: state, error: true };\n  var targets = parsed.targets || [];\n  var ns = resolveNamespace(state, p.flags) || state.currentNamespace || 'shop';\n  var t = targets[0];\n  if (!t.name) return { output: 'error: you must specify a resource name', state: state, error: true };\n\n  if (t.kind === 'pod') {\n    var pod = state.pods.find(function (item) { return (item.namespace = ns || ns = null) && item.name = t.name; });\n    if (!pod) return { output: 'Error from server (NotFound): pods \"' + t.name + '\" not found', state: state, error: true };\n    return {\n      output: [\n        'Name:         ' + pod.name, 'Namespace:    ' + pod.namespace, 'Node:         ' + pod.node,\n        'Status:       ' + pod.status, 'IP:           ' + pod.ip, 'Containers:   ' + pod.containerNames.join(', '),\n        'Sidecar:      ' + (pod.sidecar ? 'injected' : 'none'), 'Ready:        ' + pod.ready\n      ].join('\\n'),\n      state: state\n    };\n  }\n\n  if (t.kind = 'deployment') {\n    var dep = state.deployments[t.name] || Object.values(state.deployments).find(function (d) { return (d.namespace = ns || ns = null) && d.name = t.name; });\n    if (!dep) return { output: 'Error from server (NotFound): deployments.apps \"' + t.name + '\" not found', state: state, error: true };\n    return {\n      output: [\n        'Name:                   ' + dep.name, 'Namespace:              ' + dep.namespace,\n        'Replicas:               ' + dep.replicas + ' desired | ' + dep.readyReplicas + ' ready',\n        'Image:                  ' + dep.image, 'Sidecar injection:      ' + (dep.sidecarInjected ? 'injected' : 'none')\n      ].join('\\n'),\n      state: state\n    };\n  }\n\n  if (t.kind = 'service') {\n    var svc = state.services[t.name];\n    if (!svc) return { output: 'Error from server (NotFound): services \"' + t.name + '\" not found', state: state, error: true };\n    return { output: ['Name: ' + svc.name, 'Namespace: ' + svc.namespace, 'Type: ' + svc.type, 'IP: ' + svc.clusterIP].join('\\n'), state: state };\n  }\n\n  if (t.kind === 'namespace') {\n    var nsObj = state.namespaces[t.name];\n    if (!nsObj) return { output: 'Error from server (NotFound): namespaces \"' + t.name + '\" not found', state: state, error: true };\n    return { output: ['Name: ' + nsObj.name, 'Status: ' + nsObj.status].join('\\n'), state: state };\n  }\n\n  if (t.kind === 'virtualservice') {\n    var vs = state.virtualServices[t.name];\n    if (!vs) return { output: 'Error from server (NotFound): virtualservices.networking.istio.io \"' + t.name + '\" not found', state: state, error: true };\n    return { output: ['Name: ' + vs.name, 'Hosts: ' + vs.hosts.join(',')].join('\\n'), state: state };\n  }\n\n  if (t.kind === 'destinationrule') {\n    var dr = state.destinationRules[t.name];\n    if (!dr) return { output: 'Error from server (NotFound): destinationrules.networking.istio.io \"' + t.name + '\" not found', state: state, error: true };\n    return { output: ['Name: ' + dr.name, 'Host: ' + dr.host].join('\\n'), state: state };\n  }\n\n  return { output: 'error: describe not supported for ' + t.rawType, state: state, error: true };\n}\n\nfunction handleKubectlGet(state, p) {\n  if (p.args.length === 0) return { output: 'error: You must specify the type of resource to get.', state: state, error: true };\n\n  var parsed = parseResourceTargets(p.args);\n  if (parsed.errorType) return { output: 'error: the server doesn\\'t have a resource type \"' + parsed.errorType + '\"', state: state, error: true };\n\n  var targets = parsed.targets || [];\n  var ns = resolveNamespace(state, p.flags);\n  var wide = Boolean(p.flags['o'] = 'wide' || p.flags['output'] = 'wide');\n  var nameOnly = Boolean(p.flags['o'] = 'name' || p.flags['output'] = 'name');\n  var selector = p.flags['l'] || p.flags['selector'];\n\n  function matchesSelector(labels) {\n    if (!selector) return true;\n    var parts = String(selector).split(',');\n    for (var k = 0; k < parts.length; k++) {\n      var pair = parts[k].split('=');\n      var key = (pair[0] || '').trim();\n      var val = (pair[1] || '').trim();\n      if (!labels[key] || labels[key] ! val) return false;\n    }\n    return true;\n  }\n\n  function renderTable(items, headers, rowFn, notFoundKind, targetName) {\n    if (targetName ! null && items.length = 0) {\n      return { err: 'Error from server (NotFound): ' + notFoundKind + ' \"' + targetName + '\" not found' };\n    }\n    if (items.length = 0) {\n      return { out: ns ? 'No resources found in ' + ns + ' namespace.' : 'No resources found.' };\n    }\n    if (nameOnly) {\n      return { out: items.map(function (it) { return it.name; }).join('\\n') };\n    }\n    return { out: formatColumns(headers, items.map(rowFn)) };\n  }\n\n  var outputs = [];\n\n  for (var tIdx = 0; tIdx < targets.length; tIdx++) {\n    var target = targets[tIdx];\n\n    if (target.kind === 'pod') {\n      var podList = state.pods.filter(function (item) {\n        if (ns ! null && item.namespace ! ns) return false;\n        if (!matchesSelector(item.labels)) return false;\n        if (target.name ! null && item.name ! target.name) return false;\n        return true;\n      });\n      var pHeaders = ns === null ? ['NAMESPACE', 'NAME', 'READY', 'STATUS', 'RESTARTS', 'AGE'] : ['NAME', 'READY', 'STATUS', 'RESTARTS', 'AGE'];\n      if (wide) pHeaders.push('IP', 'NODE');\n      var rP = renderTable(podList, pHeaders, function (pod) {\n        var row = ns = null\n          ? [pod.namespace, pod.name, pod.ready, pod.status, String(pod.restarts), formatAge(pod.ageSeconds)]\n          : [pod.name, pod.ready, pod.status, String(pod.restarts), formatAge(pod.ageSeconds)];\n        if (wide) row.push(pod.ip, pod.node);\n        return row;\n      }, 'pods', target.name);\n      if (rP.err) return { output: rP.err, state: state, error: true };\n      outputs.push(rP.out);\n    } else if (target.kind = 'deployment') {\n      var depList = Object.values(state.deployments).filter(function (item) {\n        if (ns ! null && item.namespace ! ns) return false;\n        if (target.name ! null && item.name ! target.name) return false;\n        return true;\n      });\n      var dHeaders = ns === null ? ['NAMESPACE', 'NAME', 'READY', 'UP-TO-DATE', 'AVAILABLE', 'AGE'] : ['NAME', 'READY', 'UP-TO-DATE', 'AVAILABLE', 'AGE'];\n      var rD = renderTable(depList, dHeaders, function (dep) {\n        var rStr = dep.readyReplicas + '/' + dep.replicas;\n        return ns = null\n          ? [dep.namespace, dep.name, rStr, String(dep.updatedReplicas), String(dep.availableReplicas), formatAge(dep.ageSeconds)]\n          : [dep.name, rStr, String(dep.updatedReplicas), String(dep.availableReplicas), formatAge(dep.ageSeconds)];\n      }, 'deployments.apps', target.name);\n      if (rD.err) return { output: rD.err, state: state, error: true };\n      outputs.push(rD.out);\n    } else if (target.kind = 'service') {\n      var svcList = Object.values(state.services).filter(function (item) {\n        if (ns ! null && item.namespace ! ns) return false;\n        if (target.name ! null && item.name ! target.name) return false;\n        return true;\n      });\n      var sHeaders = ns === null ? ['NAMESPACE', 'NAME', 'TYPE', 'CLUSTER-IP', 'EXTERNAL-IP', 'PORT(S)', 'AGE'] : ['NAME', 'TYPE', 'CLUSTER-IP', 'EXTERNAL-IP', 'PORT(S)', 'AGE'];\n      var rS = renderTable(svcList, sHeaders, function (svc) {\n        return ns = null\n          ? [svc.namespace, svc.name, svc.type, svc.clusterIP, svc.externalIP, svc.ports, formatAge(svc.ageSeconds)]\n          : [svc.name, svc.type, svc.clusterIP, svc.externalIP, svc.ports, formatAge(svc.ageSeconds)];\n      }, 'services', target.name);\n      if (rS.err) return { output: rS.err, state: state, error: true };\n      outputs.push(rS.out);\n    } else if (target.kind = 'namespace') {\n      var nsList = Object.values(state.namespaces).filter(function (item) {\n        if (target.name ! null && item.name ! target.name) return false;\n        return true;\n      });\n      var rN = renderTable(nsList, ['NAME', 'STATUS', 'AGE'], function (item) {\n        return [item.name, item.status, formatAge(item.ageSeconds)];\n      }, 'namespaces', target.name);\n      if (rN.err) return { output: rN.err, state: state, error: true };\n      outputs.push(rN.out);\n    } else {\n      var genericMap = {\n        virtualservice: { map: 'virtualServices', cols: ['NAME', 'GATEWAYS', 'HOSTS', 'AGE'], fn: function (vs) { return [vs.name, '[mesh]', vs.hosts.join(','), formatAge(vs.ageSeconds)]; } },\n        destinationrule: { map: 'destinationRules', cols: ['NAME', 'HOST', 'AGE'], fn: function (dr) { return [dr.name, dr.host, formatAge(dr.ageSeconds)]; } },\n        peerauthentication: { map: 'peerAuthentications', cols: ['NAME', 'MODE', 'AGE'], fn: function (pa) { return [pa.name, pa.mode, formatAge(pa.ageSeconds)]; } },\n        gateway: { map: 'gateways', cols: ['NAME', 'AGE'], fn: function (gw) { return [gw.name, formatAge(gw.ageSeconds)]; } },\n        node: { list: state.nodes, cols: ['NAME', 'STATUS', 'ROLES', 'AGE', 'VERSION'], fn: function (n) { return [n.name, n.status, n.roles, formatAge(n.ageSeconds), n.version]; } },\n        replicaset: { map: 'replicaSets', cols: ['NAME', 'DESIRED', 'CURRENT', 'READY', 'AGE'], fn: function (rs) { return [rs.name, String(rs.desired), String(rs.current), String(rs.ready), formatAge(rs.ageSeconds)]; } }\n      }[target.kind];\n\n      if (genericMap) {\n        var rawItems = genericMap.list || Object.values(state[genericMap.map]);\n        var filtered = rawItems.filter(function (item) {\n          if (genericMap.map && item.namespace && ns ! null && item.namespace ! ns) return false;\n          if (target.name ! null && item.name ! target.name) return false;\n          return true;\n        });\n        var resGen = renderTable(filtered, genericMap.cols, genericMap.fn, ERROR_TYPE_NAMES[target.kind], target.name);\n        if (resGen.err) return { output: resGen.err, state: state, error: true };\n        outputs.push(resGen.out);\n      }\n    }\n  }\n\n  return { output: outputs.join('\\n'), state: state };\n}\n\nfunction handleIstioctl(state, p) {\n  var sub = p.subCmd;\n  var ns = resolveNamespace(state, p.flags) || state.currentNamespace || 'shop';\n\n  if (sub = 'version') {\n    return {\n      output: [\n        'client version: 1.22.0',\n        'control plane version: 1.22.0',\n        'data plane version: 1.22.0 (8 proxies)'\n      ].join('\\n'),\n      state: state\n    };\n  }\n\n  if (sub = 'proxy-status' || sub === 'ps') {\n    var rows = [\n      ['istio-ingressgateway-6d8fc9f4-x9q7s.istio-system', 'SYNCED', 'SYNCED', 'SYNCED', 'SYNCED', 'istiod-75f8f5c98d-j2k9l', '1.22.0']\n    ];\n    for (var i = 0; i < state.pods.length; i++) {\n      var pod = state.pods[i];\n      if (pod.sidecar) {\n        rows.push([\n          pod.name + '.' + pod.namespace,\n          'SYNCED',\n          'SYNCED',\n          'SYNCED',\n          'SYNCED',\n          'istiod-75f8f5c98d-j2k9l',\n          '1.22.0'\n        ]);\n      }\n    }\n    return { output: formatColumns(['NAME', 'CDS', 'LDS', 'EDS', 'RDS', 'ISTIOD', 'VERSION'], rows), state: state };\n  }\n\n  if (sub === 'analyze') {\n    var targetNs = ns || 'shop';\n    var isNsInjected = Boolean(state.namespaces[targetNs] && state.namespaces[targetNs].labels['istio-injection'] === 'enabled');\n    if (!isNsInjected) {\n      return {\n        output: [\n          'Warning [IST0102] (Namespace ' + targetNs + ') The namespace is not enabled for Istio injection.',\n          '  Run \\'kubectl label namespace ' + targetNs + ' istio-injection=enabled\\' to enable automatic injection.',\n          'Info [IST0118] (Service details.shop) Port name details-port (service: details.shop) does not follow Istio naming convention.'\n        ].join('\\n'),\n        state: state\n      };\n    }\n    return { output: '✔ No validation issues found when analyzing namespace: ' + targetNs + '.', state: state };\n  }\n\n  if (sub = 'x' && p.args[0] = 'describe') {\n    var pName = p.args[2] || p.args[1] || '';\n    if (pName.indexOf('/') !== -1) pName = pName.slice(pName.indexOf('/') + 1);\n    var pPod = state.pods.find(function (item) { return (item.namespace = ns || ns = null) && item.name === pName; });\n    if (!pPod) return { output: 'Error: pod \"' + pName + '\" not found in namespace \"' + ns + '\"', state: state, error: true };\n    if (!pPod.sidecar) return { output: 'Pod ' + pPod.name + ' has no Istio sidecar injected.', state: state };\n    return {\n      output: [\n        'Pod: ' + pPod.name, 'Namespace: ' + pPod.namespace, 'Sidecar: istio-proxy (docker.io/istio/proxyv2:1.22.0)',\n        'Inbound Port: 9080/HTTP -> 127.0.0.1:9080', 'mTLS: PERMISSIVE (PeerAuthentication default.shop)',\n        'VirtualService: ' + (state.virtualServices['reviews'] ? 'reviews.shop (HTTP routes: v2=50%, v3=50%)' : 'none')\n      ].join('\\n'),\n      state: state\n    };\n  }\n\n  return { output: 'istioctl: unknown subcommand \"' + sub + '\". Try \"istioctl proxy-status\" or \"istioctl analyze\".', state: state, error: true };\n}\n\nfunction executeCommand(state, line) {\n  var p = parseCommand(line);\n  if (!p.tool) return { output: '', state: state };\n\n  if (p.tool = 'clear') return { output: '__CLEAR__', state: state };\n\n  if (p.tool = 'reset') {\n    var fresh = createInitialCluster();\n    Object.keys(state).forEach(function (k) { delete state[k]; });\n    Object.assign(state, fresh);\n    return { output: 'Cluster reset to initial state.', state: state };\n  }\n\n  if (p.tool === 'help') {\n    return {\n      output: [\n        'Supported commands:',\n        '  kubectl get pods|deploy|svc|ns|vs|dr|pa|nodes [-n ns|-A] [-o wide|name] [-l key=val]',\n        '  kubectl describe pod|deploy|svc|ns|vs|dr <name> [-n ns]',\n        '  kubectl logs <pod> [-c container] [-n ns]',\n        '  kubectl scale deploy <name> --replicas=N',\n        '  kubectl delete pod|deploy|svc <name> [-n ns]',\n        '  kubectl rollout status|restart|undo deploy/<name> [-n ns]',\n        '  kubectl set image deploy/<name> <container>=<image>',\n        '  kubectl label namespace <name> <key>=<val>',\n        '  kubectl apply -f <manifest.yaml>',\n        '  kubectl exec <pod> -- curl -s productpage:9080/productpage',\n        '  istioctl version | proxy-status | analyze [-n ns] | x describe pod <p>',\n        '  cat <manifest> | ls | hint | reset | clear'\n      ].join('\\n'),\n      state: state\n    };\n  }\n\n  if (p.tool = 'ls') {\n    return { output: 'destination-rule.yaml   reviews-v2-v3-split.yaml   fault-delay.yaml   mtls-strict.yaml', state: state };\n  }\n\n  if (p.tool = 'cat') {\n    var fileName = p.subCmd || p.args[0];\n    if (fileName && BUILTIN_MANIFESTS[fileName]) return { output: BUILTIN_MANIFESTS[fileName], state: state };\n    return { output: 'cat: ' + (fileName || 'missing') + ': No such file or directory', state: state, error: true };\n  }\n\n  if (p.tool === 'hint') {\n    var chks = checkExercises(state);\n    if (!chks[0]) return { output: 'Hint (Step 1): Reviews pods show 1/1 READY (no sidecar) because namespace \"shop\" is not labeled for automatic injection. Run:\\n  kubectl label namespace shop istio-injection=enabled', state: state };\n    if (!chks[1]) return { output: 'Hint (Step 2): Namespace injection is enabled, but running pods must be restarted to inject Envoy sidecars (2/2). Restart the deployments:\\n  kubectl rollout restart deployment reviews-v1 reviews-v2 reviews-v3', state: state };\n    if (!chks[2]) return { output: 'Hint (Step 3): Apply destination-rule.yaml and reviews-v2-v3-split.yaml to define subsets and split traffic 50/50 between v2 and v3:\\n  kubectl apply -f destination-rule.yaml\\n  kubectl apply -f reviews-v2-v3-split.yaml', state: state };\n    if (!chks[3]) return { output: 'Hint (Step 4): Apply fault-delay.yaml to introduce a 2s delay on 20% of ratings calls:\\n  kubectl apply -f fault-delay.yaml', state: state };\n    if (!chks[4]) return { output: 'Hint (Step 5): Apply mtls-strict.yaml to enforce mutual TLS across the shop namespace:\\n  kubectl apply -f mtls-strict.yaml', state: state };\n    return { output: 'All 5 guided exercises completed! Try traffic burst or kubectl exec curl.', state: state };\n  }\n\n  if (p.tool === 'kubectl') {\n    var sub = p.subCmd;\n    if (sub = 'get') return handleKubectlGet(state, p);\n    if (sub = 'describe') return handleKubectlDescribe(state, p);\n    if (sub = 'delete') return handleKubectlDelete(state, p);\n    if (sub = 'scale') return handleKubectlScale(state, p);\n    if (sub = 'rollout') return handleKubectlRollout(state, p);\n    if (sub = 'set') return handleKubectlSet(state, p);\n    if (sub = 'label') return handleKubectlLabel(state, p);\n    if (sub = 'apply') return handleKubectlApply(state, p);\n    if (sub = 'logs') return handleKubectlLogs(state, p);\n    if (sub = 'exec') return handleKubectlExec(state, p);\n    return { output: 'kubectl: unknown subcommand \"' + sub + '\". Type \"help\" for usage.', state: state, error: true };\n  }\n\n  if (p.tool === 'istioctl') return handleIstioctl(state, p);\n\n  return { output: p.tool + ': command not found. Type \"help\" for usage.', state: state, error: true };\n}\n\n\n  var root = document.querySelector('.kubectl-sim') || document.body;\n  var termLog = root.querySelector('#termLog');\n  var termInput = root.querySelector('#termInput');\n  var mtlsStatus = root.querySelector('#mtlsStatus');\n  var activeNs = root.querySelector('#activeNs');\n  var statTotal = root.querySelector('#statTotal');\n  var statV1 = root.querySelector('#statV1');\n  var statV2 = root.querySelector('#statV2');\n  var statV3 = root.querySelector('#statV3');\n  var statRatings = root.querySelector('#statRatings');\n  var statLatency = root.querySelector('#statLatency');\n  var meshSvg = root.querySelector('#meshSvg');\n  var hintOutput = root.querySelector('#hintOutput');\n\n  var btnLoad = root.querySelector('#btnLoad');\n  var btnReset = root.querySelector('#btnReset');\n  var btnHelp = root.querySelector('#btnHelp');\n  var btnHint = root.querySelector('#btnHint');\n\n  var cluster = createInitialCluster();\n  var history = [];\n  var historyIndex = 0;\n\n  function appendLog(line, type) {\n    if (!termLog) return;\n    var p = document.createElement('div');\n    p.className = type = 'cmd' ? 'cmd-line' : type = 'err' ? 'cmd-err' : 'cmd-out';\n    p.textContent = line;\n    termLog.appendChild(p);\n    termLog.scrollTop = termLog.scrollHeight;\n  }\n\n  function renderExercises() {\n    var checks = checkExercises(cluster);\n    for (var i = 1; i <= 5; i++) {\n      var chk = root.querySelector('#chk' + i);\n      var item = chk ? chk.parentElement : null;\n      if (chk) {\n        if (checks[i - 1]) {\n          chk.innerHTML = '&#10003;';\n          if (item) item.classList.add('done');\n        } else {\n          chk.innerHTML = '&#9675;';\n          if (item) item.classList.remove('done');\n        }\n      }\n    }\n  }\n\n  function renderTrafficMesh() {\n    var topo = computeTopology(cluster);\n    var isMtlsStrict = Boolean(cluster.peerAuthentications['default'] && cluster.peerAuthentications['default'].mode === 'STRICT');\n    if (mtlsStatus) {\n      mtlsStatus.textContent = isMtlsStrict ? 'mTLS strict' : 'mTLS permissive';\n      if (isMtlsStrict) mtlsStatus.classList.add('strict');\n      else mtlsStatus.classList.remove('strict');\n    }\n\n    if (activeNs) activeNs.textContent = cluster.currentNamespace;\n    if (statTotal) statTotal.textContent = String(cluster.traffic.total);\n    if (statV1) statV1.textContent = cluster.traffic.lastSplitV1 + '%';\n    if (statV2) statV2.textContent = cluster.traffic.lastSplitV2 + '%';\n    if (statV3) statV3.textContent = cluster.traffic.lastSplitV3 + '%';\n    if (statRatings) statRatings.textContent = String(cluster.traffic.ratings);\n    if (statLatency) statLatency.textContent = cluster.traffic.avgLatencyMs + ' ms';\n\n    if (!meshSvg) return;\n\n    var parts = [\n      '<defs><marker id=\"arrow\" viewBox=\"0 0 10 10\" refX=\"10\" refY=\"5\" markerWidth=\"6\" markerHeight=\"6\" orient=\"auto-start-reverse\"><path d=\"M 0 1 L 10 5 L 0 9 z\" fill=\"var(--ink-2)\"/></marker></defs>'\n    ];\n\n    for (var i = 0; i < topo.edges.length; i++) {\n      var e = topo.edges[i];\n      parts.push('<path d=\"' + e.d + '\" fill=\"none\" stroke=\"var(--ink-2)\" stroke-width=\"1.2\" marker-end=\"url(#arrow)\"/>');\n      if (e.labelBox) {\n        var lb = e.labelBox;\n        parts.push('<rect x=\"' + lb.x + '\" y=\"' + lb.y + '\" width=\"' + lb.width + '\" height=\"' + lb.height + '\" fill=\"var(--paper)\"/>');\n        parts.push('<text x=\"' + (lb.x + lb.width / 2) + '\" y=\"' + (lb.y + lb.height / 2 + 3) + '\" font-family=\"var(--sans)\" font-size=\"9\" text-anchor=\"middle\" fill=\"var(--ink-2)\" stroke=\"var(--paper)\" stroke-width=\"2.5\" paint-order=\"stroke fill\">' + lb.text + '</text>');\n      }\n    }\n\n    for (var j = 0; j < topo.nodes.length; j++) {\n      var n = topo.nodes[j];\n      parts.push('<g class=\"mesh-node\" transform=\"translate(' + n.x + ', ' + n.y + ')\">');\n      parts.push('<rect width=\"' + n.width + '\" height=\"' + n.height + '\" rx=\"3\" fill=\"var(--paper)\" stroke=\"var(--ink)\" stroke-width=\"1\"/>');\n      parts.push('<text x=\"' + (n.width / 2) + '\" y=\"' + (n.height / 2 - 1) + '\" font-family=\"var(--sans)\" font-size=\"10\" font-weight=\"600\" text-anchor=\"middle\" fill=\"var(--ink)\">' + n.title + '</text>');\n      parts.push('<text x=\"' + (n.width / 2) + '\" y=\"' + (n.height / 2 + 10) + '\" font-family=\"var(--mono)\" font-size=\"8\" text-anchor=\"middle\" fill=\"var(--ink-2)\">' + n.sub + '</text>');\n      parts.push('</g>');\n    }\n\n    meshSvg.innerHTML = parts.join('');\n  }\n  function renderUi() {\n    renderExercises();\n    renderTrafficMesh();\n  }\n\n  function handleLine(cmd) {\n    if (!cmd.trim()) return;\n    history.push(cmd);\n    historyIndex = history.length;\n    appendLog('$ ' + cmd, 'cmd');\n\n    var res = executeCommand(cluster, cmd);\n    if (res.output === '__CLEAR__') {\n      if (termLog) termLog.innerHTML = '';\n    } else if (res.output) {\n      appendLog(res.output, res.error ? 'err' : 'out');\n    }\n    renderUi();\n  }\n\n  if (termInput) {\n    termInput.addEventListener('keydown', function (e) {\n      if (e.key === 'Enter') {\n        e.preventDefault();\n        var val = termInput.value;\n        termInput.value = '';\n        handleLine(val);\n      } else if (e.key === 'ArrowUp') {\n        e.preventDefault();\n        if (history.length > 0) {\n          if (historyIndex > 0) historyIndex--;\n          else historyIndex = 0;\n          termInput.value = history[historyIndex] || '';\n        }\n      } else if (e.key === 'ArrowDown') {\n        e.preventDefault();\n        if (historyIndex < history.length - 1) {\n          historyIndex++;\n          termInput.value = history[historyIndex] || '';\n        } else {\n          historyIndex = history.length;\n          termInput.value = '';\n        }\n      } else if (e.key === 'l' && e.ctrlKey) {\n        e.preventDefault();\n        if (termLog) termLog.innerHTML = '';\n      } else if (e.key === 'Tab') {\n        e.preventDefault();\n        var current = termInput.value;\n        var tokens = current.split(' ');\n        var lastTok = tokens[tokens.length - 1] || '';\n        var candidates = [\n          'kubectl', 'istioctl', 'help', 'hint', 'clear', 'reset', 'cat', 'ls',\n          'get', 'describe', 'logs', 'scale', 'delete', 'rollout', 'set', 'label', 'apply', 'exec',\n          'pods', 'deployments', 'services', 'virtualservices', 'destinationrules', 'peerauthentications',\n          'destination-rule.yaml', 'reviews-v2-v3-split.yaml', 'fault-delay.yaml', 'mtls-strict.yaml'\n        ];\n        Object.keys(cluster.deployments).forEach(function (d) {\n          candidates.push(d);\n          candidates.push('deploy/' + d);\n          candidates.push('deployment/' + d);\n        });\n        cluster.pods.forEach(function (pod) {\n          candidates.push(pod.name);\n          candidates.push('pod/' + pod.name);\n        });\n        var matches = candidates.filter(function (c) { return c.indexOf(lastTok) = 0; });\n        if (matches.length = 1) {\n          tokens[tokens.length - 1] = matches[0];\n          termInput.value = tokens.join(' ') + ' ';\n        } else if (matches.length > 1) {\n          appendLog(matches.join('   '), 'out');\n        }\n      }\n    });\n  }\n\n  if (btnLoad) {\n    btnLoad.addEventListener('click', function () {\n      var res = simulateTraffic(cluster, 100);\n      appendLog('$ simulate-traffic 100', 'cmd');\n      appendLog(res.output, 'out');\n      renderUi();\n    });\n  }\n\n  if (btnReset) {\n    btnReset.addEventListener('click', function () {\n      var res = executeCommand(cluster, 'reset');\n      if (termLog) termLog.innerHTML = '';\n      appendLog(res.output, 'out');\n      renderUi();\n    });\n  }\n\n  if (btnHelp) {\n    btnHelp.addEventListener('click', function () {\n      var res = executeCommand(cluster, 'help');\n      appendLog('$ help', 'cmd');\n      appendLog(res.output, 'out');\n    });\n  }\n\n  if (btnHint) {\n    btnHint.addEventListener('click', function () {\n      var res = executeCommand(cluster, 'hint');\n      if (hintOutput) hintOutput.textContent = res.output;\n      appendLog('$ hint', 'cmd');\n      appendLog(res.output, 'out');\n    });\n  }\n\n  appendLog('Kubernetes v1.28.2 · Istio 1.22.0 · Bookinfo demo in namespace \"shop\"', 'out');\n  appendLog('Type \"help\" for commands, \"hint\" for exercises, or click buttons above.', 'out');\n  renderUi();\n\n})();"
}
```

Steps, specimen 2:

```element steps
{
  "label": "Split reviews traffic 50/50",
  "steps": [
    {
      "title": "Check the sidecars",
      "body": "Each reviews pod should show 2/2 READY and Running.",
      "code": "kubectl get pods -n shop"
    },
    {
      "title": "Read the destination rule",
      "body": "It names three subsets of reviews, v1, v2 and v3, by each pod’s version label.",
      "code": "cat destination-rule.yaml"
    },
    {
      "title": "Define the subsets",
      "body": "Expect: destinationrule.networking.istio.io/reviews created.",
      "code": "kubectl apply -f destination-rule.yaml"
    },
    {
      "title": "Split the traffic",
      "body": "The virtual service sends half of reviews traffic to v2 and half to v3. Weights must add up to 100.",
      "code": "kubectl apply -f reviews-v2-v3-split.yaml"
    },
    {
      "title": "Send traffic and read the split",
      "body": "Press **Load (100 reqs)**: reviews v1 falls to 0 per cent, and v2 and v3 take 50 each. On a real cluster, read the weights back with this command.",
      "code": "kubectl get virtualservice reviews -n shop -o yaml"
    }
  ]
}
```

Table, specimen 3:

```element table
{
  "caption": "The afternoon, block by block",
  "columns": [
    {
      "label": "T",
      "align": "left"
    },
    {
      "label": "W",
      "align": "left"
    }
  ],
  "rows": [
    [
      "13:00, setup",
      "Check your context is kind-mesh-lab; Bookinfo running in shop"
    ],
    [
      "13:20, the mesh",
      "istiod, Envoy sidecars, and who applies a route, in ten minutes"
    ],
    [
      "13:30, exercises 1–2",
      "Label shop for injection; restart until pods show 2/2"
    ],
    [
      "14:10, exercise 3",
      "Subsets, then a 50/50 split between reviews v2 and v3"
    ],
    [
      "14:50, break",
      "Stretch; pair up with anyone still on exercise 2"
    ],
    [
      "15:05, exercise 4",
      "A 2-second delay on 20 per cent of ratings calls"
    ],
    [
      "15:45, exercise 5",
      "Strict mTLS in shop, and what breaks without sidecars"
    ],
    [
      "16:25, wrap-up",
      "Where the simulator differs, and delete your cluster"
    ]
  ],
  "notes": "Every exercise can be done in the simulator; the real cluster is the one on your laptop."
}
```
