@a5c-ai/krate 5.0.1-staging.f672fe79b
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 +29 -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 +2407 -0
- package/dist/krate-lifecycle.json +201 -0
- package/dist/krate-runtime-snapshot.json +2955 -0
- package/dist/krate-summary.json +687 -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/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/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 +207 -0
- package/src/agent-approval-controller.js +123 -0
- package/src/agent-context-bundles.js +242 -0
- package/src/agent-dispatch-controller.js +86 -0
- package/src/agent-mux-client.js +280 -0
- package/src/agent-permission-review.js +162 -0
- package/src/agent-stack-controller.js +296 -0
- package/src/agent-trigger-controller.js +108 -0
- package/src/api-controller.js +206 -0
- package/src/argocd-gitops.js +43 -0
- package/src/auth.js +265 -0
- package/src/component-catalog.js +41 -0
- package/src/control-plane.js +136 -0
- package/src/controller-client.js +38 -0
- package/src/controller-ui.js +538 -0
- package/src/data-plane.js +178 -0
- package/src/gitea-backend.js +95 -0
- package/src/handoff.js +98 -0
- package/src/hooks-events.js +63 -0
- package/src/http-server.js +151 -0
- package/src/identity-policy.js +86 -0
- package/src/index.js +30 -0
- package/src/kubernetes-controller.js +812 -0
- package/src/kubernetes-resource-gateway.js +48 -0
- package/src/operations.js +112 -0
- package/src/resource-model.js +203 -0
- package/src/runners-ci.js +48 -0
- package/src/runtime.js +196 -0
- package/src/web-ui.js +40 -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 +176 -0
- package/tests/agent-mux-client.test.js +204 -0
- package/tests/agent-permission-review.test.js +209 -0
- package/tests/agent-resources.test.js +212 -0
- package/tests/agent-stack-controller.test.js +221 -0
- package/tests/agent-trigger-controller.test.js +211 -0
- package/tests/deployment.test.js +395 -0
- package/tests/e2e/lifecycle.test.js +117 -0
- package/tests/krate.test.js +727 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { createKubernetesResourceGateway } from './kubernetes-resource-gateway.js';
|
|
2
|
+
import { createPermissionReviewer } from './agent-permission-review.js';
|
|
3
|
+
import { createAgentDispatchController } from './agent-dispatch-controller.js';
|
|
4
|
+
import { createAgentApprovalController } from './agent-approval-controller.js';
|
|
5
|
+
import { createAgentTriggerController } from './agent-trigger-controller.js';
|
|
6
|
+
|
|
7
|
+
export const KRATE_API_CONTROLLER_BOUNDARY = {
|
|
8
|
+
role: 'krate-api-controller',
|
|
9
|
+
scope: 'HTTP/application facade for validation, request orchestration, user-facing DTOs, API errors, and workflow affordances',
|
|
10
|
+
owns: ['input validation', 'forge DTOs', 'API errors', 'workflow affordances', 'controller UI snapshots'],
|
|
11
|
+
delegatesTo: ['kubernetes-resource-gateway', 'git-data-plane'],
|
|
12
|
+
mustNotOwn: ['kubectl process execution', 'Kubernetes reconciliation loops', 'watch stream internals', 'repository storage internals']
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function createKrateApiController(options = {}) {
|
|
16
|
+
const resourceGateway = options.resourceGateway || createKubernetesResourceGateway(options);
|
|
17
|
+
const namespace = options.namespace || resourceGateway.namespace || process.env.KRATE_NAMESPACE || 'krate-system';
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
role: 'krate-api-controller',
|
|
21
|
+
namespace,
|
|
22
|
+
resourceGateway,
|
|
23
|
+
resourceDefinitions: resourceGateway.resourceDefinitions,
|
|
24
|
+
async snapshot() {
|
|
25
|
+
return withArchitecture(await resourceGateway.snapshot(), namespace);
|
|
26
|
+
},
|
|
27
|
+
async listRepositoriesForForge() {
|
|
28
|
+
const resources = await resourceGateway.list('Repository');
|
|
29
|
+
return normalizeResourceList(resources).map((resource) => repositoryForgeSummary(resource, namespace));
|
|
30
|
+
},
|
|
31
|
+
async getRepositoryForgeView(name) {
|
|
32
|
+
const resource = await resourceGateway.get('Repository', name);
|
|
33
|
+
const repository = resource?.resource || resource;
|
|
34
|
+
return repositoryForgeView(repository, namespace);
|
|
35
|
+
},
|
|
36
|
+
async listResource(kindOrPlural) {
|
|
37
|
+
return resourceGateway.list(kindOrPlural);
|
|
38
|
+
},
|
|
39
|
+
async getResource(kindOrPlural, name) {
|
|
40
|
+
return resourceGateway.get(kindOrPlural, name);
|
|
41
|
+
},
|
|
42
|
+
async applyResource(resource) {
|
|
43
|
+
return resourceGateway.apply(resource);
|
|
44
|
+
},
|
|
45
|
+
async deleteResource(kindOrPlural, name) {
|
|
46
|
+
return resourceGateway.delete(kindOrPlural, name);
|
|
47
|
+
},
|
|
48
|
+
async createRepository(input) {
|
|
49
|
+
const created = await resourceGateway.createRepository(input);
|
|
50
|
+
const repository = created?.resource || created;
|
|
51
|
+
return {
|
|
52
|
+
operation: created?.operation || 'create-repository',
|
|
53
|
+
command: created?.command || 'kubectl apply -f -',
|
|
54
|
+
repository: repositoryForgeSummary(repository, namespace),
|
|
55
|
+
resource: repository
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
async createOrganization(input) {
|
|
59
|
+
return resourceGateway.createOrganization(input);
|
|
60
|
+
},
|
|
61
|
+
watchResource(resourcePath, handlers = {}) {
|
|
62
|
+
return resourceGateway.watch(resourcePath, handlers);
|
|
63
|
+
},
|
|
64
|
+
async reviewAgentPermissions(input) {
|
|
65
|
+
const reviewer = createPermissionReviewer();
|
|
66
|
+
const snapshot = await this.snapshot();
|
|
67
|
+
return reviewer.reviewPermissions({
|
|
68
|
+
...input,
|
|
69
|
+
resources: snapshot.resources
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
async dispatchAgent(input) {
|
|
73
|
+
const snapshot = await this.snapshot();
|
|
74
|
+
const controller = createAgentDispatchController(input.controllerOptions || {});
|
|
75
|
+
return controller.createManualDispatch({
|
|
76
|
+
...input,
|
|
77
|
+
resources: snapshot.resources
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
async approveAgentAction(input) {
|
|
81
|
+
const snapshot = await this.snapshot();
|
|
82
|
+
const approvalController = createAgentApprovalController();
|
|
83
|
+
return approvalController.recordDecision({
|
|
84
|
+
...input,
|
|
85
|
+
decision: 'approve',
|
|
86
|
+
resources: snapshot.resources
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
async denyAgentAction(input) {
|
|
90
|
+
const snapshot = await this.snapshot();
|
|
91
|
+
const approvalController = createAgentApprovalController();
|
|
92
|
+
return approvalController.recordDecision({
|
|
93
|
+
...input,
|
|
94
|
+
decision: 'deny',
|
|
95
|
+
resources: snapshot.resources
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
async processWebhookEvent(input) {
|
|
99
|
+
const snapshot = await this.snapshot();
|
|
100
|
+
const dispatchController = createAgentDispatchController(input.controllerOptions || {});
|
|
101
|
+
const triggerController = createAgentTriggerController({ dispatchController });
|
|
102
|
+
return triggerController.processEvent({
|
|
103
|
+
event: input.event,
|
|
104
|
+
resources: snapshot.resources,
|
|
105
|
+
namespace: input.namespace || namespace,
|
|
106
|
+
organizationRef: input.organizationRef || 'default',
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function withArchitecture(snapshot, namespace = snapshot?.namespace || 'default') {
|
|
113
|
+
return {
|
|
114
|
+
...snapshot,
|
|
115
|
+
architecture: {
|
|
116
|
+
apiController: {
|
|
117
|
+
...KRATE_API_CONTROLLER_BOUNDARY,
|
|
118
|
+
owns: [...KRATE_API_CONTROLLER_BOUNDARY.owns, '/api/controller', '/api/orgs/:org/resources', '/api/orgs/:org/repositories', '/api/watch/orgs/:org/*'],
|
|
119
|
+
scope: `${KRATE_API_CONTROLLER_BOUNDARY.scope}; never owns Kubernetes reconciliation loops`
|
|
120
|
+
},
|
|
121
|
+
resourceGateway: {
|
|
122
|
+
role: 'kubernetes-resource-gateway',
|
|
123
|
+
scope: 'Narrow application port translating API controller intent into Kubernetes resource-client calls',
|
|
124
|
+
namespace,
|
|
125
|
+
delegatesTo: ['kubernetes-resource-client']
|
|
126
|
+
},
|
|
127
|
+
kubernetesClient: {
|
|
128
|
+
role: 'kubernetes-resource-client',
|
|
129
|
+
scope: 'kubectl-backed Kubernetes API discovery, SubjectAccessReview checks, list/get/apply/delete/watch; no UI flow or product workflow ownership',
|
|
130
|
+
namespace,
|
|
131
|
+
owns: ['Krate CRDs', 'aggregated API resources', 'Kubernetes watch streams']
|
|
132
|
+
},
|
|
133
|
+
kubernetesReconciler: {
|
|
134
|
+
role: 'krate-kubernetes-reconciler',
|
|
135
|
+
scope: 'Repository status projection, repository hosting intent, policy projection, and data-plane sync intent; never owns HTTP routes or browser flows',
|
|
136
|
+
namespace,
|
|
137
|
+
delegatesTo: ['kubernetes-resource-gateway', 'git-data-plane']
|
|
138
|
+
},
|
|
139
|
+
dataPlane: {
|
|
140
|
+
role: 'git-data-plane',
|
|
141
|
+
scope: 'Repository streaming, SSH hosting, object storage, search indexing, and warm receive-pack paths',
|
|
142
|
+
boundary: process.env.KRATE_GITEA_HTTP_URL || 'repository service not configured'
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function repositoryForgeSummary(resource, namespace = 'krate-system') {
|
|
149
|
+
const metadata = resource?.metadata || {};
|
|
150
|
+
const spec = resource?.spec || {};
|
|
151
|
+
const name = metadata.name || 'unknown-repository';
|
|
152
|
+
const repositoryNamespace = metadata.namespace || namespace;
|
|
153
|
+
const org = spec.organizationRef || metadata.labels?.['krate.a5c.ai/org'] || 'default';
|
|
154
|
+
const repoPath = `/orgs/${encodeURIComponent(org)}/repositories/${encodeURIComponent(name)}`;
|
|
155
|
+
return {
|
|
156
|
+
kind: 'Repository',
|
|
157
|
+
name,
|
|
158
|
+
org,
|
|
159
|
+
namespace: repositoryNamespace,
|
|
160
|
+
visibility: spec.visibility || 'internal',
|
|
161
|
+
defaultBranch: spec.defaultBranch || 'main',
|
|
162
|
+
phase: resource?.status?.phase || (resource ? 'Ready' : 'Unknown'),
|
|
163
|
+
href: `${repoPath}/code`,
|
|
164
|
+
cloneUrl: spec.gitHosting?.httpUrl || `<krate-repository-service>/${encodeURIComponent(org)}/${name}.git`,
|
|
165
|
+
actions: {
|
|
166
|
+
code: `${repoPath}/code`,
|
|
167
|
+
pullRequests: `${repoPath}/pull-requests`,
|
|
168
|
+
issues: `${repoPath}/issues`,
|
|
169
|
+
runs: `${repoPath}/runs`,
|
|
170
|
+
pipelines: `${repoPath}/runs`,
|
|
171
|
+
hooks: `${repoPath}/hooks`,
|
|
172
|
+
settings: `${repoPath}/settings`,
|
|
173
|
+
yaml: `/orgs/${encodeURIComponent(org)}/advanced-plans?kind=Repository&name=${encodeURIComponent(name)}`
|
|
174
|
+
},
|
|
175
|
+
kubectl: {
|
|
176
|
+
get: `kubectl get repositories.krate.a5c.ai ${name} -n ${repositoryNamespace} -o yaml`,
|
|
177
|
+
delete: `kubectl delete repositories.krate.a5c.ai ${name} -n ${repositoryNamespace}`
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function repositoryForgeView(resource, namespace = 'default') {
|
|
183
|
+
const summary = repositoryForgeSummary(resource, namespace);
|
|
184
|
+
return {
|
|
185
|
+
...summary,
|
|
186
|
+
primaryFlow: 'browse-code-open-pr-review-merge',
|
|
187
|
+
emptyState: resource ? null : 'Repository resource is not available from the Kubernetes resource gateway.',
|
|
188
|
+
sections: [
|
|
189
|
+
{ id: 'code', label: 'Code', href: summary.actions.code, state: 'branch-and-path-aware' },
|
|
190
|
+
{ id: 'pull-requests', label: 'Pull requests', href: summary.actions.pullRequests, state: 'review-merge-checks' },
|
|
191
|
+
{ id: 'issues', label: 'Issues', href: summary.actions.issues, state: 'triage-policy-aware' },
|
|
192
|
+
{ id: 'runs', label: 'Runs', href: summary.actions.runs, state: 'runner-and-job-aware' },
|
|
193
|
+
{ id: 'hooks', label: 'Hooks', href: summary.actions.hooks, state: 'delivery-replay-aware' },
|
|
194
|
+
{ id: 'settings', label: 'Settings', href: summary.actions.settings, state: 'branch-protection-rbac-danger-actions' }
|
|
195
|
+
]
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function normalizeResourceList(result) {
|
|
200
|
+
if (Array.isArray(result)) return result;
|
|
201
|
+
if (Array.isArray(result?.items)) return result.items;
|
|
202
|
+
if (Array.isArray(result?.resources)) return result.resources;
|
|
203
|
+
if (result?.resource) return [result.resource];
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export function createArgoCdApplication({
|
|
2
|
+
name = 'krate',
|
|
3
|
+
namespace = 'argocd',
|
|
4
|
+
project = 'default',
|
|
5
|
+
repoURL,
|
|
6
|
+
path = 'charts/krate',
|
|
7
|
+
targetRevision = 'HEAD',
|
|
8
|
+
destinationNamespace = 'krate-system',
|
|
9
|
+
destinationServer = 'https://kubernetes.default.svc',
|
|
10
|
+
automated = true
|
|
11
|
+
} = {}) {
|
|
12
|
+
if (!repoURL) throw new Error('Argo CD Application requires repoURL');
|
|
13
|
+
return {
|
|
14
|
+
apiVersion: 'argoproj.io/v1alpha1',
|
|
15
|
+
kind: 'Application',
|
|
16
|
+
metadata: {
|
|
17
|
+
name,
|
|
18
|
+
namespace,
|
|
19
|
+
labels: {
|
|
20
|
+
'app.kubernetes.io/part-of': 'krate',
|
|
21
|
+
'krate.a5c.ai/gitops-engine': 'argocd'
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
spec: {
|
|
25
|
+
project,
|
|
26
|
+
source: { repoURL, targetRevision, path },
|
|
27
|
+
destination: { server: destinationServer, namespace: destinationNamespace },
|
|
28
|
+
syncPolicy: automated ? {
|
|
29
|
+
automated: { prune: true, selfHeal: true },
|
|
30
|
+
syncOptions: ['CreateNamespace=true']
|
|
31
|
+
} : { syncOptions: ['CreateNamespace=true'] }
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function createKrateGitOpsPlan({ repoURL, namespace = 'krate-system', applicationName = 'krate' }) {
|
|
37
|
+
return {
|
|
38
|
+
engine: 'argocd',
|
|
39
|
+
application: createArgoCdApplication({ name: applicationName, repoURL, destinationNamespace: namespace }),
|
|
40
|
+
requiredClusterResources: ['Application.argoproj.io', 'Namespace', 'ServiceAccount', 'RBAC', 'APIService', 'Krate CRDs'],
|
|
41
|
+
syncGuarantees: ['automated prune', 'automated selfHeal', 'namespace creation']
|
|
42
|
+
};
|
|
43
|
+
}
|
package/src/auth.js
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { createResource, clone } from './resource-model.js';
|
|
2
|
+
import { mapOidcIdentity } from './identity-policy.js';
|
|
3
|
+
|
|
4
|
+
const defaultScopes = {
|
|
5
|
+
github: 'read:user user:email',
|
|
6
|
+
sso: 'openid profile email groups'
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function createAuthProviderConfig(env = process.env) {
|
|
10
|
+
const githubEnabled = env.KRATE_AUTH_GITHUB_ENABLED !== 'false';
|
|
11
|
+
const ssoEnabled = env.KRATE_AUTH_SSO_ENABLED === 'true';
|
|
12
|
+
return {
|
|
13
|
+
session: { cookieName: env.KRATE_AUTH_COOKIE_NAME || 'krate_session' },
|
|
14
|
+
delegatedIdentity: {
|
|
15
|
+
enabled: env.KRATE_AUTH_DELEGATED_IDENTITY_ENABLED === 'true',
|
|
16
|
+
userHeader: env.KRATE_AUTH_DELEGATED_USER_HEADER || 'x-forwarded-user',
|
|
17
|
+
groupsHeader: env.KRATE_AUTH_DELEGATED_GROUPS_HEADER || 'x-forwarded-groups',
|
|
18
|
+
emailHeader: env.KRATE_AUTH_DELEGATED_EMAIL_HEADER || 'x-forwarded-email',
|
|
19
|
+
localDevelopment: {
|
|
20
|
+
enabled: delegatedLocalDevelopmentEnabled(env),
|
|
21
|
+
user: env.KRATE_AUTH_DELEGATED_LOCAL_USER || 'local-developer',
|
|
22
|
+
email: env.KRATE_AUTH_DELEGATED_LOCAL_EMAIL || '',
|
|
23
|
+
groups: env.KRATE_AUTH_DELEGATED_LOCAL_GROUPS || 'krate:repo-admins'
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
providers: {
|
|
27
|
+
github: {
|
|
28
|
+
id: 'github',
|
|
29
|
+
label: 'GitHub',
|
|
30
|
+
type: 'github',
|
|
31
|
+
enabled: githubEnabled,
|
|
32
|
+
clientId: env.KRATE_AUTH_GITHUB_CLIENT_ID || '',
|
|
33
|
+
clientSecret: env.KRATE_AUTH_GITHUB_CLIENT_SECRET || '',
|
|
34
|
+
clientSecretConfigured: Boolean(env.KRATE_AUTH_GITHUB_CLIENT_SECRET),
|
|
35
|
+
authorizationUrl: env.KRATE_AUTH_GITHUB_AUTHORIZATION_URL || 'https://github.com/login/oauth/authorize',
|
|
36
|
+
tokenUrl: env.KRATE_AUTH_GITHUB_TOKEN_URL || 'https://github.com/login/oauth/access_token',
|
|
37
|
+
userInfoUrl: env.KRATE_AUTH_GITHUB_USERINFO_URL || 'https://api.github.com/user',
|
|
38
|
+
scopes: env.KRATE_AUTH_GITHUB_SCOPES || defaultScopes.github
|
|
39
|
+
},
|
|
40
|
+
sso: {
|
|
41
|
+
id: 'sso',
|
|
42
|
+
label: env.KRATE_AUTH_SSO_PROVIDER_NAME || 'Workspace SSO',
|
|
43
|
+
type: 'oidc',
|
|
44
|
+
enabled: ssoEnabled,
|
|
45
|
+
issuerUrl: env.KRATE_AUTH_SSO_ISSUER_URL || '',
|
|
46
|
+
clientId: env.KRATE_AUTH_SSO_CLIENT_ID || '',
|
|
47
|
+
clientSecret: env.KRATE_AUTH_SSO_CLIENT_SECRET || '',
|
|
48
|
+
clientSecretConfigured: Boolean(env.KRATE_AUTH_SSO_CLIENT_SECRET),
|
|
49
|
+
authorizationUrl: env.KRATE_AUTH_SSO_AUTHORIZATION_URL || '',
|
|
50
|
+
tokenUrl: env.KRATE_AUTH_SSO_TOKEN_URL || '',
|
|
51
|
+
userInfoUrl: env.KRATE_AUTH_SSO_USERINFO_URL || '',
|
|
52
|
+
scopes: env.KRATE_AUTH_SSO_SCOPES || defaultScopes.sso
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function listEnabledAuthProviders(config = createAuthProviderConfig()) {
|
|
59
|
+
return Object.values(config.providers).filter((provider) => provider.enabled && provider.clientId && provider.authorizationUrl);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function buildAuthorizationRedirect({ provider, requestUrl, state = cryptoSafeState() }) {
|
|
63
|
+
if (!provider?.enabled) throw new Error(`${provider?.label || 'Provider'} sign-in is disabled`);
|
|
64
|
+
if (!provider.clientId) throw new Error(`${provider.label} client id is not configured`);
|
|
65
|
+
if (!provider.authorizationUrl) throw new Error(`${provider.label} authorization endpoint is not configured`);
|
|
66
|
+
const request = new URL(requestUrl || 'http://localhost/login');
|
|
67
|
+
const redirectUri = new URL(`/api/auth/callback/${provider.id}`, `${request.protocol}//${request.host}`);
|
|
68
|
+
const target = new URL(provider.authorizationUrl);
|
|
69
|
+
target.searchParams.set('response_type', 'code');
|
|
70
|
+
target.searchParams.set('client_id', provider.clientId);
|
|
71
|
+
target.searchParams.set('redirect_uri', redirectUri.toString());
|
|
72
|
+
target.searchParams.set('scope', provider.scopes || 'openid profile email');
|
|
73
|
+
target.searchParams.set('state', state);
|
|
74
|
+
return { url: target.toString(), state, redirectUri: redirectUri.toString() };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
export async function exchangeOAuthCodeForProfile({ provider, code, requestUrl, fetchImpl = globalThis.fetch }) {
|
|
79
|
+
if (!provider?.enabled) throw new Error(`${provider?.label || 'Provider'} sign-in is disabled`);
|
|
80
|
+
if (!code) throw new Error('authorization code is required');
|
|
81
|
+
if (!provider.tokenUrl || !provider.userInfoUrl) throw new Error(`${provider.label} token and profile endpoints are required`);
|
|
82
|
+
if (!provider.clientId || !provider.clientSecret) throw new Error(`${provider.label} client credentials are not configured`);
|
|
83
|
+
const request = new URL(requestUrl || 'http://localhost/login');
|
|
84
|
+
const redirectUri = new URL(`/api/auth/callback/${provider.id}`, `${request.protocol}//${request.host}`).toString();
|
|
85
|
+
const tokenResponse = await fetchImpl(provider.tokenUrl, {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
headers: { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
88
|
+
body: new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri: redirectUri, client_id: provider.clientId, client_secret: provider.clientSecret })
|
|
89
|
+
});
|
|
90
|
+
if (!tokenResponse.ok) throw new Error(`${provider.label} token exchange failed with ${tokenResponse.status}`);
|
|
91
|
+
const token = await tokenResponse.json();
|
|
92
|
+
const accessToken = token.access_token;
|
|
93
|
+
if (!accessToken) throw new Error(`${provider.label} did not return an access token`);
|
|
94
|
+
const profileResponse = await fetchImpl(provider.userInfoUrl, { headers: { Accept: 'application/json', Authorization: `Bearer ${accessToken}` } });
|
|
95
|
+
if (!profileResponse.ok) throw new Error(`${provider.label} profile lookup failed with ${profileResponse.status}`);
|
|
96
|
+
const profile = await profileResponse.json();
|
|
97
|
+
return normalizeProviderProfile(provider, profile);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function normalizeProviderProfile(provider, profile = {}) {
|
|
101
|
+
if (provider.id === 'github' || provider.type === 'github') {
|
|
102
|
+
const username = profile.login || profile.username || profile.name;
|
|
103
|
+
return {
|
|
104
|
+
provider: provider.id,
|
|
105
|
+
subject: String(profile.id || username || profile.email),
|
|
106
|
+
email: profile.email || (username ? `${username}@users.noreply.github.com` : undefined),
|
|
107
|
+
displayName: profile.name || username || profile.email,
|
|
108
|
+
username,
|
|
109
|
+
groups: [],
|
|
110
|
+
teams: [],
|
|
111
|
+
admin: false
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const groups = Array.isArray(profile.groups) ? profile.groups : String(profile.groups || '').split(',').map((group) => group.trim()).filter(Boolean);
|
|
115
|
+
return {
|
|
116
|
+
provider: provider.id,
|
|
117
|
+
subject: profile.sub || profile.id || profile.email,
|
|
118
|
+
email: profile.email,
|
|
119
|
+
displayName: profile.name || profile.preferred_username || profile.email,
|
|
120
|
+
username: profile.preferred_username || profile.username || profile.email,
|
|
121
|
+
groups,
|
|
122
|
+
teams: [],
|
|
123
|
+
admin: groups.includes('krate:platform-engineers') || groups.includes('krate:repo-admins')
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function profileFromDelegatedHeaders(headers, config = createAuthProviderConfig(), options = {}) {
|
|
128
|
+
if (!config.delegatedIdentity.enabled) throw new Error('Delegated identity sign-in is disabled');
|
|
129
|
+
const getHeader = (name) => typeof headers.get === 'function' ? headers.get(name) : headers[name] || headers[name.toLowerCase()];
|
|
130
|
+
const localProfile = localDelegatedDevelopmentProfile(config, options);
|
|
131
|
+
const headerUser = getHeader(config.delegatedIdentity.userHeader);
|
|
132
|
+
const user = headerUser || localProfile.user;
|
|
133
|
+
if (!user) throw new Error(`Delegated identity header ${config.delegatedIdentity.userHeader} is missing`);
|
|
134
|
+
const email = getHeader(config.delegatedIdentity.emailHeader) || localProfile.email || (String(user).includes('@') ? user : undefined);
|
|
135
|
+
const groups = String(getHeader(config.delegatedIdentity.groupsHeader) || localProfile.groups || '').split(',').map((group) => group.trim()).filter(Boolean);
|
|
136
|
+
return { provider: 'delegated', subject: user, email, displayName: user, username: normalizeName(user), groups, teams: [], admin: groups.includes('krate:platform-engineers') || groups.includes('krate:repo-admins'), delegatedIdentitySource: headerUser ? 'proxy-header' : 'local-development' };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function registerLoginProfile({ controller, namespace = process.env.KRATE_NAMESPACE || 'default', profile }) {
|
|
140
|
+
const mapped = mapLoginProfileToKrateIdentity({ ...profile, namespace });
|
|
141
|
+
const userResult = await controller.applyResource(mapped.user);
|
|
142
|
+
const mappingResult = await controller.applyResource(mapped.mapping);
|
|
143
|
+
return { ...mapped, userResult, mappingResult };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function createSessionCookie(config, profile) {
|
|
147
|
+
const value = Buffer.from(JSON.stringify({ provider: profile.provider, subject: profile.subject, user: profile.username || profile.email })).toString('base64url');
|
|
148
|
+
return `${config.session.cookieName}=${value}; Path=/; HttpOnly; SameSite=Lax`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function parseSessionCookie(config, cookieValue) {
|
|
152
|
+
if (!cookieValue || typeof cookieValue !== 'string') return null;
|
|
153
|
+
try {
|
|
154
|
+
const session = JSON.parse(Buffer.from(cookieValue, 'base64url').toString('utf8'));
|
|
155
|
+
const user = typeof session.user === 'string' ? session.user.trim() : '';
|
|
156
|
+
const subject = typeof session.subject === 'string' ? session.subject.trim() : '';
|
|
157
|
+
const provider = typeof session.provider === 'string' ? session.provider.trim() : '';
|
|
158
|
+
if (!user && !subject) return null;
|
|
159
|
+
return {
|
|
160
|
+
cookieName: config.session.cookieName,
|
|
161
|
+
provider: provider || 'krate',
|
|
162
|
+
subject: subject || user,
|
|
163
|
+
user: user || subject
|
|
164
|
+
};
|
|
165
|
+
} catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function mapLoginProfileToKrateIdentity({ provider = 'sso', subject, email, displayName, username, groups = [], teams = [], admin = false, namespace = 'krate-org-default', organizationRef = 'default' }) {
|
|
171
|
+
const userName = username || normalizeName(email || subject || displayName || 'user');
|
|
172
|
+
const krateGroups = [...new Set(['krate:users', admin ? 'krate:platform-engineers' : 'krate:developers', ...groups])];
|
|
173
|
+
const identity = mapOidcIdentity({ subject, email, groups: krateGroups });
|
|
174
|
+
const user = createResource('User', { name: userName, namespace, labels: { role: admin ? 'admin' : 'member' } }, {
|
|
175
|
+
organizationRef,
|
|
176
|
+
displayName: displayName || userName,
|
|
177
|
+
email,
|
|
178
|
+
username: userName,
|
|
179
|
+
teams,
|
|
180
|
+
admin,
|
|
181
|
+
disabled: false
|
|
182
|
+
}, { phase: 'Active', lastLoginProvider: provider, groups: identity.groups });
|
|
183
|
+
const mapping = createResource('IdentityMapping', { name: `${provider}-${userName}`, namespace }, {
|
|
184
|
+
organizationRef,
|
|
185
|
+
user: userName,
|
|
186
|
+
provider,
|
|
187
|
+
subject: subject || email,
|
|
188
|
+
email,
|
|
189
|
+
workspaceIdentity: { name: identity.name, uid: identity.uid, groups: identity.groups },
|
|
190
|
+
repositoryIdentity: { username: userName, email }
|
|
191
|
+
}, { phase: 'Synced' });
|
|
192
|
+
return { identity, user, mapping };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function createInviteResource({ email, role = 'member', teams = [], invitedBy = 'admin', namespace = 'krate-org-default', organizationRef = 'default', expiresInDays = 7 }) {
|
|
196
|
+
const expiresAt = new Date(Date.now() + expiresInDays * 24 * 60 * 60 * 1000).toISOString();
|
|
197
|
+
return createResource('Invite', { name: normalizeName(email), namespace, labels: { role } }, { organizationRef, email, role, teams, invitedBy, expiresAt }, { phase: 'Pending' });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function createTeamResource({ name, displayName = name, members = [], maintainers = [], repositoryGrants = [], namespace = 'krate-org-default', organizationRef = 'default' }) {
|
|
201
|
+
return createResource('Team', { name, namespace }, { organizationRef, displayName, members, maintainers, repositoryGrants }, { phase: 'Active', memberCount: members.length });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function createAuthProviderResources(config = createAuthProviderConfig(), namespace = 'krate-org-default', organizationRef = 'default') {
|
|
205
|
+
return Object.values(config.providers).map((provider) => createResource('AuthProvider', { name: provider.id, namespace }, {
|
|
206
|
+
organizationRef,
|
|
207
|
+
type: provider.type,
|
|
208
|
+
label: provider.label,
|
|
209
|
+
enabled: provider.enabled,
|
|
210
|
+
scopes: provider.scopes,
|
|
211
|
+
delegatedIdentity: clone(publicDelegatedIdentityConfig(config.delegatedIdentity))
|
|
212
|
+
}, { phase: provider.enabled ? 'Configured' : 'Disabled', clientConfigured: Boolean(provider.clientId) }));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function identityBackendSyncPlan({ users = [], teams = [], invites = [], mappings = [], permissions = [], sshKeys = [] } = {}) {
|
|
216
|
+
return {
|
|
217
|
+
users: users.map((user) => ({ action: 'ensure-user', user: user.metadata?.name, email: user.spec?.email, disabled: Boolean(user.spec?.disabled) })),
|
|
218
|
+
teams: teams.map((team) => ({ action: 'ensure-team', team: team.metadata?.name, members: team.spec?.members || [], maintainers: team.spec?.maintainers || [] })),
|
|
219
|
+
invites: invites.map((invite) => ({ action: 'send-invite', email: invite.spec?.email, teams: invite.spec?.teams || [], role: invite.spec?.role || 'member' })),
|
|
220
|
+
mappings: mappings.map((mapping) => ({ action: 'link-identity', user: mapping.spec?.user, provider: mapping.spec?.provider, repositoryIdentity: mapping.spec?.repositoryIdentity })),
|
|
221
|
+
permissions: permissions.map((permission) => ({ action: 'sync-repository-permission', repository: permission.spec?.repository, subject: permission.spec?.subject, subjectKind: permission.spec?.subjectKind || 'user', permission: permission.spec?.permission })),
|
|
222
|
+
sshKeys: sshKeys.map((key) => ({ action: 'sync-ssh-key', owner: key.spec?.owner, repository: key.spec?.repository, scope: key.spec?.scope, title: key.spec?.title }))
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function normalizeName(value) {
|
|
227
|
+
return String(value || 'user').toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 63) || 'user';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function localDelegatedDevelopmentProfile(config, options = {}) {
|
|
231
|
+
if (!isLocalDevelopmentRequest(options.requestUrl, options.env)) return {};
|
|
232
|
+
const localConfig = config.delegatedIdentity.localDevelopment || {};
|
|
233
|
+
if (!localConfig.enabled) return {};
|
|
234
|
+
const request = new URL(options.requestUrl);
|
|
235
|
+
const user = request.searchParams.get('user') || request.searchParams.get('username') || localConfig.user;
|
|
236
|
+
const email = request.searchParams.get('email') || localConfig.email;
|
|
237
|
+
const groups = request.searchParams.get('groups') || localConfig.groups;
|
|
238
|
+
return { user, email, groups };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function delegatedLocalDevelopmentEnabled(env = process.env) {
|
|
242
|
+
if (env.KRATE_AUTH_DELEGATED_LOCAL_DEVELOPMENT === 'true') return true;
|
|
243
|
+
if (env.KRATE_AUTH_DELEGATED_LOCAL_DEVELOPMENT === 'false') return false;
|
|
244
|
+
return env.NODE_ENV !== 'production';
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function isLocalDevelopmentRequest(requestUrl) {
|
|
248
|
+
if (!requestUrl) return false;
|
|
249
|
+
const { hostname } = new URL(requestUrl);
|
|
250
|
+
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '0.0.0.0' || hostname === '::1' || hostname === '::';
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function publicDelegatedIdentityConfig(delegatedIdentity) {
|
|
254
|
+
return {
|
|
255
|
+
enabled: delegatedIdentity.enabled,
|
|
256
|
+
userHeader: delegatedIdentity.userHeader,
|
|
257
|
+
groupsHeader: delegatedIdentity.groupsHeader,
|
|
258
|
+
emailHeader: delegatedIdentity.emailHeader,
|
|
259
|
+
localDevelopment: clone(delegatedIdentity.localDevelopment)
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function cryptoSafeState() {
|
|
264
|
+
return Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
265
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const KRATE_COMPONENTS = Object.freeze([
|
|
2
|
+
{ id: 'control-plane', title: 'Control Plane', area: 'api', resources: ['Repository', 'BranchProtection', 'RefPolicy'], evidence: ['src/control-plane.js', 'charts/krate/crds'] },
|
|
3
|
+
{ id: 'data-plane', title: 'Repository Data Plane', area: 'git', resources: ['Repository'], evidence: ['src/data-plane.js', 'src/gitea-backend.js', 'charts/krate/templates/gitea.yaml'] },
|
|
4
|
+
{ id: 'identity-policy', title: 'Identity, RBAC, and Policy', area: 'security', resources: ['BranchProtection', 'RefPolicy'], evidence: ['src/identity-policy.js', 'examples/policy-kyverno-pr-title.yaml'] },
|
|
5
|
+
{ id: 'runners-ci', title: 'Runners and CI', area: 'automation', resources: ['RunnerPool', 'Pipeline', 'Job'], evidence: ['src/runners-ci.js', 'docs/components/runners-ci.md'] },
|
|
6
|
+
{ id: 'hooks-events', title: 'Hooks and Events', area: 'integrations', resources: ['WebhookSubscription', 'WebhookDelivery'], evidence: ['src/hooks-events.js', 'docs/components/hooks-events.md'] },
|
|
7
|
+
{ id: 'operations-publishing', title: 'Operations and Publishing', area: 'release', resources: ['APIService', 'Deployment'], evidence: ['src/operations.js', 'charts/krate', 'scripts/setup-minikube.mjs'] },
|
|
8
|
+
{ id: 'web-ui', title: 'Web UI', area: 'experience', resources: ['View'], evidence: ['src/web-ui.js', 'public/index.html'] }
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
export function createKrateComponentCatalog(demo) {
|
|
12
|
+
const resourceKinds = new Set(Object.values(demo.resources).flatMap((value) => value?.kind ? [value.kind] : [value?.pipeline?.kind, ...(value?.jobs || []).map((job) => job.kind)]).filter(Boolean));
|
|
13
|
+
return KRATE_COMPONENTS.map((component) => ({
|
|
14
|
+
...component,
|
|
15
|
+
implemented: component.resources.some((kind) => resourceKinds.has(kind)) || component.id === 'operations-publishing' || component.id === 'web-ui',
|
|
16
|
+
resourceCount: component.resources.filter((kind) => resourceKinds.has(kind)).length,
|
|
17
|
+
docs: `docs/components/${component.id}.md`
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createKrateLifecycleSnapshot(demo, { packageInfo = {}, generatedAt = new Date().toISOString() } = {}) {
|
|
22
|
+
const catalog = createKrateComponentCatalog(demo);
|
|
23
|
+
const smoke = demo.smoke || { ok: true, assertions: [] };
|
|
24
|
+
return {
|
|
25
|
+
project: 'Krate',
|
|
26
|
+
version: packageInfo.version || '0.1.0',
|
|
27
|
+
generatedAt,
|
|
28
|
+
status: smoke.ok && catalog.every((component) => component.implemented) ? 'ready-for-local-development' : 'needs-attention',
|
|
29
|
+
components: catalog,
|
|
30
|
+
resources: Object.fromEntries(Object.entries(demo.resources).map(([name, value]) => [name, value?.kind || value?.pipeline?.kind || 'workflow'])),
|
|
31
|
+
storage: demo.controlPlane.storageReport(),
|
|
32
|
+
flows: demo.ui.dashboard.excellentFlows,
|
|
33
|
+
operations: {
|
|
34
|
+
chart: demo.operations.chartPackage.chart,
|
|
35
|
+
setup: demo.operations.localSetup.script,
|
|
36
|
+
releaseGates: demo.operations.releaseGates,
|
|
37
|
+
observability: demo.operations.observability
|
|
38
|
+
},
|
|
39
|
+
validation: smoke.assertions.map(([name, passed]) => ({ name, passed }))
|
|
40
|
+
};
|
|
41
|
+
}
|