@mark1russell7/kind 0.2.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -77,8 +77,26 @@ declare const cidrRange: (cidr: string) => [number, number];
77
77
  declare const cidrOverlap: (a: string, b: string) => boolean;
78
78
  //#endregion
79
79
  //#region src/cell.d.ts
80
- /** A template is the topology of a cell: T07, the endpoint lifecycle of a Service, or T18, a health-gated fleet. */
81
- type Template = "T07" | "T18";
80
+ /**
81
+ * A template is the topology of a cell:
82
+ * - `T07` is the endpoint lifecycle of a Service.
83
+ * - `T15` is the same lifecycle through an Istio mesh: a mesh hop at the client and a mesh hop at the server.
84
+ * - `T18` is a health-gated fleet.
85
+ */
86
+ type Template = "T07" | "T15" | "T18";
87
+ /**
88
+ * A mesh flavor is the install of Istio 1.31.3 in the cluster:
89
+ * - `sidecar`: the profile `minimal`, istiod only. The injector puts an Envoy sidecar in each pod of a labeled namespace.
90
+ * - `ambient`: the profile `ambient`, with istio-cni and ztunnel. The lab does not install it yet.
91
+ */
92
+ type MeshFlavor = "sidecar" | "ambient";
93
+ /** A mesh mode is the data plane of the pods of a cell: an Envoy sidecar in each pod, or ztunnel on each node. */
94
+ type MeshMode = "sidecar" | "ambient";
95
+ /** A mesh is the install of Istio in the cluster of a cell, and the data plane of the pods of the cell. */
96
+ interface Mesh {
97
+ readonly flavor: MeshFlavor;
98
+ readonly mode: MeshMode;
99
+ }
82
100
  /**
83
101
  * A stack is the server and the clients of a cell:
84
102
  * - `service`: the service of this repository with its declaration, and the HTTP client, which opens a
@@ -99,9 +117,10 @@ type Stack = "service" | "grpc";
99
117
  * - `app-stall` blocks the event loop of one replica.
100
118
  * - `rollout` is a rolling update while Redis is down.
101
119
  * - `cpu-throttle` sets `cpu.max` in the cgroup of one replica.
120
+ * - `authorization-policy` applies an AuthorizationPolicy of Istio that denies the clients, for the hold (T15).
102
121
  * - `none` injects nothing: a calibration measures the path itself.
103
122
  */
104
- type TriggerKind = "pod-deletion" | "rolling-update" | "node-drain" | "scale-in" | "crash" | "liveness-restart" | "freeze-process" | "rst" | "blackhole" | "peer-gone-without-close" | "app-stall" | "rollout" | "cpu-throttle";
123
+ type TriggerKind = "pod-deletion" | "rolling-update" | "node-drain" | "scale-in" | "crash" | "liveness-restart" | "freeze-process" | "rst" | "blackhole" | "peer-gone-without-close" | "app-stall" | "rollout" | "cpu-throttle" | "authorization-policy";
105
124
  /**
106
125
  * A trigger is its kind and the settings of its fault. The parameter `A` is the form of an amount:
107
126
  * a quantity in the format `/2`, a number in the format `/1`.
@@ -117,6 +136,12 @@ interface Trigger<A = Quantity> {
117
136
  readonly quota: A;
118
137
  readonly period: A;
119
138
  } | undefined;
139
+ /**
140
+ * This name is the service account that an AuthorizationPolicy denies (`authorization-policy`). The
141
+ * default is `default`, the account of the clients. An account that no pod has changes the policy,
142
+ * and denies no call.
143
+ */
144
+ readonly account?: string | undefined;
120
145
  }
121
146
  /** A kubelet probe is the period, the failure threshold and the timeout of one probe of the pod. */
122
147
  interface KubeletProbe {
@@ -193,8 +218,9 @@ interface Band<A = Quantity> {
193
218
  * - `intake`: the stop of the intake of the target pod, as its service records it.
194
219
  * - `fault`: the commit of the fault, as the injector or the agent stamps it.
195
220
  * - `resume`: the end of a pause of kube-proxy, as the agent stamps it.
221
+ * - `heal`: the start of the first act that ends the fault, for example the delete of a policy.
196
222
  */
197
- type Reference = "deletion" | "intake" | "fault" | "resume";
223
+ type Reference = "deletion" | "intake" | "fault" | "resume" | "heal";
198
224
  /**
199
225
  * A reset reuse is how a reset fails a later connection on the same port. A reset from the server
200
226
  * leaves the conntrack entry of its flow in CLOSE, with the NAT to that server, for `life`. A SYN on
@@ -247,12 +273,16 @@ type Prediction<A = Quantity> = {
247
273
  readonly each?: boolean | undefined;
248
274
  /**
249
275
  * This band is the propagation that the window assumes. It starts at the deletion of the replica
250
- * of a window. It ends at the first poll of the rules of each node without that replica. The
251
- * judge holds each such poll to `hi` ± δ, and it does not test a window without the band. A
252
- * later poll agrees with the law, but the band is short, thus its cell is `extend` (D-EXTEND).
253
- * An earlier drop only closes the window earlier.
276
+ * of a window. It ends at the first poll of each node without that replica. That poll is of the
277
+ * rules, or of the client sidecar in a sidecar mesh (`propagationSource`).
278
+ *
279
+ * The judge holds each poll to `hi` ± δ, and it does not test a window without the band. A later
280
+ * poll agrees with the law, but the band is short, thus its cell is `extend` (D-EXTEND). An
281
+ * earlier drop only closes the window earlier.
254
282
  */
255
283
  readonly propagation?: Band<A> | undefined;
284
+ /** These outcomes are the only failures that the window permits. A failure with another outcome fails the run. Without them, each outcome but `ok` can fail. */
285
+ readonly outcomes?: readonly string[] | undefined;
256
286
  readonly why: string;
257
287
  } | {
258
288
  readonly kind: "removal";
@@ -328,6 +358,8 @@ interface KindCell<A = Quantity> {
328
358
  readonly template: Template;
329
359
  /** This stack is the server and the clients of the cell. Without it, the cell has the service (`service`). */
330
360
  readonly stack?: Stack | undefined;
361
+ /** This mesh is the Istio of the cell (T15). Without it, the cell has no mesh. */
362
+ readonly mesh?: Mesh | undefined;
331
363
  /** This text names a system of the plan that the cell replaces with the service of this repository, or null. */
332
364
  readonly substitutes: string | null;
333
365
  readonly mode: KubeProxyMode;
@@ -436,13 +468,24 @@ interface SliceLine {
436
468
  readonly svc: string | null;
437
469
  readonly eps: readonly Endpoint[];
438
470
  }
439
- /** A rules line is the backends of the Service in the rules of one node, after a change. `prev` is the start of the poll before it. */
471
+ /**
472
+ * A rules line is the backends of the Service in the rules of one node, after a change. `prev` is the
473
+ * start of the poll before it. An EDS line has the same fields (`ev` is `eds`). It gives the healthy
474
+ * hosts of the server cluster in the Envoy of the client sidecar on the node.
475
+ */
440
476
  interface RulesLine {
441
477
  readonly t: number;
442
478
  readonly prev: number | null;
443
479
  readonly node: string;
444
480
  readonly b: readonly string[];
445
481
  }
482
+ /**
483
+ * A propagation source is the poll whose lines give the backends of each node:
484
+ * - `rules`: the rules of kube-proxy in the namespaces of the node.
485
+ * - `eds`: the hosts of the server cluster in the Envoy of the client sidecar on the node. In a sidecar
486
+ * mesh, the client sidecar picks the backend of each call, thus kube-proxy is not on the path.
487
+ */
488
+ type PropagationSource = "rules" | "eds";
446
489
  /** A record line is one transition of a machine of the service, with its own stamp `at`. */
447
490
  interface RecordLine {
448
491
  readonly t: number;
@@ -458,7 +501,7 @@ interface RecordLine {
458
501
  declare const requestsOf: (lines: readonly Line[]) => Request[];
459
502
  declare const podsOf: (lines: readonly Line[]) => PodLine[];
460
503
  declare const slicesOf: (lines: readonly Line[], svc: string) => SliceLine[];
461
- declare const rulesOf: (lines: readonly Line[]) => RulesLine[];
504
+ declare const rulesOf: (lines: readonly Line[], source?: PropagationSource) => RulesLine[];
462
505
  declare const recordsOf: (lines: readonly Line[]) => RecordLine[];
463
506
  /** This function gives the requests that did not succeed, in sequence of their start. */
464
507
  declare const failuresOf: (lines: readonly Line[]) => Request[];
@@ -494,8 +537,9 @@ declare const signalOf: (lines: readonly Line[], pod: string, after: number) =>
494
537
  /**
495
538
  * This function gives when the rules of a node stopped the selection of a backend: the first poll
496
539
  * after a time without it. The change came after `prev`, the poll before it, and at the latest at `t`.
540
+ * The source `eds` gives the same for the client sidecar of the node.
497
541
  */
498
- declare const dropOf: (lines: readonly Line[], node: string, backend: string, after: number) => RulesLine | null;
542
+ declare const dropOf: (lines: readonly Line[], node: string, backend: string, after: number, source?: PropagationSource) => RulesLine | null;
499
543
  /** This function gives when the rules of a node had no backend of the Service, after a time. */
500
544
  declare const emptyOf: (lines: readonly Line[], node: string, after: number) => RulesLine | null;
501
545
  /** This function gives when an EndpointSlice of the Service first had no ready endpoint, after a time. */
@@ -562,6 +606,10 @@ interface RunFacts<A = Quantity> {
562
606
  * run with a deletion after its start, with the stamps of its intake and of its deletion.
563
607
  */
564
608
  readonly deletions?: readonly Deleted[] | undefined;
609
+ /** These are the first LDS updates of the sidecar of each server pod after the fault, by pod (`updatesOf`). */
610
+ readonly updates?: Readonly<Record<string, Propagated<A> | null>> | undefined;
611
+ /** These are the first LDS updates of the sidecar of each server pod after the heal, by pod. */
612
+ readonly healUpdates?: Readonly<Record<string, Propagated<A> | null>> | undefined;
565
613
  }
566
614
  /** A deleted replica is a replica of a run that got a deletion. It gives its pod, and the stamps of its intake (or null) and of its deletion. */
567
615
  interface Deleted {
@@ -619,12 +667,27 @@ declare const reusesReset: (request: Request, requests: readonly Request[], reus
619
667
  * The references `deletion` and `intake` are those of the target, or of the replica `pod`.
620
668
  */
621
669
  declare const referenceOf: (run: Pick<KindRun, "start" | "acts" | "target" | "lines">, from: Reference, pod?: string | undefined) => number | null;
670
+ /**
671
+ * This function gives the source of the propagation of a cell. In a sidecar mesh, the client sidecar
672
+ * picks the backend of each call from its endpoints (EDS), thus the source is `eds`. Each other cell
673
+ * goes through the rules of kube-proxy (`rules`).
674
+ */
675
+ declare const propagationSource: (cell: Pick<KindCell, "mesh">) => PropagationSource;
622
676
  /**
623
677
  * This function gives the propagation of each node for the target of a run, or for the replica of
624
678
  * the address `ip`. It is the time from its deletion (or its crash) to the first poll of the rules
625
679
  * of the node without it. The change came between the poll before (`after`) and that poll (`by`).
680
+ * The source `eds` gives the poll of the client sidecar of each node.
681
+ */
682
+ declare const propagationOf: (run: KindRun, from: number | null, ip?: string | undefined, source?: PropagationSource) => Record<string, Propagated | null>;
683
+ /**
684
+ * This function gives the first LDS update of the sidecar of each server pod after the stamp `from`.
685
+ * The update came between the poll before (`after`) and the poll that saw it (`by`), after `from`.
686
+ * A poller gives a line at each change of its counters. Thus a line after `from` with a poll before
687
+ * it is an update, when its count `mod` is above the count before `from`. The first line of a poller
688
+ * has no poll before it (`prev` is null), and it is no update.
626
689
  */
627
- declare const propagationOf: (run: KindRun, from: number | null, ip?: string | undefined) => Record<string, Propagated | null>;
690
+ declare const updatesOf: (run: Pick<KindRun, "lines">, from: number | null) => Record<string, Propagated | null>;
628
691
  /**
629
692
  * This function gives each replica of a run that got a deletion after the start of the run, in the
630
693
  * sequence of the deletions. A rolling update deletes each old replica in turn.
@@ -768,12 +831,17 @@ declare const cellsTable: (judged: readonly Judged[]) => string;
768
831
  * This function gives the propagation of each calibration, by node. It is the time from the
769
832
  * deletion to the poll of the rules of each node without the pod. It also gives the time to the
770
833
  * EndpointSlice and to the stop of the intake.
834
+ *
835
+ * For a policy of Istio, the propagation is the LDS update of each server sidecar after the apply,
836
+ * by node (`updatesOf`). Then `heal` is the update after the delete of the policy. `slice` and
837
+ * `intake` are null.
771
838
  */
772
839
  declare const calibrationOf: (judged: Judged) => {
773
840
  readonly nodes: Readonly<Record<string, TimeSpread | null>>;
774
841
  readonly all: TimeSpread | null;
775
842
  readonly slice: TimeSpread | null;
776
843
  readonly intake: TimeSpread | null;
844
+ readonly heal?: TimeSpread | null | undefined;
777
845
  };
778
846
  //#endregion
779
847
  //#region src/schema.d.ts
@@ -808,5 +876,5 @@ declare const quantileOf: (sorted: readonly number[], p: number) => number;
808
876
  */
809
877
  declare const medianInterval: (sorted: readonly number[]) => [number, number];
810
878
  //#endregion
811
- export { Line as $, Workload as $t, TriggerV1 as A, LOG_LABEL as At, RunCheck as B, KindCell as Bt, KindCellResult as C, sliceDropOf as Ct, PredictionV1 as D, transitionOf as Dt, LoadSample as E, stoppingOf as Et, KIND_NOISE as F, seconds as Ft, judgeKind as G, RedisDeclaration as Gt, checkRun as H, KubeletProbe as Ht, KindDecision as I, secondsOf as It, propagationOf as J, ResetReuse as Jt, observationAfter as K, Reference as Kt, KindVerdict as L, showBand as Lt, upgrade as M, REDIS as Mt, upgradeCell as N, SERVER as Nt, RunCheckV1 as O, CLIENT as Ot, Deleted as P, bandOf as Pt, Endpoint as Q, TriggerKind as Qt, LAST_SLACK as R, Band as Rt, KindCellRecordV1 as S, signalsOf as St, KindVerdictV1 as T, slicesOf as Tt, deletionsOf as U, PodSettings as Ut, RunFacts as V, KindRun as Vt, factsOf as W, Prediction as Wt, reusesReset as X, Template as Xt, referenceOf as Y, Stack as Yt, unitOf as Z, Trigger as Zt, ClusterManifest as _, recordsOf as _t, STAGES as a, NodeImage as an, backendsAt as at, FORMAT_V1 as b, rulesOf as bt, readRecord as c, goDuration as cn, deletionOf as ct, calibrationOf as d, nodeNames as dn, failuresOf as dt, KUBE_PROXY_MODES as en, PodLine as et, cellsTable as f, scenario as fn, intakeOf as ft, BandV1 as g, podsOf as gt, timeSpread as h, podsAt as ht, quantileOf as i, NODE_IMAGES as in, SliceLine as it, judge as j, PORT as jt, RunFactsV1 as k, HEALTH_PORT as kt, Judged as l, kindConfig as ln, dropOf as lt, predictionText as m, outcomesOf as mt, medianInterval as n, KindScenario as nn, Request as nt, kindCellRecordSchema as o, cidrOverlap as on, byClients as ot, medianText as p, scenarioProblems as pn, kernelCounts as pt, observationEnd as q, Replica as qt, middle as r, KubeProxyMode as rn, RulesLine as rt, kindCellRecordV1Schema as s, cidrRange as sn, byNode as st, RELATIVE as t, KindConfig as tn, RecordLine as tt, TimeSpread as u, kubeProxyPatch as un, emptyOf as ut, EnvironmentManifest as v, requestsOf as vt, KindCellV1 as w, sliceEmptyOf as wt, KindCellRecord as x, signalOf as xt, FORMAT as y, restartsAt as yt, Propagated as z, Declaration as zt };
812
- //# sourceMappingURL=judge-CctoUEo8.d.ts.map
879
+ export { updatesOf as $, Replica as $t, TriggerV1 as A, transitionOf as At, RunCheck as B, showBand as Bt, KindCellResult as C, rulesOf as Ct, PredictionV1 as D, sliceEmptyOf as Dt, LoadSample as E, sliceDropOf as Et, KIND_NOISE as F, REDIS as Ft, judgeKind as G, KubeletProbe as Gt, checkRun as H, Declaration as Ht, KindDecision as I, SERVER as It, propagationOf as J, MeshMode as Jt, observationAfter as K, Mesh as Kt, KindVerdict as L, bandOf as Lt, upgrade as M, HEALTH_PORT as Mt, upgradeCell as N, LOG_LABEL as Nt, RunCheckV1 as O, slicesOf as Ot, Deleted as P, PORT as Pt, unitOf as Q, Reference as Qt, LAST_SLACK as R, seconds as Rt, KindCellRecordV1 as S, restartsAt as St, KindVerdictV1 as T, signalsOf as Tt, deletionsOf as U, KindCell as Ut, RunFacts as V, Band as Vt, factsOf as W, KindRun as Wt, referenceOf as X, Prediction as Xt, propagationSource as Y, PodSettings as Yt, reusesReset as Z, RedisDeclaration as Zt, ClusterManifest as _, nodeNames as _n, outcomesOf as _t, STAGES as a, Workload as an, Request as at, FORMAT_V1 as b, recordsOf as bt, readRecord as c, KindScenario as cn, backendsAt as ct, calibrationOf as d, NodeImage as dn, deletionOf as dt, ResetReuse as en, Endpoint as et, cellsTable as f, cidrOverlap as fn, dropOf as ft, BandV1 as g, kubeProxyPatch as gn, kernelCounts as gt, timeSpread as h, kindConfig as hn, intakeOf as ht, quantileOf as i, TriggerKind as in, RecordLine as it, judge as j, CLIENT as jt, RunFactsV1 as k, stoppingOf as kt, Judged as l, KubeProxyMode as ln, byClients as lt, predictionText as m, goDuration as mn, failuresOf as mt, medianInterval as n, Template as nn, PodLine as nt, kindCellRecordSchema as o, KUBE_PROXY_MODES as on, RulesLine as ot, medianText as p, cidrRange as pn, emptyOf as pt, observationEnd as q, MeshFlavor as qt, middle as r, Trigger as rn, PropagationSource as rt, kindCellRecordV1Schema as s, KindConfig as sn, SliceLine as st, RELATIVE as t, Stack as tn, Line as tt, TimeSpread as u, NODE_IMAGES as un, byNode as ut, EnvironmentManifest as v, scenario as vn, podsAt as vt, KindCellV1 as w, signalOf as wt, KindCellRecord as x, requestsOf as xt, FORMAT as y, scenarioProblems as yn, podsOf as yt, Propagated as z, secondsOf as zt };
880
+ //# sourceMappingURL=judge-DMuI3aDn.d.ts.map
package/build/judge.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { $ as Line, $t as Workload, A as TriggerV1, At as LOG_LABEL, B as RunCheck, Bt as KindCell, C as KindCellResult, Ct as sliceDropOf, D as PredictionV1, Dt as transitionOf, E as LoadSample, Et as stoppingOf, F as KIND_NOISE, Ft as seconds, G as judgeKind, Gt as RedisDeclaration, H as checkRun, Ht as KubeletProbe, I as KindDecision, It as secondsOf, J as propagationOf, Jt as ResetReuse, K as observationAfter, Kt as Reference, L as KindVerdict, Lt as showBand, M as upgrade, Mt as REDIS, N as upgradeCell, Nt as SERVER, O as RunCheckV1, Ot as CLIENT, P as Deleted, Pt as bandOf, Q as Endpoint, Qt as TriggerKind, R as LAST_SLACK, Rt as Band, S as KindCellRecordV1, St as signalsOf, T as KindVerdictV1, Tt as slicesOf, U as deletionsOf, Ut as PodSettings, V as RunFacts, Vt as KindRun, W as factsOf, Wt as Prediction, X as reusesReset, Xt as Template, Y as referenceOf, Yt as Stack, Z as unitOf, Zt as Trigger, _ as ClusterManifest, _t as recordsOf, a as STAGES, at as backendsAt, b as FORMAT_V1, bt as rulesOf, c as readRecord, ct as deletionOf, d as calibrationOf, dt as failuresOf, et as PodLine, f as cellsTable, ft as intakeOf, g as BandV1, gt as podsOf, h as timeSpread, ht as podsAt, i as quantileOf, it as SliceLine, j as judge, jt as PORT, k as RunFactsV1, kt as HEALTH_PORT, l as Judged, lt as dropOf, m as predictionText, mt as outcomesOf, n as medianInterval, nt as Request, o as kindCellRecordSchema, ot as byClients, p as medianText, pt as kernelCounts, q as observationEnd, qt as Replica, r as middle, rn as KubeProxyMode, rt as RulesLine, s as kindCellRecordV1Schema, st as byNode, t as RELATIVE, tt as RecordLine, u as TimeSpread, ut as emptyOf, v as EnvironmentManifest, vt as requestsOf, w as KindCellV1, wt as sliceEmptyOf, x as KindCellRecord, xt as signalOf, y as FORMAT, yt as restartsAt, z as Propagated, zt as Declaration } from "./judge-CctoUEo8.js";
2
- export { type Band, type BandV1, CLIENT, type ClusterManifest, type Declaration, type Deleted, Endpoint, type EnvironmentManifest, FORMAT, FORMAT_V1, HEALTH_PORT, type Judged, KIND_NOISE, type KindCell, type KindCellRecord, type KindCellRecordV1, type KindCellResult, type KindCellV1, type KindDecision, type KindRun, type KindVerdict, type KindVerdictV1, type KubeProxyMode, type KubeletProbe, LAST_SLACK, LOG_LABEL, Line, type LoadSample, PORT, PodLine, type PodSettings, type Prediction, type PredictionV1, type Propagated, REDIS, RELATIVE, RecordLine, type RedisDeclaration, type Reference, type Replica, Request, type ResetReuse, RulesLine, type RunCheck, type RunCheckV1, type RunFacts, type RunFactsV1, SERVER, STAGES, SliceLine, type Stack, type Template, type TimeSpread, type Trigger, type TriggerKind, type TriggerV1, type Workload, backendsAt, bandOf, byClients, byNode, calibrationOf, cellsTable, checkRun, deletionOf, deletionsOf, dropOf, emptyOf, factsOf, failuresOf, intakeOf, judge, judgeKind, kernelCounts, kindCellRecordSchema, kindCellRecordV1Schema, medianInterval, medianText, middle, observationAfter, observationEnd, outcomesOf, podsAt, podsOf, predictionText, propagationOf, quantileOf, readRecord, recordsOf, referenceOf, requestsOf, restartsAt, reusesReset, rulesOf, seconds, secondsOf, showBand, signalOf, signalsOf, sliceDropOf, sliceEmptyOf, slicesOf, stoppingOf, timeSpread, transitionOf, unitOf, upgrade, upgradeCell };
1
+ import { $ as updatesOf, $t as Replica, A as TriggerV1, At as transitionOf, B as RunCheck, Bt as showBand, C as KindCellResult, Ct as rulesOf, D as PredictionV1, Dt as sliceEmptyOf, E as LoadSample, Et as sliceDropOf, F as KIND_NOISE, Ft as REDIS, G as judgeKind, Gt as KubeletProbe, H as checkRun, Ht as Declaration, I as KindDecision, It as SERVER, J as propagationOf, Jt as MeshMode, K as observationAfter, Kt as Mesh, L as KindVerdict, Lt as bandOf, M as upgrade, Mt as HEALTH_PORT, N as upgradeCell, Nt as LOG_LABEL, O as RunCheckV1, Ot as slicesOf, P as Deleted, Pt as PORT, Q as unitOf, Qt as Reference, R as LAST_SLACK, Rt as seconds, S as KindCellRecordV1, St as restartsAt, T as KindVerdictV1, Tt as signalsOf, U as deletionsOf, Ut as KindCell, V as RunFacts, Vt as Band, W as factsOf, Wt as KindRun, X as referenceOf, Xt as Prediction, Y as propagationSource, Yt as PodSettings, Z as reusesReset, Zt as RedisDeclaration, _ as ClusterManifest, _t as outcomesOf, a as STAGES, an as Workload, at as Request, b as FORMAT_V1, bt as recordsOf, c as readRecord, ct as backendsAt, d as calibrationOf, dt as deletionOf, en as ResetReuse, et as Endpoint, f as cellsTable, ft as dropOf, g as BandV1, gt as kernelCounts, h as timeSpread, ht as intakeOf, i as quantileOf, in as TriggerKind, it as RecordLine, j as judge, jt as CLIENT, k as RunFactsV1, kt as stoppingOf, l as Judged, ln as KubeProxyMode, lt as byClients, m as predictionText, mt as failuresOf, n as medianInterval, nn as Template, nt as PodLine, o as kindCellRecordSchema, ot as RulesLine, p as medianText, pt as emptyOf, q as observationEnd, qt as MeshFlavor, r as middle, rn as Trigger, rt as PropagationSource, s as kindCellRecordV1Schema, st as SliceLine, t as RELATIVE, tn as Stack, tt as Line, u as TimeSpread, ut as byNode, v as EnvironmentManifest, vt as podsAt, w as KindCellV1, wt as signalOf, x as KindCellRecord, xt as requestsOf, y as FORMAT, yt as podsOf, z as Propagated, zt as secondsOf } from "./judge-DMuI3aDn.js";
2
+ export { type Band, type BandV1, CLIENT, type ClusterManifest, type Declaration, type Deleted, Endpoint, type EnvironmentManifest, FORMAT, FORMAT_V1, HEALTH_PORT, type Judged, KIND_NOISE, type KindCell, type KindCellRecord, type KindCellRecordV1, type KindCellResult, type KindCellV1, type KindDecision, type KindRun, type KindVerdict, type KindVerdictV1, type KubeProxyMode, type KubeletProbe, LAST_SLACK, LOG_LABEL, Line, type LoadSample, type Mesh, type MeshFlavor, type MeshMode, PORT, PodLine, type PodSettings, type Prediction, type PredictionV1, type Propagated, PropagationSource, REDIS, RELATIVE, RecordLine, type RedisDeclaration, type Reference, type Replica, Request, type ResetReuse, RulesLine, type RunCheck, type RunCheckV1, type RunFacts, type RunFactsV1, SERVER, STAGES, SliceLine, type Stack, type Template, type TimeSpread, type Trigger, type TriggerKind, type TriggerV1, type Workload, backendsAt, bandOf, byClients, byNode, calibrationOf, cellsTable, checkRun, deletionOf, deletionsOf, dropOf, emptyOf, factsOf, failuresOf, intakeOf, judge, judgeKind, kernelCounts, kindCellRecordSchema, kindCellRecordV1Schema, medianInterval, medianText, middle, observationAfter, observationEnd, outcomesOf, podsAt, podsOf, predictionText, propagationOf, propagationSource, quantileOf, readRecord, recordsOf, referenceOf, requestsOf, restartsAt, reusesReset, rulesOf, seconds, secondsOf, showBand, signalOf, signalsOf, sliceDropOf, sliceEmptyOf, slicesOf, stoppingOf, timeSpread, transitionOf, unitOf, updatesOf, upgrade, upgradeCell };
package/build/judge.js CHANGED
@@ -1 +1 @@
1
- export { et as CLIENT, u as FORMAT, d as FORMAT_V1, tt as HEALTH_PORT, h as KIND_NOISE, g as LAST_SLACK, nt as LOG_LABEL, rt as PORT, it as REDIS, D as RELATIVE, at as SERVER, t as STAGES, j as backendsAt, ot as bandOf, M as byClients, N as byNode, a as calibrationOf, o as cellsTable, _ as checkRun, P as deletionOf, v as deletionsOf, F as dropOf, I as emptyOf, y as factsOf, L as failuresOf, R as intakeOf, f as judge, b as judgeKind, z as kernelCounts, n as kindCellRecordSchema, r as kindCellRecordV1Schema, O as medianInterval, s as medianText, k as middle, x as observationAfter, S as observationEnd, B as outcomesOf, V as podsAt, H as podsOf, c as predictionText, C as propagationOf, A as quantileOf, i as readRecord, U as recordsOf, w as referenceOf, W as requestsOf, G as restartsAt, T as reusesReset, K as rulesOf, st as seconds, ct as secondsOf, lt as showBand, q as signalOf, J as signalsOf, Y as sliceDropOf, X as sliceEmptyOf, Z as slicesOf, Q as stoppingOf, l as timeSpread, $ as transitionOf, E as unitOf, p as upgrade, m as upgradeCell } from "./judge-B3CHoiTW.js";
1
+ export { nt as CLIENT, u as FORMAT, d as FORMAT_V1, rt as HEALTH_PORT, h as KIND_NOISE, g as LAST_SLACK, it as LOG_LABEL, at as PORT, ot as REDIS, k as RELATIVE, st as SERVER, t as STAGES, N as backendsAt, ct as bandOf, P as byClients, F as byNode, a as calibrationOf, o as cellsTable, _ as checkRun, I as deletionOf, v as deletionsOf, L as dropOf, R as emptyOf, y as factsOf, z as failuresOf, B as intakeOf, f as judge, b as judgeKind, V as kernelCounts, n as kindCellRecordSchema, r as kindCellRecordV1Schema, A as medianInterval, s as medianText, j as middle, x as observationAfter, S as observationEnd, H as outcomesOf, U as podsAt, W as podsOf, c as predictionText, C as propagationOf, w as propagationSource, M as quantileOf, i as readRecord, G as recordsOf, T as referenceOf, K as requestsOf, q as restartsAt, E as reusesReset, J as rulesOf, lt as seconds, ut as secondsOf, dt as showBand, Y as signalOf, X as signalsOf, Z as sliceDropOf, Q as sliceEmptyOf, $ as slicesOf, et as stoppingOf, l as timeSpread, tt as transitionOf, D as unitOf, O as updatesOf, p as upgrade, m as upgradeCell } from "./judge-Bkbs7VVG.js";
package/image/Dockerfile CHANGED
@@ -14,5 +14,5 @@ RUN for name in quantity algebra service; do \
14
14
  && tar -xzf /tmp/workspace/mark1russell7-$name-*.tgz -C "node_modules/@mark1russell7/$name" --strip-components=1; \
15
15
  done \
16
16
  && rm -rf /tmp/workspace
17
- COPY app.mjs client.mjs client-grpc.mjs agent.mjs watch.mjs compose.ts backends.ts conntrack.ts drops.ts ./
17
+ COPY app.mjs client.mjs client-grpc.mjs agent.mjs watch.mjs compose.ts backends.ts conntrack.ts drops.ts eds.ts lds.ts ./
18
18
  CMD ["node", "app.mjs"]
package/image/agent.mjs CHANGED
@@ -9,17 +9,28 @@
9
9
  // handshake that stopped for a second (`stalled`). Each line
10
10
  // `rules-alive` also counts the reads of the table (`ct`). Each
11
11
  // second it reads the counters of dropped packets of the node and
12
- // of each client pod, and prints each one that grew (`drops`)
12
+ // of each client pod, and prints each one that grew (`drops`).
13
+ // As /tmp/eds.json asks, it starts the EDS poller of each client pod
14
+ // of the node, and stops it when the file goes. /tmp/lds.json does the
15
+ // same for the LDS poller of each server pod
16
+ // node agent.mjs eds JSON the EDS poller: in the network namespace of a client pod (nsenter -n),
17
+ // it reads /clusters of the Envoy of the sidecar each `every` ms, and
18
+ // prints the hosts of the server cluster at each change (`eds`)
19
+ // node agent.mjs lds JSON the LDS poller: in the network namespace of a server pod, it reads
20
+ // the counters of the listener manager of the sidecar each `every` ms,
21
+ // and prints them at each change (`lds`)
13
22
  // node agent.mjs act JSON does one act and prints its stamps on CLOCK_MONOTONIC
14
23
  //
15
24
  // The acts: signal (a signal to a PID), pause (SIGSTOP, then SIGCONT after ms), write (a file, for
16
25
  // example cpu.max or a flag file in the root of a container), remove (a file), stall (the stall
17
26
  // seam of the app: the ms in /tmp/lab-stall, then SIGUSR2), and now.
18
- import { spawnSync } from "node:child_process";
27
+ import { spawn, spawnSync } from "node:child_process";
19
28
  import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
20
29
  import { backendsOf } from "./backends.ts";
21
30
  import { entriesOf, flowsOf, staleOf, stalledOf } from "./conntrack.ts";
22
31
  import { devOf, grownOf, snmpOf, softnetOf, unresolvedOf } from "./drops.ts";
32
+ import { clusterFilter, hostsOf } from "./eds.ts";
33
+ import { LDS_FILTER, listenerCountsOf } from "./lds.ts";
23
34
 
24
35
  const now = () => Number(process.hrtime.bigint()) / 1e6;
25
36
  const node = process.env.NODE_NAME ?? "";
@@ -254,6 +265,120 @@ const serve = async () => {
254
265
  }
255
266
  };
256
267
 
268
+ /**
269
+ * the pollers of the sidecars of the node, of one kind: `eds` or `lds`. /tmp/<kind>.json gives the
270
+ * interval and each pod of the node with a PID in it, and for `eds` the server cluster. The agent
271
+ * starts one poller for each pod in the network namespace of that PID, and stops each one when the
272
+ * file changes or goes. nsenter -n keeps the mount namespace of the agent, thus the poller is this
273
+ * file, and its clock is the one of the VM. The EDS pollers read the client pods, and the LDS
274
+ * pollers read the server pods
275
+ */
276
+ const pollers = async (kind) => {
277
+ const file = `/tmp/${kind}.json`;
278
+ let config = null;
279
+ let children = [];
280
+ const stop = () => {
281
+ for (const child of children) child.kill("SIGKILL");
282
+ children = [];
283
+ };
284
+ for (;;) {
285
+ let text = null;
286
+ let spec = null;
287
+ try {
288
+ text = existsSync(file) ? readFileSync(file, "utf8") : null;
289
+ spec = text === null ? null : JSON.parse(text);
290
+ } catch (error) {
291
+ say({ ev: `${kind}-fail`, t: now(), why: String(error?.message ?? error).slice(0, 300) });
292
+ text = config;
293
+ }
294
+ if (text !== config) {
295
+ stop();
296
+ config = text;
297
+ if (spec !== null) {
298
+ for (const { pod, pid } of spec.pods) {
299
+ const argv = ["-t", String(pid), "-n", "--", process.execPath, process.argv[1], kind, JSON.stringify({ cluster: spec.cluster, every: spec.every, pod })];
300
+ const child = spawn("nsenter", argv, { stdio: ["ignore", "inherit", "inherit"] });
301
+ child.on("exit", (code, signal) => {
302
+ if (signal !== "SIGKILL") say({ ev: `${kind}-exit`, t: now(), pod, code, signal });
303
+ });
304
+ child.on("error", (error) => say({ ev: `${kind}-fail`, t: now(), pod, why: String(error?.message ?? error).slice(0, 300) }));
305
+ children.push(child);
306
+ }
307
+ say({ ev: `${kind}-start`, t: now(), ...(spec.cluster === undefined ? {} : { cluster: spec.cluster }), every: spec.every, pods: spec.pods.map((each) => each.pod) });
308
+ }
309
+ }
310
+ await sleep(250);
311
+ }
312
+ };
313
+
314
+ /**
315
+ * one poll loop of the admin endpoint of a sidecar, 127.0.0.1:15000 in the namespace of the pod
316
+ * (envoy_bootstrap.json of Istio 1.31.3), each `every` ms. `read` gives the state of one answer, and
317
+ * a line comes at each change of that state, with the start of the poll before it (`prev`), as a rules
318
+ * line. Each second, a line `<kind>-alive` counts the polls
319
+ */
320
+ const poll = async (kind, url, every, pod, read) => {
321
+ let last = null;
322
+ let previous = null;
323
+ let failure = null;
324
+ let polls = 0;
325
+ let spent = 0;
326
+ let beat = now();
327
+ for (;;) {
328
+ const t = now();
329
+ try {
330
+ const response = await fetch(url, { signal: AbortSignal.timeout(1000) });
331
+ const text = await response.text();
332
+ const took = now() - t;
333
+ polls += 1;
334
+ spent += took;
335
+ if (!response.ok) throw new Error(`the admin endpoint answered ${response.status}: ${text.slice(0, 200)}`);
336
+ const state = read(text);
337
+ const key = JSON.stringify(state);
338
+ if (key !== last) {
339
+ say({ ev: kind, t, prev: previous, pod, took: Math.round(took * 1000) / 1000, ...state });
340
+ last = key;
341
+ }
342
+ failure = null;
343
+ } catch (error) {
344
+ // each failure once, until a poll succeeds
345
+ const why = String(error?.message ?? error).slice(0, 300);
346
+ if (why !== failure) say({ ev: `${kind}-fail`, t, pod, why });
347
+ failure = why;
348
+ }
349
+ previous = t;
350
+ if (t - beat >= 1000) {
351
+ say({ ev: `${kind}-alive`, t, pod, polls, ms: polls === 0 ? 0 : Math.round((spent / polls) * 100) / 100 });
352
+ beat = t;
353
+ polls = 0;
354
+ spent = 0;
355
+ }
356
+ const wait = every - (now() - t);
357
+ if (wait > 0) await sleep(wait);
358
+ }
359
+ };
360
+
361
+ /**
362
+ * one EDS poller. The filter of /clusters keeps the server cluster only (clusters_handler.cc of Envoy
363
+ * v1.39.4). A line `eds` gives the healthy hosts (`b`), and each other host with its flags (`u`)
364
+ */
365
+ const eds = ({ cluster, every, pod }) =>
366
+ poll("eds", `http://127.0.0.1:15000/clusters?filter=${encodeURIComponent(clusterFilter(cluster))}`, every, pod, (text) => {
367
+ const { b, u } = hostsOf(text, cluster);
368
+ return { b, ...(Object.keys(u).length === 0 ? {} : { u }) };
369
+ });
370
+
371
+ /**
372
+ * one LDS poller, in a server pod. The filter of /stats keeps the two counters of the listener
373
+ * manager. A line `lds` gives each update of a listener (`mod`) and each update in place (`inPlace`)
374
+ */
375
+ const lds = ({ every, pod }) =>
376
+ poll("lds", `http://127.0.0.1:15000/stats?filter=${encodeURIComponent(LDS_FILTER)}`, every, pod, (text) => {
377
+ const counts = listenerCountsOf(text);
378
+ if (counts === null) throw new Error(`the stats have no listener_modified: ${text.slice(0, 200)}`);
379
+ return counts;
380
+ });
381
+
257
382
  const act = async (spec) => {
258
383
  const t0 = now();
259
384
  if (spec.op === "now") return { t0, t1: t0 };
@@ -292,7 +417,13 @@ const act = async (spec) => {
292
417
  const [mode, arg] = process.argv.slice(2);
293
418
  if (mode === "serve") {
294
419
  say({ ev: "agent", what: "start", t: now() });
420
+ void pollers("eds");
421
+ void pollers("lds");
295
422
  await serve();
423
+ } else if (mode === "eds") {
424
+ await eds(JSON.parse(arg));
425
+ } else if (mode === "lds") {
426
+ await lds(JSON.parse(arg));
296
427
  } else if (mode === "act") {
297
428
  const spec = JSON.parse(arg);
298
429
  try {
@@ -64,6 +64,8 @@ const outcomeOf = (error) => {
64
64
  if (/ECONNRESET|EPIPE/.test(text)) return "reset";
65
65
  if (/EHOSTUNREACH|ENETUNREACH/.test(text)) return "unreachable";
66
66
  if (error.code === grpc.status.DEADLINE_EXCEEDED) return "timeout";
67
+ // the local reply 403 of an RBAC deny of a sidecar of Istio gives PERMISSION_DENIED (status.cc of Envoy v1.39.4, lines 14–15)
68
+ if (error.code === grpc.status.PERMISSION_DENIED) return "denied";
67
69
  return "error";
68
70
  };
69
71
 
package/image/watch.mjs CHANGED
@@ -3,6 +3,13 @@
3
3
  // EndpointSlices and the events of one namespace through the API, and it follows the log of each
4
4
  // pod with the label lab.systems/log=1. A log line is JSON with its own stamp, and the watcher
5
5
  // passes it on as it is.
6
+ //
7
+ // The sidecar of Istio (the container istio-proxy) is a native sidecar: an init container that
8
+ // runs beside the app. The watcher follows it too. Its lines are the access log of Envoy (JSON, a
9
+ // stamp of the wall clock) and the text of pilot-agent. Each one becomes a line `proxy` with the
10
+ // stamp of its arrival: evidence, not a time that a band reads. An access line of a call that
11
+ // succeeded at its first attempt is only counted (`proxy-ok`, each second), thus the record keeps
12
+ // the failures, the retries and the local replies.
6
13
  import https from "node:https";
7
14
 
8
15
  const kube = JSON.parse(process.env.KUBE ?? "{}");
@@ -75,6 +82,38 @@ const stateOf = (state) => {
75
82
  return "none";
76
83
  };
77
84
 
85
+ /** the container of the sidecar of Istio */
86
+ const PROXY = "istio-proxy";
87
+ /** the access lines of each sidecar that the watcher only counted, by pod, since the last count */
88
+ const counted = new Map();
89
+ setInterval(() => {
90
+ const t = now();
91
+ for (const [pod, n] of counted) say({ ev: "proxy-ok", t, p: pod, n });
92
+ counted.clear();
93
+ }, 1000);
94
+
95
+ /** an access line of Envoy that tells nothing more than its call: status 200, no flag, gRPC status 0, one attempt */
96
+ const plain = (log) =>
97
+ String(log.code) === "200" && (log.flags === "-" || log.flags === "" || log.flags === undefined) && String(log.grpc) === "0" && Number(log.attempts ?? 1) <= 1;
98
+
99
+ /** a line of the sidecar: an access line of Envoy as JSON, or a text line of pilot-agent */
100
+ const proxyLine = (pod, text) => {
101
+ if (text.startsWith("{")) {
102
+ try {
103
+ const log = JSON.parse(text);
104
+ if (plain(log)) {
105
+ counted.set(pod, (counted.get(pod) ?? 0) + 1);
106
+ return;
107
+ }
108
+ say({ ev: "proxy", t: now(), p: pod, log });
109
+ return;
110
+ } catch {
111
+ // a line that is not whole JSON is text
112
+ }
113
+ }
114
+ say({ ev: "proxy", t: now(), p: pod, text: text.slice(0, 500) });
115
+ };
116
+
78
117
  const following = new Set();
79
118
  const follow = (ns, pod, container, id) => {
80
119
  const key = `${ns}/${pod}/${container}/${id}`;
@@ -84,7 +123,8 @@ const follow = (ns, pod, container, id) => {
84
123
  `/api/v1/namespaces/${ns}/pods/${pod}/log`,
85
124
  { container, follow: "true" },
86
125
  (text) => {
87
- if (text.startsWith("{")) pending.push(`${text}\n`);
126
+ if (container === PROXY) proxyLine(pod, text);
127
+ else if (text.startsWith("{")) pending.push(`${text}\n`);
88
128
  else say({ ev: "text", t: now(), p: pod, text: text.slice(0, 500) });
89
129
  },
90
130
  false,
@@ -109,6 +149,10 @@ for (const ns of namespaces) {
109
149
  last: stateOf(status.lastState),
110
150
  id: status.containerID ?? null,
111
151
  }));
152
+ // the native sidecar of Istio is a running init container: its state, apart from the containers
153
+ const sidecars = (pod.status?.initContainerStatuses ?? [])
154
+ .filter((status) => status.name === PROXY)
155
+ .map((status) => ({ n: status.name, r: status.ready, rc: status.restartCount, st: stateOf(status.state), id: status.containerID ?? null }));
112
156
  say({
113
157
  ev: "pod",
114
158
  t,
@@ -121,11 +165,13 @@ for (const ns of namespaces) {
121
165
  ready,
122
166
  phase: pod.status?.phase ?? null,
123
167
  cs: containers,
168
+ ...(sidecars.length === 0 ? {} : { sc: sidecars }),
124
169
  labels: pod.metadata.labels ?? {},
125
170
  });
126
- if (event.type !== "DELETED" && pod.metadata.labels?.["lab.systems/log"] === "1")
127
- for (const status of pod.status?.containerStatuses ?? [])
128
- if (status.state?.running && status.containerID) follow(ns, pod.metadata.name, status.name, status.containerID);
171
+ if (event.type !== "DELETED" && pod.metadata.labels?.["lab.systems/log"] === "1") {
172
+ const running = [...(pod.status?.containerStatuses ?? []), ...(pod.status?.initContainerStatuses ?? []).filter((status) => status.name === PROXY)];
173
+ for (const status of running) if (status.state?.running && status.containerID) follow(ns, pod.metadata.name, status.name, status.containerID);
174
+ }
129
175
  return pod.metadata.resourceVersion;
130
176
  });
131
177
  stream(`/apis/discovery.k8s.io/v1/namespaces/${ns}/endpointslices`, { watch: "1", allowWatchBookmarks: "true" }, (text, t) => {
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@mark1russell7/kind",
4
- "version": "0.2.3",
4
+ "version": "0.4.0",
5
5
  "description": "Kubernetes in Docker as a lab for connection timers: a kind cluster from a scenario, faults in the namespaces of its pods, observations on the one monotonic clock of the VM, and predictions from the ordering law. Its judge entry reads and judges a raw record without Docker.",
6
6
  "keywords": [
7
7
  "kubernetes",
8
8
  "kind",
9
9
  "kube-proxy",
10
+ "istio",
10
11
  "endpoints",
11
12
  "readiness",
12
13
  "graceful-shutdown",
@@ -54,6 +55,8 @@
54
55
  "src/backends.ts",
55
56
  "src/conntrack.ts",
56
57
  "src/drops.ts",
58
+ "src/eds.ts",
59
+ "src/lds.ts",
57
60
  "README.md",
58
61
  "LICENSE"
59
62
  ],
package/src/eds.ts ADDED
@@ -0,0 +1,40 @@
1
+ // The hosts of one cluster in the Envoy of a sidecar, from the text of its admin endpoint
2
+ // `/clusters`. The agent of the lab image reads it with Node, thus this module imports nothing.
3
+
4
+ /**
5
+ * These are the hosts of a cluster: the healthy ones (`b`, in sequence), and each other host with its
6
+ * health flags (`u`). Each host is `ip:port`.
7
+ */
8
+ export interface ClusterHosts {
9
+ readonly b: readonly string[];
10
+ readonly u: Readonly<Record<string, string>>;
11
+ }
12
+
13
+ /**
14
+ * This function gives the name of the outbound cluster of a Service port in a sidecar of Istio:
15
+ * `outbound|<port>||<service>.<namespace>.svc.cluster.local`.
16
+ */
17
+ export const outboundCluster = (service: string, namespace: string, port: number): string => `outbound|${port}||${service}.${namespace}.svc.cluster.local`;
18
+
19
+ /** This function gives the filter of `/clusters` that selects one cluster: a regular expression of RE2 with anchors. */
20
+ export const clusterFilter = (cluster: string): string => `^${cluster.replace(/[.|\\^$*+?()[\]{}]/g, (c) => `\\${c}`)}$`;
21
+
22
+ /**
23
+ * This function gives the hosts of a cluster from the text of `/clusters`. Each host of the text has
24
+ * a line `<cluster>::<ip:port>::health_flags::<flags>`. The flags are `healthy`, or the names of the
25
+ * flags that the host has (`clusters_handler.cc` and `host_utility.cc` of Envoy v1.39.4).
26
+ */
27
+ export const hostsOf = (text: string, cluster: string): ClusterHosts => {
28
+ const healthy: string[] = [];
29
+ const other: Record<string, string> = {};
30
+ const prefix = `${cluster}::`;
31
+ for (const line of text.split("\n")) {
32
+ if (!line.startsWith(prefix)) continue;
33
+ const parts = line.slice(prefix.length).split("::");
34
+ if (parts.length !== 3 || parts[1] !== "health_flags") continue;
35
+ const [host, , flags] = parts as [string, string, string];
36
+ if (flags.trim() === "healthy") healthy.push(host);
37
+ else other[host] = flags.trim();
38
+ }
39
+ return { b: healthy.sort(), u: other };
40
+ };
package/src/lds.ts ADDED
@@ -0,0 +1,31 @@
1
+ // This module has no imports: the agent of the lab image imports it as it is.
2
+
3
+ /**
4
+ * This filter of RE2 selects the two counters of the listener manager of Envoy that an update of LDS
5
+ * moves (`/stats?filter=`, `source/server/admin/stats_params.cc` of Envoy v1.39.4). An update of a
6
+ * listener counts `listener_modified`. An update in place, of its filter chains only, also counts
7
+ * `listener_in_place_updated` (`listener_manager_impl.cc:644`, `:692`).
8
+ */
9
+ export const LDS_FILTER = "^listener_manager\\.listener_(in_place_updated|modified)$";
10
+
11
+ /** The listener counts are the two counters of LDS of one sidecar: each update (`mod`), and each update in place (`inPlace`). */
12
+ export interface ListenerCounts {
13
+ readonly mod: number;
14
+ readonly inPlace: number;
15
+ }
16
+
17
+ /**
18
+ * This function gives the listener counts of the text of `/stats`, one `name: value` on each line
19
+ * (`stats_render.cc:15`). It gives null when the text has no counter `listener_modified`.
20
+ */
21
+ export const listenerCountsOf = (text: string): ListenerCounts | null => {
22
+ let mod: number | null = null;
23
+ let inPlace = 0;
24
+ for (const line of text.split("\n")) {
25
+ const match = /^listener_manager\.listener_(in_place_updated|modified): (\d+)$/.exec(line.trim());
26
+ if (match === null) continue;
27
+ if (match[1] === "modified") mod = Number(match[2]);
28
+ else inPlace = Number(match[2]);
29
+ }
30
+ return mod === null ? null : { mod, inPlace };
31
+ };