@cat-factory/cli 0.10.5 → 0.12.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.
@@ -1,32 +1,39 @@
1
+ import { ingressHostTemplate, ingressUrlPort } from './k3s-ingress.js';
1
2
  import {} from './k3s-provision.js';
2
3
  /**
3
4
  * The secret-bundle key the Kubernetes env backend reads the ServiceAccount token from. Mirrors
4
- * the contracts constant `KUBERNETES_ENV_TOKEN_SECRET_KEY` kept inline so the CLI stays free of a
5
+ * the contracts constant `KUBERNETES_ENV_TOKEN_SECRET_KEY`, kept inline so the CLI stays free of a
5
6
  * backend/contract RUNTIME dependency (the unit test validates the built handler against the real
6
7
  * `registerEnvironmentHandlerSchema`, so any drift from the contract fails a test).
7
8
  */
8
9
  export const KUBERNETES_ENV_TOKEN_SECRET_KEY = 'apiToken';
9
10
  /** Default per-PR namespace name template written into the handler (rendered with the PR number). */
10
11
  export const DEFAULT_NAMESPACE_TEMPLATE = 'cf-env-{{pullNumber}}';
11
- /**
12
- * Default ingress host template. `nip.io` is a wildcard DNS service that resolves
13
- * `<anything>.127.0.0.1.nip.io` to loopback with no local DNS setup, so a per-branch env URL works
14
- * against a local k3s/k3d/kind ingress out of the box.
15
- */
16
- export const DEFAULT_INGRESS_HOST_TEMPLATE = '{{branch}}.127.0.0.1.nip.io';
17
12
  /** Human label for the auto-provisioned connection (names the created cluster when there is one). */
18
13
  export function handlerLabel(connection) {
19
14
  return connection.clusterName ? `Local k3s (${connection.clusterName})` : 'Local k3s';
20
15
  }
21
16
  /**
22
- * Build the `local-k3s` infra handler registration input from a provisioned connection. The minted
23
- * ServiceAccount token rides ONLY in the write-only `secrets` bundle (never in the config, never in
24
- * the deep-link). Everything else the loopback apiserver URL, the skip-TLS flag (a local k3s
25
- * apiserver self-signs its cert), the per-PR namespace + nip.io ingress host defaults — is
26
- * non-secret config. The result is exactly what the Settings → Infrastructure → Local k3s form's
27
- * Test/Save posts, so the guided flow reuses the #557 probe + registration unchanged.
17
+ * Build the `local-k3s` infra handler registration input from a provisioned connection, or `null`
18
+ * when there is nothing honest to register. The minted ServiceAccount token rides ONLY in the
19
+ * write-only `secrets` bundle (never in the config, never in the deep-link). Everything else (the
20
+ * loopback apiserver URL, the skip-TLS flag for a self-signed local apiserver, the per-PR namespace
21
+ * and the nip.io ingress host) is non-secret config. The result is exactly what the
22
+ * Settings → Infrastructure → Local k3s form's Test/Save posts, so the guided flow reuses the #557
23
+ * probe + registration unchanged.
24
+ *
25
+ * `null` is the whole point of the return type. The contract REQUIRES a `url` source, and
26
+ * `ingressTemplate` is the only one the CLI can fill on its own (the status-backed sources name a
27
+ * Service or Ingress that belongs to the operator's manifests), so a cluster whose ingress path the
28
+ * probe did not establish has no handler to build: filling the host template in anyway is how the
29
+ * unserved URL got saved. It used to fall back to the literal template, which was worse than stale
30
+ * on a `--ingress-port 8080` run, naming a port that run had not even asked for.
28
31
  */
29
32
  export function buildK3sHandler(connection) {
33
+ const hostTemplate = ingressHostTemplate(connection.ingress);
34
+ if (hostTemplate === null)
35
+ return null;
36
+ const port = ingressUrlPort(connection.ingress);
30
37
  return {
31
38
  provisionType: 'kubernetes',
32
39
  config: {
@@ -36,29 +43,50 @@ export function buildK3sHandler(connection) {
36
43
  apiServerUrl: connection.apiServerUrl,
37
44
  insecureSkipTlsVerify: true,
38
45
  namespaceTemplate: DEFAULT_NAMESPACE_TEMPLATE,
39
- url: { source: 'ingressTemplate', hostTemplate: DEFAULT_INGRESS_HOST_TEMPLATE },
46
+ url: {
47
+ source: 'ingressTemplate',
48
+ hostTemplate,
49
+ ...(port === null ? {} : { port }),
50
+ // A local ingress controller serves TLS with a self-signed certificate, so the derived
51
+ // environment URL is plain HTTP: the derivation's own default is `https`, which would
52
+ // hand the tester a URL that fails on the certificate rather than on the connection.
53
+ scheme: 'http',
54
+ },
40
55
  },
41
56
  },
42
57
  secrets: { [KUBERNETES_ENV_TOKEN_SECRET_KEY]: connection.apiToken },
43
58
  };
44
59
  }
45
60
  /**
46
- * Build the deep-link that opens the SPA's Local k3s connect form pre-filled with the handler's
47
- * NON-SECRET fields. The ServiceAccount token is deliberately omitted a secret in a URL would
48
- * leak into browser history / server logs so the user pastes it (printed once to the terminal)
49
- * before running Test → Save. Slice 4 teaches the SPA to read these params; until then the link
50
- * simply opens the app. Param names mirror the connect form's fields.
61
+ * Build the deep-link that opens the SPA's Local k3s connect form pre-filled with the connection's
62
+ * NON-SECRET fields. The ServiceAccount token is deliberately omitted (a secret in a URL would leak
63
+ * into browser history / server logs), so the user pastes it (printed once to the terminal) before
64
+ * running Test → Save. Param names mirror the connect form's fields.
65
+ *
66
+ * The URL params are prefilled ONLY when the ingress probe established that the cluster can serve
67
+ * them, and that is read off the CONNECTION rather than passed in: the withholding used to ride an
68
+ * `{ ingressVerified }` option DEFAULTED to true, so every caller that forgot it (the integration
69
+ * spec among them) re-established the promise this exists to remove. The connect form treats the
70
+ * host template as required for an `ingressTemplate` source, so an operator whose cluster has no
71
+ * ingress path cannot save a URL nothing answers, and the printed summary tells them what to fix or
72
+ * which source to pick instead.
51
73
  */
52
- export function buildK3sSetupUrl(spaBaseUrl, handler) {
53
- const k = handler.config.kubernetes;
74
+ export function buildK3sSetupUrl(spaBaseUrl, connection) {
54
75
  const url = new URL(spaBaseUrl);
55
76
  const params = url.searchParams;
56
77
  params.set('infraSetup', 'local-k3s');
57
- params.set('label', k.label);
58
- params.set('apiServerUrl', k.apiServerUrl);
59
- params.set('namespaceTemplate', k.namespaceTemplate);
60
- params.set('hostTemplate', k.url.hostTemplate);
61
- if (k.insecureSkipTlsVerify)
78
+ params.set('label', handlerLabel(connection));
79
+ params.set('apiServerUrl', connection.apiServerUrl);
80
+ params.set('namespaceTemplate', DEFAULT_NAMESPACE_TEMPLATE);
81
+ const handler = buildK3sHandler(connection);
82
+ if (handler) {
83
+ const k = handler.config.kubernetes.url;
84
+ params.set('hostTemplate', k.hostTemplate);
85
+ params.set('scheme', k.scheme);
86
+ if (k.port !== undefined)
87
+ params.set('ingressPort', String(k.port));
88
+ }
89
+ if (connection.insecureSkipTlsVerify)
62
90
  params.set('insecureSkipTlsVerify', '1');
63
91
  return url.toString();
64
92
  }
@@ -1 +1 @@
1
- {"version":3,"file":"k3s-handler.js","sourceRoot":"","sources":["../src/k3s-handler.ts"],"names":[],"mappings":"AAAA,OAAO,EAA2B,MAAM,oBAAoB,CAAA;AAE5D;;;;;GAKG;AACH,MAAM,CAAC,MAAM,+BAA+B,GAAG,UAAU,CAAA;AAEzD,qGAAqG;AACrG,MAAM,CAAC,MAAM,0BAA0B,GAAG,uBAAuB,CAAA;AAEjE;;;;GAIG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAG,6BAA6B,CAAA;AAuB1E,qGAAqG;AACrG,MAAM,UAAU,YAAY,CAAC,UAA8B;IACzD,OAAO,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC,cAAc,UAAU,CAAC,WAAW,GAAG,CAAC,CAAC,CAAC,WAAW,CAAA;AACvF,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,UAA8B;IAC5D,OAAO;QACL,aAAa,EAAE,YAAY;QAC3B,MAAM,EAAE;YACN,MAAM,EAAE,WAAW;YACnB,UAAU,EAAE;gBACV,KAAK,EAAE,YAAY,CAAC,UAAU,CAAC;gBAC/B,YAAY,EAAE,UAAU,CAAC,YAAY;gBACrC,qBAAqB,EAAE,IAAI;gBAC3B,iBAAiB,EAAE,0BAA0B;gBAC7C,GAAG,EAAE,EAAE,MAAM,EAAE,iBAAiB,EAAE,YAAY,EAAE,6BAA6B,EAAE;aAChF;SACF;QACD,OAAO,EAAE,EAAE,CAAC,+BAA+B,CAAC,EAAE,UAAU,CAAC,QAAQ,EAAE;KACpE,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,UAAkB,EAAE,OAAwB;IAC3E,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,UAAU,CAAA;IACnC,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,CAAA;IAC/B,MAAM,MAAM,GAAG,GAAG,CAAC,YAAY,CAAA;IAC/B,MAAM,CAAC,GAAG,CAAC,YAAY,EAAE,WAAW,CAAC,CAAA;IACrC,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAA;IAC5B,MAAM,CAAC,GAAG,CAAC,cAAc,EAAE,CAAC,CAAC,YAAY,CAAC,CAAA;IAC1C,MAAM,CAAC,GAAG,CAAC,mBAAmB,EAAE,CAAC,CAAC,iBAAiB,CAAC,CAAA;IACpD,MAAM,CAAC,GAAG,CAAC,cAAc,EAAE,CAAC,CAAC,GAAG,CAAC,YAAY,CAAC,CAAA;IAC9C,IAAI,CAAC,CAAC,qBAAqB;QAAE,MAAM,CAAC,GAAG,CAAC,uBAAuB,EAAE,GAAG,CAAC,CAAA;IACrE,OAAO,GAAG,CAAC,QAAQ,EAAE,CAAA;AACvB,CAAC"}
1
+ {"version":3,"file":"k3s-handler.js","sourceRoot":"","sources":["../src/k3s-handler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AACtE,OAAO,EAA2B,MAAM,oBAAoB,CAAA;AAE5D;;;;;GAKG;AACH,MAAM,CAAC,MAAM,+BAA+B,GAAG,UAAU,CAAA;AAEzD,qGAAqG;AACrG,MAAM,CAAC,MAAM,0BAA0B,GAAG,uBAAuB,CAAA;AA2BjE,qGAAqG;AACrG,MAAM,UAAU,YAAY,CAAC,UAA8B;IACzD,OAAO,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC,cAAc,UAAU,CAAC,WAAW,GAAG,CAAC,CAAC,CAAC,WAAW,CAAA;AACvF,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,eAAe,CAAC,UAA8B;IAC5D,MAAM,YAAY,GAAG,mBAAmB,CAAC,UAAU,CAAC,OAAO,CAAC,CAAA;IAC5D,IAAI,YAAY,KAAK,IAAI;QAAE,OAAO,IAAI,CAAA;IACtC,MAAM,IAAI,GAAG,cAAc,CAAC,UAAU,CAAC,OAAO,CAAC,CAAA;IAC/C,OAAO;QACL,aAAa,EAAE,YAAY;QAC3B,MAAM,EAAE;YACN,MAAM,EAAE,WAAW;YACnB,UAAU,EAAE;gBACV,KAAK,EAAE,YAAY,CAAC,UAAU,CAAC;gBAC/B,YAAY,EAAE,UAAU,CAAC,YAAY;gBACrC,qBAAqB,EAAE,IAAI;gBAC3B,iBAAiB,EAAE,0BAA0B;gBAC7C,GAAG,EAAE;oBACH,MAAM,EAAE,iBAAiB;oBACzB,YAAY;oBACZ,GAAG,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;oBAClC,uFAAuF;oBACvF,sFAAsF;oBACtF,qFAAqF;oBACrF,MAAM,EAAE,MAAM;iBACf;aACF;SACF;QACD,OAAO,EAAE,EAAE,CAAC,+BAA+B,CAAC,EAAE,UAAU,CAAC,QAAQ,EAAE;KACpE,CAAA;AACH,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,gBAAgB,CAAC,UAAkB,EAAE,UAA8B;IACjF,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,CAAA;IAC/B,MAAM,MAAM,GAAG,GAAG,CAAC,YAAY,CAAA;IAC/B,MAAM,CAAC,GAAG,CAAC,YAAY,EAAE,WAAW,CAAC,CAAA;IACrC,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,YAAY,CAAC,UAAU,CAAC,CAAC,CAAA;IAC7C,MAAM,CAAC,GAAG,CAAC,cAAc,EAAE,UAAU,CAAC,YAAY,CAAC,CAAA;IACnD,MAAM,CAAC,GAAG,CAAC,mBAAmB,EAAE,0BAA0B,CAAC,CAAA;IAC3D,MAAM,OAAO,GAAG,eAAe,CAAC,UAAU,CAAC,CAAA;IAC3C,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,CAAA;QACvC,MAAM,CAAC,GAAG,CAAC,cAAc,EAAE,CAAC,CAAC,YAAY,CAAC,CAAA;QAC1C,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC,CAAA;QAC9B,IAAI,CAAC,CAAC,IAAI,KAAK,SAAS;YAAE,MAAM,CAAC,GAAG,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAA;IACrE,CAAC;IACD,IAAI,UAAU,CAAC,qBAAqB;QAAE,MAAM,CAAC,GAAG,CAAC,uBAAuB,EAAE,GAAG,CAAC,CAAA;IAC9E,OAAO,GAAG,CAAC,QAAQ,EAAE,CAAA;AACvB,CAAC"}
@@ -0,0 +1,259 @@
1
+ import { type Command, type HostShell, type ShellResult } from './host-shell.js';
2
+ /**
3
+ * Whether an environment URL derived from an ingress HOST TEMPLATE can actually be served, which
4
+ * takes two independent things and not one:
5
+ *
6
+ * 1. an ingress CONTROLLER inside the cluster (an `IngressClass` is its observable trace), and
7
+ * 2. a HOST PORT published into it, because every local distribution runs the cluster inside
8
+ * Docker and forwards only the ports it was asked for at create time.
9
+ *
10
+ * Both were previously assumed. `cat-factory k3s` printed `{{branch}}.127.0.0.1.nip.io` as wired
11
+ * on every path, including a reused cluster it had never looked at, and the failure landed at the
12
+ * `tester` step against a URL that answered nothing: environment readiness is WORKLOAD readiness,
13
+ * so provisioning still reported success.
14
+ */
15
+ /**
16
+ * Default host port published into the ingress controller's plain-HTTP entrypoint.
17
+ *
18
+ * ONE port is published and it maps to the controller's plain-HTTP entrypoint, so `http` is the
19
+ * scheme that port serves and the only one the CLI can claim. Publishing the TLS entrypoint
20
+ * instead would trade a connection error for a CERTIFICATE error, because a local ingress
21
+ * controller serves a self-signed default cert: a worse version of the failure this module exists
22
+ * to remove, since it surfaces at the tester rather than here.
23
+ */
24
+ export declare const DEFAULT_INGRESS_PORT = 80;
25
+ /**
26
+ * The host the rendered template resolves to. `nip.io` is wildcard DNS that maps
27
+ * `<anything>.127.0.0.1.nip.io` to loopback with no local DNS setup, so the port probe below
28
+ * targets loopback directly rather than resolving a sample hostname.
29
+ */
30
+ export declare const INGRESS_PROBE_HOST = "127.0.0.1";
31
+ /**
32
+ * The ingress HOST template, with no port in it. It is deliberately portless: the rendered value
33
+ * is also the Ingress `spec.rules[].host` a service's manifests declare, and Kubernetes rejects a
34
+ * `host` carrying a port. A non-default host port travels as the URL source's own `port` field
35
+ * instead (see {@link ingressUrlPort}).
36
+ */
37
+ export declare const INGRESS_HOST_TEMPLATE = "{{branch}}.127.0.0.1.nip.io";
38
+ /** The container port an ingress controller serves plain HTTP on (Traefik `web`, nginx `http`). */
39
+ export declare const INGRESS_CONTAINER_HTTP_PORT = 80;
40
+ /** What a TCP connect attempt established. `unknown` is NOT `closed`: see {@link classifyIngress}. */
41
+ export type PortState = 'open' | 'closed' | 'unknown';
42
+ /** Which half of the ingress path is missing. Two halves, two different fixes. */
43
+ export type IngressGap = 'controller' | 'hostPort';
44
+ /**
45
+ * WHY a probe could not answer. Four causes that need four different things done about them, kept
46
+ * apart rather than folded into one "could not read the cluster": telling an operator with no
47
+ * `kubectl` on PATH to "re-run once the cluster has settled" is advice for a problem they do not
48
+ * have.
49
+ */
50
+ export type IngressProbeCause = 'kubectl-missing' | 'cluster-unreachable' | 'cluster-refused' | 'unparseable' | 'host-port-filtered';
51
+ /**
52
+ * What established that the answering host port belongs to THIS cluster.
53
+ *
54
+ * A TCP connect proves only that something listens: an unrelated web server on port 80 answers it
55
+ * exactly as an ingress controller does, which is how a cluster with no published port could be
56
+ * reported as ready. `cluster` means the container runtime confirmed the cluster publishes its
57
+ * controller entrypoint on that host port; `unattributed` means the port answers and the check
58
+ * could not run, which the summary says out loud rather than claiming the stronger fact.
59
+ */
60
+ export type PortAttribution = 'cluster' | 'unattributed';
61
+ /**
62
+ * The three-state verdict. `unknown` is its own state rather than folded into `missing` for the
63
+ * reason the acceptance suite's `preflight.ts` states: a probe that could not read an answer has
64
+ * not established the negative either, and reporting one as the other sends an operator to fix a
65
+ * cluster that was fine.
66
+ */
67
+ export type IngressReadiness = {
68
+ status: 'ready';
69
+ port: number;
70
+ controller: string;
71
+ attribution: PortAttribution;
72
+ } | {
73
+ status: 'missing';
74
+ port: number;
75
+ gaps: readonly IngressGap[];
76
+ /**
77
+ * The host port the cluster DOES publish its controller on, when the runtime reported one
78
+ * that is not the requested port. It turns "nothing serves 8080" into "the cluster serves
79
+ * 18080", which is a different (and much shorter) fix.
80
+ */
81
+ publishedOn?: number;
82
+ } | {
83
+ status: 'unknown';
84
+ port: number;
85
+ cause: IngressProbeCause;
86
+ probeFailure: string;
87
+ };
88
+ /**
89
+ * The outcome of reading the cluster's `IngressClass` list: the names, or the CAUSE that stopped
90
+ * it. A bare `null` for "no answer" is what collapsed a missing binary, an unreachable apiserver,
91
+ * an RBAC refusal and a garbled payload into one message.
92
+ */
93
+ export type IngressClassRead = {
94
+ ok: true;
95
+ controllers: readonly string[];
96
+ } | {
97
+ ok: false;
98
+ cause: IngressProbeCause;
99
+ detail: string;
100
+ };
101
+ /** Whether the cluster publishes its controller entrypoint on the host, and where. */
102
+ export type PortPublication = {
103
+ checked: true;
104
+ hostPorts: readonly number[];
105
+ }
106
+ /** The check could not run (no such container, no Docker, not a Docker-hosted cluster). */
107
+ | {
108
+ checked: false;
109
+ };
110
+ /** The raw facts {@link classifyIngress} reduces. Kept as plain data so the reduction is pure. */
111
+ export interface IngressFacts {
112
+ port: number;
113
+ classes: IngressClassRead;
114
+ hostPort: PortState;
115
+ publication: PortPublication;
116
+ }
117
+ /** `kubectl get ingressclass -o json`, targeting `context` when one is supplied. */
118
+ export declare function listIngressClassesCommand(context?: string): Command;
119
+ /**
120
+ * `docker port <cluster container>`: every host port the cluster's own container forwards. This is
121
+ * the ONLY check that attributes an answering host port to the cluster rather than to whatever
122
+ * else may be bound there, and it is the same one used by hand to establish that a default k3d
123
+ * create publishes nothing but the apiserver.
124
+ *
125
+ * The container is the one each distribution puts the forward on: k3d's load balancer, kind's
126
+ * control-plane node. The whole table is asked for rather than one port, because a container that
127
+ * forwards NOTHING answers that with an empty success, where naming the port makes the same
128
+ * cluster fail exactly as an unknown container does.
129
+ */
130
+ export declare function publishedPortsCommand(runtime: 'k3d' | 'kind', clusterName: string): Command;
131
+ /**
132
+ * Host ports mapped to the ingress container port, out of `docker port` output
133
+ * (`80/tcp -> 0.0.0.0:18080`, one mapping per line). Reads the text after the LAST colon so an
134
+ * IPv6 bind address (`[::]:18080`) does not parse as the port.
135
+ */
136
+ export declare function parsePublishedHostPorts(stdout: string): number[];
137
+ /**
138
+ * Controller names out of a `kubectl get ingressclass -o json` payload (an `items` list).
139
+ * Returns `null` for anything unparseable, which the reduction reports as `unknown` rather than
140
+ * as an absent controller.
141
+ */
142
+ export declare function parseIngressClasses(stdout: string): string[] | null;
143
+ /**
144
+ * Turn the `kubectl get ingressclass` result into a read outcome that NAMES its cause.
145
+ *
146
+ * The shell already distinguishes a missing binary from a watchdog kill from a real non-zero
147
+ * exit, and the exit carries the apiserver's own words (an RBAC refusal, a bad context). Throwing
148
+ * that away and reporting "could not read the cluster's IngressClasses" is the degrade-quietly
149
+ * failure: four fixes, one message, and a remedy that fits none of them.
150
+ */
151
+ export declare function readIngressClasses(result: ShellResult): IngressClassRead;
152
+ /**
153
+ * Reduce the probed facts to one verdict.
154
+ *
155
+ * An UNREADABLE cluster is `unknown` outright: with no controller list, neither half is settled.
156
+ * An EMPTY list is a definitive `missing`, even when the port probe could not tell, because a
157
+ * cluster with no ingress controller cannot serve the URL whatever the host port does.
158
+ *
159
+ * The PUBLICATION check outranks the socket on the port half, in both directions: a runtime that
160
+ * reports the cluster forwarding no such host port makes an answering socket somebody ELSE'S
161
+ * listener (the case that used to read as ready), and one that reports the forward makes an
162
+ * answering socket attributable to the cluster rather than merely coincident with it.
163
+ */
164
+ export declare function classifyIngress(facts: IngressFacts): IngressReadiness;
165
+ /** Context the remedy lines are rendered from, so each names the operator's actual cluster. */
166
+ export interface IngressRemedyContext {
167
+ /** The distribution behind the cluster when it is known: it changes the CONTROLLER remedy. */
168
+ runtime?: 'k3d' | 'kind';
169
+ /**
170
+ * The command that rebuilds the cluster with the port published, when there IS one to give.
171
+ *
172
+ * Absent means no recreate line is printed at all, and that case is real: on the reuse path the
173
+ * cluster may be one this CLI cannot name (a bare k3s service, a shared context), and
174
+ * `--recreate` only ever targets a k3d/kind cluster it can name. Printing it anyway produced a
175
+ * command the CLI itself refuses, which is worse than printing none.
176
+ */
177
+ recreateCommand?: string;
178
+ }
179
+ /**
180
+ * The remedy for each gap, rendered from what the probe just read.
181
+ *
182
+ * A missing HOST PORT has exactly one fix on every local distribution: neither k3d's `-p` nor
183
+ * kind's `extraPortMappings` can be added to a cluster that already exists, so the cluster has to
184
+ * be built again. That is why a recreate is named here rather than a `docker` incantation that
185
+ * does not work.
186
+ */
187
+ export declare function ingressRemedies(readiness: IngressReadiness, context?: IngressRemedyContext): string[];
188
+ /**
189
+ * The `hostTemplate` an `ingressTemplate` URL source should carry for a verified ingress, or
190
+ * `null` when the ingress was not established.
191
+ *
192
+ * Always PORTLESS. The rendered value is the Ingress `host` a service's manifests declare, and
193
+ * Kubernetes rejects a `host` with a port in it, so a non-default port travels as the URL source's
194
+ * own `port` field ({@link ingressUrlPort}) instead of being smuggled into the host string.
195
+ */
196
+ export declare function ingressHostTemplate(readiness: Extract<IngressReadiness, {
197
+ status: 'ready';
198
+ }>): string;
199
+ export declare function ingressHostTemplate(readiness: IngressReadiness): string | null;
200
+ /**
201
+ * The `port` an `ingressTemplate` URL source should carry: the verified host port when it is not
202
+ * the scheme's default, else `null` (the derivation composes `scheme://host` with no port).
203
+ */
204
+ export declare function ingressUrlPort(readiness: IngressReadiness): number | null;
205
+ /**
206
+ * TCP reachability seam, the sibling of {@link HostShell}: the ingress half that no `kubectl`
207
+ * command can answer is whether the HOST forwards a port into the cluster.
208
+ */
209
+ export interface TcpProbe {
210
+ /** Never rejects: an unreachable port is a {@link PortState}, not an exception. */
211
+ probe(host: string, port: number, timeoutMs: number): Promise<PortState>;
212
+ }
213
+ /**
214
+ * The real, socket-backed probe.
215
+ *
216
+ * `ECONNREFUSED` is the one error that settles the negative: something answered the SYN with a
217
+ * reset, so nothing is listening. A timeout or any other error leaves it undecided (a firewall
218
+ * drops packets silently), and the reduction above keeps that distinct.
219
+ */
220
+ export declare function createNodeTcpProbe(): TcpProbe;
221
+ /** Injectable dependencies for {@link probeIngress}. */
222
+ export interface IngressProbeDeps {
223
+ shell: HostShell;
224
+ tcp: TcpProbe;
225
+ /** Delay between attempts (real setTimeout; a no-op in tests). */
226
+ sleep?: (ms: number) => Promise<void>;
227
+ /**
228
+ * The clock the wait is measured against (default `Date.now`). Injected because the budget is
229
+ * WALL CLOCK: charging only the sleeps let each attempt's own cost (a 2s port timeout plus a 5s
230
+ * apiserver request) ride for free, so a documented 90s wait ran for minutes.
231
+ */
232
+ now?: () => number;
233
+ }
234
+ /** How long a freshly created cluster is given to bring its bundled controller up. */
235
+ export declare const INGRESS_SETTLE_WAIT_MS = 90000;
236
+ /** Which cluster the publication check reads, when the CLI can name one. */
237
+ export interface IngressProbeCluster {
238
+ runtime: 'k3d' | 'kind';
239
+ clusterName: string;
240
+ }
241
+ /**
242
+ * Probe every half and reduce, retrying while `waitMs` of WALL CLOCK remains.
243
+ *
244
+ * The wait exists because a k3d cluster's bundled Traefik is installed by a Job that completes
245
+ * ~20-30s AFTER `k3d cluster create` returns: probing once, immediately, would report a
246
+ * definitive `missing` for a cluster that is merely still starting, which is the same class of
247
+ * lie as the promise this replaces. A settled cluster answers on the first attempt, so the wait
248
+ * costs nothing on the reuse path, where callers pass 0.
249
+ *
250
+ * The budget covers the attempts as well as the gaps between them, so a run whose every probe is
251
+ * slow gives up on time rather than multiplying the deadline by the cost of an attempt.
252
+ */
253
+ export declare function probeIngress(deps: IngressProbeDeps, options: {
254
+ context?: string;
255
+ port: number;
256
+ waitMs?: number;
257
+ cluster?: IngressProbeCluster;
258
+ }): Promise<IngressReadiness>;
259
+ //# sourceMappingURL=k3s-ingress.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"k3s-ingress.d.ts","sourceRoot":"","sources":["../src/k3s-ingress.ts"],"names":[],"mappings":"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"}