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