@a5c-ai/krate 5.0.1-staging.04a3db697
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/Dockerfile +31 -0
- package/README.md +183 -0
- package/bin/krate-demo.mjs +23 -0
- package/bin/krate-server.mjs +14 -0
- package/dist/krate-controller-ui.json +3067 -0
- package/dist/krate-lifecycle.json +201 -0
- package/dist/krate-runtime-snapshot.json +2955 -0
- package/dist/krate-summary.json +722 -0
- package/docs/README.md +61 -0
- package/docs/agents/README.md +83 -0
- package/docs/agents/acceptance-test-matrix.md +193 -0
- package/docs/agents/agent-mux-adapter-contract.md +167 -0
- package/docs/agents/agent-mux-source-map.md +310 -0
- package/docs/agents/agent-run-memory-import-spec.md +256 -0
- package/docs/agents/agent-stack-management-spec.md +421 -0
- package/docs/agents/api-contract-spec.md +309 -0
- package/docs/agents/artifacts-writeback-spec.md +145 -0
- package/docs/agents/chart-packaging-spec.md +128 -0
- package/docs/agents/ci-orchestration-spec.md +140 -0
- package/docs/agents/context-assembly-spec.md +219 -0
- package/docs/agents/controller-reconciliation-spec.md +255 -0
- package/docs/agents/crd-schema-spec.md +315 -0
- package/docs/agents/decision-log-open-questions.md +169 -0
- package/docs/agents/developer-implementation-checklist.md +329 -0
- package/docs/agents/dispatching-design.md +262 -0
- package/docs/agents/gaps-agent-mux-to-krate-crds.md +298 -0
- package/docs/agents/glossary.md +66 -0
- package/docs/agents/implementation-blueprint.md +324 -0
- package/docs/agents/implementation-rollout-slices.md +251 -0
- package/docs/agents/memory-context-integration-spec.md +194 -0
- package/docs/agents/memory-ontology-schema-spec.md +253 -0
- package/docs/agents/memory-operations-runbook.md +121 -0
- package/docs/agents/mvp-vertical-slice-spec.md +146 -0
- package/docs/agents/observability-audit-spec.md +265 -0
- package/docs/agents/operator-runbook.md +174 -0
- package/docs/agents/org-memory-api-payload-examples.md +333 -0
- package/docs/agents/org-memory-controller-sequence-spec.md +181 -0
- package/docs/agents/org-memory-e2e-fixture-plan.md +161 -0
- package/docs/agents/org-memory-ui-implementation-map.md +114 -0
- package/docs/agents/org-memory-vertical-slice-spec.md +168 -0
- package/docs/agents/org-resource-model-delta-spec.md +111 -0
- package/docs/agents/org-route-resource-model-spec.md +183 -0
- package/docs/agents/org-scoping-namespace-spec.md +114 -0
- package/docs/agents/rbac-secrets-management-spec.md +406 -0
- package/docs/agents/repository-page-integration-spec.md +255 -0
- package/docs/agents/resource-contract-examples.md +808 -0
- package/docs/agents/resource-relationship-map.md +190 -0
- package/docs/agents/security-threat-model.md +188 -0
- package/docs/agents/shared-memory-company-brain-spec.md +358 -0
- package/docs/agents/storage-migration-spec.md +168 -0
- package/docs/agents/subagent-orchestration-spec.md +152 -0
- package/docs/agents/system-overview.md +88 -0
- package/docs/agents/tools-mcp-skills-spec.md +189 -0
- package/docs/agents/traceability-matrix.md +79 -0
- package/docs/agents/ui-flow-spec.md +211 -0
- package/docs/agents/ui-ux-system-spec.md +426 -0
- package/docs/agents/workspace-lifecycle-spec.md +166 -0
- package/docs/architecture-spec.md +78 -0
- package/docs/components/control-plane.md +78 -0
- package/docs/components/data-plane.md +69 -0
- package/docs/components/hooks-events.md +67 -0
- package/docs/components/identity-rbac-policy.md +73 -0
- package/docs/components/kubevela-oam.md +70 -0
- package/docs/components/operations-publishing.md +81 -0
- package/docs/components/runners-ci.md +66 -0
- package/docs/components/web-ui.md +94 -0
- package/docs/external/README.md +47 -0
- package/docs/external/bidirectional-sync-design.md +134 -0
- package/docs/external/cicd-interface.md +64 -0
- package/docs/external/external-backend-controllers.md +170 -0
- package/docs/external/external-backend-crds.md +234 -0
- package/docs/external/external-backend-ui-spec.md +151 -0
- package/docs/external/external-backend-ux-flows.md +115 -0
- package/docs/external/external-object-mapping.md +125 -0
- package/docs/external/git-forge-interface.md +68 -0
- package/docs/external/github-integration-design.md +151 -0
- package/docs/external/issue-tracking-interface.md +66 -0
- package/docs/external/provider-capability-manifests.md +204 -0
- package/docs/external/provider-catalog.md +139 -0
- package/docs/external/provider-rollout-testing.md +78 -0
- package/docs/external/research-results.md +48 -0
- package/docs/external/security-auth-permissions.md +81 -0
- package/docs/external/sync-state-machines.md +108 -0
- package/docs/external/unified-external-backend-model.md +107 -0
- package/docs/external/user-facing-changes.md +67 -0
- package/docs/gaps.md +161 -0
- package/docs/install.md +94 -0
- package/docs/krate-design.md +334 -0
- package/docs/local-minikube.md +55 -0
- package/docs/ontology/README.md +32 -0
- package/docs/ontology/bounded-contexts.md +29 -0
- package/docs/ontology/events-and-hooks.md +32 -0
- package/docs/ontology/oam-kubevela.md +32 -0
- package/docs/ontology/operations-and-release.md +25 -0
- package/docs/ontology/personas-and-actors.md +32 -0
- package/docs/ontology/policies-and-invariants.md +33 -0
- package/docs/ontology/problem-space.md +30 -0
- package/docs/ontology/resource-contracts.md +40 -0
- package/docs/ontology/resource-taxonomy.md +42 -0
- package/docs/ontology/runners-and-ci.md +29 -0
- package/docs/ontology/solution-space.md +24 -0
- package/docs/ontology/storage-and-data-boundaries.md +29 -0
- package/docs/ontology/validation-matrix.md +24 -0
- package/docs/ontology/web-ui-excellent-flows.md +32 -0
- package/docs/ontology/workflows.md +39 -0
- package/docs/ontology/world.md +35 -0
- package/docs/product-requirements.md +62 -0
- package/docs/roadmap-mvp.md +87 -0
- package/docs/system-requirements.md +90 -0
- package/docs/tests/README.md +53 -0
- package/docs/tests/agent-qa-plan.md +63 -0
- package/docs/tests/browser-ui-tests.md +62 -0
- package/docs/tests/ci-quality-gates.md +48 -0
- package/docs/tests/coverage-model.md +64 -0
- package/docs/tests/e2e-scenario-tests.md +53 -0
- package/docs/tests/fixtures-test-data.md +63 -0
- package/docs/tests/observability-reliability-tests.md +54 -0
- package/docs/tests/product-test-matrix.md +145 -0
- package/docs/tests/qa-adoption-roadmap.md +130 -0
- package/docs/tests/qa-automation-plan.md +101 -0
- package/docs/tests/security-compliance-tests.md +57 -0
- package/docs/tests/test-framework-tools.md +88 -0
- package/docs/tests/test-suite-layout.md +121 -0
- package/docs/tests/unit-integration-tests.md +48 -0
- package/docs/todo-kyverno +714 -0
- package/docs/todos.md +4 -0
- package/docs/user-stories.md +78 -0
- package/examples/minikube-demo.yaml +190 -0
- package/examples/oam-application.yaml +23 -0
- package/examples/policy-kyverno-pr-title.yaml +18 -0
- package/package.json +63 -0
- package/scripts/build.mjs +29 -0
- package/scripts/setup-minikube.mjs +65 -0
- package/scripts/smoke.mjs +37 -0
- package/scripts/validate-doc-coverage.mjs +152 -0
- package/scripts/validate-package.mjs +93 -0
- package/scripts/validate-ui.mjs +236 -0
- package/src/agent-adapter-controller.js +169 -0
- package/src/agent-approval-controller.js +170 -0
- package/src/agent-context-bundles.js +242 -0
- package/src/agent-dispatch-controller.js +209 -0
- package/src/agent-gateway-config-controller.js +147 -0
- package/src/agent-memory-controller.js +357 -0
- package/src/agent-memory-import.js +327 -0
- package/src/agent-memory-query.js +292 -0
- package/src/agent-memory-repository-source-controller.js +255 -0
- package/src/agent-mux-client.js +280 -0
- package/src/agent-permission-review.js +250 -0
- package/src/agent-project-controller.js +117 -0
- package/src/agent-provider-config-controller.js +150 -0
- package/src/agent-secret-config-grant-controller.js +282 -0
- package/src/agent-session-transcript-controller.js +189 -0
- package/src/agent-stack-controller.js +347 -0
- package/src/agent-subagent-controller.js +160 -0
- package/src/agent-transport-binding-controller.js +121 -0
- package/src/agent-trigger-controller.js +321 -0
- package/src/agent-workspace-controller.js +447 -0
- package/src/agent-writeback-controller.js +302 -0
- package/src/api-controller.js +541 -0
- package/src/argocd-gitops.js +43 -0
- package/src/async-controller.js +207 -0
- package/src/audit-controller.js +191 -0
- package/src/auth.js +307 -0
- package/src/component-catalog.js +41 -0
- package/src/control-plane.js +136 -0
- package/src/controller-client.js +50 -0
- package/src/controller-ui.js +551 -0
- package/src/data-plane.js +178 -0
- package/src/event-bus.js +61 -0
- package/src/external/conflict-controller.js +225 -0
- package/src/external/github/auth.js +96 -0
- package/src/external/github/cicd.js +180 -0
- package/src/external/github/git-forge.js +240 -0
- package/src/external/github/index.js +144 -0
- package/src/external/github/issue-tracking.js +163 -0
- package/src/external/provider-adapter.js +161 -0
- package/src/external/provider-resource-factory.js +161 -0
- package/src/external/sync-controller.js +235 -0
- package/src/external/webhook-controller.js +144 -0
- package/src/external/write-controller.js +283 -0
- package/src/gitea-backend.js +95 -0
- package/src/gitea-service.js +173 -0
- package/src/handoff.js +98 -0
- package/src/hooks-events.js +63 -0
- package/src/http-server.js +377 -0
- package/src/identity-policy.js +86 -0
- package/src/index.js +55 -0
- package/src/kubernetes-controller-async.js +511 -0
- package/src/kubernetes-controller.js +878 -0
- package/src/kubernetes-resource-gateway.js +48 -0
- package/src/operations.js +112 -0
- package/src/org-scoping.js +5 -0
- package/src/resource-model.js +221 -0
- package/src/runners-ci.js +48 -0
- package/src/runtime.js +196 -0
- package/src/snapshot-cache.js +157 -0
- package/src/web-ui.js +40 -0
- package/tests/agent-adapter-controller.test.js +361 -0
- package/tests/agent-approval-controller.test.js +173 -0
- package/tests/agent-context-bundles.test.js +278 -0
- package/tests/agent-dispatch-controller.test.js +315 -0
- package/tests/agent-gateway-config-controller.test.js +386 -0
- package/tests/agent-memory-controller.test.js +308 -0
- package/tests/agent-memory-import-snapshot.test.js +477 -0
- package/tests/agent-memory-query.test.js +404 -0
- package/tests/agent-memory-repository-source.test.js +514 -0
- package/tests/agent-mux-client.test.js +204 -0
- package/tests/agent-permission-review-v2.test.js +317 -0
- package/tests/agent-permission-review.test.js +209 -0
- package/tests/agent-project-controller.test.js +302 -0
- package/tests/agent-provider-config-controller.test.js +376 -0
- package/tests/agent-resources.test.js +228 -0
- package/tests/agent-secret-config-grant.test.js +231 -0
- package/tests/agent-session-transcript-controller.test.js +499 -0
- package/tests/agent-stack-controller.test.js +221 -0
- package/tests/agent-subagent-controller.test.js +201 -0
- package/tests/agent-transport-binding-controller.test.js +294 -0
- package/tests/agent-trigger-controller.test.js +211 -0
- package/tests/agent-trigger-routes.test.js +190 -0
- package/tests/agent-trigger-sources.test.js +245 -0
- package/tests/agent-workspace-controller.test.js +181 -0
- package/tests/agent-writeback.test.js +292 -0
- package/tests/approval-persistence.test.js +171 -0
- package/tests/async-controller.test.js +252 -0
- package/tests/audit-controller.test.js +227 -0
- package/tests/deployment.test.js +396 -0
- package/tests/e2e/lifecycle.test.js +117 -0
- package/tests/external-github-forge.test.js +560 -0
- package/tests/external-github-issues-cicd.test.js +520 -0
- package/tests/external-integration.test.js +470 -0
- package/tests/external-persistence.test.js +340 -0
- package/tests/external-provider-adapter.test.js +365 -0
- package/tests/external-resource-model.test.js +215 -0
- package/tests/external-webhook-sync.test.js +287 -0
- package/tests/external-write-conflict.test.js +353 -0
- package/tests/gitea-service.test.js +253 -0
- package/tests/health-check-real.test.js +165 -0
- package/tests/integration/full-flow.test.js +266 -0
- package/tests/krate.test.js +727 -0
- package/tests/memory-search-wiring.test.js +270 -0
- package/tests/org-scoping.test.js +687 -0
- package/tests/session-cookie-hmac.test.js +151 -0
- package/tests/snapshot-performance.test.js +247 -0
- package/tests/sse-events.test.js +107 -0
- package/tests/workspace-volumes.test.js +312 -0
- package/tests/writeback-persistence.test.js +207 -0
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
// Async kubectl helpers for parallel snapshot fetching.
|
|
2
|
+
//
|
|
3
|
+
// Exports:
|
|
4
|
+
// - runKubectlAsync -- Promise-based kubectl wrapper using child_process.spawn
|
|
5
|
+
// - getControllerSnapshotAsync -- parallel version of getControllerSnapshot
|
|
6
|
+
// - getPartialSnapshot -- query only a subset of resource kinds
|
|
7
|
+
// - watchResourceChanges -- lightweight watch that invalidates the snapshot cache
|
|
8
|
+
|
|
9
|
+
import { spawn } from 'node:child_process';
|
|
10
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
11
|
+
import { randomUUID } from 'node:crypto';
|
|
12
|
+
import {
|
|
13
|
+
KRATE_API_GROUP,
|
|
14
|
+
KRATE_API_VERSIONED_GROUP,
|
|
15
|
+
KUBEVELA_API_GROUP,
|
|
16
|
+
KYVERNO_API_GROUP,
|
|
17
|
+
KYVERNO_POLICIES_API_GROUP,
|
|
18
|
+
POLICY_REPORT_API_GROUP,
|
|
19
|
+
KRATE_RESOURCES,
|
|
20
|
+
KRATE_PLATFORM_NAMESPACE,
|
|
21
|
+
findResourceDefinition,
|
|
22
|
+
apiResourceName,
|
|
23
|
+
orgNamespaceName
|
|
24
|
+
} from './kubernetes-controller.js';
|
|
25
|
+
import { clearSnapshotCache, setOrgCache } from './snapshot-cache.js';
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// In-cluster kubectl args (same logic as the sync version, but using static
|
|
29
|
+
// node:fs imports instead of a lazy require helper).
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
function inClusterArgs(env) {
|
|
33
|
+
if (String(env.KRATE_DISABLE_IN_CLUSTER_KUBECTL || '').toLowerCase() === 'true') return null;
|
|
34
|
+
if (env.KUBECONFIG) return null;
|
|
35
|
+
const host = env.KUBERNETES_SERVICE_HOST;
|
|
36
|
+
const port = env.KUBERNETES_SERVICE_PORT || '443';
|
|
37
|
+
if (!host) return null;
|
|
38
|
+
const serviceAccountDir = env.KRATE_SERVICE_ACCOUNT_DIR || '/var/run/secrets/kubernetes.io/serviceaccount';
|
|
39
|
+
const tokenPath = env.KRATE_SERVICE_ACCOUNT_TOKEN || `${serviceAccountDir}/token`;
|
|
40
|
+
const caPath = env.KRATE_SERVICE_ACCOUNT_CA || `${serviceAccountDir}/ca.crt`;
|
|
41
|
+
if (!existsSync(tokenPath) || !existsSync(caPath)) return null;
|
|
42
|
+
const token = readFileSync(tokenPath, 'utf8').trim();
|
|
43
|
+
if (!token) return null;
|
|
44
|
+
return [
|
|
45
|
+
`--server=https://${host}:${port}`,
|
|
46
|
+
`--certificate-authority=${caPath}`,
|
|
47
|
+
`--token=${token}`
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function kubectlArgs(args, env) {
|
|
52
|
+
const extra = inClusterArgs(env);
|
|
53
|
+
return extra ? [...extra, ...args] : args;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Low-level async kubectl runner
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Run a kubectl command asynchronously using child_process.spawn.
|
|
62
|
+
* Returns a Promise that resolves to the same shape as runKubectl (sync).
|
|
63
|
+
*
|
|
64
|
+
* @param {string[]} args
|
|
65
|
+
* @param {object} [options]
|
|
66
|
+
* @param {string} [options.kubectl]
|
|
67
|
+
* @param {number} [options.timeoutMs]
|
|
68
|
+
* @param {object} [options.env]
|
|
69
|
+
* @param {string} [options.input]
|
|
70
|
+
* @param {boolean} [options.allowFailure]
|
|
71
|
+
* @returns {Promise<{ok: boolean, status: number|null, signal: string|null, stdout: string, stderr: string, error: string|null, command: string}>}
|
|
72
|
+
*/
|
|
73
|
+
export function runKubectlAsync(args, options = {}) {
|
|
74
|
+
const kubectl = options.kubectl || process.env.KRATE_KUBECTL || 'kubectl';
|
|
75
|
+
const timeoutMs = Number(options.timeoutMs || process.env.KRATE_KUBECTL_TIMEOUT_MS || 3_000);
|
|
76
|
+
const env = options.env || process.env;
|
|
77
|
+
const command = `kubectl ${args.join(' ')}`;
|
|
78
|
+
const effectiveArgs = kubectlArgs(args, env);
|
|
79
|
+
|
|
80
|
+
return new Promise((resolve, reject) => {
|
|
81
|
+
let stdout = '';
|
|
82
|
+
let stderr = '';
|
|
83
|
+
let settled = false;
|
|
84
|
+
|
|
85
|
+
const child = spawn(kubectl, effectiveArgs, {
|
|
86
|
+
env,
|
|
87
|
+
windowsHide: true,
|
|
88
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (options.input) {
|
|
92
|
+
child.stdin.write(options.input);
|
|
93
|
+
}
|
|
94
|
+
child.stdin.end();
|
|
95
|
+
|
|
96
|
+
child.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
|
|
97
|
+
child.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
|
|
98
|
+
|
|
99
|
+
const timer = setTimeout(() => {
|
|
100
|
+
if (settled) return;
|
|
101
|
+
settled = true;
|
|
102
|
+
try { child.kill('SIGTERM'); } catch { /* ignore */ }
|
|
103
|
+
const result = {
|
|
104
|
+
ok: false, status: null, signal: 'SIGTERM', stdout, stderr,
|
|
105
|
+
error: `kubectl timed out after ${timeoutMs}ms`, command
|
|
106
|
+
};
|
|
107
|
+
if (options.allowFailure) resolve(result);
|
|
108
|
+
else reject(new Error(result.error));
|
|
109
|
+
}, timeoutMs);
|
|
110
|
+
|
|
111
|
+
child.on('error', (error) => {
|
|
112
|
+
if (settled) return;
|
|
113
|
+
settled = true;
|
|
114
|
+
clearTimeout(timer);
|
|
115
|
+
const result = { ok: false, status: null, signal: null, stdout, stderr, error: error.message, command };
|
|
116
|
+
if (options.allowFailure) resolve(result);
|
|
117
|
+
else reject(error);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
child.on('close', (code, signal) => {
|
|
121
|
+
if (settled) return;
|
|
122
|
+
settled = true;
|
|
123
|
+
clearTimeout(timer);
|
|
124
|
+
const ok = code === 0 && !signal;
|
|
125
|
+
const result = { ok, status: code, signal: signal || null, stdout, stderr, error: null, command };
|
|
126
|
+
if (!ok && !options.allowFailure) {
|
|
127
|
+
const detail = (stderr || stdout || '').trim();
|
|
128
|
+
reject(new Error(`${command}: ${detail || `exit ${code ?? 'unknown'}`}`));
|
|
129
|
+
} else {
|
|
130
|
+
resolve(result);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// Parallel snapshot
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Fetch the full controller snapshot with all resource kinds queried in
|
|
142
|
+
* parallel (Promise.all) instead of sequentially with spawnSync.
|
|
143
|
+
*
|
|
144
|
+
* Falls back to the synchronous getControllerSnapshot on any unexpected error.
|
|
145
|
+
*
|
|
146
|
+
* @param {object} [options]
|
|
147
|
+
* @returns {Promise<object>}
|
|
148
|
+
*/
|
|
149
|
+
export async function getControllerSnapshotAsync(options = {}) {
|
|
150
|
+
const kubectl = options.kubectl || process.env.KRATE_KUBECTL || 'kubectl';
|
|
151
|
+
const namespace = options.namespace || process.env.KRATE_NAMESPACE || 'krate-system';
|
|
152
|
+
const timeoutMs = Number(options.timeoutMs || process.env.KRATE_KUBECTL_TIMEOUT_MS || 3_000);
|
|
153
|
+
const env = { ...process.env, ...(options.env || {}) };
|
|
154
|
+
const correlationId = randomUUID();
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
// Phase 1: context + version in parallel
|
|
158
|
+
const [contextResult, versionResult] = await Promise.all([
|
|
159
|
+
runKubectlAsync(['config', 'current-context'], { kubectl, timeoutMs, env, allowFailure: true }),
|
|
160
|
+
runKubectlAsync(['version', '--client=true', '-o', 'json'], { kubectl, timeoutMs, env, allowFailure: true })
|
|
161
|
+
]);
|
|
162
|
+
|
|
163
|
+
if (!contextResult.ok || !versionResult.ok) {
|
|
164
|
+
return buildEmptySnapshot({ correlationId, namespace, kubectl, contextResult, versionResult, available: false });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Phase 2: API service + CRD discovery in parallel
|
|
168
|
+
const [apiServiceResult, crdResult] = await Promise.all([
|
|
169
|
+
runKubectlAsync(['get', 'apiservice', KRATE_API_VERSIONED_GROUP, '-o', 'json'], { kubectl, timeoutMs, env, allowFailure: true }),
|
|
170
|
+
runKubectlAsync(['get', 'crd', '-o', 'json'], { kubectl, timeoutMs, env, allowFailure: true })
|
|
171
|
+
]);
|
|
172
|
+
|
|
173
|
+
if (!apiServiceResult.ok && !crdResult.ok) {
|
|
174
|
+
return buildEmptySnapshot({ correlationId, namespace, kubectl, contextResult, versionResult, available: true });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const KYVERNO_DISCOVERY_GROUPS = new Set([KYVERNO_API_GROUP, KYVERNO_POLICIES_API_GROUP, POLICY_REPORT_API_GROUP]);
|
|
178
|
+
const discoveredCrds = crdResult.ok
|
|
179
|
+
? parseKubernetesList(crdResult.stdout).items.filter((crd) =>
|
|
180
|
+
[KRATE_API_GROUP, KUBEVELA_API_GROUP].includes(crd.spec?.group) ||
|
|
181
|
+
KYVERNO_DISCOVERY_GROUPS.has(crd.spec?.group))
|
|
182
|
+
: [];
|
|
183
|
+
const discoveredPluralSet = new Set(
|
|
184
|
+
discoveredCrds.map((crd) => `${crd.spec?.group || KRATE_API_GROUP}/${crd.spec?.names?.plural}`).filter(Boolean)
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
if (!apiServiceResult.ok && discoveredCrds.length === 0) {
|
|
188
|
+
return buildEmptySnapshot({ correlationId, namespace, kubectl, contextResult, versionResult, available: true, apiServiceResult });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Phase 3: list all resources in parallel
|
|
192
|
+
const snapshotResources = KRATE_RESOURCES.filter((d) => d.storage !== 'core');
|
|
193
|
+
const resources = Object.fromEntries(snapshotResources.map((d) => [d.kind, []]));
|
|
194
|
+
|
|
195
|
+
const platformScopedDefs = snapshotResources.filter((d) => d.platformScoped);
|
|
196
|
+
const orgScopedDefs = snapshotResources.filter((d) => !d.platformScoped);
|
|
197
|
+
|
|
198
|
+
// Fetch platform-scoped resources first so we can derive org namespaces
|
|
199
|
+
const platformResults = await Promise.all(
|
|
200
|
+
platformScopedDefs
|
|
201
|
+
.filter((d) => discoveredPluralSet.has(`${d.group || KRATE_API_GROUP}/${d.plural}`))
|
|
202
|
+
.map(async (definition) => {
|
|
203
|
+
const resourceNamespace = definition.namespace || namespace;
|
|
204
|
+
const result = await runKubectlAsync(
|
|
205
|
+
['get', apiResourceName(definition), ...namespaceArgs(definition, resourceNamespace), '-o', 'json', '--ignore-not-found'],
|
|
206
|
+
{ kubectl, timeoutMs, env, allowFailure: true }
|
|
207
|
+
);
|
|
208
|
+
return { definition, result };
|
|
209
|
+
})
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
for (const { definition, result } of platformResults) {
|
|
213
|
+
resources[definition.kind] = result.ok ? parseKubernetesList(result.stdout).items : [];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const orgNamespaces = resolveOrgNamespaces(resources.Organization, resources.OrgNamespaceBinding, namespace);
|
|
217
|
+
|
|
218
|
+
// Fetch org-scoped resources in parallel
|
|
219
|
+
const orgResults = await Promise.all(
|
|
220
|
+
orgScopedDefs
|
|
221
|
+
.filter((d) => discoveredPluralSet.has(`${d.group || KRATE_API_GROUP}/${d.plural}`))
|
|
222
|
+
.map(async (definition) => {
|
|
223
|
+
const namespaces = definition.namespaced === false
|
|
224
|
+
? [null]
|
|
225
|
+
: [definition.namespace || null].filter(Boolean).concat(definition.namespace ? [] : orgNamespaces);
|
|
226
|
+
|
|
227
|
+
const itemArrays = await Promise.all(
|
|
228
|
+
namespaces.map(async (resourceNamespace) => {
|
|
229
|
+
const effectiveNamespace = resourceNamespace || namespace;
|
|
230
|
+
const result = await runKubectlAsync(
|
|
231
|
+
['get', apiResourceName(definition), ...namespaceArgs(definition, effectiveNamespace), '-o', 'json', '--ignore-not-found'],
|
|
232
|
+
{ kubectl, timeoutMs, env, allowFailure: true }
|
|
233
|
+
);
|
|
234
|
+
return result.ok ? parseKubernetesList(result.stdout).items : [];
|
|
235
|
+
})
|
|
236
|
+
);
|
|
237
|
+
return { definition, items: itemArrays.flat() };
|
|
238
|
+
})
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
for (const { definition, items } of orgResults) {
|
|
242
|
+
resources[definition.kind] = items;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const eventsResult = await runKubectlAsync(
|
|
246
|
+
['get', 'events', '-n', namespace, '-o', 'json', '--ignore-not-found'],
|
|
247
|
+
{ kubectl, timeoutMs, env, allowFailure: true }
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
source: 'kubernetes',
|
|
252
|
+
mode: 'kubernetes-api',
|
|
253
|
+
namespace,
|
|
254
|
+
generatedAt: new Date().toISOString(),
|
|
255
|
+
correlationId,
|
|
256
|
+
kubectl: {
|
|
257
|
+
binary: kubectl,
|
|
258
|
+
context: contextResult.stdout.trim(),
|
|
259
|
+
clientVersion: safeJson(versionResult.stdout)?.clientVersion?.gitVersion || null,
|
|
260
|
+
available: true,
|
|
261
|
+
errors: []
|
|
262
|
+
},
|
|
263
|
+
apiService: apiServiceResult.ok ? safeJson(apiServiceResult.stdout) : null,
|
|
264
|
+
crds: discoveredCrds,
|
|
265
|
+
resources,
|
|
266
|
+
kyverno: emptyKyverno(),
|
|
267
|
+
events: eventsResult.ok ? parseKubernetesList(eventsResult.stdout).items : [],
|
|
268
|
+
permissions: [],
|
|
269
|
+
storage: storageBoundaries(),
|
|
270
|
+
commands: []
|
|
271
|
+
};
|
|
272
|
+
} catch (error) {
|
|
273
|
+
// Fallback to synchronous snapshot
|
|
274
|
+
const { getControllerSnapshot } = await import('./kubernetes-controller.js');
|
|
275
|
+
return getControllerSnapshot(options);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
// Partial snapshot
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Fetch only the requested resource kinds from Kubernetes.
|
|
285
|
+
* Useful for pages that only need a subset (e.g. only AgentStack, AgentSession).
|
|
286
|
+
*
|
|
287
|
+
* @param {string[]} kinds - Array of kind names or plurals to query
|
|
288
|
+
* @param {object} [options]
|
|
289
|
+
* @returns {Promise<object>} Snapshot with only the requested resource keys populated
|
|
290
|
+
*/
|
|
291
|
+
export async function getPartialSnapshot(kinds = [], options = {}) {
|
|
292
|
+
if (!Array.isArray(kinds) || kinds.length === 0) {
|
|
293
|
+
return { source: 'kubernetes', mode: 'partial', resources: {}, generatedAt: new Date().toISOString() };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const kubectl = options.kubectl || process.env.KRATE_KUBECTL || 'kubectl';
|
|
297
|
+
const namespace = options.namespace || process.env.KRATE_NAMESPACE || 'krate-system';
|
|
298
|
+
const timeoutMs = Number(options.timeoutMs || process.env.KRATE_KUBECTL_TIMEOUT_MS || 3_000);
|
|
299
|
+
const env = { ...process.env, ...(options.env || {}) };
|
|
300
|
+
|
|
301
|
+
// Resolve requested kinds to definition objects (ignore unknown)
|
|
302
|
+
const requestedDefs = kinds
|
|
303
|
+
.map((k) => { try { return findResourceDefinition(k); } catch { return null; } })
|
|
304
|
+
.filter(Boolean);
|
|
305
|
+
|
|
306
|
+
const resources = Object.fromEntries(requestedDefs.map((d) => [d.kind, []]));
|
|
307
|
+
|
|
308
|
+
// Determine org namespaces only when necessary
|
|
309
|
+
const needsOrgNs = requestedDefs.some((d) => !d.platformScoped && d.namespaced !== false && !d.namespace);
|
|
310
|
+
let orgNamespaces = [namespace];
|
|
311
|
+
|
|
312
|
+
if (needsOrgNs) {
|
|
313
|
+
try {
|
|
314
|
+
const orgDef = findResourceDefinition('Organization');
|
|
315
|
+
const bindingDef = findResourceDefinition('OrgNamespaceBinding');
|
|
316
|
+
const [orgResult, bindingResult] = await Promise.all([
|
|
317
|
+
runKubectlAsync(['get', apiResourceName(orgDef), '-n', namespace, '-o', 'json', '--ignore-not-found'], { kubectl, timeoutMs, env, allowFailure: true }),
|
|
318
|
+
runKubectlAsync(['get', apiResourceName(bindingDef), '-n', namespace, '-o', 'json', '--ignore-not-found'], { kubectl, timeoutMs, env, allowFailure: true })
|
|
319
|
+
]);
|
|
320
|
+
orgNamespaces = resolveOrgNamespaces(
|
|
321
|
+
orgResult.ok ? parseKubernetesList(orgResult.stdout).items : [],
|
|
322
|
+
bindingResult.ok ? parseKubernetesList(bindingResult.stdout).items : [],
|
|
323
|
+
namespace
|
|
324
|
+
);
|
|
325
|
+
} catch {
|
|
326
|
+
orgNamespaces = [namespace];
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Fetch all requested definitions in parallel
|
|
331
|
+
await Promise.all(requestedDefs.map(async (definition) => {
|
|
332
|
+
const namespaces = definition.namespaced === false
|
|
333
|
+
? [null]
|
|
334
|
+
: definition.platformScoped
|
|
335
|
+
? [definition.namespace || namespace]
|
|
336
|
+
: [definition.namespace || null].filter(Boolean).concat(definition.namespace ? [] : orgNamespaces);
|
|
337
|
+
|
|
338
|
+
const itemArrays = await Promise.all(
|
|
339
|
+
namespaces.map(async (resourceNamespace) => {
|
|
340
|
+
const effectiveNamespace = resourceNamespace || namespace;
|
|
341
|
+
const result = await runKubectlAsync(
|
|
342
|
+
['get', apiResourceName(definition), ...namespaceArgs(definition, effectiveNamespace), '-o', 'json', '--ignore-not-found'],
|
|
343
|
+
{ kubectl, timeoutMs, env, allowFailure: true }
|
|
344
|
+
);
|
|
345
|
+
return result.ok ? parseKubernetesList(result.stdout).items : [];
|
|
346
|
+
})
|
|
347
|
+
);
|
|
348
|
+
resources[definition.kind] = itemArrays.flat();
|
|
349
|
+
}));
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
source: 'kubernetes',
|
|
353
|
+
mode: 'partial',
|
|
354
|
+
namespace,
|
|
355
|
+
generatedAt: new Date().toISOString(),
|
|
356
|
+
resources
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ---------------------------------------------------------------------------
|
|
361
|
+
// Watch resource changes
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Watch key resource kinds with `kubectl get --watch -o json`.
|
|
366
|
+
* On any change event, the snapshot cache is cleared so the next read
|
|
367
|
+
* triggers a fresh fetch.
|
|
368
|
+
*
|
|
369
|
+
* @param {Function} callback - Called with (kind, item) on each watch event
|
|
370
|
+
* @param {object} [options]
|
|
371
|
+
* @param {string[]} [options.kinds] - Defaults to ['Organization', 'AgentStack', 'AgentSession']
|
|
372
|
+
* @returns {{ stop: () => void }} Cleanup handle
|
|
373
|
+
*/
|
|
374
|
+
export function watchResourceChanges(callback, options = {}) {
|
|
375
|
+
const kubectl = options.kubectl || process.env.KRATE_KUBECTL || 'kubectl';
|
|
376
|
+
const namespace = options.namespace || process.env.KRATE_NAMESPACE || 'krate-system';
|
|
377
|
+
const env = { ...process.env, ...(options.env || {}) };
|
|
378
|
+
const watchKinds = options.kinds || ['Organization', 'AgentStack', 'AgentSession'];
|
|
379
|
+
|
|
380
|
+
const children = [];
|
|
381
|
+
|
|
382
|
+
for (const kind of watchKinds) {
|
|
383
|
+
let definition;
|
|
384
|
+
try { definition = findResourceDefinition(kind); } catch { continue; }
|
|
385
|
+
|
|
386
|
+
const resourceNamespace = definition.namespace || namespace;
|
|
387
|
+
const args = [
|
|
388
|
+
'get', apiResourceName(definition),
|
|
389
|
+
...namespaceArgs(definition, resourceNamespace),
|
|
390
|
+
'--watch', '-o', 'json'
|
|
391
|
+
];
|
|
392
|
+
|
|
393
|
+
const child = spawn(kubectl, kubectlArgs(args, env), {
|
|
394
|
+
env,
|
|
395
|
+
windowsHide: true,
|
|
396
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
let buffer = '';
|
|
400
|
+
child.stdout.on('data', (chunk) => {
|
|
401
|
+
buffer += chunk.toString();
|
|
402
|
+
let newlineIdx;
|
|
403
|
+
while ((newlineIdx = buffer.indexOf('\n')) !== -1) {
|
|
404
|
+
const line = buffer.slice(0, newlineIdx).trim();
|
|
405
|
+
buffer = buffer.slice(newlineIdx + 1);
|
|
406
|
+
if (!line) continue;
|
|
407
|
+
const item = safeJson(line);
|
|
408
|
+
if (item) {
|
|
409
|
+
// Invalidate the snapshot cache so the next read fetches fresh data
|
|
410
|
+
clearSnapshotCache();
|
|
411
|
+
try { callback(kind, item); } catch { /* user callback errors are non-fatal */ }
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
children.push(child);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return {
|
|
420
|
+
stop() {
|
|
421
|
+
for (const child of children) {
|
|
422
|
+
try { child.kill('SIGTERM'); } catch { /* ignore */ }
|
|
423
|
+
}
|
|
424
|
+
children.length = 0;
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ---------------------------------------------------------------------------
|
|
430
|
+
// Private helpers
|
|
431
|
+
// ---------------------------------------------------------------------------
|
|
432
|
+
|
|
433
|
+
function namespaceArgs(definition, namespace) {
|
|
434
|
+
return definition.namespaced === false ? [] : ['-n', namespace];
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function parseKubernetesList(stdout) {
|
|
438
|
+
const parsed = safeJson(stdout);
|
|
439
|
+
if (!parsed) return { items: [] };
|
|
440
|
+
if (Array.isArray(parsed.items)) return parsed;
|
|
441
|
+
if (parsed.kind && parsed.metadata) return { items: [parsed] };
|
|
442
|
+
return { items: [] };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function safeJson(text) {
|
|
446
|
+
try { return text ? JSON.parse(text) : null; }
|
|
447
|
+
catch { return null; }
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function resolveOrgNamespaces(organizations = [], bindings = [], fallbackNamespace = KRATE_PLATFORM_NAMESPACE) {
|
|
451
|
+
const namespaces = [...new Set([
|
|
452
|
+
...organizations.map((o) => o.spec?.namespaceName || o.metadata?.labels?.['krate.a5c.ai/namespace']).filter(Boolean),
|
|
453
|
+
...bindings.map((b) => b.spec?.namespace || b.metadata?.labels?.['krate.a5c.ai/namespace']).filter(Boolean)
|
|
454
|
+
])];
|
|
455
|
+
if (namespaces.length) return namespaces;
|
|
456
|
+
const fallbackOrgs = new Set();
|
|
457
|
+
const adminOrg = process.env.KRATE_ADMIN_ORG;
|
|
458
|
+
const defaultOrg = process.env.KRATE_ORG || 'default';
|
|
459
|
+
if (adminOrg) fallbackOrgs.add(orgNamespaceName(adminOrg));
|
|
460
|
+
fallbackOrgs.add(orgNamespaceName(defaultOrg));
|
|
461
|
+
return fallbackOrgs.size ? [...fallbackOrgs] : [fallbackNamespace];
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function storageBoundaries() {
|
|
465
|
+
return {
|
|
466
|
+
etcd: 'Krate CRDs: Organization, Repository, SSHKey, RepositoryPermission, BranchProtection, RefPolicy, WebhookSubscription, RunnerPool, View, Selector',
|
|
467
|
+
kubevela: 'Krate deployment CRDs: Application, ApplicationRevision, ComponentDefinition, WorkloadDefinition, TraitDefinition, ScopeDefinition, PolicyDefinition, Policy, WorkflowStepDefinition, Workflow, ResourceTracker',
|
|
468
|
+
postgres: 'Aggregated API resources: PullRequest, Issue, Review, Pipeline, Job, WebhookDelivery',
|
|
469
|
+
repositories: 'Repository backend Deployment, repository storage, and integration plans',
|
|
470
|
+
objects: 'Object storage referenced by Repository specs and Pipeline artifacts'
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function emptyKyverno() {
|
|
475
|
+
return {
|
|
476
|
+
mode: 'auto',
|
|
477
|
+
detected: false,
|
|
478
|
+
resources: {},
|
|
479
|
+
reports: { policyReports: [], clusterPolicyReports: [], results: [], violations: [] },
|
|
480
|
+
permissions: [],
|
|
481
|
+
degraded: [],
|
|
482
|
+
controllers: []
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function buildEmptySnapshot({ correlationId, namespace, kubectl, contextResult, versionResult, available, apiServiceResult }) {
|
|
487
|
+
return {
|
|
488
|
+
source: 'kubernetes',
|
|
489
|
+
mode: 'kubernetes-api',
|
|
490
|
+
namespace,
|
|
491
|
+
generatedAt: new Date().toISOString(),
|
|
492
|
+
correlationId,
|
|
493
|
+
kubectl: {
|
|
494
|
+
binary: kubectl,
|
|
495
|
+
context: contextResult?.ok ? contextResult.stdout.trim() : null,
|
|
496
|
+
clientVersion: versionResult?.ok ? safeJson(versionResult.stdout)?.clientVersion?.gitVersion || null : null,
|
|
497
|
+
available: Boolean(available),
|
|
498
|
+
errors: [contextResult, versionResult, apiServiceResult]
|
|
499
|
+
.filter((r) => r && !r.ok)
|
|
500
|
+
.map((r) => `${r.command}: ${(r.stderr || r.error || '').trim() || `exit ${r.status}`}`)
|
|
501
|
+
},
|
|
502
|
+
apiService: null,
|
|
503
|
+
crds: [],
|
|
504
|
+
resources: Object.fromEntries(KRATE_RESOURCES.filter((d) => d.storage !== 'core').map((d) => [d.kind, []])),
|
|
505
|
+
kyverno: emptyKyverno(),
|
|
506
|
+
events: [],
|
|
507
|
+
permissions: [],
|
|
508
|
+
storage: storageBoundaries(),
|
|
509
|
+
commands: []
|
|
510
|
+
};
|
|
511
|
+
}
|