@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.
- package/README.md +37 -1
- package/build/index.d.ts +177 -5
- package/build/index.js +478 -33
- package/build/index.js.map +1 -1
- package/build/{judge-B3CHoiTW.js → judge-Bkbs7VVG.js} +147 -29
- package/build/judge-Bkbs7VVG.js.map +1 -0
- package/build/{judge-CctoUEo8.d.ts → judge-DMuI3aDn.d.ts} +82 -14
- package/build/judge.d.ts +2 -2
- package/build/judge.js +1 -1
- package/image/Dockerfile +1 -1
- package/image/agent.mjs +133 -2
- package/image/client-grpc.mjs +2 -0
- package/image/watch.mjs +50 -4
- package/package.json +4 -1
- package/src/eds.ts +40 -0
- package/src/lds.ts +31 -0
- package/build/judge-B3CHoiTW.js.map +0 -1
|
@@ -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
|
-
/**
|
|
81
|
-
|
|
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
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
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 {
|
|
812
|
-
//# sourceMappingURL=judge-
|
|
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
|
|
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 {
|
|
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 {
|
package/image/client-grpc.mjs
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
128
|
-
|
|
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.
|
|
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
|
+
};
|