npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pixelbitsltd/envtest-js

v2.0.0

Published

Run a real Kubernetes API server (kube-apiserver + etcd) for integration tests, envtest-style, from Node or Bun

Readme

envtest-js

npm npm provenance

Run a real Kubernetes API server (kube-apiserver + etcd) for integration tests, from Node or Bun — a pure-TypeScript port of controller-runtime's envtest.

No Docker, no cluster, no Go toolchain. Startup is ~2–5 seconds once binaries are cached.

npm install --save-dev @pixelbitsltd/envtest-js   # or: bun add -d @pixelbitsltd/envtest-js
import { TestEnvironment, restRequestOk } from "@pixelbitsltd/envtest-js";

const env = new TestEnvironment({
  crdDirectoryPaths: ["./config/crd"], // optional: files or directories
});

const config = await env.start();
// config.server            -> https://127.0.0.1:<port>
// config.kubeconfigPath    -> ready-to-use kubeconfig (works with kubectl and @kubernetes/client-node)
// config.caPem / certPem / keyPem (+ base64 caData/certData/keyData)

// Tiny built-in REST client for tests that don't want a client library:
await restRequestOk(config, "POST", "/api/v1/namespaces", {
  apiVersion: "v1", kind: "Namespace", metadata: { name: "test" },
});

await env.stop();

With @kubernetes/client-node:

import { KubeConfig, CoreV1Api } from "@kubernetes/client-node";

const kc = new KubeConfig();
kc.loadFromFile(config.kubeconfigPath); // or kc.loadFromString(config.kubeconfigYaml)
const core = kc.makeApiClient(CoreV1Api);

Why this exists

If you build Kubernetes operators, controllers, or admission webhooks in TypeScript, your integration-test options used to be two bad extremes.

  1. Mocked clients don't exercise anything the apiserver actually does — CRD structural-schema validation, RBAC, resourceVersion conflicts, finalizers, watch semantics — and admission/conversion webhooks can't be tested against a mock at all, because the real behavior is the apiserver calling back into your handler over TLS it trusts.
  2. Real clusters via Docker (@testcontainers/k3s, kind) validate everything but cost 20–30s startup, require Docker, and aren't cheap enough to be hermetic per suite.

Go teams have had the middle ground for years: envtest — the real kube-apiserver + etcd, no Docker, throwaway state, seconds to start. But upstream envtest is two layers, only one of which is reusable from Node:

  • The binaries (etcd, kube-apiserver, kubectl) are language-agnostic, and envtest-js reuses that layer entirely: same release index, same SHA-512 verification, KUBEBUILDER_ASSETS/TEST_ASSET_* honored with upstream precedence — a CI host set up for Go envtest works unchanged.
  • The orchestration (throwaway PKI, apiserver flags, readiness, kubeconfig, CRD install, webhook injection) is a Go library, not a CLI — there's no envtest start to shell out to.

Same deliberate limits as upstream envtest: this is the API surface only — no scheduler or controller-manager, so Pods never actually run, Deployments don't create ReplicaSets, and garbage collection doesn't fire. If you need a complete cluster and have Docker available, @testcontainers/k3s is the better tool for that job.

What it does

  1. Binary acquisition — fetches the upstream release index (envtest-releases.yaml from kubernetes-sigs/controller-tools), downloads the per-OS/arch tarball, verifies its SHA-512, and caches the binaries in the platform data dir (~/.local/share/envtest-js, %LOCALAPPDATA%\envtest-js, …). Compatible with existing CI setups: KUBEBUILDER_ASSETS and TEST_ASSET_ETCD / TEST_ASSET_KUBE_APISERVER / TEST_ASSET_KUBECTL are honored with the same precedence as Go envtest. The cache is inspectable and prunable like setup-envtest list / cleanup: listCachedVersions() returns the cached versions (newest first), and cleanupCachedVersions({ version: "<1.30" }) removes the matching ones — both accept version (exact or semver range), os, and arch filters.
  2. Full-fidelity PKI (no insecure-skip-tls-verify anywhere) — a throwaway CA (ECDSA P-256, via @peculiar/x509 on WebCrypto) signs:
    • the apiserver serving cert (SANs: localhost, 127.0.0.1, ::1, kubernetes.default.svc, …),
    • an admin client cert with CN=envtest-admin, O=system:masters — Kubernetes maps CN→username and O→groups, so this is real RBAC-backed client-cert auth;
    • plus an RSA-2048 keypair for genuine service-account token signing (--service-account-signing-key-file).
  3. Process lifecycle — starts etcd on a free port, then kube-apiserver with the same default flag set as upstream envtest (secure serving only, --authorization-mode=RBAC, ServiceAccount admission disabled), polls /readyz over mTLS, and tears down hard on stop() (SIGTERM → SIGKILL, temp dirs removed). Processes are spawned via execa with cleanup enabled, so children are killed even when the test runner dies from a signal.
  4. Client config — a self-contained kubeconfig (verified in e2e against real kubectl and the official @kubernetes/client-node) plus in-memory PEM/base64 credentials. env.addUser({ name, groups }) provisions additional identities (like upstream Environment.AddUser): the environment's CA signs a client cert with CN=name, O=groups, returned as a REST config plus its own kubectl-ready kubeconfig — e.g. for testing RBAC as a non-admin.
  5. CRD install — applies CustomResourceDefinition manifests (create-or-replace), from files/directories and/or in-memory objects, and waits for the Established condition, like envtest.InstallCRDs; uninstallCRDs deletes them again (missing ones skipped), like envtest.UninstallCRDs.
  6. Webhook support — runs your admission and CRD conversion webhooks in the test process, like upstream's WebhookInstallOptions: a separate throwaway CA mints a serving cert for your HTTPS server; each (Validating|Mutating)WebhookConfiguration gets its clientConfig rewritten to https://127.0.0.1:<port><service.path> with the CA injected as caBundle; CRDs declaring spec.conversion.strategy: Webhook get the same treatment (defaulting to controller-runtime's /convert path). See below.

Admission webhooks

import https from "node:https";

const env = new TestEnvironment({
  webhookInstallOptions: { paths: ["./config/webhook"] },
});
const config = await env.start();

const wh = config.webhook!; // host, port, certPem/keyPem (+ certDir with tls.crt/tls.key)
const server = https.createServer({ cert: wh.certPem, key: wh.keyPem }, admissionHandler);
await new Promise<void>((resolve) => server.listen(wh.port, wh.host, resolve));
await env.waitForWebhookServer(); // dial-check verifying the serving cert against the webhook CA

// ...requests matching your webhook rules now round-trip through your handler.

The webhook configurations are installed with failurePolicy as authored — with Fail, matching requests are rejected until your server is up, exactly as in Go envtest.

CRD conversion webhooks: any CRD passed via crdDirectoryPaths that declares spec.conversion.strategy: Webhook is automatically pointed at the same local serving address (path from the authored service.path, defaulting to /convert) with the CA bundle injected. Serve ConversionReview on that path from the same HTTPS server. Upstream decides convertibility from the Go scheme; with no scheme in JS, the manifest's declared strategy is the trigger.

Options

new TestEnvironment({
  version: "1.36",              // exact ("v1.36.2"), semver range ("1.36", ">=1.35 <1.37"), or omit for latest stable
  binaryAssetsDirectory: "...", // skip download, use these binaries
  crdDirectoryPaths: [...],     // CRD manifests to install on start
  crds: [{ ... }],              // in-memory CRD manifests, installed alongside crdDirectoryPaths
  apiServerFlags: { "max-requests-inflight": "800", "allow-privileged": null }, // override / remove (null) defaults; arrays repeat the flag
  etcdFlags: { ... },
  listenAddress: "172.17.0.1",  // apiserver bind + serving-cert SAN + kubeconfig URL move together
                                // (e.g. a Docker bridge IP so containers reach the host's apiserver);
                                // default 127.0.0.1. Wildcards (0.0.0.0) bind everything but only
                                // loopback names land in the SANs.
  securePort: 6443,             // fixed apiserver port; default: an OS-assigned free port
  attachOutput: true,           // pipe etcd/apiserver logs to stderr
  startTimeoutMs: 60_000,
  readyPollIntervalMs: 150,     // interval between etcd/apiserver readiness checks
  useExistingCluster: true,     // attach to a pre-existing cluster instead of spawning one (below)
  config: { server, caPem, certPem, keyPem }, // explicit credentials for useExistingCluster
})

Attaching to an existing cluster

Like upstream's Environment.UseExistingCluster, useExistingCluster: true (or the USE_EXISTING_CLUSTER=true environment variable, when the option is unset) skips etcd/kube-apiserver entirely and attaches to a cluster you already have. Credentials come from config when provided, otherwise from the kubeconfig at KUBECONFIG (first readable entry — missing files are skipped like kubectl, but kubectl-style merging is not done) or ~/.kube/config; only client-certificate kubeconfigs are supported (what kind/k3d/minikube issue — token, exec-plugin, and basic auth are not). Scheme-less server: values are normalized to https:// like kubectl; plain http is rejected. config.user is the client certificate's CN — the identity the apiserver actually sees — not the kubeconfig's arbitrary user-entry name. CRDs and webhook configurations still install on start(), and stop() leaves the cluster — including anything installed into it — running. Careful with webhooks on shared clusters: installed webhook configurations point back at the test process, so once it exits, a failurePolicy: Fail webhook blocks matching requests until you delete the configuration. To leave the cluster as you found it, opt into cleanup: crdInstallOptions: { cleanUpAfterUse: true } (upstream: CRDInstallOptions.CleanUpAfterUse) makes stop() uninstall the CRDs start() installed, and webhookInstallOptions.cleanUpAfterUse (an envtest-js extension — upstream always leaves webhook configurations behind) does the same for webhook configurations; both run before teardown, while the apiserver is still reachable. config.binaries/config.etcdURL are absent in this mode, and addUser() is unavailable (the environment doesn't own the cluster's CA). The parsing helpers are exported too: parseKubeconfig(yaml) / loadKubeconfig({ path?, context? }).

Recommended test-runner pattern (same as upstream): one control plane per suite, not per test. Glue for the two main runners ships with the package:

Security posture

Everything client-facing is verified mTLS: the apiserver serves only HTTPS with a throwaway CA, clients authenticate with certs (no tokens, no insecure-skip-tls-verify), webhook callbacks are verified via the injected caBundle, and waitForWebhookServer verifies the serving cert rather than dial-checking blindly. Private keys and the kubeconfig are written 0600 inside a 0700 temp dir. Two deliberate limits, both inherited from upstream envtest: etcd listens in plaintext without authentication on loopback (only the co-located apiserver is meant to talk to it, but any local process could — don't run envtest on hosts with untrusted local users), and the apiserver's default anonymous-auth stays enabled (RBAC denies anonymous everything beyond health/version discovery, and the health endpoints need it).

Runtime support

Node ≥ 24 and Bun.

One known Bun limitation: Bun caches the TLS trust context of the first mTLS request process-wide (node:https, node:http, and native fetch all sit behind that cache; per-request ca/cert/key are ignored afterwards). Since every TestEnvironment mints its own throwaway CA, this means one environment per Bun process — a second environment's apiserver can never be verified. The recommended pattern (one shared environment per run, via the test-runner glue) is unaffected. On Node, multiple concurrent environments work fine.

Not yet implemented

  • Structured helpers beyond CRDs (e.g. applying arbitrary manifests) — use restRequest/kubectl/a client library.