@a5c-ai/krate 5.0.1-staging.00fa5317c
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +31 -0
- package/README.md +183 -0
- package/bin/krate-demo.mjs +23 -0
- package/bin/krate-server.mjs +14 -0
- package/dist/krate-controller-ui.json +3205 -0
- package/dist/krate-lifecycle.json +201 -0
- package/dist/krate-runtime-snapshot.json +3125 -0
- package/dist/krate-summary.json +724 -0
- package/docs/README.md +61 -0
- package/docs/agents/README.md +83 -0
- package/docs/agents/acceptance-test-matrix.md +193 -0
- package/docs/agents/agent-mux-adapter-contract.md +167 -0
- package/docs/agents/agent-mux-source-map.md +310 -0
- package/docs/agents/agent-run-memory-import-spec.md +256 -0
- package/docs/agents/agent-stack-management-spec.md +421 -0
- package/docs/agents/api-contract-spec.md +309 -0
- package/docs/agents/artifacts-writeback-spec.md +145 -0
- package/docs/agents/chart-packaging-spec.md +128 -0
- package/docs/agents/ci-orchestration-spec.md +140 -0
- package/docs/agents/context-assembly-spec.md +219 -0
- package/docs/agents/controller-reconciliation-spec.md +255 -0
- package/docs/agents/crd-schema-spec.md +315 -0
- package/docs/agents/decision-log-open-questions.md +169 -0
- package/docs/agents/developer-implementation-checklist.md +329 -0
- package/docs/agents/dispatching-design.md +262 -0
- package/docs/agents/gaps-agent-mux-to-krate-crds.md +298 -0
- package/docs/agents/glossary.md +66 -0
- package/docs/agents/implementation-blueprint.md +324 -0
- package/docs/agents/implementation-rollout-slices.md +251 -0
- package/docs/agents/memory-context-integration-spec.md +194 -0
- package/docs/agents/memory-ontology-schema-spec.md +253 -0
- package/docs/agents/memory-operations-runbook.md +121 -0
- package/docs/agents/mvp-vertical-slice-spec.md +146 -0
- package/docs/agents/observability-audit-spec.md +265 -0
- package/docs/agents/operator-runbook.md +174 -0
- package/docs/agents/org-memory-api-payload-examples.md +333 -0
- package/docs/agents/org-memory-controller-sequence-spec.md +181 -0
- package/docs/agents/org-memory-e2e-fixture-plan.md +161 -0
- package/docs/agents/org-memory-ui-implementation-map.md +114 -0
- package/docs/agents/org-memory-vertical-slice-spec.md +168 -0
- package/docs/agents/org-resource-model-delta-spec.md +111 -0
- package/docs/agents/org-route-resource-model-spec.md +183 -0
- package/docs/agents/org-scoping-namespace-spec.md +114 -0
- package/docs/agents/rbac-secrets-management-spec.md +406 -0
- package/docs/agents/repository-page-integration-spec.md +255 -0
- package/docs/agents/resource-contract-examples.md +808 -0
- package/docs/agents/resource-relationship-map.md +190 -0
- package/docs/agents/security-threat-model.md +188 -0
- package/docs/agents/shared-memory-company-brain-spec.md +358 -0
- package/docs/agents/storage-migration-spec.md +168 -0
- package/docs/agents/subagent-orchestration-spec.md +152 -0
- package/docs/agents/system-overview.md +88 -0
- package/docs/agents/tools-mcp-skills-spec.md +189 -0
- package/docs/agents/traceability-matrix.md +79 -0
- package/docs/agents/ui-flow-spec.md +211 -0
- package/docs/agents/ui-ux-system-spec.md +426 -0
- package/docs/agents/workspace-lifecycle-spec.md +166 -0
- package/docs/architecture-spec.md +78 -0
- package/docs/components/control-plane.md +78 -0
- package/docs/components/data-plane.md +69 -0
- package/docs/components/hooks-events.md +67 -0
- package/docs/components/identity-rbac-policy.md +73 -0
- package/docs/components/kubevela-oam.md +70 -0
- package/docs/components/operations-publishing.md +81 -0
- package/docs/components/runners-ci.md +66 -0
- package/docs/components/web-ui.md +94 -0
- package/docs/external/README.md +47 -0
- package/docs/external/bidirectional-sync-design.md +134 -0
- package/docs/external/cicd-interface.md +64 -0
- package/docs/external/external-backend-controllers.md +170 -0
- package/docs/external/external-backend-crds.md +234 -0
- package/docs/external/external-backend-ui-spec.md +151 -0
- package/docs/external/external-backend-ux-flows.md +115 -0
- package/docs/external/external-object-mapping.md +125 -0
- package/docs/external/git-forge-interface.md +68 -0
- package/docs/external/github-integration-design.md +151 -0
- package/docs/external/issue-tracking-interface.md +66 -0
- package/docs/external/provider-capability-manifests.md +204 -0
- package/docs/external/provider-catalog.md +139 -0
- package/docs/external/provider-rollout-testing.md +78 -0
- package/docs/external/research-results.md +48 -0
- package/docs/external/security-auth-permissions.md +81 -0
- package/docs/external/sync-state-machines.md +108 -0
- package/docs/external/unified-external-backend-model.md +107 -0
- package/docs/external/user-facing-changes.md +67 -0
- package/docs/gaps.md +161 -0
- package/docs/install.md +94 -0
- package/docs/krate-design.md +334 -0
- package/docs/local-minikube.md +55 -0
- package/docs/ontology/README.md +32 -0
- package/docs/ontology/bounded-contexts.md +29 -0
- package/docs/ontology/events-and-hooks.md +32 -0
- package/docs/ontology/oam-kubevela.md +32 -0
- package/docs/ontology/operations-and-release.md +25 -0
- package/docs/ontology/personas-and-actors.md +32 -0
- package/docs/ontology/policies-and-invariants.md +33 -0
- package/docs/ontology/problem-space.md +30 -0
- package/docs/ontology/resource-contracts.md +40 -0
- package/docs/ontology/resource-taxonomy.md +42 -0
- package/docs/ontology/runners-and-ci.md +29 -0
- package/docs/ontology/solution-space.md +24 -0
- package/docs/ontology/storage-and-data-boundaries.md +29 -0
- package/docs/ontology/validation-matrix.md +24 -0
- package/docs/ontology/web-ui-excellent-flows.md +32 -0
- package/docs/ontology/workflows.md +39 -0
- package/docs/ontology/world.md +35 -0
- package/docs/openapi.yaml +1275 -0
- package/docs/product-requirements.md +62 -0
- package/docs/roadmap-mvp.md +87 -0
- package/docs/system-requirements.md +90 -0
- package/docs/tests/README.md +53 -0
- package/docs/tests/agent-qa-plan.md +63 -0
- package/docs/tests/browser-ui-tests.md +62 -0
- package/docs/tests/ci-quality-gates.md +48 -0
- package/docs/tests/coverage-model.md +64 -0
- package/docs/tests/e2e-scenario-tests.md +53 -0
- package/docs/tests/fixtures-test-data.md +63 -0
- package/docs/tests/observability-reliability-tests.md +54 -0
- package/docs/tests/product-test-matrix.md +145 -0
- package/docs/tests/qa-adoption-roadmap.md +130 -0
- package/docs/tests/qa-automation-plan.md +101 -0
- package/docs/tests/security-compliance-tests.md +57 -0
- package/docs/tests/test-framework-tools.md +88 -0
- package/docs/tests/test-suite-layout.md +121 -0
- package/docs/tests/unit-integration-tests.md +48 -0
- package/docs/todo-kyverno +714 -0
- package/docs/todos.md +4 -0
- package/docs/user-stories.md +78 -0
- package/examples/minikube-demo.yaml +190 -0
- package/examples/oam-application.yaml +23 -0
- package/examples/policy-kyverno-pr-title.yaml +18 -0
- package/package.json +63 -0
- package/scripts/build.mjs +29 -0
- package/scripts/setup-minikube.mjs +65 -0
- package/scripts/smoke.mjs +37 -0
- package/scripts/validate-doc-coverage.mjs +152 -0
- package/scripts/validate-package.mjs +93 -0
- package/scripts/validate-ui.mjs +278 -0
- package/src/agent-adapter-controller.js +169 -0
- package/src/agent-approval-controller.js +170 -0
- package/src/agent-context-bundles.js +242 -0
- package/src/agent-dispatch-controller.js +209 -0
- package/src/agent-gateway-config-controller.js +147 -0
- package/src/agent-memory-controller.js +357 -0
- package/src/agent-memory-import.js +327 -0
- package/src/agent-memory-query.js +292 -0
- package/src/agent-memory-repository-source-controller.js +255 -0
- package/src/agent-mux-client.js +280 -0
- package/src/agent-permission-review.js +250 -0
- package/src/agent-project-controller.js +117 -0
- package/src/agent-provider-config-controller.js +150 -0
- package/src/agent-secret-config-grant-controller.js +282 -0
- package/src/agent-session-transcript-controller.js +189 -0
- package/src/agent-stack-controller.js +347 -0
- package/src/agent-subagent-controller.js +160 -0
- package/src/agent-transport-binding-controller.js +121 -0
- package/src/agent-trigger-controller.js +381 -0
- package/src/agent-workspace-controller.js +702 -0
- package/src/agent-writeback-controller.js +302 -0
- package/src/api-controller.js +541 -0
- package/src/argocd-gitops.js +43 -0
- package/src/async-controller.js +207 -0
- package/src/audit-controller.js +191 -0
- package/src/auth.js +307 -0
- package/src/component-catalog.js +41 -0
- package/src/control-plane.js +136 -0
- package/src/controller-client.js +72 -0
- package/src/controller-ui.js +617 -0
- package/src/data-plane.js +179 -0
- package/src/event-bus.js +61 -0
- package/src/external/conflict-controller.js +225 -0
- package/src/external/github/auth.js +96 -0
- package/src/external/github/cicd.js +180 -0
- package/src/external/github/git-forge.js +240 -0
- package/src/external/github/index.js +144 -0
- package/src/external/github/issue-tracking.js +163 -0
- package/src/external/provider-adapter.js +161 -0
- package/src/external/provider-resource-factory.js +161 -0
- package/src/external/sync-controller.js +235 -0
- package/src/external/webhook-controller.js +144 -0
- package/src/external/write-controller.js +283 -0
- package/src/gitea-backend.js +131 -0
- package/src/gitea-service.js +173 -0
- package/src/handoff.js +98 -0
- package/src/hooks-events.js +63 -0
- package/src/http-server.js +377 -0
- package/src/identity-policy.js +86 -0
- package/src/index.js +57 -0
- package/src/kubernetes-controller-async.js +511 -0
- package/src/kubernetes-controller.js +878 -0
- package/src/kubernetes-resource-gateway.js +48 -0
- package/src/notification-controller.js +178 -0
- package/src/operations.js +112 -0
- package/src/org-scoping.js +5 -0
- package/src/resource-model.js +221 -0
- package/src/runner-controller.js +272 -0
- package/src/runners-ci.js +48 -0
- package/src/runtime.js +196 -0
- package/src/snapshot-cache.js +157 -0
- package/src/web-ui.js +40 -0
- package/tests/agent-adapter-controller.test.js +361 -0
- package/tests/agent-approval-controller.test.js +173 -0
- package/tests/agent-context-bundles.test.js +278 -0
- package/tests/agent-dispatch-controller.test.js +315 -0
- package/tests/agent-gateway-config-controller.test.js +386 -0
- package/tests/agent-memory-controller.test.js +308 -0
- package/tests/agent-memory-import-snapshot.test.js +477 -0
- package/tests/agent-memory-query.test.js +404 -0
- package/tests/agent-memory-repository-source.test.js +514 -0
- package/tests/agent-mux-client.test.js +204 -0
- package/tests/agent-permission-review-v2.test.js +317 -0
- package/tests/agent-permission-review.test.js +209 -0
- package/tests/agent-project-controller.test.js +302 -0
- package/tests/agent-provider-config-controller.test.js +376 -0
- package/tests/agent-resources.test.js +228 -0
- package/tests/agent-secret-config-grant.test.js +231 -0
- package/tests/agent-session-transcript-controller.test.js +499 -0
- package/tests/agent-stack-controller.test.js +221 -0
- package/tests/agent-subagent-controller.test.js +201 -0
- package/tests/agent-transport-binding-controller.test.js +294 -0
- package/tests/agent-trigger-controller.test.js +211 -0
- package/tests/agent-trigger-routes.test.js +190 -0
- package/tests/agent-trigger-sources.test.js +245 -0
- package/tests/agent-workspace-controller.test.js +181 -0
- package/tests/agent-writeback.test.js +292 -0
- package/tests/approval-persistence.test.js +171 -0
- package/tests/async-controller.test.js +252 -0
- package/tests/audit-controller.test.js +227 -0
- package/tests/codespace-controller.test.js +318 -0
- package/tests/deployment.test.js +407 -0
- package/tests/e2e/lifecycle.test.js +117 -0
- package/tests/event-bus-integration.test.js +190 -0
- package/tests/external-github-forge.test.js +560 -0
- package/tests/external-github-issues-cicd.test.js +520 -0
- package/tests/external-integration.test.js +470 -0
- package/tests/external-persistence.test.js +340 -0
- package/tests/external-provider-adapter.test.js +365 -0
- package/tests/external-resource-model.test.js +215 -0
- package/tests/external-webhook-sync.test.js +287 -0
- package/tests/external-write-conflict.test.js +353 -0
- package/tests/gitea-service.test.js +253 -0
- package/tests/health-check-real.test.js +165 -0
- package/tests/integration/full-flow.test.js +266 -0
- package/tests/krate.test.js +756 -0
- package/tests/memory-search-wiring.test.js +270 -0
- package/tests/notification-controller.test.js +196 -0
- package/tests/notification-integration.test.js +179 -0
- package/tests/org-scoping.test.js +687 -0
- package/tests/runner-controller.test.js +327 -0
- package/tests/runner-integration.test.js +231 -0
- package/tests/session-cookie-hmac.test.js +151 -0
- package/tests/snapshot-performance.test.js +247 -0
- package/tests/sse-events.test.js +107 -0
- package/tests/webhook-trigger.test.js +198 -0
- package/tests/workspace-volumes.test.js +312 -0
- package/tests/writeback-persistence.test.js +207 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
5
|
+
import { createControllerUiModel, createKrateApiController, createKrateHttpServer } from '../src/index.js';
|
|
6
|
+
|
|
7
|
+
function read(path) {
|
|
8
|
+
return readFileSync(path, 'utf8');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function workflowJobBlock(workflow, jobName) {
|
|
12
|
+
const match = workflow.match(new RegExp(`\r?\n ${jobName}:\r?\n([\\s\\S]*?)(?=\r?\n [a-zA-Z0-9_-]+:\r?\n|$)`));
|
|
13
|
+
assert.ok(match, `workflow has ${jobName} job`);
|
|
14
|
+
return match[1];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function fixtureKubernetesController() {
|
|
18
|
+
return {
|
|
19
|
+
async snapshot() {
|
|
20
|
+
return {
|
|
21
|
+
source: 'kubernetes',
|
|
22
|
+
namespace: 'krate-org-default',
|
|
23
|
+
generatedAt: 'test-time',
|
|
24
|
+
correlationId: 'test-correlation',
|
|
25
|
+
kubectl: { available: true, context: 'kind-krate', clientVersion: 'v1.test', errors: [] },
|
|
26
|
+
apiService: { metadata: { name: 'v1alpha1.krate.a5c.ai' } },
|
|
27
|
+
crds: [{ metadata: { name: 'repositories.krate.a5c.ai' } }],
|
|
28
|
+
storage: { etcd: 'etcd', postgres: 'postgres', repositories: 'rwx', objects: 'object' },
|
|
29
|
+
commands: [],
|
|
30
|
+
permissions: [],
|
|
31
|
+
events: [],
|
|
32
|
+
resources: {
|
|
33
|
+
Organization: [{ apiVersion: 'krate.a5c.ai/v1alpha1', kind: 'Organization', metadata: { name: 'default', namespace: 'krate-system' }, spec: { slug: 'default', namespaceName: 'krate-org-default', displayName: 'Default org' } }],
|
|
34
|
+
User: [{ apiVersion: 'krate.a5c.ai/v1alpha1', kind: 'User', metadata: { name: 'alice', namespace: 'krate-org-default' }, spec: { organizationRef: 'default', displayName: 'Alice', email: 'alice@example.com', teams: ['maintainers'] }, status: { phase: 'Active' } }],
|
|
35
|
+
Team: [{ apiVersion: 'krate.a5c.ai/v1alpha1', kind: 'Team', metadata: { name: 'maintainers', namespace: 'krate-org-default' }, spec: { organizationRef: 'default', displayName: 'Maintainers', members: ['alice'] }, status: { phase: 'Active' } }],
|
|
36
|
+
Invite: [],
|
|
37
|
+
IdentityMapping: [{ apiVersion: 'krate.a5c.ai/v1alpha1', kind: 'IdentityMapping', metadata: { name: 'sso-alice', namespace: 'krate-org-default' }, spec: { organizationRef: 'default', user: 'alice', provider: 'sso', subject: 'alice', repositoryIdentity: { username: 'alice' } }, status: { phase: 'Synced' } }],
|
|
38
|
+
AuthProvider: [{ apiVersion: 'krate.a5c.ai/v1alpha1', kind: 'AuthProvider', metadata: { name: 'sso', namespace: 'krate-org-default' }, spec: { organizationRef: 'default', type: 'oidc', label: 'Company SSO', enabled: true }, status: { phase: 'Configured' } }],
|
|
39
|
+
Repository: [{ apiVersion: 'krate.a5c.ai/v1alpha1', kind: 'Repository', metadata: { name: 'app', namespace: 'krate-org-default' }, spec: { organizationRef: 'default', visibility: 'internal', defaultBranch: 'main' }, status: { phase: 'Ready' } }],
|
|
40
|
+
PolicyProfile: [{ apiVersion: 'krate.a5c.ai/v1alpha1', kind: 'PolicyProfile', metadata: { name: 'default-profile', namespace: 'krate-org-default' }, spec: { organizationRef: 'default', displayName: 'Default policy posture', mode: 'audit' }, status: { phase: 'Synced', lastViolationCount: 1 } }],
|
|
41
|
+
PolicyTemplate: [{ apiVersion: 'krate.a5c.ai/v1alpha1', kind: 'PolicyTemplate', metadata: { name: 'require-pr-description', namespace: 'krate-org-default' }, spec: { organizationRef: 'default', displayName: 'Require PR description', targetKinds: ['PullRequest'], kyverno: { kind: 'ValidatingPolicy' } }, status: { phase: 'Ready' } }],
|
|
42
|
+
PolicyBinding: [{ apiVersion: 'krate.a5c.ai/v1alpha1', kind: 'PolicyBinding', metadata: { name: 'require-pr-description-audit', namespace: 'krate-org-default' }, spec: { organizationRef: 'default', templateRef: 'require-pr-description', mode: 'audit' }, status: { phase: 'Applied', lastViolationCount: 1 } }],
|
|
43
|
+
PolicyExceptionRequest: [{ apiVersion: 'krate.a5c.ai/v1alpha1', kind: 'PolicyExceptionRequest', metadata: { name: 'pr-1-exception', namespace: 'krate-org-default' }, spec: { organizationRef: 'default', policyRef: { name: 'require-pr-description' }, justification: 'temporary rollout', expiresAt: '2026-06-01T00:00:00Z' }, status: { phase: 'Requested' } }],
|
|
44
|
+
PullRequest: [],
|
|
45
|
+
Pipeline: [],
|
|
46
|
+
RunnerPool: [],
|
|
47
|
+
WebhookSubscription: []
|
|
48
|
+
},
|
|
49
|
+
kyverno: {
|
|
50
|
+
mode: 'byo',
|
|
51
|
+
namespace: 'kyverno',
|
|
52
|
+
policyNamespace: 'krate-org-default',
|
|
53
|
+
detected: true,
|
|
54
|
+
controllers: [{ name: 'kyverno-admission-controller', ready: true, readyReplicas: 1, replicas: 1 }],
|
|
55
|
+
permissions: [{ kind: 'PolicyReport', verbs: { list: true, watch: true } }],
|
|
56
|
+
resources: { KyvernoValidatingPolicy: [{ metadata: { name: 'require-pr-description' } }], PolicyReport: [{ metadata: { name: 'app-policy', namespace: 'krate-org-default' }, results: [{ policy: 'require-pr-description', rule: 'description', result: 'fail', message: 'description required', resources: [{ kind: 'PullRequest', name: 'pr-1' }] }] }] },
|
|
57
|
+
reports: { policyReports: [{ metadata: { name: 'app-policy', namespace: 'krate-org-default' } }], clusterPolicyReports: [], results: [{ policy: 'require-pr-description', rule: 'description', result: 'fail', message: 'description required' }], violations: [{ policy: 'require-pr-description', rule: 'description', result: 'fail', message: 'description required' }] },
|
|
58
|
+
degraded: []
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
async applyResource(resource) {
|
|
63
|
+
return { operation: 'apply', resource };
|
|
64
|
+
},
|
|
65
|
+
async deleteResource(kind, name) {
|
|
66
|
+
return { operation: 'delete', resource: { kind, metadata: { name } } };
|
|
67
|
+
},
|
|
68
|
+
async createRepository(input) {
|
|
69
|
+
return { operation: 'apply', resource: { kind: 'Repository', metadata: { name: input.name } } };
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
test('controller deployment assets build and publish the runnable controller', () => {
|
|
75
|
+
for (const file of ['Dockerfile', '.dockerignore', '.github/workflows/publish.yml']) assert.equal(existsSync(file), true, `${file} exists`);
|
|
76
|
+
|
|
77
|
+
const dockerfile = read('Dockerfile');
|
|
78
|
+
assert.match(dockerfile, /FROM node:\d+-alpine AS (deps|build)/);
|
|
79
|
+
assert.match(dockerfile, /npm (ci|install)/);
|
|
80
|
+
assert.match(dockerfile, /HEALTHCHECK/);
|
|
81
|
+
assert.match(dockerfile, /krate-server\.mjs/);
|
|
82
|
+
for (const runtimePath of ['/app/bin', '/app/src', '/app/dist']) assert.ok(dockerfile.includes(runtimePath), `Dockerfile copies ${runtimePath}`);
|
|
83
|
+
|
|
84
|
+
const dockerignore = read('.dockerignore');
|
|
85
|
+
for (const ignored of ['.a5c', 'node_modules', '**/.next', 'dist']) assert.ok(dockerignore.includes(ignored), `.dockerignore excludes ${ignored}`);
|
|
86
|
+
|
|
87
|
+
const chartDeployment = read('../charts/templates/deployments.yaml');
|
|
88
|
+
const chartRbac = read('../charts/templates/rbac.yaml');
|
|
89
|
+
const chartIngress = read('../charts/templates/ingress.yaml');
|
|
90
|
+
const chartValues = read('../charts/values.yaml');
|
|
91
|
+
const authSecret = read('../charts/templates/auth-secret.yaml');
|
|
92
|
+
assert.ok(chartValues.includes('auth:'), 'chart values include auth configuration');
|
|
93
|
+
assert.ok(chartValues.includes('github:') && chartValues.includes('sso:'), 'chart values expose GitHub and SSO configuration');
|
|
94
|
+
assert.ok(chartValues.includes('mode: auto') && chartValues.includes('policyReporter:'), 'chart values expose Kyverno auto-discovery modes and policy reporter settings');
|
|
95
|
+
assert.ok(chartRbac.includes('"*"') && chartRbac.includes('policies.kyverno.io') && chartRbac.includes('policyreports'), 'RBAC covers all Krate resources via wildcard and Kyverno read/write surfaces');
|
|
96
|
+
assert.ok(chartDeployment.includes('KRATE_KYVERNO_MODE') && chartDeployment.includes('KRATE_KYVERNO_POLICY_NAMESPACE') && chartDeployment.includes('KRATE_KYVERNO_DISCOVER_EXISTING'), 'deployments receive Kyverno discovery env');
|
|
97
|
+
assert.ok(read('../charts/templates/argocd-kyverno-application.yaml').includes('krate.a5c.ai/policy-engine: kyverno'), 'managed Kyverno Argo CD application template is present');
|
|
98
|
+
assert.ok(authSecret.includes('github-client-id') && authSecret.includes('sso-client-secret'), 'chart renders auth secret keys');
|
|
99
|
+
assert.ok(chartIngress.includes('kind: Ingress') && chartIngress.includes('ingressClassName') && chartIngress.includes('app.kubernetes.io/component: web'), 'chart renders web ingress');
|
|
100
|
+
assert.ok(chartDeployment.includes('imagePullSecrets') && chartDeployment.includes('global.imagePullSecrets'), 'workloads can use registry pull secrets');
|
|
101
|
+
assert.ok(chartDeployment.includes('KRATE_AUTH_GITHUB_ENABLED') && chartDeployment.includes('KRATE_AUTH_SSO_ENABLED') && chartDeployment.includes('KRATE_AUTH_DELEGATED_EMAIL_HEADER'), 'workloads receive auth provider configuration');
|
|
102
|
+
assert.ok(chartDeployment.includes('readinessProbe:') && chartDeployment.includes('path: /login'), 'web deployment has an HTTP readiness probe');
|
|
103
|
+
assert.ok(chartDeployment.includes('KRATE_AUTH_DELEGATED_LOCAL_DEVELOPMENT') && chartDeployment.includes('KRATE_AUTH_DELEGATED_LOCAL_GROUPS'), 'workloads can opt into local delegated development login');
|
|
104
|
+
assert.ok(chartRbac.includes('core.oam.dev') && chartRbac.includes('applications') && chartRbac.includes('create'), 'delivery resources can be composed through Krate');
|
|
105
|
+
assert.ok(chartValues.includes('localDevelopment:') && chartValues.includes('enabled: false'), 'local delegated development login is off by default');
|
|
106
|
+
for (const token of ['command: ["node", "bin/krate-server.mjs"]', '--port=3080', 'app.kubernetes.io/component: controllers']) {
|
|
107
|
+
assert.ok(chartDeployment.includes(token), `chart deployment includes ${token}`);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('web proxy protects UI pages and authenticated APIs behind login', async () => {
|
|
112
|
+
let NextRequest, proxy, config;
|
|
113
|
+
try {
|
|
114
|
+
({ NextRequest } = await import('next/server.js'));
|
|
115
|
+
({ proxy, config } = await import(`../../web/proxy.js?test=${Date.now()}`));
|
|
116
|
+
} catch { return; }
|
|
117
|
+
|
|
118
|
+
assert.ok(config.matcher.some((entry) => entry.includes('_next/static')));
|
|
119
|
+
for (const path of ['/', '/orgs/default/repositories?tab=code', '/orgs/default/repositories/demo/code', '/orgs/default/people', '/logout', '/api/controller']) {
|
|
120
|
+
const response = proxy(new NextRequest(`http://localhost:3000${path}`));
|
|
121
|
+
assert.equal(response.status, 307, `${path} redirects to login without session`);
|
|
122
|
+
assert.equal(new URL(response.headers.get('location')).pathname, '/login');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const nextResponse = proxy(new NextRequest('http://localhost:3000/orgs/default/repositories?tab=code'));
|
|
126
|
+
assert.equal(new URL(nextResponse.headers.get('location')).searchParams.get('next'), '/orgs/default/repositories?tab=code');
|
|
127
|
+
assert.equal(proxy(new NextRequest('http://localhost:3000/login')).headers.get('x-middleware-next'), '1');
|
|
128
|
+
assert.equal(proxy(new NextRequest('http://localhost:3000/api/auth/delegated')).headers.get('x-middleware-next'), '1');
|
|
129
|
+
assert.equal(proxy(new NextRequest('http://localhost:3000/orgs/default/repositories', { headers: { cookie: 'krate_session=dev' } })).headers.get('x-middleware-next'), '1');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('login page stays minimal and does not expose the authenticated console shell', () => {
|
|
133
|
+
const layout = read('../web/app/layout.jsx');
|
|
134
|
+
const managePages = read('../web/app/pages/manage-pages.jsx');
|
|
135
|
+
const loginStart = managePages.indexOf('export function LoginPage');
|
|
136
|
+
assert.notEqual(loginStart, -1, 'LoginPage is defined in manage-pages module');
|
|
137
|
+
assert.ok(!layout.includes('<AppShell>{children}</AppShell>'), 'root layout does not wrap public routes in AppShell');
|
|
138
|
+
const loginSource = managePages.slice(loginStart, managePages.indexOf('export ', loginStart + 1));
|
|
139
|
+
assert.ok(loginSource.includes('loginMain') || loginSource.includes('login'), 'login page uses standalone login layout');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('auth chart uses existing secrets without rendering empty provider keys', () => {
|
|
143
|
+
const mixed = execFileSync('helm', [
|
|
144
|
+
'template', 'krate', '../charts', '-n', 'krate-system',
|
|
145
|
+
'--set', 'argocd.enabled=false',
|
|
146
|
+
'--set', 'auth.github.clientId=github-client',
|
|
147
|
+
'--set', 'auth.github.clientSecret=github-secret',
|
|
148
|
+
'--set', 'auth.sso.enabled=true',
|
|
149
|
+
'--set', 'auth.sso.existingSecret=shared-auth'
|
|
150
|
+
], { encoding: 'utf8' });
|
|
151
|
+
assert.match(mixed, /name: krate-krate-auth/);
|
|
152
|
+
assert.match(mixed, /github-client-id: "github-client"/);
|
|
153
|
+
assert.doesNotMatch(mixed, /sso-client-id:/);
|
|
154
|
+
assert.match(mixed, /name: "shared-auth"[\s\S]*?key: sso-client-id/);
|
|
155
|
+
|
|
156
|
+
const externalOnly = execFileSync('helm', [
|
|
157
|
+
'template', 'krate', '../charts', '-n', 'krate-system',
|
|
158
|
+
'--set', 'argocd.enabled=false',
|
|
159
|
+
'--set', 'auth.github.existingSecret=shared-auth',
|
|
160
|
+
'--set', 'auth.sso.enabled=true',
|
|
161
|
+
'--set', 'auth.sso.existingSecret=shared-auth'
|
|
162
|
+
], { encoding: 'utf8' });
|
|
163
|
+
assert.doesNotMatch(externalOnly, /name: krate-krate-auth/);
|
|
164
|
+
assert.match(externalOnly, /name: "shared-auth"[\s\S]*?key: github-client-id/);
|
|
165
|
+
assert.match(externalOnly, /name: "shared-auth"[\s\S]*?key: sso-client-secret/);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
test('web UI and controller API expose live Kubernetes deployment and publishing metadata', async () => {
|
|
170
|
+
const controller = fixtureKubernetesController();
|
|
171
|
+
const model = createControllerUiModel(await controller.snapshot());
|
|
172
|
+
assert.equal(model.controller.mode, 'krate-workspace');
|
|
173
|
+
assert.ok(model.controller.endpoints.some((endpoint) => endpoint.path === '/api/controller?org=:org'));
|
|
174
|
+
assert.ok(model.controller.endpoints.some((endpoint) => endpoint.path === '/api/orgs/:org/resources'));
|
|
175
|
+
assert.equal(model.controller.architecture.apiController.role, 'krate-api-controller');
|
|
176
|
+
assert.equal(model.controller.architecture.resourceGateway.role, 'krate-resource-gateway');
|
|
177
|
+
assert.equal(model.controller.architecture.resourceClient.role, 'krate-resource-client');
|
|
178
|
+
assert.equal(model.controller.architecture.repositoryService.role, 'repository-service');
|
|
179
|
+
assert.deepEqual(model.controller.architecture.apiController.delegatesTo, ['krate-resource-gateway', 'repository-service']);
|
|
180
|
+
assert.ok(model.resources.some((resource) => resource.kind === 'Repository' && resource.count > 0));
|
|
181
|
+
assert.ok(model.resources.some((resource) => resource.kind === 'KrateProject'));
|
|
182
|
+
assert.ok(model.views.dashboard.issueSync?.gitea?.repo, 'dashboard exposes issue sync backend plan');
|
|
183
|
+
assert.equal(model.policyEngine.health, 'ready');
|
|
184
|
+
assert.equal(model.policyEngine.violations.length, 1);
|
|
185
|
+
assert.ok(model.resources.some((resource) => resource.kind === 'PolicyBinding' && resource.count > 0));
|
|
186
|
+
assert.ok(model.resources.find((resource) => resource.kind === 'Repository').action.list.includes('Open Repository records'));
|
|
187
|
+
assert.match(model.operations.image, /krate-controller/);
|
|
188
|
+
assert.equal(model.operations.chart, 'charts/krate');
|
|
189
|
+
for (const gate of ['npm run check', 'docker build', 'helm package charts/krate', 'npm pack --json']) assert.ok(model.operations.releaseGates.includes(gate), `release gate ${gate}`);
|
|
190
|
+
assert.ok(model.validation.every((item) => typeof item.evidence === 'string' && item.evidence.length > 0));
|
|
191
|
+
|
|
192
|
+
const server = createKrateHttpServer({ controller });
|
|
193
|
+
await new Promise((resolve) => server.listen(0, resolve));
|
|
194
|
+
try {
|
|
195
|
+
const response = await fetch(`http://127.0.0.1:${server.address().port}/api/controller`);
|
|
196
|
+
assert.equal(response.status, 200);
|
|
197
|
+
const body = await response.json();
|
|
198
|
+
assert.deepEqual(body.operations.releaseGates, model.operations.releaseGates);
|
|
199
|
+
assert.equal(body.metrics.resources, model.metrics.resources);
|
|
200
|
+
} finally {
|
|
201
|
+
await new Promise((resolve) => server.close(resolve));
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('web UI is wired to the Kubernetes controller API instead of a static local snapshot', () => {
|
|
206
|
+
const page = read('../web/app/page.jsx');
|
|
207
|
+
const orgPage = read('../web/app/orgs/[org]/page.jsx');
|
|
208
|
+
const shellModules = ['../web/app/lib/krate-ui.jsx', '../web/app/lib/page-frame.jsx', '../web/app/pages/agent-pages.jsx', '../web/app/pages/repo-pages.jsx', '../web/app/pages/manage-pages.jsx', '../web/app/pages/settings-pages.jsx', '../web/app/pages/external-pages.jsx'];
|
|
209
|
+
const shell = shellModules.map((m) => { try { return readFileSync(m, 'utf8'); } catch { return ''; } }).join('\n');
|
|
210
|
+
const actions = read('../web/app/components/resource-actions.jsx');
|
|
211
|
+
const client = read('src/controller-client.js');
|
|
212
|
+
const apiController = read('src/api-controller.js');
|
|
213
|
+
const gateway = read('src/kubernetes-resource-gateway.js');
|
|
214
|
+
const kubernetes = read('src/kubernetes-controller.js');
|
|
215
|
+
const server = read('src/http-server.js');
|
|
216
|
+
const webControllerRoute = read('../web/app/api/controller/route.js');
|
|
217
|
+
assert.ok(page.includes("redirect('/orgs/'"));
|
|
218
|
+
assert.ok(orgPage.includes('DashboardPage'));
|
|
219
|
+
assert.ok(read('../web/app/orgs/page.jsx').includes('Choose an organization'));
|
|
220
|
+
assert.ok(client.includes('KRATE_CONTROLLER_URL'));
|
|
221
|
+
assert.ok(client.includes('KRATE_CONTROLLER_REQUEST_TIMEOUT_MS'));
|
|
222
|
+
assert.ok(client.includes('AbortSignal.timeout'));
|
|
223
|
+
assert.ok(client.includes('if (!useCache) return revalidateFn();'));
|
|
224
|
+
assert.ok(client.includes('staleWhileRevalidate(organization, revalidateFn, swrOptions)'));
|
|
225
|
+
assert.ok(client.includes('createKrateApiController'));
|
|
226
|
+
assert.ok(client.includes('createKubernetesResourceGateway'));
|
|
227
|
+
assert.ok(apiController.includes('resourceGateway'));
|
|
228
|
+
assert.ok(apiController.includes('withArchitecture'));
|
|
229
|
+
assert.ok(apiController.includes('kubernetes-resource-gateway'));
|
|
230
|
+
assert.ok(!apiController.includes('createKubernetesController'));
|
|
231
|
+
assert.ok(gateway.includes('createKubernetesResourceClient'));
|
|
232
|
+
assert.ok(kubernetes.includes('createKubernetesController(options = {})')); // resource-client alias
|
|
233
|
+
assert.ok(kubernetes.includes('/var/run/secrets/kubernetes.io/serviceaccount'), 'Kubernetes client can use in-cluster service-account credentials');
|
|
234
|
+
assert.ok(gateway.includes('repositoryManifest'));
|
|
235
|
+
assert.ok(shell.includes('/api/controller'));
|
|
236
|
+
assert.ok(webControllerRoute.includes('KRATE_CONTROLLER_URL'));
|
|
237
|
+
assert.ok(!webControllerRoute.includes('createKrateApiController'), 'web API route proxies the controller service instead of shelling out through local kubectl');
|
|
238
|
+
assert.ok(shell.includes('ArchitectureMap'));
|
|
239
|
+
assert.ok(shell.includes('Repository home'));
|
|
240
|
+
assert.ok(shell.includes('IssueWorkspace'));
|
|
241
|
+
assert.ok(shell.includes('IssueViewSwitcher'));
|
|
242
|
+
assert.ok(shell.includes('issuesForScope'));
|
|
243
|
+
assert.ok(shell.includes('issueRepositoryRefs'));
|
|
244
|
+
assert.ok(shell.includes('issueProjectRefs'));
|
|
245
|
+
assert.ok(shell.includes('DeploymentCenter'));
|
|
246
|
+
assert.ok(shell.includes('DeploymentManager'));
|
|
247
|
+
assert.ok(shell.includes('Krate deployment center'));
|
|
248
|
+
assert.ok(shell.includes('PlanCard'));
|
|
249
|
+
assert.ok(shell.includes('Advanced resource details'));
|
|
250
|
+
assert.ok(shell.includes('Releases'));
|
|
251
|
+
assert.ok(shell.includes('managed resources'));
|
|
252
|
+
assert.ok(shell.includes('Advanced resource records'));
|
|
253
|
+
assert.ok(shell.includes('Deployments'));
|
|
254
|
+
assert.ok(actions.includes('Create deployment'));
|
|
255
|
+
assert.ok(actions.includes('Prepare deployment'));
|
|
256
|
+
assert.ok(shell.includes('environments'));
|
|
257
|
+
assert.ok(shell.includes('Repository code browser'));
|
|
258
|
+
assert.ok(shell.includes('Manage access'));
|
|
259
|
+
assert.ok(shell.includes('PeopleAdmin'));
|
|
260
|
+
assert.ok(shell.includes('getSignedInUser'));
|
|
261
|
+
assert.ok(shell.includes('topbarAccount'));
|
|
262
|
+
assert.ok(shell.includes('Signed-in user'));
|
|
263
|
+
assert.ok(shell.includes('Sign out'));
|
|
264
|
+
assert.ok(shell.includes('Invite people'));
|
|
265
|
+
assert.ok(shell.includes('identity links'));
|
|
266
|
+
assert.ok(shell.includes('repository permissions'));
|
|
267
|
+
assert.ok(shell.includes('Access readiness'));
|
|
268
|
+
assert.match(actions, /Mark accepted/i);
|
|
269
|
+
assert.ok(actions.includes('Revoke invite'));
|
|
270
|
+
assert.ok(actions.includes('Disable user'));
|
|
271
|
+
assert.ok(actions.includes('Restore user'));
|
|
272
|
+
assert.ok(actions.includes('Revoke grant'));
|
|
273
|
+
assert.ok(actions.includes('SSH keys'));
|
|
274
|
+
assert.ok(actions.includes('Save SSH key'));
|
|
275
|
+
assert.ok(actions.includes('Revoke SSH key'));
|
|
276
|
+
assert.equal(existsSync('../web/app/orgs/[org]/people/page.jsx'), true);
|
|
277
|
+
assert.equal(existsSync('../web/app/login/page.jsx'), true);
|
|
278
|
+
assert.equal(existsSync('../web/app/logout/page.jsx'), true);
|
|
279
|
+
assert.equal(existsSync('../web/app/orgs/[org]/runs/page.jsx'), true);
|
|
280
|
+
assert.equal(existsSync('../web/app/runs/page.jsx'), false);
|
|
281
|
+
assert.equal(existsSync('../web/app/pipelines/page.jsx'), false);
|
|
282
|
+
assert.equal(existsSync('../web/app/orgs/[org]/repositories/[repo]/runs/page.jsx'), true);
|
|
283
|
+
assert.equal(existsSync('../web/app/orgs/[org]/repositories/[repo]/issues/[issue]/page.jsx'), true);
|
|
284
|
+
assert.equal(existsSync('../web/app/orgs/[org]/agents/projects/[projectId]/issues/page.jsx'), true);
|
|
285
|
+
assert.equal(existsSync('../web/app/orgs/[org]/agents/projects/[projectId]/issues/[issue]/page.jsx'), true);
|
|
286
|
+
assert.equal(existsSync('../web/app/repositories/[repo]/runs/page.jsx'), false);
|
|
287
|
+
assert.equal(existsSync('../web/app/repositories/[repo]/pipelines/page.jsx'), false);
|
|
288
|
+
assert.equal(existsSync('../web/proxy.js'), true);
|
|
289
|
+
assert.equal(existsSync('../web/app/api/auth/[provider]/route.js'), true);
|
|
290
|
+
assert.equal(existsSync('../web/app/api/auth/callback/[provider]/route.js'), true);
|
|
291
|
+
assert.equal(existsSync('../web/app/api/auth/delegated/route.js'), true);
|
|
292
|
+
assert.ok(shell.includes('RepositoryCodePage'));
|
|
293
|
+
assert.ok(shell.includes('RepositoryPullRequestsPage'));
|
|
294
|
+
assert.ok(shell.includes('RepositorySettingsPage'));
|
|
295
|
+
assert.ok(shell.includes('PullRequestReviewPanel'));
|
|
296
|
+
assert.ok(shell.includes('RunCenter'));
|
|
297
|
+
assert.ok(shell.includes('Workspace runs'));
|
|
298
|
+
assert.ok(shell.includes('Run event stream'));
|
|
299
|
+
assert.ok(shell.includes('/runs'));
|
|
300
|
+
assert.ok(!shell.includes("['/pipelines', 'Runs']"));
|
|
301
|
+
assert.ok(!shell.includes('PipelinesPage'), 'legacy pipelines UI component is not exported');
|
|
302
|
+
assert.ok(!shell.includes('RepositoryPipelinesPage'), 'legacy repository pipelines UI component is not exported');
|
|
303
|
+
assert.ok(!shell.includes('function PipelineDebugger'), 'runs page does not use the old debugger-first panel');
|
|
304
|
+
assert.ok(shell.includes('Automation inspector'));
|
|
305
|
+
assert.ok(shell.includes('Readiness checklist'));
|
|
306
|
+
assert.ok(kubernetes.includes('spawnSync'));
|
|
307
|
+
assert.ok(kubernetes.includes('kubectl'));
|
|
308
|
+
assert.ok(!client.includes('createKrateUiDemoRuntime'));
|
|
309
|
+
assert.ok(!page.includes('createKrateMvpDemo'));
|
|
310
|
+
assert.ok(!page.includes('createKrateLifecycleSnapshot'));
|
|
311
|
+
assert.ok(apiController.includes('const repoPath = `/orgs/${encodeURIComponent(org)}/repositories/${encodeURIComponent(name)}`'));
|
|
312
|
+
assert.ok(apiController.includes('runs: `${repoPath}/runs`'));
|
|
313
|
+
assert.ok(server.includes('/api/controller'));
|
|
314
|
+
assert.ok(server.includes('orgResourceCollectionMatch'));
|
|
315
|
+
assert.ok(!server.includes("url.pathname === '/api/repositories'"));
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test('API controller delegates through resource gateway instead of owning Kubernetes scope', async () => {
|
|
319
|
+
const calls = [];
|
|
320
|
+
const resourceGateway = {
|
|
321
|
+
namespace: 'krate-org-default',
|
|
322
|
+
resourceDefinitions: [],
|
|
323
|
+
async snapshot() { calls.push('snapshot'); return { source: 'kubernetes', namespace: 'krate-org-default', resources: {}, kubectl: { available: true, errors: [] }, commands: [], events: [], permissions: [], storage: {} }; },
|
|
324
|
+
async list(kind) { calls.push(`list:${kind}`); return { kind, items: [] }; },
|
|
325
|
+
async get(kind, name) { calls.push(`get:${kind}/${name}`); return { kind, metadata: { name } }; },
|
|
326
|
+
async apply(resource) { calls.push(`apply:${resource.kind}`); return { operation: 'apply', resource }; },
|
|
327
|
+
async delete(kind, name) { calls.push(`delete:${kind}/${name}`); return { operation: 'delete', kind, name }; },
|
|
328
|
+
async createRepository(input) { calls.push(`createRepository:${input.name}`); return { operation: 'apply', resource: { kind: 'Repository', metadata: { name: input.name } } }; },
|
|
329
|
+
async createOrganization(input) { calls.push(`createOrganization:${input.name}`); return { operation: 'create-organization', organization: { metadata: { name: input.name } } }; },
|
|
330
|
+
watch(resourcePath) { calls.push(`watch:${resourcePath}`); return { child: { kill() {} } }; }
|
|
331
|
+
};
|
|
332
|
+
const controller = createKrateApiController({ resourceGateway });
|
|
333
|
+
await controller.snapshot();
|
|
334
|
+
await controller.listResource('Repository');
|
|
335
|
+
await controller.getResource('Repository', 'app');
|
|
336
|
+
await controller.applyResource({ kind: 'Repository', metadata: { name: 'app' } });
|
|
337
|
+
await controller.deleteResource('Repository', 'app');
|
|
338
|
+
await controller.createRepository({ name: 'next-app' });
|
|
339
|
+
await controller.createOrganization({ name: 'product' });
|
|
340
|
+
controller.watchResource('orgs/default/repositories');
|
|
341
|
+
assert.deepEqual(calls, ['snapshot', 'list:Repository', 'get:Repository/app', 'apply:Repository', 'delete:Repository/app', 'createRepository:next-app', 'createOrganization:product', 'watch:orgs/default/repositories']);
|
|
342
|
+
assert.equal(controller.resourceGateway, resourceGateway);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test('GitHub workflow publishes deployable image and chart artifacts with safe gates', () => {
|
|
346
|
+
const workflow = read('.github/workflows/publish.yml');
|
|
347
|
+
const validate = workflowJobBlock(workflow, 'validate');
|
|
348
|
+
assert.ok(workflow.includes('branches: [develop, staging, main]'));
|
|
349
|
+
assert.ok(workflow.includes('concurrency:') && workflow.includes('group: publish-${{ github.ref_name }}'));
|
|
350
|
+
assert.ok(validate.includes('npm run check'));
|
|
351
|
+
assert.ok(validate.includes('npm pack --json'));
|
|
352
|
+
assert.ok(validate.includes('name: npm-package'));
|
|
353
|
+
assert.ok(validate.includes('name: dist-artifacts'));
|
|
354
|
+
assert.ok(validate.includes('name: ui-standalone'));
|
|
355
|
+
assert.ok(validate.includes('Generate release checksums'));
|
|
356
|
+
assert.ok(validate.includes('sha256sum'));
|
|
357
|
+
assert.ok(validate.includes('name: release-checksums'));
|
|
358
|
+
assert.ok(read('../charts/templates/gitea.yaml').includes('gitea-backend'));
|
|
359
|
+
const argocdTemplate = read('../charts/templates/argocd-application.yaml');
|
|
360
|
+
const values = read('../charts/values.yaml');
|
|
361
|
+
assert.ok(argocdTemplate.includes('kind: Application'));
|
|
362
|
+
assert.ok(argocdTemplate.includes('.Values.argocd.syncPolicy.prune'));
|
|
363
|
+
assert.ok(argocdTemplate.includes('.Values.argocd.syncPolicy.selfHeal'));
|
|
364
|
+
assert.ok(!values.includes('`n'));
|
|
365
|
+
assert.match(values, /^ syncPolicy:\r?\n\s+automated: true\r?\n\s+prune: true\r?\n\s+selfHeal: true/m);
|
|
366
|
+
|
|
367
|
+
const image = workflowJobBlock(workflow, 'publish-image');
|
|
368
|
+
assert.ok(image.includes('needs: validate'));
|
|
369
|
+
assert.ok(image.includes("push: ${{ github.event_name != 'pull_request' }}"));
|
|
370
|
+
assert.ok(image.includes('docker/build-push-action'));
|
|
371
|
+
assert.ok(image.includes('ghcr.io/${{ github.repository }}/krate-controller'));
|
|
372
|
+
|
|
373
|
+
const chart = workflowJobBlock(workflow, 'publish-chart');
|
|
374
|
+
assert.ok(chart.includes('needs: validate'));
|
|
375
|
+
assert.ok(chart.includes('helm package charts/krate'));
|
|
376
|
+
assert.ok(chart.includes('SHA256SUMS'));
|
|
377
|
+
assert.ok(chart.includes('sha256sum dist/charts/*.tgz'));
|
|
378
|
+
assert.ok(chart.includes("if: startsWith(github.ref, 'refs/tags/v')"));
|
|
379
|
+
assert.ok(chart.includes('helm push dist/charts/*.tgz'));
|
|
380
|
+
|
|
381
|
+
const deploy = workflowJobBlock(workflow, 'deploy-krate');
|
|
382
|
+
assert.ok(deploy.includes('Deploy Krate To AKS'));
|
|
383
|
+
assert.ok(deploy.includes("github.ref == 'refs/heads/develop'"));
|
|
384
|
+
assert.ok(deploy.includes("github.ref == 'refs/heads/staging'"));
|
|
385
|
+
assert.ok(deploy.includes("github.ref == 'refs/heads/main'"));
|
|
386
|
+
assert.ok(deploy.includes('environment:') && deploy.includes('krate-production') && deploy.includes('https://krate.a5c.ai'));
|
|
387
|
+
assert.ok(deploy.includes('AZURE_ACR_NAME') && deploy.includes('KUBE_CONFIG'));
|
|
388
|
+
assert.ok(deploy.includes('KRATE_GITHUB_CLIENT_ID') && deploy.includes('KRATE_GITHUB_CLIENT_SECRET'));
|
|
389
|
+
assert.ok(deploy.includes('krate-develop.a5c.ai') && deploy.includes('krate-staging.a5c.ai') && deploy.includes('krate.a5c.ai'));
|
|
390
|
+
assert.ok(deploy.includes('docker build -f Dockerfile') && deploy.includes('docker push'));
|
|
391
|
+
assert.ok(deploy.includes('create secret docker-registry acr-pull'));
|
|
392
|
+
assert.ok(deploy.includes('helm upgrade --install'));
|
|
393
|
+
assert.ok(deploy.includes('--values /tmp/krate-deploy-values.yaml'));
|
|
394
|
+
assert.ok(deploy.includes('--wait'));
|
|
395
|
+
assert.ok(deploy.includes('rollout status deployment/"${HELM_RELEASE}-krate-web"'));
|
|
396
|
+
assert.doesNotMatch(workflow, /publish-npm:/);
|
|
397
|
+
assert.doesNotMatch(workflow, /npm publish/);
|
|
398
|
+
assert.doesNotMatch(workflow, /PUBLISH_NPM/);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
|
|
5
|
+
import { execFileSync } from 'node:child_process';
|
|
6
|
+
|
|
7
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
8
|
+
|
|
9
|
+
import { buildMinikubePlan } from '../../scripts/setup-minikube.mjs';
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
const requiredChartFiles = [
|
|
14
|
+
|
|
15
|
+
'../charts/Chart.yaml',
|
|
16
|
+
|
|
17
|
+
'../charts/values.yaml',
|
|
18
|
+
|
|
19
|
+
'../charts/crds/repositories.yaml',
|
|
20
|
+
|
|
21
|
+
'../charts/crds/aggregated-resources.yaml',
|
|
22
|
+
|
|
23
|
+
'../charts/templates/apiservice.yaml',
|
|
24
|
+
|
|
25
|
+
'../charts/templates/deployments.yaml',
|
|
26
|
+
|
|
27
|
+
'../charts/templates/rbac.yaml',
|
|
28
|
+
|
|
29
|
+
'../charts/templates/services.yaml',
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
'../charts/templates/networkpolicy.yaml',
|
|
33
|
+
'../charts/templates/gitea.yaml',
|
|
34
|
+
'../charts/templates/argocd-application.yaml',
|
|
35
|
+
|
|
36
|
+
'examples/minikube-demo.yaml'
|
|
37
|
+
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
test('chart package contains the MVP Kubernetes install surface', () => {
|
|
43
|
+
|
|
44
|
+
for (const file of requiredChartFiles) assert.equal(existsSync(file), true, `${file} exists`);
|
|
45
|
+
|
|
46
|
+
const chart = requiredChartFiles.map((file) => readFileSync(file, 'utf8')).join('\n');
|
|
47
|
+
|
|
48
|
+
for (const kind of ['CustomResourceDefinition', 'Deployment', 'Service', 'ServiceAccount', 'ClusterRole', 'NetworkPolicy', 'PersistentVolumeClaim']) assert.ok(chart.includes(`kind: ${kind}`), `chart includes ${kind}`);
|
|
49
|
+
|
|
50
|
+
for (const resource of ['Organization', 'Repository', 'SSHKey', 'RepositoryPermission', 'BranchProtection', 'RefPolicy', 'PullRequest', 'Issue', 'Review', 'Pipeline', 'Job', 'RunnerPool', 'WebhookSubscription', 'WebhookDelivery', 'View', 'Selector']) assert.ok(chart.includes(`kind: ${resource}`), `package covers ${resource}`);
|
|
51
|
+
assert.ok(chart.includes('kind: Pipeline'), 'package includes demo Pipeline workflow');
|
|
52
|
+
assert.ok(chart.includes('kind: Application'), 'package includes Argo CD Application');
|
|
53
|
+
assert.ok(chart.includes('krate-kubevela'), 'package installs KubeVela through Argo CD');
|
|
54
|
+
assert.ok(chart.includes('core.oam.dev'), 'Krate service account can manage delivery resources');
|
|
55
|
+
assert.ok(chart.includes('create') && chart.includes('delete'), 'Krate service account can compose and remove delivery resources');
|
|
56
|
+
assert.ok(chart.includes('vela-core'), 'package references upstream KubeVela vela-core chart');
|
|
57
|
+
assert.ok(chart.includes('apiService') && chart.includes('enabled: false'), 'APIService registration is optional for CRD-backed local installs');
|
|
58
|
+
assert.ok(chart.includes('gitea/gitea'), 'package includes Gitea backend image');
|
|
59
|
+
assert.ok(chart.includes('readinessProbe'), 'workloads expose readiness probes for live installs');
|
|
60
|
+
assert.ok(chart.includes('serviceAccountName'), 'workloads run with the Krate service account');
|
|
61
|
+
assert.ok(chart.includes('customresourcedefinitions'), 'service account can discover installed Krate CRDs');
|
|
62
|
+
assert.ok(chart.includes('krate.webImage'), 'web deployment uses the web container image');
|
|
63
|
+
assert.ok(chart.includes('sshkeys.krate.a5c.ai'), 'package includes SSH key reconciliation resources');
|
|
64
|
+
assert.ok(chart.includes('repositorypermissions.krate.a5c.ai'), 'package includes Gitea permission reconciliation resources');
|
|
65
|
+
assert.ok(chart.includes('revoked'), 'package access CRDs allow revocation state');
|
|
66
|
+
assert.ok(chart.includes('KRATE_CONTROLLER_URL'), 'web deployment points at the in-cluster controller API');
|
|
67
|
+
assert.ok(chart.includes('containerPort: 2222'), 'rootless Gitea SSH service targets port 2222');
|
|
68
|
+
assert.ok(chart.includes('krate.fullname') && chart.includes('gitea-http'), 'Gitea and Argo CD URLs derive the release-scoped service name');
|
|
69
|
+
assert.ok(chart.includes('krate.a5c.ai/gitops-engine: argocd'), 'package labels Argo CD GitOps engine');
|
|
70
|
+
assert.ok(!chart.includes('`n'), 'package values use real YAML newlines');
|
|
71
|
+
assert.ok(chart.includes('.Values.argocd.syncPolicy.prune'), 'Argo CD template consumes syncPolicy.prune');
|
|
72
|
+
assert.ok(chart.includes('.Values.argocd.syncPolicy.selfHeal'), 'Argo CD template consumes syncPolicy.selfHeal');
|
|
73
|
+
assert.ok(chart.includes('- CreateNamespace=true'), 'values expose Argo CD CreateNamespace sync option');
|
|
74
|
+
for (const valueTerm of ['externalDependencies', 'objectStorage', 'oidc', 'gatekeeper', 'autoscaling', 'targetCPUUtilizationPercentage', 'gitea', 'argocd', 'repoURL', 'syncPolicy']) assert.ok(chart.includes(valueTerm), `values expose ${valueTerm}`);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
test('minikube setup dry-run exposes deterministic local install commands', () => {
|
|
80
|
+
|
|
81
|
+
const plan = buildMinikubePlan({ apply: false, json: true, profile: 'krate-test', namespace: 'krate-system', release: 'krate', driver: 'docker', chart: '../charts' });
|
|
82
|
+
|
|
83
|
+
const commands = plan.commands.map(([cmd, args]) => [cmd, ...args].join(' '));
|
|
84
|
+
|
|
85
|
+
assert.equal(plan.mode, 'dry-run');
|
|
86
|
+
|
|
87
|
+
assert.ok(commands.some((command) => command.startsWith('minikube start -p krate-test')));
|
|
88
|
+
|
|
89
|
+
assert.ok(commands.includes('helm lint ../charts'));
|
|
90
|
+
|
|
91
|
+
assert.ok(commands.some((command) => command.includes('helm upgrade --install krate ../charts')));
|
|
92
|
+
|
|
93
|
+
assert.ok(commands.includes('kubectl apply -n krate-system -f examples/minikube-demo.yaml'));
|
|
94
|
+
|
|
95
|
+
assert.ok(commands.includes('node scripts/smoke.mjs'));
|
|
96
|
+
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
test('setup:minikube --dry-run --json is machine-readable', () => {
|
|
102
|
+
|
|
103
|
+
const output = execFileSync(process.execPath, ['scripts/setup-minikube.mjs', '--dry-run', '--json', '--profile=krate-test'], { encoding: 'utf8' });
|
|
104
|
+
|
|
105
|
+
const parsed = JSON.parse(output);
|
|
106
|
+
|
|
107
|
+
assert.equal(parsed.mode, 'dry-run');
|
|
108
|
+
|
|
109
|
+
assert.ok(parsed.requiredTools.includes('minikube'));
|
|
110
|
+
|
|
111
|
+
assert.ok(parsed.commands.some((command) => command.includes('helm lint ../charts')));
|
|
112
|
+
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
|