@vibesdotdev/integrations-kubernetes 0.0.0-stage → 1.0.0-rc.1

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.
Files changed (95) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +19 -2
  3. package/dist/helm-chart.d.ts +13 -0
  4. package/dist/helm-chart.d.ts.map +1 -0
  5. package/dist/helm-chart.js +508 -0
  6. package/dist/helm-chart.js.map +1 -0
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +2 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/k8s-configmap.d.ts +25 -0
  12. package/dist/k8s-configmap.d.ts.map +1 -0
  13. package/dist/k8s-configmap.js +168 -0
  14. package/dist/k8s-configmap.js.map +1 -0
  15. package/dist/k8s-gpu-operator.d.ts +3 -0
  16. package/dist/k8s-gpu-operator.d.ts.map +1 -0
  17. package/dist/k8s-gpu-operator.js +53 -0
  18. package/dist/k8s-gpu-operator.js.map +1 -0
  19. package/dist/k8s-namespace.d.ts +51 -0
  20. package/dist/k8s-namespace.d.ts.map +1 -0
  21. package/dist/k8s-namespace.js +280 -0
  22. package/dist/k8s-namespace.js.map +1 -0
  23. package/dist/k8s-pod-health.d.ts +60 -0
  24. package/dist/k8s-pod-health.d.ts.map +1 -0
  25. package/dist/k8s-pod-health.js +376 -0
  26. package/dist/k8s-pod-health.js.map +1 -0
  27. package/dist/k8s-secret.d.ts +55 -0
  28. package/dist/k8s-secret.d.ts.map +1 -0
  29. package/dist/k8s-secret.js +391 -0
  30. package/dist/k8s-secret.js.map +1 -0
  31. package/dist/kind-cluster.d.ts +84 -0
  32. package/dist/kind-cluster.d.ts.map +1 -0
  33. package/dist/kind-cluster.js +694 -0
  34. package/dist/kind-cluster.js.map +1 -0
  35. package/dist/kubectl-budget.client.d.ts +13 -0
  36. package/dist/kubectl-budget.client.d.ts.map +1 -0
  37. package/dist/kubectl-budget.client.js +38 -0
  38. package/dist/kubectl-budget.client.js.map +1 -0
  39. package/dist/kubectl.d.ts +59 -0
  40. package/dist/kubectl.d.ts.map +1 -0
  41. package/dist/kubectl.js +121 -0
  42. package/dist/kubectl.js.map +1 -0
  43. package/dist/port-forward-command.d.ts +6 -0
  44. package/dist/port-forward-command.d.ts.map +1 -0
  45. package/dist/port-forward-command.js +24 -0
  46. package/dist/port-forward-command.js.map +1 -0
  47. package/dist/port-forward-compat.d.ts +11 -0
  48. package/dist/port-forward-compat.d.ts.map +1 -0
  49. package/dist/port-forward-compat.js +21 -0
  50. package/dist/port-forward-compat.js.map +1 -0
  51. package/dist/port-forward-identity.d.ts +23 -0
  52. package/dist/port-forward-identity.d.ts.map +1 -0
  53. package/dist/port-forward-identity.js +72 -0
  54. package/dist/port-forward-identity.js.map +1 -0
  55. package/dist/port-forward.consumer.plugin.d.ts +3 -0
  56. package/dist/port-forward.consumer.plugin.d.ts.map +1 -0
  57. package/dist/port-forward.consumer.plugin.js +10 -0
  58. package/dist/port-forward.consumer.plugin.js.map +1 -0
  59. package/dist/port-forward.d.ts +18 -0
  60. package/dist/port-forward.d.ts.map +1 -0
  61. package/dist/port-forward.descriptor.d.ts +7 -0
  62. package/dist/port-forward.descriptor.d.ts.map +1 -0
  63. package/dist/port-forward.descriptor.js +3 -0
  64. package/dist/port-forward.descriptor.js.map +1 -0
  65. package/dist/port-forward.impl.consumer.d.ts +22 -0
  66. package/dist/port-forward.impl.consumer.d.ts.map +1 -0
  67. package/dist/port-forward.impl.consumer.js +32 -0
  68. package/dist/port-forward.impl.consumer.js.map +1 -0
  69. package/dist/port-forward.js +26 -0
  70. package/dist/port-forward.js.map +1 -0
  71. package/dist/port-forward.kind.d.ts +13 -0
  72. package/dist/port-forward.kind.d.ts.map +1 -0
  73. package/dist/port-forward.kind.js +12 -0
  74. package/dist/port-forward.kind.js.map +1 -0
  75. package/dist/resource-providers.consumer.plugin.d.ts +3 -0
  76. package/dist/resource-providers.consumer.plugin.d.ts.map +1 -0
  77. package/dist/resource-providers.consumer.plugin.js +56 -0
  78. package/dist/resource-providers.consumer.plugin.js.map +1 -0
  79. package/dist/schemas/helm-chart.d.ts +14 -0
  80. package/dist/schemas/helm-chart.d.ts.map +1 -0
  81. package/dist/schemas/helm-chart.js +12 -0
  82. package/dist/schemas/helm-chart.js.map +1 -0
  83. package/dist/schemas/k8s-pod-health.d.ts +17 -0
  84. package/dist/schemas/k8s-pod-health.d.ts.map +1 -0
  85. package/dist/schemas/k8s-pod-health.js +21 -0
  86. package/dist/schemas/k8s-pod-health.js.map +1 -0
  87. package/dist/schemas/port-forward.d.ts +62 -0
  88. package/dist/schemas/port-forward.d.ts.map +1 -0
  89. package/dist/schemas/port-forward.js +24 -0
  90. package/dist/schemas/port-forward.js.map +1 -0
  91. package/dist/tool-env.d.ts +19 -0
  92. package/dist/tool-env.d.ts.map +1 -0
  93. package/dist/tool-env.js +32 -0
  94. package/dist/tool-env.js.map +1 -0
  95. package/package.json +80 -4
@@ -0,0 +1,694 @@
1
+ /**
2
+ * Kind Cluster Resource Provider
3
+ * Manages Kind (Kubernetes in Docker) cluster lifecycle including local registry
4
+ *
5
+ * Handles:
6
+ * - Cluster creation with local registry
7
+ * - Orphaned registry detection and cleanup
8
+ * - Cluster recovery (restart vs recreate)
9
+ * - Full lifecycle management
10
+ */
11
+ import { CapabilityError, requireRuntimeCwd } from '@vibesdotdev/kernel';
12
+ import { getLogger } from '@vibesdotdev/logging';
13
+ import * as z from 'zod/v4';
14
+ import { createSha256 } from '@vibesdotdev/shared/hashing';
15
+ import { composePMChildEnv } from '@vibesdotdev/pm/helpers/process-env';
16
+ import { pollUntil } from '@vibesdotdev/pm/utils/bounded-wait';
17
+ import { runCapture, runTimedCapture } from '@vibesdotdev/pm/utils/exec';
18
+ import { attempt } from '@vibesdotdev/pm/utils/attempt';
19
+ import { hasErrno, rethrowFailure } from '@vibesdotdev/pm/helpers/failure';
20
+ import { pmFileSystem, readBytesFileIfPresent } from '@vibesdotdev/pm/helpers/runtime-files';
21
+ import { toolFailure, toolOutputExcerpt } from '@vibesdotdev/pm/resources/tool-env';
22
+ import { DOCKER_TOOL_INHERIT_ENV } from "./tool-env.js";
23
+ /**
24
+ * Kind cluster provider configuration schema
25
+ */
26
+ const KindClusterConfigSchema = z.object({
27
+ /** Cluster name (defaults to resource ID) */
28
+ name: z.string().min(1).optional(),
29
+ /** Enable local registry (default: true) */
30
+ registry: z.boolean().default(true),
31
+ /** Registry port (default: 5001) */
32
+ registryPort: z.number().int().min(1024).max(65535).default(5001),
33
+ /** Registry container name (default: kind-registry) */
34
+ registryName: z.string().default('kind-registry'),
35
+ /** Kind config file path (optional) */
36
+ configFile: z.string().optional(),
37
+ /** Node image (optional, uses kind default) */
38
+ nodeImage: z.string().optional(),
39
+ /** Wait timeout for cluster ready (ms) */
40
+ waitTimeout: z.number().int().min(0).default(120000)
41
+ });
42
+ /** What a cluster's stored state remembers: the names it was created under. */
43
+ const KindStateMetadataSchema = z.object({
44
+ clusterName: z.string().optional().catch(undefined),
45
+ registryName: z.string().optional().catch(undefined)
46
+ });
47
+ /** A container's published ports (`docker inspect -f '{{json .NetworkSettings.Ports}}'`): container port to host bindings. */
48
+ const PortBindingsSchema = z
49
+ .record(z.string(), z.array(z.object({ HostIp: z.string().optional(), HostPort: z.string().optional() })).nullable())
50
+ .nullable()
51
+ .catch(null);
52
+ const STATUS_CHECK_TIMEOUT_MS = 5000;
53
+ async function detectDockerHost(runtime) {
54
+ const tryDocker = async (env) => {
55
+ const probe = await attempt(() => runTimedCapture(runtime, 'docker', ['info'], {
56
+ env,
57
+ inheritEnv: DOCKER_TOOL_INHERIT_ENV,
58
+ signal: runtime.lifecycle.signal,
59
+ timeoutMs: 3000
60
+ }));
61
+ if (probe.ok)
62
+ return !probe.value.timedOut && probe.value.exitCode === 0;
63
+ // No docker binary (ENOENT) or a daemon that did not answer within the
64
+ // probe's 3s budget: this host is not usable, a typed "no". Anything
65
+ // else is real.
66
+ const { failure } = probe;
67
+ const timedOut = failure instanceof Error && failure.name === 'TimeoutError';
68
+ if (hasErrno(failure, 'ENOENT') || timedOut)
69
+ return false;
70
+ return rethrowFailure(failure);
71
+ };
72
+ if (await tryDocker())
73
+ return null;
74
+ const home = runtime.getEnvSnapshot().HOME || '';
75
+ const candidates = [
76
+ `unix://${home}/Library/Containers/com.docker.docker/Data/docker.raw.sock`,
77
+ `unix://${home}/.docker/run/docker.sock`,
78
+ 'unix:///var/run/docker.sock'
79
+ ];
80
+ const files = await pmFileSystem(runtime);
81
+ for (const candidate of candidates) {
82
+ const socketPath = candidate.replace('unix://', '');
83
+ // A candidate socket that is not there is skipped.
84
+ if (!(await files.exists(socketPath)))
85
+ continue;
86
+ if (await tryDocker({ ...(await dockerBaseEnv(runtime)), DOCKER_HOST: candidate })) {
87
+ return candidate;
88
+ }
89
+ }
90
+ return null;
91
+ }
92
+ /**
93
+ * The docker/kind CLIs' environment (C15): PM's base set and the host's
94
+ * re-entry facts and roots, plus the container-engine variables they read.
95
+ */
96
+ function dockerBaseEnv(runtime) {
97
+ return composePMChildEnv(runtime, {
98
+ inheritEnv: DOCKER_TOOL_INHERIT_ENV
99
+ });
100
+ }
101
+ async function dockerEnv(runtime) {
102
+ const host = await detectDockerHost(runtime);
103
+ const env = await dockerBaseEnv(runtime);
104
+ if (host)
105
+ env.DOCKER_HOST = host;
106
+ return env;
107
+ }
108
+ /**
109
+ * Check if a Docker container exists
110
+ */
111
+ async function containerExists(runtime, name) {
112
+ const { exitCode } = await runCapture(runtime, 'docker', ['inspect', name], {
113
+ env: await dockerEnv(runtime),
114
+ signal: runtime.lifecycle.signal,
115
+ inheritEnv: DOCKER_TOOL_INHERIT_ENV
116
+ });
117
+ return exitCode === 0;
118
+ }
119
+ /**
120
+ * Check if a Docker container is running
121
+ */
122
+ async function containerRunning(runtime, name) {
123
+ const { stdout, exitCode } = await runCapture(runtime, 'docker', ['inspect', '--format', '{{.State.Running}}', name], {
124
+ env: await dockerEnv(runtime),
125
+ signal: runtime.lifecycle.signal,
126
+ inheritEnv: DOCKER_TOOL_INHERIT_ENV
127
+ });
128
+ return exitCode === 0 && stdout.trim() === 'true';
129
+ }
130
+ /** Check the cluster list within the caller's probe budget. */
131
+ async function clusterExists(runtime, name, timeoutMs = STATUS_CHECK_TIMEOUT_MS) {
132
+ const result = await runTimedCapture(runtime, 'kind', ['get', 'clusters'], {
133
+ timeoutMs,
134
+ env: await dockerEnv(runtime),
135
+ signal: runtime.lifecycle.signal,
136
+ inheritEnv: DOCKER_TOOL_INHERIT_ENV
137
+ });
138
+ return !result.timedOut && result.exitCode === 0 && result.stdout.trim().split('\n').includes(name);
139
+ }
140
+ /**
141
+ * Check if kind cluster node is ready
142
+ */
143
+ async function isNodeReady(runtime, clusterName, timeoutMs) {
144
+ const args = [
145
+ '--context', `kind-${clusterName}`, 'get', 'nodes', '-o',
146
+ 'jsonpath={.items[0].status.conditions[?(@.type=="Ready")].status}'
147
+ ];
148
+ const options = { signal: runtime.lifecycle.signal, inheritEnv: DOCKER_TOOL_INHERIT_ENV };
149
+ const result = timeoutMs === undefined
150
+ ? await runCapture(runtime, 'kubectl', args, options)
151
+ : await runTimedCapture(runtime, 'kubectl', args, { ...options, timeoutMs });
152
+ return !('timedOut' in result && result.timedOut) && result.exitCode === 0 && result.stdout.trim() === 'True';
153
+ }
154
+ /**
155
+ * Detect orphaned registry container (registry exists but cluster doesn't)
156
+ */
157
+ async function detectOrphanedRegistry(runtime, clusterName, registryName) {
158
+ const [hasCluster, registryExists, registryRunning] = await Promise.all([
159
+ clusterExists(runtime, clusterName),
160
+ containerExists(runtime, registryName),
161
+ containerRunning(runtime, registryName)
162
+ ]);
163
+ // Orphaned if registry exists but cluster doesn't
164
+ if (!hasCluster && registryExists) {
165
+ return { orphaned: true, running: registryRunning };
166
+ }
167
+ return { orphaned: false, running: registryRunning };
168
+ }
169
+ /**
170
+ * Clean up orphaned registry container
171
+ */
172
+ async function cleanupOrphanedRegistry(runtime, registryName) {
173
+ const env = await dockerEnv(runtime);
174
+ await runCapture(runtime, 'docker', ['stop', registryName], {
175
+ env,
176
+ signal: runtime.lifecycle.signal,
177
+ inheritEnv: DOCKER_TOOL_INHERIT_ENV
178
+ });
179
+ await runCapture(runtime, 'docker', ['rm', registryName], {
180
+ env,
181
+ signal: runtime.lifecycle.signal,
182
+ inheritEnv: DOCKER_TOOL_INHERIT_ENV
183
+ });
184
+ }
185
+ /**
186
+ * Create local Docker registry for kind
187
+ */
188
+ async function createRegistry(runtime, name, port) {
189
+ // Check if already exists and running
190
+ if (await containerRunning(runtime, name)) {
191
+ return;
192
+ }
193
+ // Remove if exists but not running
194
+ if (await containerExists(runtime, name)) {
195
+ await runCapture(runtime, 'docker', ['rm', name], {
196
+ env: await dockerEnv(runtime),
197
+ signal: runtime.lifecycle.signal,
198
+ inheritEnv: DOCKER_TOOL_INHERIT_ENV
199
+ });
200
+ }
201
+ // Create registry container
202
+ const { exitCode, stderr } = await runCapture(runtime, 'docker', [
203
+ 'run',
204
+ '-d',
205
+ '--restart=always',
206
+ '-p',
207
+ `127.0.0.1:${port}:5000`,
208
+ '--network',
209
+ 'bridge',
210
+ '--name',
211
+ name,
212
+ 'registry:2'
213
+ ], {
214
+ env: await dockerEnv(runtime),
215
+ signal: runtime.lifecycle.signal,
216
+ inheritEnv: DOCKER_TOOL_INHERIT_ENV
217
+ });
218
+ if (exitCode !== 0) {
219
+ throw toolFailure({ tool: 'docker', action: 'run registry', outcome: { exitCode, stderr } });
220
+ }
221
+ }
222
+ /**
223
+ * Create kind cluster with registry configuration
224
+ */
225
+ async function createCluster(runtime, name, config) {
226
+ const args = ['create', 'cluster', '--name', name];
227
+ if (config.nodeImage) {
228
+ args.push('--image', config.nodeImage);
229
+ }
230
+ if (config.configFile) {
231
+ args.push('--config', config.configFile);
232
+ }
233
+ // Create cluster
234
+ const { exitCode, stderr } = await runCapture(runtime, 'kind', args, {
235
+ env: await dockerEnv(runtime),
236
+ signal: runtime.lifecycle.signal,
237
+ inheritEnv: DOCKER_TOOL_INHERIT_ENV
238
+ });
239
+ if (exitCode !== 0) {
240
+ throw toolFailure({ tool: 'kind', action: 'create cluster', outcome: { exitCode, stderr } });
241
+ }
242
+ // Connect registry to kind network if registry is enabled
243
+ if (config.registry) {
244
+ await connectRegistryToCluster(runtime, name, config.registryName);
245
+ }
246
+ }
247
+ /**
248
+ * Connect registry container to kind network
249
+ */
250
+ async function connectRegistryToCluster(runtime, clusterName, registryName) {
251
+ const networkName = 'kind';
252
+ const env = await dockerEnv(runtime);
253
+ // Check if already connected
254
+ const { stdout } = await runCapture(runtime, 'docker', [
255
+ 'inspect',
256
+ '--format',
257
+ '{{range $key, $value := .NetworkSettings.Networks}}{{$key}} {{end}}',
258
+ registryName
259
+ ], {
260
+ env,
261
+ signal: runtime.lifecycle.signal,
262
+ inheritEnv: DOCKER_TOOL_INHERIT_ENV
263
+ });
264
+ if (stdout.includes(networkName)) {
265
+ return; // Already connected
266
+ }
267
+ // Connect to kind network
268
+ const { exitCode, stderr } = await runCapture(runtime, 'docker', ['network', 'connect', networkName, registryName], {
269
+ env,
270
+ signal: runtime.lifecycle.signal,
271
+ inheritEnv: DOCKER_TOOL_INHERIT_ENV
272
+ });
273
+ // Ignore "already connected" errors
274
+ if (exitCode !== 0 && !stderr.includes('already exists')) {
275
+ getLogger('pm:kind-cluster', runtime).warn('Could not connect registry to kind network', {
276
+ registryName,
277
+ exitCode,
278
+ output: toolOutputExcerpt(stderr)
279
+ });
280
+ }
281
+ // Configure nodes to use the local registry
282
+ await configureRegistryInCluster(runtime, clusterName, registryName);
283
+ }
284
+ /**
285
+ * Configure kind cluster nodes to use local registry
286
+ */
287
+ async function configureRegistryInCluster(runtime, clusterName, _registryName) {
288
+ const context = `kind-${clusterName}`;
289
+ // Create ConfigMap for local registry hosting
290
+ const configMap = `
291
+ apiVersion: v1
292
+ kind: ConfigMap
293
+ metadata:
294
+ name: local-registry-hosting
295
+ namespace: kube-public
296
+ data:
297
+ localRegistryHosting.v1: |
298
+ host: "localhost:5001"
299
+ help: "https://kind.sigs.k8s.io/docs/user/local-registry/"
300
+ `;
301
+ await runCapture(runtime, 'kubectl', ['--context', context, 'apply', '-f', '-'], {
302
+ signal: runtime.lifecycle.signal,
303
+ inheritEnv: DOCKER_TOOL_INHERIT_ENV,
304
+ stdin: configMap
305
+ });
306
+ }
307
+ /**
308
+ * Read a container's host-side port bindings.
309
+ * Returns a map of containerPort/proto → [{ HostIp, HostPort }, ...].
310
+ * Empty object on inspect failure.
311
+ */
312
+ async function getContainerPortBindings(runtime, containerName) {
313
+ const { stdout, exitCode } = await runCapture(runtime, 'docker', ['inspect', '--format', '{{json .NetworkSettings.Ports}}', containerName], {
314
+ env: await dockerEnv(runtime),
315
+ signal: runtime.lifecycle.signal,
316
+ inheritEnv: DOCKER_TOOL_INHERIT_ENV
317
+ });
318
+ if (exitCode !== 0)
319
+ return {};
320
+ // docker's own JSON that does not parse is a failure, never "no ports".
321
+ return PortBindingsSchema.parse(JSON.parse(stdout)) ?? {};
322
+ }
323
+ /**
324
+ * List PersistentVolumeClaims in the cluster whose underlying storage
325
+ * would be lost on `kind delete cluster`. By default, kind uses the
326
+ * local-path-provisioner which writes data into the kind container's
327
+ * filesystem — NOT a host bind mount — so PVCs are destroyed with the
328
+ * cluster. Returns a list of `<namespace>/<name>` strings.
329
+ *
330
+ * Returns an empty list when kubectl is unavailable, the context doesn't
331
+ * exist, or there are no PVCs — the recreate command treats that as
332
+ * "no stateful data at risk".
333
+ */
334
+ export async function listAtRiskPVCs(runtime, clusterName) {
335
+ const context = `kind-${clusterName}`;
336
+ const { stdout, exitCode, timedOut } = await runTimedCapture(runtime, 'kubectl', [
337
+ '--context',
338
+ context,
339
+ 'get',
340
+ 'pvc',
341
+ '--all-namespaces',
342
+ '-o',
343
+ 'jsonpath={range .items[*]}{.metadata.namespace}/{.metadata.name}|{.spec.storageClassName}{"\\n"}{end}'
344
+ ], {
345
+ timeoutMs: STATUS_CHECK_TIMEOUT_MS,
346
+ env: await dockerEnv(runtime),
347
+ signal: runtime.lifecycle.signal,
348
+ inheritEnv: DOCKER_TOOL_INHERIT_ENV
349
+ });
350
+ if (timedOut || exitCode !== 0 || !stdout)
351
+ return [];
352
+ const atRisk = [];
353
+ for (const line of stdout.split('\n')) {
354
+ const trimmed = line.trim();
355
+ if (!trimmed)
356
+ continue;
357
+ const [identity, storageClass = ''] = trimmed.split('|');
358
+ // local-path is kind's default provisioner — backed by the kind
359
+ // container's filesystem, lost on cluster delete. Any other
360
+ // storageClass is opt-in and assumed to be persistent.
361
+ if (storageClass === '' ||
362
+ storageClass === 'standard' ||
363
+ storageClass.startsWith('local-path')) {
364
+ atRisk.push(identity);
365
+ }
366
+ }
367
+ return atRisk;
368
+ }
369
+ /**
370
+ * Check if a container's port bindings are actually active.
371
+ * After a Docker Desktop restart, containers may have HostConfig.PortBindings
372
+ * configured but NetworkSettings.Ports empty — meaning Docker failed to bind them.
373
+ */
374
+ async function hasActivePortBindings(runtime, containerName) {
375
+ const ports = await getContainerPortBindings(runtime, containerName);
376
+ for (const bindings of Object.values(ports)) {
377
+ if (bindings && bindings.length > 0)
378
+ return true;
379
+ }
380
+ return false;
381
+ }
382
+ /**
383
+ * Assert that the registry container is bound to the configured host
384
+ * port. Docker won't change port mappings on a running container, so a
385
+ * mismatch means the user changed `registryPort` in config but the
386
+ * change can't take effect without recreating the container.
387
+ */
388
+ async function assertRegistryPortMatches(runtime, registryName, desiredHostPort) {
389
+ if (!(await containerExists(runtime, registryName)))
390
+ return;
391
+ const bindings = await getContainerPortBindings(runtime, registryName);
392
+ const containerPort = '5000/tcp';
393
+ const actual = bindings[containerPort] ?? [];
394
+ const matched = actual.some((b) => b.HostPort === String(desiredHostPort));
395
+ if (matched)
396
+ return;
397
+ const actualPorts = actual
398
+ .map((b) => b.HostPort)
399
+ .filter(Boolean)
400
+ .join(', ') || '<none>';
401
+ throw new CapabilityError(`Registry container '${registryName}' is bound to host port ${actualPorts} but config requests ${desiredHostPort}. ` +
402
+ `Docker can't change port mappings on a running container — run ` +
403
+ `\`vibes pm resource recreate ${registryName === 'kind-registry' ? 'kind-vibes-local' : registryName}\` ` +
404
+ `(with --force if the cluster has stateful data) to rebuild with the new port.`, 'CONFLICT', {
405
+ reason: 'pm-kind-registry-port-mismatch',
406
+ retryable: false,
407
+ details: { registryName, desiredHostPort }
408
+ });
409
+ }
410
+ /**
411
+ * Attempt to restart a stopped cluster container
412
+ */
413
+ async function restartClusterContainer(runtime, clusterName) {
414
+ const containerName = `${clusterName}-control-plane`;
415
+ // Check if container exists
416
+ if (!(await containerExists(runtime, containerName))) {
417
+ return false;
418
+ }
419
+ // Check for broken port bindings (common after Docker Desktop restart).
420
+ // If port bindings are gone, a simple restart won't fix it — the cluster
421
+ // must be fully recreated.
422
+ if (!(await hasActivePortBindings(runtime, containerName))) {
423
+ getLogger('pm:kind-cluster', runtime).info('Port bindings lost (Docker Desktop restart?), the cluster must be recreated', {
424
+ containerName
425
+ });
426
+ return false;
427
+ }
428
+ // Try to restart
429
+ const { exitCode } = await runCapture(runtime, 'docker', ['restart', containerName], {
430
+ env: await dockerEnv(runtime),
431
+ signal: runtime.lifecycle.signal,
432
+ inheritEnv: DOCKER_TOOL_INHERIT_ENV
433
+ });
434
+ if (exitCode !== 0) {
435
+ return false;
436
+ }
437
+ // Wait for the node to become ready (up to 30s), bounded by the runtime
438
+ // lifecycle (RC07). Poll: the kind node (owner) answers readiness only when
439
+ // asked (`kubectl get nodes`); it publishes no event to this process.
440
+ return pollUntil(runtime, () => isNodeReady(runtime, clusterName), {
441
+ timeoutMs: 30_000,
442
+ intervalMs: 1_000,
443
+ reason: 'pm-kind-node-ready'
444
+ });
445
+ }
446
+ /**
447
+ * Delete kind cluster
448
+ */
449
+ async function deleteCluster(runtime, name) {
450
+ const { exitCode, stderr } = await runCapture(runtime, 'kind', ['delete', 'cluster', '--name', name], {
451
+ env: await dockerEnv(runtime),
452
+ signal: runtime.lifecycle.signal,
453
+ inheritEnv: DOCKER_TOOL_INHERIT_ENV
454
+ });
455
+ if (exitCode !== 0 && !stderr.includes('no kind cluster')) {
456
+ throw toolFailure({ tool: 'kind', action: 'delete cluster', outcome: { exitCode, stderr } });
457
+ }
458
+ }
459
+ /**
460
+ * Delete registry container
461
+ */
462
+ async function deleteRegistry(runtime, name) {
463
+ if (await containerExists(runtime, name)) {
464
+ const env = await dockerEnv(runtime);
465
+ await runCapture(runtime, 'docker', ['stop', name], {
466
+ env,
467
+ signal: runtime.lifecycle.signal,
468
+ inheritEnv: DOCKER_TOOL_INHERIT_ENV
469
+ });
470
+ await runCapture(runtime, 'docker', ['rm', name], {
471
+ env,
472
+ signal: runtime.lifecycle.signal,
473
+ inheritEnv: DOCKER_TOOL_INHERIT_ENV
474
+ });
475
+ }
476
+ }
477
+ /**
478
+ * Kind Cluster Provider
479
+ * Creates and manages Kind clusters with local registry support
480
+ */
481
+ export class KindClusterProvider {
482
+ type = 'kind';
483
+ subType = 'cluster';
484
+ configStore = new Map();
485
+ /**
486
+ * Setup (create or ensure) a kind cluster
487
+ */
488
+ async setup(config, { runtime }) {
489
+ const clusterConfig = this.validateConfig(config.config);
490
+ const clusterName = clusterConfig.name || config.id;
491
+ const registryName = clusterConfig.registryName;
492
+ this.configStore.set(config.id, config);
493
+ const log = getLogger('pm:kind-cluster', runtime);
494
+ // Step 1: Check for orphaned registry and clean up
495
+ const orphanCheck = await detectOrphanedRegistry(runtime, clusterName, registryName);
496
+ if (orphanCheck.orphaned) {
497
+ log.info('Cleaning up orphaned registry', { registryName });
498
+ await cleanupOrphanedRegistry(runtime, registryName);
499
+ }
500
+ // Step 2: Check if cluster already exists and is healthy
501
+ const hasCluster = await clusterExists(runtime, clusterName);
502
+ if (hasCluster) {
503
+ // Catch port-mapping drift before reporting success — Docker
504
+ // won't change port bindings on a running container, so a
505
+ // config change to `registryPort` is silently ineffective
506
+ // without an explicit recreate.
507
+ if (clusterConfig.registry) {
508
+ await assertRegistryPortMatches(runtime, registryName, clusterConfig.registryPort);
509
+ }
510
+ const nodeReady = await isNodeReady(runtime, clusterName);
511
+ if (nodeReady) {
512
+ // Cluster exists and is healthy
513
+ return this.createSuccessResult(config, clusterName, clusterConfig, 'existing');
514
+ }
515
+ // Cluster exists but node not ready - try restart
516
+ log.info('Cluster exists but its node is not ready, attempting restart', { clusterName });
517
+ const restarted = await restartClusterContainer(runtime, clusterName);
518
+ if (restarted) {
519
+ return this.createSuccessResult(config, clusterName, clusterConfig, 'restarted');
520
+ }
521
+ // Restart failed - recreate
522
+ log.info('Restart failed, recreating the cluster', { clusterName });
523
+ await deleteCluster(runtime, clusterName);
524
+ }
525
+ // Step 3: Create registry if enabled
526
+ if (clusterConfig.registry) {
527
+ log.info('Creating local registry', { registryName, port: clusterConfig.registryPort });
528
+ await createRegistry(runtime, registryName, clusterConfig.registryPort);
529
+ }
530
+ // Step 4: Create cluster
531
+ log.info('Creating kind cluster', { clusterName });
532
+ await createCluster(runtime, clusterName, clusterConfig);
533
+ return this.createSuccessResult(config, clusterName, clusterConfig, 'created');
534
+ }
535
+ /**
536
+ * Create success result
537
+ */
538
+ createSuccessResult(config, clusterName, clusterConfig, action) {
539
+ const state = {
540
+ id: config.id,
541
+ type: 'kind/cluster',
542
+ status: 'ready',
543
+ createdAt: new Date(),
544
+ lastChecked: new Date(),
545
+ metadata: {
546
+ clusterName,
547
+ context: `kind-${clusterName}`,
548
+ registryEnabled: clusterConfig.registry,
549
+ registryName: clusterConfig.registryName,
550
+ registryPort: clusterConfig.registryPort,
551
+ action
552
+ }
553
+ };
554
+ return {
555
+ state,
556
+ metadata: {
557
+ clusterName,
558
+ context: `kind-${clusterName}`,
559
+ action,
560
+ message: action === 'created'
561
+ ? `Kind cluster '${clusterName}' created successfully`
562
+ : action === 'restarted'
563
+ ? `Kind cluster '${clusterName}' restarted successfully`
564
+ : `Kind cluster '${clusterName}' already exists and is healthy`
565
+ }
566
+ };
567
+ }
568
+ /**
569
+ * Teardown (delete) cluster and registry
570
+ */
571
+ async teardown(id, state, { runtime }) {
572
+ const storedConfig = this.configStore.get(id);
573
+ const clusterConfig = storedConfig
574
+ ? this.validateConfig(storedConfig.config)
575
+ : {
576
+ name: id,
577
+ registry: true,
578
+ registryName: 'kind-registry',
579
+ registryPort: 5001,
580
+ waitTimeout: 120000
581
+ };
582
+ const remembered = KindStateMetadataSchema.parse(state?.metadata ?? {});
583
+ const clusterName = remembered.clusterName || clusterConfig.name || id;
584
+ const registryName = remembered.registryName || clusterConfig.registryName;
585
+ // Delete cluster first
586
+ await deleteCluster(runtime, clusterName);
587
+ // Delete registry if it was created by us
588
+ if (clusterConfig.registry) {
589
+ await deleteRegistry(runtime, registryName);
590
+ }
591
+ this.configStore.delete(id);
592
+ }
593
+ /**
594
+ * Get status of cluster
595
+ */
596
+ async status(id, { runtime }) {
597
+ const storedConfig = this.configStore.get(id);
598
+ const clusterName = storedConfig ? this.validateConfig(storedConfig.config).name || id : id;
599
+ const hasCluster = await clusterExists(runtime, clusterName, STATUS_CHECK_TIMEOUT_MS);
600
+ if (!hasCluster) {
601
+ return { status: 'missing' };
602
+ }
603
+ const nodeReady = await isNodeReady(runtime, clusterName, STATUS_CHECK_TIMEOUT_MS);
604
+ if (!nodeReady) {
605
+ return {
606
+ status: 'error',
607
+ error: 'Cluster exists but node is not ready'
608
+ };
609
+ }
610
+ return {
611
+ status: 'ready',
612
+ metadata: {
613
+ clusterName,
614
+ context: `kind-${clusterName}`
615
+ }
616
+ };
617
+ }
618
+ /**
619
+ * Check if cluster is ready
620
+ */
621
+ async checkReady(config, _state, { runtime }) {
622
+ const clusterConfig = this.validateConfig(config.config);
623
+ const clusterName = clusterConfig.name || config.id;
624
+ // Check cluster exists
625
+ const hasCluster = await clusterExists(runtime, clusterName, STATUS_CHECK_TIMEOUT_MS);
626
+ if (!hasCluster) {
627
+ return {
628
+ ready: false,
629
+ reason: `Kind cluster '${clusterName}' does not exist`,
630
+ retryAfter: 5000
631
+ };
632
+ }
633
+ // Check node is ready
634
+ const nodeReady = await isNodeReady(runtime, clusterName, STATUS_CHECK_TIMEOUT_MS);
635
+ if (!nodeReady) {
636
+ return {
637
+ ready: false,
638
+ reason: `Kind cluster '${clusterName}' node is not ready`,
639
+ retryAfter: 2000
640
+ };
641
+ }
642
+ return { ready: true };
643
+ }
644
+ /**
645
+ * Validate provider-specific configuration
646
+ */
647
+ validateConfig(config) {
648
+ return KindClusterConfigSchema.parse(config);
649
+ }
650
+ /**
651
+ * Get human-readable description
652
+ */
653
+ describe(config) {
654
+ const clusterConfig = this.validateConfig(config.config);
655
+ const clusterName = clusterConfig.name || config.id;
656
+ const registry = clusterConfig.registry
657
+ ? ` with registry on port ${clusterConfig.registryPort}`
658
+ : '';
659
+ return `Kind cluster '${clusterName}'${registry}`;
660
+ }
661
+ /**
662
+ * Data a `kind delete cluster` would destroy: the cluster's PVCs on
663
+ * kind's default local-path provisioner (see {@link listAtRiskPVCs}).
664
+ */
665
+ async listAtRiskData(config, { runtime }) {
666
+ const clusterConfig = this.validateConfig(config.config);
667
+ return listAtRiskPVCs(runtime, clusterConfig.name || config.id);
668
+ }
669
+ async computeSourceHash(config, { runtime }) {
670
+ const clusterConfig = this.validateConfig(config.config);
671
+ const hash = createSha256();
672
+ hash.update(`name:${clusterConfig.name || config.id}\n`);
673
+ hash.update(`registry:${clusterConfig.registry}\n`);
674
+ hash.update(`registryPort:${clusterConfig.registryPort}\n`);
675
+ hash.update(`registryName:${clusterConfig.registryName}\n`);
676
+ if (clusterConfig.nodeImage)
677
+ hash.update(`nodeImage:${clusterConfig.nodeImage}\n`);
678
+ if (clusterConfig.configFile) {
679
+ const files = await pmFileSystem(runtime);
680
+ const absPath = files.resolve(requireRuntimeCwd(runtime), clusterConfig.configFile);
681
+ const content = await readBytesFileIfPresent(files, absPath);
682
+ if (content === undefined) {
683
+ // A missing config file is part of the hash.
684
+ hash.update(`configFile:${clusterConfig.configFile}:missing\n`);
685
+ }
686
+ else {
687
+ hash.update(`configFile:${clusterConfig.configFile}\n`);
688
+ hash.update(content);
689
+ }
690
+ }
691
+ return hash.digestHex();
692
+ }
693
+ }
694
+ //# sourceMappingURL=kind-cluster.js.map