@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,48 @@
|
|
|
1
|
+
import { createKubernetesResourceClient, repositoryManifest } from './kubernetes-controller.js';
|
|
2
|
+
|
|
3
|
+
export const KUBERNETES_RESOURCE_GATEWAY_BOUNDARY = {
|
|
4
|
+
role: 'kubernetes-resource-gateway',
|
|
5
|
+
scope: 'Application port translating API controller intent into Kubernetes resource-client operations',
|
|
6
|
+
owns: ['resource definitions', 'list/get/apply/delete/watch delegation', 'Repository manifest application', 'namespace scoping'],
|
|
7
|
+
delegatesTo: ['kubernetes-resource-client'],
|
|
8
|
+
mustNotOwn: ['HTTP routes', 'Next.js page flow decisions', 'forge DTO composition', 'Kubernetes reconciliation scheduling']
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function createKubernetesResourceGateway(options = {}) {
|
|
12
|
+
const resourceClient = options.resourceClient || options.kubernetesClient || createKubernetesResourceClient(options);
|
|
13
|
+
const namespace = options.namespace || resourceClient.namespace || process.env.KRATE_NAMESPACE || 'krate-system';
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
...KUBERNETES_RESOURCE_GATEWAY_BOUNDARY,
|
|
17
|
+
namespace,
|
|
18
|
+
resourceDefinitions: resourceClient.resourceDefinitions,
|
|
19
|
+
async snapshot() {
|
|
20
|
+
return resourceClient.snapshot();
|
|
21
|
+
},
|
|
22
|
+
async list(kindOrPlural) {
|
|
23
|
+
return resourceClient.listResource(kindOrPlural);
|
|
24
|
+
},
|
|
25
|
+
async get(kindOrPlural, name) {
|
|
26
|
+
return resourceClient.getResource(kindOrPlural, name);
|
|
27
|
+
},
|
|
28
|
+
async apply(resource) {
|
|
29
|
+
return resourceClient.applyResource(resource);
|
|
30
|
+
},
|
|
31
|
+
async delete(kindOrPlural, name) {
|
|
32
|
+
return resourceClient.deleteResource(kindOrPlural, name);
|
|
33
|
+
},
|
|
34
|
+
async createRepository(input) {
|
|
35
|
+
return resourceClient.applyResource(repositoryManifest(input, namespace));
|
|
36
|
+
},
|
|
37
|
+
async createOrganization(input) {
|
|
38
|
+
return resourceClient.createOrganization(input);
|
|
39
|
+
},
|
|
40
|
+
watch(resourcePath, handlers = {}) {
|
|
41
|
+
return resourceClient.watchResource(resourcePath, handlers);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { ControlPlane } from './control-plane.js';
|
|
2
|
+
import { GiteaGitService, GiteaRepositoryStore } from './data-plane.js';
|
|
3
|
+
import { WebhookBus } from './hooks-events.js';
|
|
4
|
+
import { RunnerScheduler } from './runners-ci.js';
|
|
5
|
+
import { createAdmissionPolicy, mapOidcIdentity, toResourceYaml } from './identity-policy.js';
|
|
6
|
+
import { createResource } from './resource-model.js';
|
|
7
|
+
import { createDashboard, createPullRequestReviewModel, createRunnerPoolEditor, createTriageView, createWebhookInspector } from './web-ui.js';
|
|
8
|
+
import { createKrateComponentCatalog, createKrateLifecycleSnapshot } from './component-catalog.js';
|
|
9
|
+
|
|
10
|
+
export function chartPackageSurface() {
|
|
11
|
+
return {
|
|
12
|
+
chart: 'charts/krate',
|
|
13
|
+
values: 'charts/krate/values.yaml',
|
|
14
|
+
crds: ['Repository', 'BranchProtection', 'RefPolicy', 'RunnerPool', 'WebhookSubscription', 'View', 'Selector'],
|
|
15
|
+
templates: ['CRDs', 'optional APIService', 'ServiceAccount', 'ClusterRole', 'ClusterRoleBinding', 'Deployment', 'Service', 'NetworkPolicy', 'Gitea backend', 'Argo CD Application'],
|
|
16
|
+
examples: ['examples/minikube-demo.yaml', 'examples/policy-kyverno-pr-title.yaml'],
|
|
17
|
+
validation: ['npm run e2e', 'npm run package:check', 'npm run setup:minikube -- --dry-run']
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function localSetupPlan() {
|
|
22
|
+
return {
|
|
23
|
+
script: 'scripts/setup-minikube.mjs',
|
|
24
|
+
defaultMode: 'dry-run',
|
|
25
|
+
applyMode: 'npm run setup:minikube -- --apply',
|
|
26
|
+
requiredTools: ['minikube', 'kubectl', 'helm', 'node', 'npm'],
|
|
27
|
+
defaultProfile: 'krate',
|
|
28
|
+
namespace: 'krate-system'
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function generateInstallManifests({ namespace = 'krate-system' } = {}) {
|
|
33
|
+
const manifests = [
|
|
34
|
+
{ apiVersion: 'apiregistration.k8s.io/v1', kind: 'APIService', metadata: { name: 'v1alpha1.krate.a5c.ai' }, spec: { group: 'krate.a5c.ai', version: 'v1alpha1', service: { namespace, name: 'krate-api' } } },
|
|
35
|
+
{ apiVersion: 'apps/v1', kind: 'Deployment', metadata: { name: 'krate-api', namespace }, spec: { replicas: 2, template: { spec: { containers: [{ name: 'api', image: 'krate/api:dev' }] } } } },
|
|
36
|
+
{ apiVersion: 'apps/v1', kind: 'Deployment', metadata: { name: 'krate-gitea', namespace, labels: { 'app.kubernetes.io/component': 'gitea-backend' } }, spec: { replicas: 1, template: { spec: { containers: [{ name: 'gitea', image: 'gitea/gitea:1.22-rootless' }] } } } },
|
|
37
|
+
{ apiVersion: 'argoproj.io/v1alpha1', kind: 'Application', metadata: { name: 'krate', namespace: 'argocd' }, spec: { source: { repoURL: 'https://gitea-http.krate-system.svc.cluster.local/krate/platform-config.git', path: 'charts/krate', targetRevision: 'main' }, destination: { namespace, server: 'https://kubernetes.default.svc' }, syncPolicy: { automated: { prune: true, selfHeal: true } } } },
|
|
38
|
+
{ apiVersion: 'apps/v1', kind: 'Deployment', metadata: { name: 'krate-web', namespace }, spec: { replicas: 1 } }
|
|
39
|
+
];
|
|
40
|
+
return manifests.map((manifest) => `---\n${toResourceYaml(manifest)}`).join('\n');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function backupPlan() {
|
|
44
|
+
return { resources: ['CRDs and low-cardinality config', 'Postgres aggregated records', 'repository storage', 'object storage'], restoreOrder: ['API/config', 'Postgres', 'repository data', 'objects', 'controllers'], validation: ['list resources', 'read Gitea repository refs', 'open PR', 'replay webhook delivery'] };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function observabilityModel() {
|
|
48
|
+
return {
|
|
49
|
+
metrics: ['api_request_latency', 'postgres_aggregate_lag', 'gitea_receive_pack_latency', 'runner_queue_depth', 'webhook_delivery_phase', 'admission_denials'],
|
|
50
|
+
logs: ['control-plane audit', 'gitea access', 'runner job', 'webhook dispatcher', 'controller reconcile'],
|
|
51
|
+
alerts: ['APIService unavailable', 'Postgres unavailable', 'repository service unavailable', 'runner saturation', 'webhook failure burst', 'backup validation failed']
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function releaseGates() {
|
|
56
|
+
return ['build', 'docs and ontology coverage', 'unit acceptance tests', 'e2e package lifecycle tests', 'package validation', 'minikube dry-run setup', 'smoke flow', 'backup restore validation', 'known limitations reviewed'];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function createKrateMvpDemo() {
|
|
60
|
+
const platformEngineer = mapOidcIdentity({ subject: 'user:platform', email: 'platform@example.com', groups: ['krate:platform-engineers'] });
|
|
61
|
+
const developer = mapOidcIdentity({ subject: 'user:dev', email: 'dev@example.com', groups: ['krate:developers'] });
|
|
62
|
+
const repoAdmin = mapOidcIdentity({ subject: 'user:admin', email: 'admin@example.com', groups: ['krate:repo-admins'] });
|
|
63
|
+
const organizationRef = 'default';
|
|
64
|
+
const namespace = 'krate-org-default';
|
|
65
|
+
const controlPlane = new ControlPlane();
|
|
66
|
+
controlPlane.addAdmissionPolicy(createAdmissionPolicy({ name: 'pr-title-required', mode: 'enforce', match: ({ resource }) => resource.kind === 'PullRequest', validate: ({ resource }) => Boolean(resource.spec.title && resource.spec.title.length >= 8), message: 'PullRequest spec.title must be descriptive' }));
|
|
67
|
+
const git = new GiteaGitService({ controlPlane, stores: [new GiteaRepositoryStore({ name: 'gitea-primary', receivePackReady: true })] });
|
|
68
|
+
const runners = new RunnerScheduler({ controlPlane });
|
|
69
|
+
const webhooks = new WebhookBus({ controlPlane });
|
|
70
|
+
const repository = git.createRepository({ name: 'krate-demo', namespace, organizationRef }, repoAdmin);
|
|
71
|
+
const branchProtection = controlPlane.create(createResource('BranchProtection', { name: 'main-protection', namespace }, { organizationRef, refs: ['refs/heads/main'], requirePullRequest: true }), repoAdmin);
|
|
72
|
+
const refPolicy = controlPlane.create(createResource('RefPolicy', { name: 'deny-internal-refs', namespace }, { organizationRef, deny: ['refs/internal/'] }), repoAdmin);
|
|
73
|
+
const runnerPool = runners.createRunnerPool({ name: 'trusted-linux', namespace, organizationRef, warmReplicas: 1, maxReplicas: 4 }, platformEngineer);
|
|
74
|
+
const subscription = webhooks.subscribe({ name: 'chatops', namespace, organizationRef, url: 'https://hooks.example.test/krate', events: ['pullrequest.created'] }, repoAdmin);
|
|
75
|
+
const pullRequest = controlPlane.create(createResource('PullRequest', { name: 'pr-1', namespace, labels: { repository: 'krate-demo' } }, { organizationRef, repository: 'krate-demo', sourceRef: 'refs/heads/feature', targetRef: 'refs/heads/main', title: 'Add Kubernetes-native forge smoke path' }, { phase: 'Open' }), developer);
|
|
76
|
+
const pipelineRun = runners.startPipeline({ name: 'pipeline-pr-1', namespace, organizationRef, repository: 'krate-demo', ref: 'refs/pull/1/head', actor: developer, fork: false }, developer);
|
|
77
|
+
const delivery = webhooks.deliver({ subscriptionName: 'chatops', namespace, organizationRef, eventType: 'pullrequest.created', payload: { pullRequest: pullRequest.metadata.name, repository: 'krate-demo' } }, repoAdmin);
|
|
78
|
+
const replay = webhooks.replay(delivery, repoAdmin);
|
|
79
|
+
const triageView = controlPlane.create(createTriageView({ name: 'priority-triage', namespace, organizationRef, selector: { labels: { priority: 'high' } } }), repoAdmin);
|
|
80
|
+
const demo = {
|
|
81
|
+
users: { platformEngineer, developer, repoAdmin }, controlPlane, git, runners, webhooks,
|
|
82
|
+
resources: { repository, branchProtection, refPolicy, runnerPool, subscription, pullRequest, pipelineRun, delivery, replay, triageView },
|
|
83
|
+
ui: { dashboard: createDashboard({ repositories: [repository], pullRequests: [pullRequest], pipelines: [pipelineRun.pipeline], runnerPools: [runnerPool], webhookDeliveries: [delivery, replay] }), review: createPullRequestReviewModel({ pullRequest, changedFiles: ['src/index.js'], pipelineRuns: [pipelineRun.pipeline] }), runnerPoolEditor: createRunnerPoolEditor(runnerPool), webhookInspector: createWebhookInspector({ subscription, deliveries: [delivery, replay] }) },
|
|
84
|
+
operations: { installManifests: generateInstallManifests(), chartPackage: chartPackageSurface(), localSetup: localSetupPlan(), backupPlan: backupPlan(), observability: observabilityModel(), releaseGates: releaseGates() }
|
|
85
|
+
};
|
|
86
|
+
demo.components = createKrateComponentCatalog(demo);
|
|
87
|
+
demo.lifecycle = createKrateLifecycleSnapshot(demo);
|
|
88
|
+
return demo;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function runSmokeAssertions(demo = createKrateMvpDemo()) {
|
|
92
|
+
const storage = demo.controlPlane.storageReport();
|
|
93
|
+
const assertions = [
|
|
94
|
+
['Repository stored as CRD/etcd config', storage.etcd.includes('Repository')],
|
|
95
|
+
['PullRequest stored as aggregated/Postgres record', storage.postgres.includes('PullRequest')],
|
|
96
|
+
['RunnerPool stored as CRD/etcd configuration', storage.etcd.includes('RunnerPool')],
|
|
97
|
+
['WebhookDelivery stored as aggregated/Postgres record', storage.postgres.includes('WebhookDelivery')],
|
|
98
|
+
['Gitea receive path is ready', demo.git.route('krate-demo').backend === 'gitea' && demo.git.route('krate-demo').receivePackReady],
|
|
99
|
+
['Repository exposes Gitea integration plan', demo.resources.repository.spec.gitHosting.backend === 'gitea' && demo.resources.repository.spec.gitHosting.integrationPlan.backend === 'gitea'],
|
|
100
|
+
['UI exposes YAML for PR review', demo.ui.review.yaml.includes('kind: PullRequest')],
|
|
101
|
+
['Install manifests expose Kubernetes API discovery', demo.operations.installManifests.includes('kind: APIService') || demo.operations.chartPackage.crds.includes('Repository')],
|
|
102
|
+
['Chart package exposes CRDs', demo.operations.chartPackage.crds.includes('Repository')],
|
|
103
|
+
['Local setup defaults to dry-run minikube flow', demo.operations.localSetup.defaultMode === 'dry-run'],
|
|
104
|
+
['Release gates include docs and ontology coverage', demo.operations.releaseGates.includes('docs and ontology coverage')],
|
|
105
|
+
['Release gates include e2e package lifecycle tests', demo.operations.releaseGates.includes('e2e package lifecycle tests')],
|
|
106
|
+
['Observability tracks webhook delivery phase', demo.operations.observability.metrics.includes('webhook_delivery_phase')],
|
|
107
|
+
['Component catalog covers every implementation area', demo.components.every((component) => component.implemented)],
|
|
108
|
+
['Lifecycle snapshot is ready for local development', demo.lifecycle.status === 'ready-for-local-development']
|
|
109
|
+
];
|
|
110
|
+
return { ok: assertions.every(([, passed]) => passed), assertions };
|
|
111
|
+
}
|
|
112
|
+
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
export const CONFIG_KINDS = new Set(['Organization', 'OrgNamespaceBinding', 'User', 'Team', 'Invite', 'IdentityMapping', 'AuthProvider', 'Repository', 'SSHKey', 'RepositoryPermission', 'WebhookSubscription', 'RefPolicy', 'BranchProtection', 'PolicyProfile', 'PolicyTemplate', 'PolicyBinding', 'PolicyExceptionRequest', 'RunnerPool', 'View', 'Selector', 'AgentStack', 'AgentSubagent', 'AgentToolProfile', 'AgentMcpServer', 'AgentSkill', 'AgentTriggerRule', 'AgentContextLabel', 'AgentWorkspacePolicy', 'AgentServiceAccount', 'AgentRoleBinding', 'AgentSecretGrant', 'AgentConfigGrant', 'AgentAdapter', 'AgentTransportBinding', 'AgentProviderConfig', 'AgentProject', 'AgentGatewayConfig']);
|
|
2
|
+
export const AGGREGATED_KINDS = new Set(['PullRequest', 'Issue', 'Review', 'Pipeline', 'Job', 'WebhookDelivery', 'AgentDispatchRun', 'AgentDispatchAttempt', 'AgentSession', 'AgentContextBundle', 'AgentArtifact', 'AgentApproval', 'AgentWorkspace', 'AgentTriggerExecution', 'AgentCapabilityRequirement', 'WorkItemSessionLink', 'WorkItemWorkspaceLink', 'AgentSessionTranscript', 'AgentSessionAttachment', 'AgentWorkspaceRuntime']);
|
|
3
|
+
export const ALL_KINDS = new Set([...CONFIG_KINDS, ...AGGREGATED_KINDS]);
|
|
4
|
+
|
|
5
|
+
export const RESOURCE_DEFINITIONS = Object.freeze({
|
|
6
|
+
Organization: { storage: 'etcd', context: 'identity', plural: 'organizations', purpose: 'Krate organization identity in the platform namespace with a bound tenant namespace', requiredSpec: ['displayName', 'namespaceName'] },
|
|
7
|
+
OrgNamespaceBinding: { storage: 'etcd', context: 'identity', plural: 'orgnamespacebindings', purpose: 'Binding from one organization to exactly one tenant namespace for resources and side effects', requiredSpec: ['organizationRef', 'namespace'] },
|
|
8
|
+
User: { storage: 'etcd', context: 'identity', plural: 'users', purpose: 'Human account profile, sign-in state, admin flag, and linked identities', requiredSpec: ['organizationRef', 'displayName', 'email'] },
|
|
9
|
+
Team: { storage: 'etcd', context: 'identity', plural: 'teams', purpose: 'Team membership, maintainers, and repository permission grants', requiredSpec: ['organizationRef', 'displayName'] },
|
|
10
|
+
Invite: { storage: 'etcd', context: 'identity', plural: 'invites', purpose: 'Pending user invitation with requested teams and expiry', requiredSpec: ['organizationRef', 'email', 'role'] },
|
|
11
|
+
IdentityMapping: { storage: 'etcd', context: 'identity', plural: 'identitymappings', purpose: 'Mapping between Krate users, sign-in subjects, workspace identities, and repository accounts', requiredSpec: ['organizationRef', 'user', 'provider', 'subject'] },
|
|
12
|
+
AuthProvider: { storage: 'etcd', context: 'identity', plural: 'authproviders', purpose: 'Installation sign-in provider visibility and delegated identity settings', requiredSpec: ['organizationRef', 'type'] },
|
|
13
|
+
Repository: { storage: 'etcd', context: 'data-plane', plural: 'repositories', purpose: 'Repository identity, visibility, repository hosting integration, object storage, and search settings', requiredSpec: ['organizationRef', 'visibility'] },
|
|
14
|
+
SSHKey: { storage: 'etcd', context: 'data-plane', plural: 'sshkeys', purpose: 'User, deploy, and automation SSH keys reconciled into repository key APIs', requiredSpec: ['organizationRef', 'scope', 'key'] },
|
|
15
|
+
RepositoryPermission: { storage: 'etcd', context: 'data-plane', plural: 'repositorypermissions', purpose: 'Repository collaborators and teams synced with repository permissions', requiredSpec: ['organizationRef', 'repository', 'subject', 'permission'] },
|
|
16
|
+
WebhookSubscription: { storage: 'etcd', context: 'hooks-events', plural: 'webhooksubscriptions', purpose: 'Endpoint, event filters, signing reference, delivery mode, and retry policy', requiredSpec: ['organizationRef', 'url', 'events'] },
|
|
17
|
+
RefPolicy: { storage: 'etcd', context: 'data-plane', plural: 'refpolicies', purpose: 'Reference deny rules, force-push policy, signing policy, and future custom hook gates', requiredSpec: ['organizationRef'] },
|
|
18
|
+
BranchProtection: { storage: 'etcd', context: 'control-plane', plural: 'branchprotections', purpose: 'Protected ref rules such as pull-request requirements', requiredSpec: ['organizationRef', 'refs'] },
|
|
19
|
+
PolicyProfile: { storage: 'etcd', context: 'policy', plural: 'policyprofiles', purpose: 'Organization policy posture, default templates, rollout mode, and exception approval rules', requiredSpec: ['organizationRef', 'displayName', 'mode'] },
|
|
20
|
+
PolicyTemplate: { storage: 'etcd', context: 'policy', plural: 'policytemplates', purpose: 'Curated Kyverno policy template metadata, parameters, rollout defaults, and remediation guidance', requiredSpec: ['displayName', 'targetKinds', 'kyverno'] },
|
|
21
|
+
PolicyBinding: { storage: 'etcd', context: 'policy', plural: 'policybindings', purpose: 'Binding from a policy template to org, repository, environment, or resource selectors with audit/enforce rollout state', requiredSpec: ['organizationRef', 'templateRef', 'mode'] },
|
|
22
|
+
PolicyExceptionRequest: { storage: 'etcd', context: 'policy', plural: 'policyexceptionrequests', purpose: 'Auditable request and approval workflow for temporary Kyverno PolicyException resources', requiredSpec: ['organizationRef', 'policyRef', 'justification', 'expiresAt'] },
|
|
23
|
+
View: { storage: 'etcd', context: 'web-ui', plural: 'views', purpose: 'Saved triage and dashboard view backed by resource selectors', requiredSpec: ['organizationRef', 'selector'] },
|
|
24
|
+
Selector: { storage: 'etcd', context: 'web-ui', plural: 'selectors', purpose: 'Reusable label/query selector for workflows and views', requiredSpec: ['organizationRef'] },
|
|
25
|
+
PullRequest: { storage: 'postgres', context: 'control-plane', plural: 'pullrequests', purpose: 'Review unit with source/target refs, title, checks, and merge lifecycle', requiredSpec: ['organizationRef', 'repository', 'title'] },
|
|
26
|
+
Issue: { storage: 'postgres', context: 'control-plane', plural: 'issues', purpose: 'Work item with labels, assignment, and lifecycle state', requiredSpec: ['organizationRef', 'title'] },
|
|
27
|
+
Review: { storage: 'postgres', context: 'control-plane', plural: 'reviews', purpose: 'Approval, comment, or change-request record for a pull request', requiredSpec: ['organizationRef', 'pullRequest'] },
|
|
28
|
+
Pipeline: { storage: 'postgres', context: 'runners-ci', plural: 'pipelines', purpose: 'CI pipeline run state, trust tier, steps, and resume point', requiredSpec: ['organizationRef', 'repository', 'ref'] },
|
|
29
|
+
Job: { storage: 'postgres', context: 'runners-ci', plural: 'jobs', purpose: 'Executable CI step with service-account scope and isolation metadata', requiredSpec: ['organizationRef', 'pipeline', 'step'] },
|
|
30
|
+
RunnerPool: { storage: 'etcd', context: 'runners-ci', plural: 'runnerpools', purpose: 'Runner capacity, warm/max replicas, cache policy, and trust boundary', requiredSpec: ['organizationRef', 'warmReplicas', 'maxReplicas'] },
|
|
31
|
+
WebhookDelivery: { storage: 'postgres', context: 'hooks-events', plural: 'webhookdeliveries', purpose: 'Durable outbound webhook delivery attempt with signature, phase, response, and replay metadata', requiredSpec: ['organizationRef', 'subscription', 'eventType', 'signature'] },
|
|
32
|
+
AgentStack: { storage: 'etcd', context: 'agents', plural: 'agentstacks', purpose: 'Reusable agent definition with model, prompt, tools, MCP servers, skills, subagents, approval mode, and runner policy', requiredSpec: ['organizationRef', 'baseAgent', 'adapter', 'runtimeIdentity'] },
|
|
33
|
+
AgentSubagent: { storage: 'etcd', context: 'agents', plural: 'agentsubagents', purpose: 'Named child-agent definition with role, task kinds, tool subset, and workspace scope', requiredSpec: ['organizationRef', 'rolePrompt', 'taskKinds'] },
|
|
34
|
+
AgentToolProfile: { storage: 'etcd', context: 'agents', plural: 'agenttoolprofiles', purpose: 'Native tool policy for filesystem, network, shell, and approval gates', requiredSpec: ['organizationRef', 'filesystemPolicy', 'approvalPolicyByTool'] },
|
|
35
|
+
AgentMcpServer: { storage: 'etcd', context: 'agents', plural: 'agentmcpservers', purpose: 'Managed MCP endpoint with transport, discovery, health, and secret/config refs', requiredSpec: ['organizationRef', 'transport', 'scope'] },
|
|
36
|
+
AgentSkill: { storage: 'etcd', context: 'agents', plural: 'agentskills', purpose: 'Reusable runbook/procedure bundle with prompt fragments, tool deps, and output contracts', requiredSpec: ['organizationRef', 'format', 'sourceRef'] },
|
|
37
|
+
AgentTriggerRule: { storage: 'etcd', context: 'agents', plural: 'agenttriggerrules', purpose: 'Event-to-stack routing for CI failures, webhooks, comments, labels, schedules, and manual dispatch', requiredSpec: ['organizationRef', 'sources', 'agentStack', 'taskKind'] },
|
|
38
|
+
AgentContextLabel: { storage: 'etcd', context: 'agents', plural: 'agentcontextlabels', purpose: 'Reviewed prompt fragment with provenance and allowlisted sources', requiredSpec: ['organizationRef', 'promptFragment', 'allowedSources'] },
|
|
39
|
+
AgentWorkspacePolicy: { storage: 'etcd', context: 'agents', plural: 'agentworkspacepolicies', purpose: 'Git worktree provisioning, cleanup, retention, and trust tier policies', requiredSpec: ['organizationRef', 'mode', 'retentionPolicy'] },
|
|
40
|
+
AgentServiceAccount: { storage: 'etcd', context: 'identity', plural: 'agentserviceaccounts', purpose: 'Kubernetes ServiceAccount wrapper for agent/runner identity binding', requiredSpec: ['organizationRef', 'namespace', 'serviceAccountName'] },
|
|
41
|
+
AgentRoleBinding: { storage: 'etcd', context: 'identity', plural: 'agentrolebindings', purpose: 'Managed projection to native Kubernetes RBAC for agent identity', requiredSpec: ['organizationRef', 'subject', 'roleRef', 'scope'] },
|
|
42
|
+
AgentSecretGrant: { storage: 'etcd', context: 'identity', plural: 'agentsecretgrants', purpose: 'Explicit permission for subject to access Secret keys with purpose scope', requiredSpec: ['organizationRef', 'subject', 'secretRef', 'purpose'] },
|
|
43
|
+
AgentConfigGrant: { storage: 'etcd', context: 'identity', plural: 'agentconfiggrants', purpose: 'Explicit permission for subject to access ConfigMap keys with purpose scope', requiredSpec: ['organizationRef', 'subject', 'configMapRef', 'purpose'] },
|
|
44
|
+
AgentDispatchRun: { storage: 'postgres', context: 'agents', plural: 'agentdispatchruns', purpose: 'Logical CI-like run visible beside Pipeline/Job records with queue, status, workspace, and cost', requiredSpec: ['organizationRef', 'repository', 'sourceRefs', 'agentStack', 'taskKind'] },
|
|
45
|
+
AgentDispatchAttempt: { storage: 'postgres', context: 'agents', plural: 'agentdispatchattempts', purpose: 'Concrete execution attempt with reason, stack snapshot, and runtime state', requiredSpec: ['organizationRef', 'agentDispatchRun', 'attemptReason', 'agentStackSnapshot'] },
|
|
46
|
+
AgentSession: { storage: 'postgres', context: 'agents', plural: 'agentsessions', purpose: 'Krate projection of Agent Mux chat/session with lifecycle state', requiredSpec: ['organizationRef', 'agentMuxSessionId', 'dispatchRun'] },
|
|
47
|
+
AgentContextBundle: { storage: 'postgres', context: 'agents', plural: 'agentcontextbundles', purpose: 'Immutable prompt/context snapshot with digest, provenance, and redaction manifest', requiredSpec: ['organizationRef', 'dispatchRun', 'digest', 'sources'] },
|
|
48
|
+
AgentArtifact: { storage: 'postgres', context: 'agents', plural: 'agentartifacts', purpose: 'Durable agent output with kind, digest, and retention policy', requiredSpec: ['organizationRef', 'dispatchRun', 'kind', 'digest'] },
|
|
49
|
+
AgentApproval: { storage: 'postgres', context: 'agents', plural: 'agentapprovals', purpose: 'Human gate for tools, secrets, write-back, and release actions', requiredSpec: ['organizationRef', 'dispatchRun', 'action', 'requestedBy'] },
|
|
50
|
+
AgentWorkspace: { storage: 'postgres', context: 'agents', plural: 'agentworkspaces', purpose: 'Git worktree/runtime inventory with lifecycle state and ownership', requiredSpec: ['organizationRef', 'repository', 'workspacePath', 'ownership'] },
|
|
51
|
+
AgentTriggerExecution: { storage: 'postgres', context: 'agents', plural: 'agenttriggerexecutions', purpose: 'Durable trigger evaluation record with dedupe, coalescing, and rejection reason', requiredSpec: ['organizationRef', 'triggerRule', 'sourceEvent', 'decision'] },
|
|
52
|
+
AgentCapabilityRequirement: { storage: 'postgres', context: 'agents', plural: 'agentcapabilityrequirements', purpose: 'Computed dependency record from tools, MCP, skills, models, and subagents', requiredSpec: ['organizationRef', 'ownerRef', 'requiredRoles'] },
|
|
53
|
+
WorkItemSessionLink: { storage: 'postgres', context: 'agents', plural: 'workitemsessionlinks', purpose: 'Association between issues/PRs and agent sessions', requiredSpec: ['organizationRef', 'workItemRef', 'agentSession'] },
|
|
54
|
+
WorkItemWorkspaceLink: { storage: 'postgres', context: 'agents', plural: 'workitemworkspacelinks', purpose: 'Association between issues/PRs and agent workspaces', requiredSpec: ['organizationRef', 'workItemRef', 'workspace'] },
|
|
55
|
+
AgentAdapter: { storage: 'etcd', context: 'agents', plural: 'agentadapters', purpose: 'Agent adapter definition with transport type, capabilities matrix, auth requirements, and installation method', requiredSpec: ['organizationRef', 'adapterType', 'transport'] },
|
|
56
|
+
AgentTransportBinding: { storage: 'etcd', context: 'agents', plural: 'agenttransportbindings', purpose: 'Connection configuration for an adapter instance with endpoint, protocol, auth, health check, and reconnect policy', requiredSpec: ['organizationRef', 'adapterRef', 'endpoint', 'protocol'] },
|
|
57
|
+
AgentProviderConfig: { storage: 'etcd', context: 'agents', plural: 'agentproviderconfigs', purpose: 'Model provider configuration with API base, auth type, default model, model translations, and rate limits', requiredSpec: ['organizationRef', 'provider', 'authType'] },
|
|
58
|
+
AgentProject: { storage: 'etcd', context: 'agents', plural: 'agentprojects', purpose: 'Project grouping issues with kanban board config, default workflow, and team refs', requiredSpec: ['organizationRef', 'displayName'] },
|
|
59
|
+
AgentGatewayConfig: { storage: 'etcd', context: 'agents', plural: 'agentgatewayconfigs', purpose: 'Runtime Agent Mux gateway connection settings with URL, auth, reconnect policy, and feature flags', requiredSpec: ['organizationRef', 'gatewayUrl'] },
|
|
60
|
+
AgentSessionTranscript: { storage: 'postgres', context: 'agents', plural: 'agentsessiontranscripts', purpose: 'Durable chat transcript with message nodes, pagination support, and cost per turn', requiredSpec: ['organizationRef', 'sessionRef', 'messages'] },
|
|
61
|
+
AgentSessionAttachment: { storage: 'postgres', context: 'agents', plural: 'agentsessionattachments', purpose: 'File attached to a session message with source type, MIME type, digest, and redaction status', requiredSpec: ['organizationRef', 'sessionRef', 'sourceType', 'digest'] },
|
|
62
|
+
AgentWorkspaceRuntime: { storage: 'postgres', context: 'agents', plural: 'agentworkspaceruntimes', purpose: 'Workspace runtime surface state with cwd, environment variables, process status, and preview URL', requiredSpec: ['organizationRef', 'workspaceRef', 'status'] }
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export function listResourceDefinitions() {
|
|
66
|
+
return Object.entries(RESOURCE_DEFINITIONS).map(([kind, definition]) => ({ kind, ...clone(definition) }));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function resourceDefinitionForKind(kind) {
|
|
70
|
+
const definition = RESOURCE_DEFINITIONS[kind];
|
|
71
|
+
if (!definition) throw new Error(`Unknown Krate resource kind: ${kind}`);
|
|
72
|
+
return { kind, ...clone(definition) };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function resourceSchemaForKind(kind) {
|
|
76
|
+
const definition = resourceDefinitionForKind(kind);
|
|
77
|
+
return {
|
|
78
|
+
apiVersion: 'krate.a5c.ai/v1alpha1',
|
|
79
|
+
kind: definition.kind,
|
|
80
|
+
plural: definition.plural,
|
|
81
|
+
storage: definition.storage,
|
|
82
|
+
context: definition.context,
|
|
83
|
+
required: {
|
|
84
|
+
metadata: ['name'],
|
|
85
|
+
spec: [...definition.requiredSpec]
|
|
86
|
+
},
|
|
87
|
+
status: ['storage', 'phase', 'conditions']
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function storageClassForKind(kind) {
|
|
92
|
+
return resourceDefinitionForKind(kind).storage;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function resourceKey(resource) {
|
|
96
|
+
const namespace = resource.metadata?.namespace || 'default';
|
|
97
|
+
const name = resource.metadata?.name;
|
|
98
|
+
if (!name) throw new Error('resource metadata.name is required');
|
|
99
|
+
return `${resource.kind}/${namespace}/${name}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function clone(value) {
|
|
103
|
+
return value === undefined ? undefined : JSON.parse(JSON.stringify(value));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function createResource(kind, metadata, spec = {}, status = {}) {
|
|
107
|
+
if (!ALL_KINDS.has(kind)) throw new Error(`Unknown Krate resource kind: ${kind}`);
|
|
108
|
+
if (!metadata?.name) throw new Error(`${kind} requires metadata.name`);
|
|
109
|
+
return {
|
|
110
|
+
apiVersion: 'krate.a5c.ai/v1alpha1',
|
|
111
|
+
kind,
|
|
112
|
+
metadata: { namespace: metadata.namespace || 'default', labels: {}, annotations: {}, ...metadata },
|
|
113
|
+
spec: clone(spec),
|
|
114
|
+
status: clone(status)
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function validateResource(resource) {
|
|
119
|
+
if (!resource || typeof resource !== 'object') throw new Error('resource must be an object');
|
|
120
|
+
const definition = resourceDefinitionForKind(resource.kind);
|
|
121
|
+
if (!resource.metadata?.name) throw new Error(`${resource.kind} metadata.name is required`);
|
|
122
|
+
if (!resource.spec || typeof resource.spec !== 'object') resource.spec = {};
|
|
123
|
+
if (!resource.status || typeof resource.status !== 'object') resource.status = {};
|
|
124
|
+
resource.metadata.namespace ||= 'default';
|
|
125
|
+
resource.metadata.labels ||= {};
|
|
126
|
+
resource.metadata.annotations ||= {};
|
|
127
|
+
for (const field of definition.requiredSpec) {
|
|
128
|
+
if (resource.spec[field] === undefined || resource.spec[field] === null || resource.spec[field] === '') {
|
|
129
|
+
throw new Error(`${resource.kind} spec.${field} is required`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return resource;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function toKubernetesList(kind, items) {
|
|
136
|
+
return { apiVersion: 'krate.a5c.ai/v1alpha1', kind: `${kind}List`, items: items.map(clone) };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function matchLabels(resource, selector = {}) {
|
|
140
|
+
const labels = resource.metadata?.labels || {};
|
|
141
|
+
return Object.entries(selector).every(([key, value]) => labels[key] === value);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function createSelector({ name, namespace = 'krate-org-default', organizationRef = 'default', labels = {}, query = '' }) {
|
|
145
|
+
return createResource('Selector', { name, namespace }, { organizationRef, labels, query });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function createView({ name, namespace = 'krate-org-default', organizationRef = 'default', selector, columns = [], sort = [] }) {
|
|
149
|
+
return createResource('View', { name, namespace }, { organizationRef, selector, columns, sort });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function resourceToYaml(resource) {
|
|
153
|
+
const lines = [];
|
|
154
|
+
const scalar = (value) => {
|
|
155
|
+
if (value === null) return 'null';
|
|
156
|
+
if (value === undefined) return 'null';
|
|
157
|
+
if (typeof value === 'string') return value.includes(': ') || value.startsWith('{') || value.startsWith('[') ? JSON.stringify(value) : value;
|
|
158
|
+
return String(value);
|
|
159
|
+
};
|
|
160
|
+
const writeValue = (key, value, indent = 0) => {
|
|
161
|
+
const pad = ' '.repeat(indent);
|
|
162
|
+
if (Array.isArray(value)) {
|
|
163
|
+
lines.push(`${pad}${key}:`);
|
|
164
|
+
writeArray(value, indent + 2);
|
|
165
|
+
} else if (value && typeof value === 'object') {
|
|
166
|
+
lines.push(`${pad}${key}:`);
|
|
167
|
+
writeObject(value, indent + 2);
|
|
168
|
+
} else {
|
|
169
|
+
lines.push(`${pad}${key}: ${scalar(value)}`);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
const writeObject = (value, indent) => {
|
|
173
|
+
for (const [childKey, childValue] of Object.entries(value)) writeValue(childKey, childValue, indent);
|
|
174
|
+
};
|
|
175
|
+
const writeArray = (value, indent) => {
|
|
176
|
+
const pad = ' '.repeat(indent);
|
|
177
|
+
for (const item of value) {
|
|
178
|
+
if (Array.isArray(item)) {
|
|
179
|
+
lines.push(`${pad}-`);
|
|
180
|
+
writeArray(item, indent + 2);
|
|
181
|
+
} else if (item && typeof item === 'object') {
|
|
182
|
+
const entries = Object.entries(item);
|
|
183
|
+
if (entries.length === 0) {
|
|
184
|
+
lines.push(`${pad}- {}`);
|
|
185
|
+
} else {
|
|
186
|
+
const [[firstKey, firstValue], ...rest] = entries;
|
|
187
|
+
if (firstValue && typeof firstValue === 'object') {
|
|
188
|
+
lines.push(`${pad}- ${firstKey}:`);
|
|
189
|
+
if (Array.isArray(firstValue)) writeArray(firstValue, indent + 4);
|
|
190
|
+
else writeObject(firstValue, indent + 4);
|
|
191
|
+
} else {
|
|
192
|
+
lines.push(`${pad}- ${firstKey}: ${scalar(firstValue)}`);
|
|
193
|
+
}
|
|
194
|
+
for (const [childKey, childValue] of rest) writeValue(childKey, childValue, indent + 2);
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
lines.push(`${pad}- ${scalar(item)}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
writeObject(resource, 0);
|
|
202
|
+
return lines.join('\n') + '\n';
|
|
203
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createResource } from './resource-model.js';
|
|
2
|
+
import { serviceAccountForJob } from './identity-policy.js';
|
|
3
|
+
|
|
4
|
+
export class RunnerScheduler {
|
|
5
|
+
constructor({ controlPlane }) { this.controlPlane = controlPlane; }
|
|
6
|
+
|
|
7
|
+
createRunnerPool({ name, namespace = 'krate-org-default', organizationRef = 'default', image = 'ubuntu:24.04', warmReplicas = 1, maxReplicas = 10, trustTier = 'trusted', cache = { type: 'object-storage' } }, user) {
|
|
8
|
+
return this.controlPlane.create(createResource('RunnerPool', { name, namespace, labels: { trustTier } }, {
|
|
9
|
+
organizationRef, image, warmReplicas, maxReplicas, trustTier, cache, scalingMetric: 'queueDepth'
|
|
10
|
+
}, { readyReplicas: warmReplicas, queueDepth: 0 }), user);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
planReplicas(pool, queueDepth) {
|
|
14
|
+
return Math.min(pool.spec.maxReplicas || 0, Math.max(pool.spec.warmReplicas || 0, queueDepth));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
startPipeline({ name, namespace = 'krate-org-default', organizationRef = 'default', repository, ref, actor, steps = ['checkout', 'test'], fork = false, resumeFrom = null }, user) {
|
|
18
|
+
const trustTier = fork ? 'untrusted' : 'trusted';
|
|
19
|
+
const pipeline = this.controlPlane.create(createResource('Pipeline', { name, namespace, labels: { repository, ref, trustTier } }, {
|
|
20
|
+
organizationRef, repository, ref, actor: actor.name, resumeFrom, trustTier, steps
|
|
21
|
+
}, { phase: 'Running', currentStep: resumeFrom || steps[0] }), user);
|
|
22
|
+
const jobs = steps.map((step, index) => this.controlPlane.create(createResource('Job', {
|
|
23
|
+
name: `${name}-${index + 1}-${step}`,
|
|
24
|
+
namespace,
|
|
25
|
+
labels: { pipeline: name, repository, trustTier }
|
|
26
|
+
}, {
|
|
27
|
+
organizationRef: pipeline.spec?.organizationRef || organizationRef,
|
|
28
|
+
pipeline: name,
|
|
29
|
+
step,
|
|
30
|
+
serviceAccount: serviceAccountForJob({ namespace, repository, pipeline: name, trustTier })
|
|
31
|
+
}, { phase: step === (resumeFrom || steps[0]) ? 'Running' : 'Pending' }), user));
|
|
32
|
+
return { pipeline, jobs };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
rerunFromStep(pipeline, step, user) {
|
|
36
|
+
return this.startPipeline({
|
|
37
|
+
name: `${pipeline.metadata.name}-rerun-${step}`,
|
|
38
|
+
namespace: pipeline.metadata.namespace,
|
|
39
|
+
organizationRef: pipeline.spec.organizationRef,
|
|
40
|
+
repository: pipeline.spec.repository,
|
|
41
|
+
ref: pipeline.spec.ref,
|
|
42
|
+
actor: { name: pipeline.spec.actor },
|
|
43
|
+
steps: pipeline.spec.steps,
|
|
44
|
+
resumeFrom: step,
|
|
45
|
+
fork: pipeline.spec.trustTier === 'untrusted'
|
|
46
|
+
}, user);
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/runtime.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { ControlPlane } from './control-plane.js';
|
|
2
|
+
import { GiteaGitService, GiteaRepositoryStore } from './data-plane.js';
|
|
3
|
+
import { WebhookBus } from './hooks-events.js';
|
|
4
|
+
import { createAdmissionPolicy, mapOidcIdentity } from './identity-policy.js';
|
|
5
|
+
import { createInviteResource, createTeamResource, createAuthProviderResources, mapLoginProfileToKrateIdentity } from './auth.js';
|
|
6
|
+
import { createResource, clone } from './resource-model.js';
|
|
7
|
+
import { RunnerScheduler } from './runners-ci.js';
|
|
8
|
+
|
|
9
|
+
const DEFAULT_ORG = 'default';
|
|
10
|
+
const DEFAULT_NAMESPACE = 'krate-org-default';
|
|
11
|
+
|
|
12
|
+
export function createDefaultKrateUsers() {
|
|
13
|
+
return {
|
|
14
|
+
platformEngineer: mapOidcIdentity({ subject: 'user:platform', email: 'platform@example.com', groups: ['krate:platform-engineers'] }),
|
|
15
|
+
developer: mapOidcIdentity({ subject: 'user:dev', email: 'dev@example.com', groups: ['krate:developers'] }),
|
|
16
|
+
repoAdmin: mapOidcIdentity({ subject: 'user:admin', email: 'admin@example.com', groups: ['krate:repo-admins'] })
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class KrateRuntime {
|
|
21
|
+
constructor({ organizationRef = DEFAULT_ORG, namespace = DEFAULT_NAMESPACE, users = createDefaultKrateUsers(), controlPlane, git, runners, webhooks } = {}) {
|
|
22
|
+
this.organizationRef = organizationRef;
|
|
23
|
+
this.namespace = namespace;
|
|
24
|
+
this.users = users;
|
|
25
|
+
this.controlPlane = controlPlane || new ControlPlane();
|
|
26
|
+
this.controlPlane.addAdmissionPolicy(createAdmissionPolicy({
|
|
27
|
+
name: 'pr-title-required',
|
|
28
|
+
mode: 'enforce',
|
|
29
|
+
match: ({ resource }) => resource.kind === 'PullRequest',
|
|
30
|
+
validate: ({ resource }) => Boolean(resource.spec.title && resource.spec.title.length >= 8),
|
|
31
|
+
message: 'PullRequest spec.title must be descriptive'
|
|
32
|
+
}));
|
|
33
|
+
this.git = git || new GiteaGitService({ controlPlane: this.controlPlane, stores: [new GiteaRepositoryStore({ name: 'gitea-primary', receivePackReady: true })] });
|
|
34
|
+
this.runners = runners || new RunnerScheduler({ controlPlane: this.controlPlane });
|
|
35
|
+
this.webhooks = webhooks || new WebhookBus({ controlPlane: this.controlPlane });
|
|
36
|
+
this.seeded = false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static fromSnapshot(snapshot, options = {}) {
|
|
40
|
+
const runtime = new KrateRuntime(options);
|
|
41
|
+
runtime.importSnapshot(snapshot);
|
|
42
|
+
return runtime;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
bootstrap({ repository = 'krate-demo', webhookUrl = 'https://hooks.example.test/krate' } = {}) {
|
|
46
|
+
if (this.seeded) return this.snapshot();
|
|
47
|
+
const { platformEngineer, repoAdmin } = this.users;
|
|
48
|
+
this.git.createRepository({ name: repository, namespace: this.namespace, organizationRef: this.organizationRef }, repoAdmin);
|
|
49
|
+
this.controlPlane.create(createResource('BranchProtection', { name: 'main-protection', namespace: this.namespace }, { organizationRef: this.organizationRef, refs: ['refs/heads/main'], requirePullRequest: true, requiredChecks: ['test'], requiredApprovals: 1 }), repoAdmin);
|
|
50
|
+
this.controlPlane.create(createResource('RefPolicy', { name: 'deny-internal-refs', namespace: this.namespace }, { organizationRef: this.organizationRef, deny: ['refs/internal/'] }), repoAdmin);
|
|
51
|
+
this.runners.createRunnerPool({ name: 'trusted-linux', namespace: this.namespace, organizationRef: this.organizationRef, warmReplicas: 1, maxReplicas: 4 }, platformEngineer);
|
|
52
|
+
this.webhooks.subscribe({ name: 'chatops', namespace: this.namespace, organizationRef: this.organizationRef, url: webhookUrl, events: ['pullrequest.created', 'pullrequest.merged'] }, repoAdmin);
|
|
53
|
+
this.controlPlane.create(createTeamResource({ name: 'maintainers', organizationRef: this.organizationRef, displayName: 'Maintainers', members: ['admin'], maintainers: ['admin'], repositoryGrants: [{ repository, permission: 'admin' }], namespace: this.namespace }), platformEngineer);
|
|
54
|
+
const identity = mapLoginProfileToKrateIdentity({ provider: 'sso', subject: 'user:admin', email: 'admin@example.com', displayName: 'Admin', username: 'admin', teams: ['maintainers'], admin: true, namespace: this.namespace, organizationRef: this.organizationRef });
|
|
55
|
+
this.controlPlane.create(identity.user, platformEngineer);
|
|
56
|
+
this.controlPlane.create(identity.mapping, platformEngineer);
|
|
57
|
+
this.controlPlane.create(createInviteResource({ email: 'new-user@example.com', role: 'member', teams: ['maintainers'], invitedBy: 'admin', namespace: this.namespace, organizationRef: this.organizationRef }), repoAdmin);
|
|
58
|
+
for (const provider of createAuthProviderResources(undefined, this.namespace, this.organizationRef)) this.controlPlane.create(provider, platformEngineer);
|
|
59
|
+
this.seeded = true;
|
|
60
|
+
return this.snapshot();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
exportSnapshot() {
|
|
64
|
+
return {
|
|
65
|
+
apiVersion: 'krate.a5c.ai/v1alpha1',
|
|
66
|
+
kind: 'KrateRuntimeSnapshot',
|
|
67
|
+
namespace: this.namespace,
|
|
68
|
+
organizationRef: this.organizationRef,
|
|
69
|
+
seeded: this.seeded,
|
|
70
|
+
controlPlane: this.controlPlane.exportSnapshot(),
|
|
71
|
+
git: this.git.snapshot?.() || null
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
importSnapshot(snapshot) {
|
|
76
|
+
const controlPlaneSnapshot = snapshot?.controlPlane || snapshot;
|
|
77
|
+
this.controlPlane.importSnapshot(controlPlaneSnapshot);
|
|
78
|
+
if (snapshot?.git) this.git.importSnapshot(snapshot.git);
|
|
79
|
+
this.seeded = Boolean(snapshot?.seeded || this.controlPlane.list('Repository', { namespace: this.namespace }).items.length);
|
|
80
|
+
return this.snapshot();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
createRepository({ name, visibility = 'private' }, user = this.users.repoAdmin) {
|
|
84
|
+
return this.git.createRepository({ name, namespace: this.namespace, organizationRef: this.organizationRef, visibility }, user);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
createPullRequest({ repository, sourceRef = 'refs/heads/feature', targetRef = 'refs/heads/main', title, actor = this.users.developer, fork = false }) {
|
|
88
|
+
this.#requireRepository(repository);
|
|
89
|
+
const index = this.controlPlane.list('PullRequest', { namespace: this.namespace }).items.length + 1;
|
|
90
|
+
const name = `pr-${index}`;
|
|
91
|
+
const pullRequest = this.controlPlane.create(createResource('PullRequest', { name, namespace: this.namespace, labels: { repository } }, {
|
|
92
|
+
organizationRef: this.organizationRef,
|
|
93
|
+
repository,
|
|
94
|
+
sourceRef,
|
|
95
|
+
targetRef,
|
|
96
|
+
title,
|
|
97
|
+
author: actor.name,
|
|
98
|
+
checks: [],
|
|
99
|
+
requiredApprovals: this.#requiredApprovals(targetRef)
|
|
100
|
+
}, { phase: 'Open', approvals: 0, mergeable: false }), actor);
|
|
101
|
+
const pipelineRun = this.runners.startPipeline({ name: `pipeline-${name}`, namespace: this.namespace, organizationRef: this.organizationRef, repository, ref: `refs/pull/${index}/head`, actor, fork, steps: ['checkout', 'test'] }, actor);
|
|
102
|
+
this.webhooks.deliver({ subscriptionName: 'chatops', namespace: this.namespace, organizationRef: this.organizationRef, eventType: 'pullrequest.created', payload: { pullRequest: name, repository, title } }, this.users.repoAdmin);
|
|
103
|
+
return { pullRequest, pipeline: pipelineRun.pipeline, jobs: pipelineRun.jobs };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
completePipeline({ pipeline, phase = 'Succeeded', failedStep = null }, user = this.users.developer) {
|
|
107
|
+
const existing = this.controlPlane.get('Pipeline', this.namespace, pipeline);
|
|
108
|
+
if (!existing) throw new Error(`Pipeline ${pipeline} not found`);
|
|
109
|
+
const jobs = this.controlPlane.list('Job', { namespace: this.namespace, labels: { pipeline } }).items.map((job) => {
|
|
110
|
+
const jobPhase = failedStep && job.spec.step === failedStep ? 'Failed' : phase;
|
|
111
|
+
return this.controlPlane.patchStatus('Job', this.namespace, job.metadata.name, { phase: jobPhase, finishedAt: new Date().toISOString() }, user);
|
|
112
|
+
});
|
|
113
|
+
const pipelinePhase = jobs.some((job) => job.status.phase === 'Failed') ? 'Failed' : phase;
|
|
114
|
+
const updatedPipeline = this.controlPlane.patchStatus('Pipeline', this.namespace, pipeline, { phase: pipelinePhase, currentStep: null, finishedAt: new Date().toISOString() }, user);
|
|
115
|
+
this.#refreshPullRequestMergeability(existing.spec.repository);
|
|
116
|
+
return { pipeline: updatedPipeline, jobs };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
addReview({ pullRequest, decision = 'approved', body = 'Looks good', reviewer = this.users.repoAdmin }) {
|
|
120
|
+
const pr = this.#requirePullRequest(pullRequest);
|
|
121
|
+
const reviewCount = this.controlPlane.list('Review', { namespace: this.namespace, labels: { pullRequest } }).items.length + 1;
|
|
122
|
+
const review = this.controlPlane.create(createResource('Review', { name: `${pullRequest}-review-${reviewCount}`, namespace: this.namespace, labels: { pullRequest, decision } }, {
|
|
123
|
+
organizationRef: this.organizationRef,
|
|
124
|
+
pullRequest,
|
|
125
|
+
repository: pr.spec.repository,
|
|
126
|
+
reviewer: reviewer.name,
|
|
127
|
+
decision,
|
|
128
|
+
body
|
|
129
|
+
}, { phase: decision === 'approved' ? 'Approved' : 'ChangesRequested' }), reviewer);
|
|
130
|
+
this.#refreshPullRequestMergeability(pr.spec.repository);
|
|
131
|
+
return review;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
mergePullRequest({ pullRequest, actor = this.users.repoAdmin }) {
|
|
135
|
+
const pr = this.#requirePullRequest(pullRequest);
|
|
136
|
+
const refreshed = this.#refreshPullRequestMergeability(pr.spec.repository).find((candidate) => candidate.metadata.name === pullRequest) || pr;
|
|
137
|
+
if (!refreshed.status.mergeable) throw new Error(`PullRequest ${pullRequest} is not mergeable`);
|
|
138
|
+
const gitEvent = this.git.receivePack({ repository: refreshed.spec.repository, namespace: this.namespace, ref: refreshed.spec.targetRef, newRev: this.#syntheticRevision(pullRequest), actor });
|
|
139
|
+
const merged = this.controlPlane.patchStatus('PullRequest', this.namespace, pullRequest, { phase: 'Merged', mergeable: false, mergedAt: new Date().toISOString(), mergeCommit: gitEvent.newRev }, actor);
|
|
140
|
+
const delivery = this.webhooks.deliver({ subscriptionName: 'chatops', namespace: this.namespace, organizationRef: this.organizationRef, eventType: 'pullrequest.merged', payload: { pullRequest, repository: refreshed.spec.repository, mergeCommit: gitEvent.newRev } }, this.users.repoAdmin);
|
|
141
|
+
return { pullRequest: merged, gitEvent, delivery };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
snapshot() {
|
|
145
|
+
const kinds = ['Organization', 'User', 'Team', 'Invite', 'IdentityMapping', 'AuthProvider', 'Repository', 'SSHKey', 'RepositoryPermission', 'BranchProtection', 'RefPolicy', 'RunnerPool', 'PullRequest', 'Issue', 'Review', 'Pipeline', 'Job', 'WebhookSubscription', 'WebhookDelivery'];
|
|
146
|
+
const resources = Object.fromEntries(kinds.map((kind) => [kind, this.controlPlane.list(kind, { namespace: this.namespace }).items]));
|
|
147
|
+
return {
|
|
148
|
+
namespace: this.namespace,
|
|
149
|
+
export: this.exportSnapshot(),
|
|
150
|
+
storage: this.controlPlane.storageReport(),
|
|
151
|
+
resources,
|
|
152
|
+
events: clone(this.controlPlane.events),
|
|
153
|
+
auditLog: clone(this.controlPlane.auditLog)
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#requireRepository(repository) {
|
|
158
|
+
const existing = this.controlPlane.get('Repository', this.namespace, repository);
|
|
159
|
+
if (!existing) throw new Error(`Repository ${repository} not found`);
|
|
160
|
+
return existing;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
#requirePullRequest(pullRequest) {
|
|
164
|
+
const existing = this.controlPlane.get('PullRequest', this.namespace, pullRequest);
|
|
165
|
+
if (!existing) throw new Error(`PullRequest ${pullRequest} not found`);
|
|
166
|
+
return existing;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#requiredApprovals(targetRef) {
|
|
170
|
+
const branchProtection = this.controlPlane.list('BranchProtection', { namespace: this.namespace }).items.find((policy) => (policy.spec.refs || []).includes(targetRef));
|
|
171
|
+
return branchProtection?.spec.requiredApprovals || 0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
#refreshPullRequestMergeability(repository) {
|
|
175
|
+
const pullRequests = this.controlPlane.list('PullRequest', { namespace: this.namespace, labels: { repository } }).items;
|
|
176
|
+
return pullRequests.map((pr) => {
|
|
177
|
+
if (pr.status.phase !== 'Open') return pr;
|
|
178
|
+
const reviews = this.controlPlane.list('Review', { namespace: this.namespace, labels: { pullRequest: pr.metadata.name } }).items;
|
|
179
|
+
const approvals = reviews.filter((review) => review.spec.decision === 'approved').length;
|
|
180
|
+
const pipelines = this.controlPlane.list('Pipeline', { namespace: this.namespace, labels: { repository } }).items.filter((pipeline) => pipeline.metadata.name === `pipeline-${pr.metadata.name}`);
|
|
181
|
+
const checksPassed = pipelines.length > 0 && pipelines.every((pipeline) => pipeline.status.phase === 'Succeeded');
|
|
182
|
+
return this.controlPlane.patchStatus('PullRequest', this.namespace, pr.metadata.name, { approvals, checksPassed, mergeable: approvals >= (pr.spec.requiredApprovals || 0) && checksPassed }, this.users.repoAdmin);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
#syntheticRevision(seed) {
|
|
187
|
+
const source = Buffer.from(`${seed}:${Date.now()}`).toString('hex');
|
|
188
|
+
return source.padEnd(40, '0').slice(0, 40);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function createKrateRuntime(options = {}) {
|
|
193
|
+
const runtime = new KrateRuntime(options);
|
|
194
|
+
if (options.bootstrap !== false) runtime.bootstrap(options.bootstrapOptions);
|
|
195
|
+
return runtime;
|
|
196
|
+
}
|