@specific.dev/spectest 0.28.1 → 0.28.2
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/dist/components/k3s.js +16 -30
- package/dist/daemon.js +62 -3
- package/dist/index.d.ts +4 -4
- package/dist/terminal.js +1 -1
- package/package.json +1 -1
package/dist/components/k3s.js
CHANGED
|
@@ -595,10 +595,9 @@ const ZOT_MIRRORS = [
|
|
|
595
595
|
/**
|
|
596
596
|
* Discover the host-side image cache gateway by reading the same
|
|
597
597
|
* `registry-mirrors` entry the in-VM dockerd already uses (baked into
|
|
598
|
-
* the
|
|
598
|
+
* the golden rootfs's `/etc/docker/daemon.json`). Returns the
|
|
599
599
|
* gateway host (`"10.42.0.1"`) when present, or `null` when there's no
|
|
600
|
-
* host cache
|
|
601
|
-
* image direct. Runs inside the daemon (VM) at `index.ts` load time, so
|
|
600
|
+
* host cache, in which case the cluster pulls every image direct. Runs inside the daemon (VM) at `index.ts` load time, so
|
|
602
601
|
* the result is stable per host and never poisons the warm-template
|
|
603
602
|
* cache.
|
|
604
603
|
*/
|
|
@@ -618,7 +617,7 @@ function detectHostMirrorGateway() {
|
|
|
618
617
|
* to be seeded via `files` (a pre-start bind mount) rather than a
|
|
619
618
|
* `setup` hook. Two jobs:
|
|
620
619
|
* 1. Mirror the cluster's image pulls through the host `zot` cache
|
|
621
|
-
* (
|
|
620
|
+
* (omitted when there's no host cache).
|
|
622
621
|
* 2. Trust the in-cluster registry, addressed as `<key>.internal:5000`
|
|
623
622
|
* (the `{{SPECTEST_SERVICE}}` token is expanded to the cluster's
|
|
624
623
|
* service key when the file is written). Image *references* use
|
|
@@ -872,30 +871,18 @@ async function collectTraefikDiagnostics(helpers) {
|
|
|
872
871
|
* the k3s container's IP (via systest-resolver), lands on Traefik,
|
|
873
872
|
* and gets dispatched to the matching Ingress rule's backend pods.
|
|
874
873
|
*
|
|
875
|
-
* **
|
|
876
|
-
*
|
|
877
|
-
*
|
|
874
|
+
* **kube-proxy runs in `nftables` mode** (`--kube-proxy-arg=proxy-mode
|
|
875
|
+
* =nftables`), not the iptables default. This started as a workaround
|
|
876
|
+
* for a stock kernel with no `xt_comment` match — kube-proxy's iptables
|
|
877
|
+
* rules carry `-m comment`, and the kernel rejected every one of them,
|
|
878
|
+
* breaking pod→ClusterIP routing and any pod talking to the in-cluster
|
|
879
|
+
* API (helm-install Jobs, CoreDNS, …). We build the guest kernel
|
|
880
|
+
* ourselves now and it has the match, but nftables mode is the better
|
|
881
|
+
* path regardless, so it stays. It is GA in k8s 1.32, which is what
|
|
882
|
+
* pins `DEFAULT_K3S_VERSION`.
|
|
878
883
|
*
|
|
879
|
-
*
|
|
880
|
-
*
|
|
881
|
-
* breaking pod→ClusterIP routing and every pod that talks to the
|
|
882
|
-
* in-cluster API (helm-install Jobs, CoreDNS, …). Fixed by
|
|
883
|
-
* `--kube-proxy-arg=proxy-mode=nftables`: kube-proxy emits
|
|
884
|
-
* native nftables rules where comments are a first-class
|
|
885
|
-
* construct, no xt_comment dependency. nftables proxy mode is
|
|
886
|
-
* GA in k8s 1.32, which is why we pin that.
|
|
887
|
-
*
|
|
888
|
-
* 2. *CNI portmap plugin* (used by klipper-lb's hostPort to expose
|
|
889
|
-
* LoadBalancer ports on the host) still uses iptables-nft and
|
|
890
|
-
* hits the same xt_comment failure — there's no equivalent
|
|
891
|
-
* flag to switch it to native nftables. Workaround: disable the
|
|
892
|
-
* bundled traefik + ServiceLB and run Traefik with
|
|
893
|
-
* `hostNetwork: true` ourselves. hostNetwork pods don't go
|
|
894
|
-
* through portmap at all (they share the node's netns directly),
|
|
895
|
-
* so the broken plugin is never invoked.
|
|
896
|
-
*
|
|
897
|
-
* Flannel uses the `host-gw` backend because Freestyle's stock kernel
|
|
898
|
-
* lacks the VXLAN module — fine for single-node clusters.
|
|
884
|
+
* Flannel uses the `host-gw` backend: single-node clusters never route
|
|
885
|
+
* pod traffic off the node, so VXLAN encapsulation is pure overhead.
|
|
899
886
|
*/
|
|
900
887
|
/**
|
|
901
888
|
* Default k3s docker image tag.
|
|
@@ -912,8 +899,7 @@ async function collectTraefikDiagnostics(helpers) {
|
|
|
912
899
|
*
|
|
913
900
|
* **Why v1.32.x:** kube-proxy's `nftables` proxy mode is GA in k8s 1.32
|
|
914
901
|
* (beta in 1.31, alpha-gated in 1.30). The component runs kube-proxy in
|
|
915
|
-
*
|
|
916
|
-
* extension; dropping below 1.31 reintroduces the broken iptables path.
|
|
902
|
+
* that mode; dropping below 1.31 falls back to the iptables path.
|
|
917
903
|
*/
|
|
918
904
|
const DEFAULT_K3S_VERSION = "v1.32.1-k3s1";
|
|
919
905
|
export function k3s(opts = {}) {
|
|
@@ -960,7 +946,7 @@ export function k3s(opts = {}) {
|
|
|
960
946
|
// Services get an address and work. It was disabled for years
|
|
961
947
|
// because klipper-lb binds its ports with a CNI portmap hostPort,
|
|
962
948
|
// and portmap's iptables-nft rules need the `xt_comment` netfilter
|
|
963
|
-
// match — absent from the
|
|
949
|
+
// match — absent from the stock kernel we used to run on, which made
|
|
964
950
|
// every LoadBalancer hang at <pending> with no svclb DaemonSet. We
|
|
965
951
|
// build the guest kernel ourselves now and it carries
|
|
966
952
|
// CONFIG_NETFILTER_XT_MATCH_COMMENT (see
|
package/dist/daemon.js
CHANGED
|
@@ -68,6 +68,17 @@ const APP_DIR = process.env.SPECTEST_APP_DIR ?? "/opt/spectest/app";
|
|
|
68
68
|
// referenced when we layer it into each image's system trust store.
|
|
69
69
|
const CA_PATH = process.env.SPECTEST_CA_PATH ?? "/etc/spectest/ca.crt";
|
|
70
70
|
const CA_KEY_PATH = process.env.SPECTEST_CA_KEY_PATH ?? "/etc/spectest/ca.key";
|
|
71
|
+
// Public roots ⧺ the spectest CA, materialized by `ensureCaBundle` and
|
|
72
|
+
// bind-mounted into every container. `SSL_CERT_FILE`/`REQUESTS_CA_BUNDLE`/
|
|
73
|
+
// `AWS_CA_BUNDLE` REPLACE the trust store rather than adding to it, so
|
|
74
|
+
// pointing them at the bare CA leaves a container able to verify our fakes
|
|
75
|
+
// and nothing else — public HTTPS then fails to verify from anything using
|
|
76
|
+
// OpenSSL, Python requests, or an AWS SDK. (`NODE_EXTRA_CA_CERTS` appends,
|
|
77
|
+
// which is why Node services never saw this.)
|
|
78
|
+
const CA_BUNDLE_PATH = process.env.SPECTEST_CA_BUNDLE_PATH ?? "/etc/spectest/ca-bundle.crt";
|
|
79
|
+
// The guest's own trust store. BASE_SETUP_SH runs `update-ca-certificates`
|
|
80
|
+
// after generating the CA, so this normally already carries both halves.
|
|
81
|
+
const SYSTEM_CA_BUNDLE = process.env.SPECTEST_SYSTEM_CA_BUNDLE ?? "/etc/ssl/certs/ca-certificates.crt";
|
|
71
82
|
let loaded = null;
|
|
72
83
|
function casesMetadata(suite) {
|
|
73
84
|
if (!suite)
|
|
@@ -902,6 +913,46 @@ RUN if command -v update-ca-certificates >/dev/null 2>&1; then \\
|
|
|
902
913
|
console.warn(`[ca-trust] could not layer spectest CA into ${serviceName} (${tag}); env-var fallback only:\n${build.stderr.trim() || build.stdout.trim()}`);
|
|
903
914
|
}
|
|
904
915
|
}
|
|
916
|
+
/** Memoized across containers — the CA never rotates within a VM's life. */
|
|
917
|
+
let caBundlePromise = null;
|
|
918
|
+
/**
|
|
919
|
+
* Materialize {@link CA_BUNDLE_PATH} (public roots ⧺ the spectest CA) and
|
|
920
|
+
* return its path, or null when there's no CA to trust (daemon running
|
|
921
|
+
* outside a base-snapshot VM).
|
|
922
|
+
*
|
|
923
|
+
* This is what the replace-semantics trust env vars must point at. Falling
|
|
924
|
+
* back to the bare CA when the guest bundle is unreadable keeps the old
|
|
925
|
+
* behaviour — fakes verify, public HTTPS doesn't — which is strictly better
|
|
926
|
+
* than dropping the CA and breaking the fakes everything else depends on.
|
|
927
|
+
*/
|
|
928
|
+
async function ensureCaBundle() {
|
|
929
|
+
caBundlePromise ??= (async () => {
|
|
930
|
+
if (!existsSync(CA_PATH))
|
|
931
|
+
return null;
|
|
932
|
+
const ca = await fs.readFile(CA_PATH, "utf8");
|
|
933
|
+
let system = "";
|
|
934
|
+
try {
|
|
935
|
+
system = await fs.readFile(SYSTEM_CA_BUNDLE, "utf8");
|
|
936
|
+
}
|
|
937
|
+
catch (err) {
|
|
938
|
+
// eslint-disable-next-line no-console
|
|
939
|
+
console.warn(`[ca-trust] no system CA bundle at ${SYSTEM_CA_BUNDLE} (${err}); containers will trust the spectest CA only`);
|
|
940
|
+
return CA_PATH;
|
|
941
|
+
}
|
|
942
|
+
// update-ca-certificates already folds our CA into the system bundle;
|
|
943
|
+
// appending again is harmless but pointless.
|
|
944
|
+
const parts = system.includes(ca.trim()) ? [system] : [system, ca];
|
|
945
|
+
const bundle = parts.map((p) => p.trimEnd()).join("\n") + "\n";
|
|
946
|
+
await fs.writeFile(CA_BUNDLE_PATH, bundle);
|
|
947
|
+
await fs.chmod(CA_BUNDLE_PATH, 0o644);
|
|
948
|
+
return CA_BUNDLE_PATH;
|
|
949
|
+
})().catch((err) => {
|
|
950
|
+
// eslint-disable-next-line no-console
|
|
951
|
+
console.warn(`[ca-trust] could not write ${CA_BUNDLE_PATH} (${err}); falling back to ${CA_PATH}`);
|
|
952
|
+
return existsSync(CA_PATH) ? CA_PATH : null;
|
|
953
|
+
});
|
|
954
|
+
return caBundlePromise;
|
|
955
|
+
}
|
|
905
956
|
async function runContainer(svc, tag, volumeFlags,
|
|
906
957
|
// Extra `--network-alias`es beyond the lowered `aliasesByService`. Used by
|
|
907
958
|
// runtime `startService` (whose service isn't in LOWERED) to give the new
|
|
@@ -970,10 +1021,18 @@ extraAliases = []) {
|
|
|
970
1021
|
// for images where the layer step couldn't run (no
|
|
971
1022
|
// update-ca-certificates).
|
|
972
1023
|
args.push(`--volume=${CA_PATH}:${CA_PATH}:ro`);
|
|
1024
|
+
// Appends to the image's roots, so it takes the bare CA.
|
|
973
1025
|
args.push("-e", `NODE_EXTRA_CA_CERTS=${CA_PATH}`);
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1026
|
+
// These three REPLACE the roots — they must get the combined bundle or
|
|
1027
|
+
// the container loses every public CA (see CA_BUNDLE_PATH).
|
|
1028
|
+
const caBundle = await ensureCaBundle();
|
|
1029
|
+
if (caBundle) {
|
|
1030
|
+
if (caBundle !== CA_PATH)
|
|
1031
|
+
args.push(`--volume=${caBundle}:${caBundle}:ro`);
|
|
1032
|
+
args.push("-e", `SSL_CERT_FILE=${caBundle}`);
|
|
1033
|
+
args.push("-e", `REQUESTS_CA_BUNDLE=${caBundle}`);
|
|
1034
|
+
args.push("-e", `AWS_CA_BUNDLE=${caBundle}`);
|
|
1035
|
+
}
|
|
977
1036
|
if (svc.env) {
|
|
978
1037
|
for (const [k, v] of Object.entries(svc.env)) {
|
|
979
1038
|
args.push("-e", `${k}=${v}`);
|
package/dist/index.d.ts
CHANGED
|
@@ -693,9 +693,9 @@ export interface TestContext<P = undefined, S extends ServicesMap = ServicesMap,
|
|
|
693
693
|
* UI the same way a one-shot `terminal(...)` is.
|
|
694
694
|
*
|
|
695
695
|
* Terminals are NOT auto-closed when the test ends. A docker exec
|
|
696
|
-
* subprocess is cheap to keep alive and
|
|
697
|
-
* cleanly
|
|
698
|
-
*
|
|
696
|
+
* subprocess is cheap to keep alive and the VM snapshot captures it
|
|
697
|
+
* cleanly along with the rest of the container, so leaking the handle
|
|
698
|
+
* past test end is fine. Call `.close()`
|
|
699
699
|
* explicitly if you want a `close` step in the timeline; otherwise
|
|
700
700
|
* `await term.exited` (after the program self-terminates) is the
|
|
701
701
|
* natural way to assert on the exit code.
|
|
@@ -747,7 +747,7 @@ export interface TestContext<P = undefined, S extends ServicesMap = ServicesMap,
|
|
|
747
747
|
readonly testName: string;
|
|
748
748
|
/**
|
|
749
749
|
* Value returned by the parent test. Carried across the fork in the
|
|
750
|
-
* daemon's own memory (
|
|
750
|
+
* daemon's own memory (a VM snapshot is memory + filesystem), so
|
|
751
751
|
* any JS value works — including Maps, Sets, class instances, and live
|
|
752
752
|
* connections that survive the fork. `undefined` for root tests or when
|
|
753
753
|
* the parent returned nothing.
|
package/dist/terminal.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
// a PTY for CMD; we set the slave's size with `stty rows R cols C` inside
|
|
25
25
|
// the wrapped command so the container app sees the cols/rows the caller
|
|
26
26
|
// asked for. `script(1)` ships in util-linux which is essential on Debian,
|
|
27
|
-
// so it's present in the
|
|
27
|
+
// so it's present in the base image without extra apt deps.
|
|
28
28
|
import { Terminal as XtermHeadless } from "@xterm/headless";
|
|
29
29
|
import { recordTerminalStep, reserveEvent, truncateUtf8 } from "./recorder.js";
|
|
30
30
|
import { wrap } from "./inspect.js";
|