@emilia-protocol/gate 0.9.5 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/LICENSE +190 -0
  3. package/README.md +75 -16
  4. package/action-control-manifest.js +26 -0
  5. package/adapters/_kit.js +47 -5
  6. package/adapters/aws.js +15 -4
  7. package/adapters/github-demo.mjs +25 -22
  8. package/adapters/github.js +1 -1
  9. package/adapters/jira.js +1 -0
  10. package/adapters/linear.js +1 -0
  11. package/adapters/salesforce.js +1 -0
  12. package/adapters/stripe.js +1 -1
  13. package/adapters/supabase.js +26 -4
  14. package/adapters/vercel.js +33 -4
  15. package/aec-execution.js +1 -3
  16. package/breakglass.js +285 -51
  17. package/control-plane.js +341 -0
  18. package/coverage.js +722 -0
  19. package/custody-demo.mjs +13 -3
  20. package/demo.mjs +9 -3
  21. package/deploy/helm/README.md +16 -8
  22. package/deploy/helm/emilia-gate/Chart.yaml +3 -1
  23. package/deploy/helm/emilia-gate/templates/NOTES.txt +3 -2
  24. package/deploy/helm/emilia-gate/templates/deployment.yaml +55 -1
  25. package/deploy/helm/emilia-gate/values.yaml +18 -2
  26. package/deploy/helm/emilia-gate-service/Chart.yaml +11 -0
  27. package/deploy/helm/emilia-gate-service/README.md +81 -0
  28. package/deploy/helm/emilia-gate-service/templates/NOTES.txt +12 -0
  29. package/deploy/helm/emilia-gate-service/templates/_helpers.tpl +83 -0
  30. package/deploy/helm/emilia-gate-service/templates/deployment.yaml +153 -0
  31. package/deploy/helm/emilia-gate-service/templates/migration-job.yaml +73 -0
  32. package/deploy/helm/emilia-gate-service/templates/networkpolicy.yaml +113 -0
  33. package/deploy/helm/emilia-gate-service/templates/pdb.yaml +14 -0
  34. package/deploy/helm/emilia-gate-service/templates/service.yaml +20 -0
  35. package/deploy/helm/emilia-gate-service/templates/serviceaccount.yaml +8 -0
  36. package/deploy/helm/emilia-gate-service/tests/fixtures/001_gate.sql +26 -0
  37. package/deploy/helm/emilia-gate-service/tests/fixtures/002_runtime_access.sql +32 -0
  38. package/deploy/helm/emilia-gate-service/tests/fixtures/gate.config.mjs +29 -0
  39. package/deploy/helm/emilia-gate-service/tests/render-check.sh +145 -0
  40. package/deploy/helm/emilia-gate-service/values.schema.json +145 -0
  41. package/deploy/helm/emilia-gate-service/values.yaml +166 -0
  42. package/deploy/sql/001-runtime.sql +707 -0
  43. package/deploy/terraform/README.md +24 -14
  44. package/deploy/terraform/main.tf +59 -0
  45. package/deploy/terraform/service/README.md +81 -0
  46. package/deploy/terraform/service/main.tf +718 -0
  47. package/deploy/terraform/service/outputs.tf +19 -0
  48. package/deploy/terraform/service/tests/validate.sh +24 -0
  49. package/deploy/terraform/service/tests/validation.tftest.hcl +168 -0
  50. package/deploy/terraform/service/variables.tf +459 -0
  51. package/deploy/terraform/service/versions.tf +10 -0
  52. package/deploy/terraform/variables.tf +95 -2
  53. package/deployment-attestation.js +248 -0
  54. package/eg1-conformance.js +46 -12
  55. package/enterprise.js +6 -2
  56. package/ep-assure.mjs +3 -2
  57. package/evidence-postgres.js +357 -0
  58. package/evidence.js +10 -3
  59. package/execution-binding.js +141 -26
  60. package/index.js +556 -115
  61. package/key-registry.js +51 -19
  62. package/mcp.js +4 -2
  63. package/network-witness.js +500 -0
  64. package/package.json +25 -5
  65. package/reliance-kernel.js +5 -6
  66. package/reliance-packet.js +43 -10
  67. package/reports/assurance-package.js +45 -19
  68. package/reports/reperform.js +78 -18
  69. package/reports/underwriter.js +1 -1
  70. package/settlement.js +300 -0
  71. package/store-postgres.js +14 -0
  72. package/store.js +26 -5
  73. package/strict-json.js +86 -0
  74. package/witness-postgres.js +97 -0
@@ -0,0 +1,73 @@
1
+ {{- if .Values.migrations.enabled }}
2
+ apiVersion: batch/v1
3
+ kind: Job
4
+ metadata:
5
+ name: {{ include "emilia-gate-service.fullname" . }}-migrate
6
+ namespace: {{ .Release.Namespace }}
7
+ labels:
8
+ {{- include "emilia-gate-service.labels" . | nindent 4 }}
9
+ app.kubernetes.io/component: migration
10
+ annotations:
11
+ helm.sh/hook: pre-install,pre-upgrade
12
+ helm.sh/hook-weight: "-5"
13
+ helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
14
+ spec:
15
+ activeDeadlineSeconds: {{ .Values.migrations.activeDeadlineSeconds }}
16
+ backoffLimit: {{ .Values.migrations.backoffLimit }}
17
+ ttlSecondsAfterFinished: {{ .Values.migrations.ttlSecondsAfterFinished }}
18
+ template:
19
+ metadata:
20
+ labels:
21
+ {{- include "emilia-gate-service.baseSelectorLabels" . | nindent 8 }}
22
+ app.kubernetes.io/component: migration
23
+ spec:
24
+ automountServiceAccountToken: false
25
+ restartPolicy: Never
26
+ securityContext:
27
+ {{- toYaml .Values.podSecurityContext | nindent 8 }}
28
+ {{- with .Values.imagePullSecrets }}
29
+ imagePullSecrets:
30
+ {{- toYaml . | nindent 8 }}
31
+ {{- end }}
32
+ containers:
33
+ - name: migrate
34
+ image: {{ include "emilia-gate-service.image" . | quote }}
35
+ imagePullPolicy: {{ .Values.image.pullPolicy }}
36
+ command:
37
+ {{- toYaml .Values.migrations.command | nindent 12 }}
38
+ env:
39
+ - name: NODE_ENV
40
+ value: production
41
+ - name: EMILIA_GATE_CONFIG
42
+ value: {{ printf "%s/%s" .Values.configuration.mountPath .Values.configuration.configKey | quote }}
43
+ - name: {{ .Values.secrets.postgres.envName }}
44
+ valueFrom:
45
+ secretKeyRef:
46
+ name: {{ include "emilia-gate-service.migrationPostgresSecret" . }}
47
+ key: {{ include "emilia-gate-service.migrationPostgresKey" . }}
48
+ optional: false
49
+ securityContext:
50
+ {{- toYaml .Values.containerSecurityContext | nindent 12 }}
51
+ resources:
52
+ {{- toYaml .Values.migrations.resources | nindent 12 }}
53
+ volumeMounts:
54
+ - name: tmp
55
+ mountPath: /tmp
56
+ - name: configuration
57
+ mountPath: {{ .Values.configuration.mountPath }}
58
+ readOnly: true
59
+ volumes:
60
+ - name: tmp
61
+ emptyDir:
62
+ sizeLimit: 32Mi
63
+ - name: configuration
64
+ secret:
65
+ secretName: {{ include "emilia-gate-service.configurationSecret" . }}
66
+ optional: false
67
+ defaultMode: 0440
68
+ items:
69
+ - key: {{ .Values.configuration.configKey }}
70
+ path: {{ .Values.configuration.configKey }}
71
+ - key: {{ .Values.configuration.migrationKey }}
72
+ path: {{ .Values.configuration.migrationKey }}
73
+ {{- end }}
@@ -0,0 +1,113 @@
1
+ {{- if .Values.networkPolicy.enabled }}
2
+ {{- $ingressPeers := default (list) .Values.networkPolicy.ingress.additionalPeers }}
3
+ {{- $postgresSelector := default (dict) .Values.networkPolicy.egress.postgres.podSelector }}
4
+ {{- $postgresCidrs := default (list) .Values.networkPolicy.egress.postgres.cidrs }}
5
+ {{- $kmsCidrs := default (list) .Values.networkPolicy.egress.kmsCidrs }}
6
+ {{- $githubCidrs := default (list) .Values.networkPolicy.egress.githubCidrs }}
7
+ {{- $siemCidrs := default (list) .Values.networkPolicy.egress.siemCidrs }}
8
+ {{- $httpsCidrs := concat $kmsCidrs $githubCidrs $siemCidrs }}
9
+ {{- $additionalEgress := default (list) .Values.networkPolicy.egress.additionalRules }}
10
+ {{- $hasIngress := or .Values.networkPolicy.ingress.allowSameNamespace (gt (len $ingressPeers) 0) }}
11
+ {{- $hasBaseEgress := or .Values.networkPolicy.egress.dns.enabled (gt (len $postgresSelector) 0) (gt (len $postgresCidrs) 0) }}
12
+ {{- $hasServiceEgress := or (gt (len $httpsCidrs) 0) (gt (len $additionalEgress) 0) }}
13
+ apiVersion: networking.k8s.io/v1
14
+ kind: NetworkPolicy
15
+ metadata:
16
+ name: {{ include "emilia-gate-service.fullname" . }}-base
17
+ namespace: {{ .Release.Namespace }}
18
+ labels:
19
+ {{- include "emilia-gate-service.labels" . | nindent 4 }}
20
+ spec:
21
+ # Selects the long-lived service and the migration hook. The migration pod
22
+ # receives DNS/Postgres only, never service-only GitHub/KMS/SIEM egress.
23
+ podSelector:
24
+ matchLabels:
25
+ {{- include "emilia-gate-service.baseSelectorLabels" . | nindent 6 }}
26
+ policyTypes:
27
+ - Ingress
28
+ - Egress
29
+ ingress: []
30
+ egress:
31
+ {{- if $hasBaseEgress }}
32
+ {{- if .Values.networkPolicy.egress.dns.enabled }}
33
+ - to:
34
+ - namespaceSelector:
35
+ {{- toYaml .Values.networkPolicy.egress.dns.namespaceSelector | nindent 12 }}
36
+ podSelector:
37
+ {{- toYaml .Values.networkPolicy.egress.dns.podSelector | nindent 12 }}
38
+ ports:
39
+ - port: 53
40
+ protocol: UDP
41
+ - port: 53
42
+ protocol: TCP
43
+ {{- end }}
44
+ {{- with $postgresSelector }}
45
+ - to:
46
+ - podSelector:
47
+ {{- toYaml . | nindent 12 }}
48
+ {{- with $.Values.networkPolicy.egress.postgres.namespaceSelector }}
49
+ namespaceSelector:
50
+ {{- toYaml . | nindent 12 }}
51
+ {{- end }}
52
+ ports:
53
+ - port: {{ $.Values.networkPolicy.egress.postgres.port }}
54
+ protocol: TCP
55
+ {{- end }}
56
+ {{- range $postgresCidrs }}
57
+ - to:
58
+ - ipBlock:
59
+ cidr: {{ . | quote }}
60
+ ports:
61
+ - port: {{ $.Values.networkPolicy.egress.postgres.port }}
62
+ protocol: TCP
63
+ {{- end }}
64
+ {{- else }}
65
+ []
66
+ {{- end }}
67
+ ---
68
+ apiVersion: networking.k8s.io/v1
69
+ kind: NetworkPolicy
70
+ metadata:
71
+ name: {{ include "emilia-gate-service.fullname" . }}-service-access
72
+ namespace: {{ .Release.Namespace }}
73
+ labels:
74
+ {{- include "emilia-gate-service.labels" . | nindent 4 }}
75
+ spec:
76
+ podSelector:
77
+ matchLabels:
78
+ {{- include "emilia-gate-service.serviceSelectorLabels" . | nindent 6 }}
79
+ policyTypes:
80
+ - Ingress
81
+ - Egress
82
+ ingress:
83
+ {{- if $hasIngress }}
84
+ - from:
85
+ {{- if .Values.networkPolicy.ingress.allowSameNamespace }}
86
+ - podSelector: {}
87
+ {{- end }}
88
+ {{- with $ingressPeers }}
89
+ {{- toYaml . | nindent 8 }}
90
+ {{- end }}
91
+ ports:
92
+ - port: {{ .Values.runtime.port }}
93
+ protocol: TCP
94
+ {{- else }}
95
+ []
96
+ {{- end }}
97
+ egress:
98
+ {{- if $hasServiceEgress }}
99
+ {{- range $httpsCidrs }}
100
+ - to:
101
+ - ipBlock:
102
+ cidr: {{ . | quote }}
103
+ ports:
104
+ - port: 443
105
+ protocol: TCP
106
+ {{- end }}
107
+ {{- with $additionalEgress }}
108
+ {{- toYaml . | nindent 4 }}
109
+ {{- end }}
110
+ {{- else }}
111
+ []
112
+ {{- end }}
113
+ {{- end }}
@@ -0,0 +1,14 @@
1
+ {{- if .Values.podDisruptionBudget.enabled }}
2
+ apiVersion: policy/v1
3
+ kind: PodDisruptionBudget
4
+ metadata:
5
+ name: {{ include "emilia-gate-service.fullname" . }}
6
+ namespace: {{ .Release.Namespace }}
7
+ labels:
8
+ {{- include "emilia-gate-service.labels" . | nindent 4 }}
9
+ spec:
10
+ minAvailable: {{ .Values.podDisruptionBudget.minAvailable }}
11
+ selector:
12
+ matchLabels:
13
+ {{- include "emilia-gate-service.serviceSelectorLabels" . | nindent 6 }}
14
+ {{- end }}
@@ -0,0 +1,20 @@
1
+ apiVersion: v1
2
+ kind: Service
3
+ metadata:
4
+ name: {{ include "emilia-gate-service.fullname" . }}
5
+ namespace: {{ .Release.Namespace }}
6
+ labels:
7
+ {{- include "emilia-gate-service.labels" . | nindent 4 }}
8
+ {{- with .Values.service.annotations }}
9
+ annotations:
10
+ {{- toYaml . | nindent 4 }}
11
+ {{- end }}
12
+ spec:
13
+ type: {{ .Values.service.type }}
14
+ selector:
15
+ {{- include "emilia-gate-service.serviceSelectorLabels" . | nindent 4 }}
16
+ ports:
17
+ - name: http
18
+ port: {{ .Values.service.port }}
19
+ targetPort: http
20
+ protocol: TCP
@@ -0,0 +1,8 @@
1
+ apiVersion: v1
2
+ kind: ServiceAccount
3
+ metadata:
4
+ name: {{ include "emilia-gate-service.fullname" . }}
5
+ namespace: {{ .Release.Namespace }}
6
+ labels:
7
+ {{- include "emilia-gate-service.labels" . | nindent 4 }}
8
+ automountServiceAccountToken: false
@@ -0,0 +1,26 @@
1
+ BEGIN;
2
+
3
+ CREATE TABLE IF NOT EXISTS ep_gate_schema_migrations (
4
+ version TEXT PRIMARY KEY,
5
+ applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
6
+ );
7
+
8
+ CREATE TABLE IF NOT EXISTS ep_gate_consumption (
9
+ consumption_key TEXT PRIMARY KEY,
10
+ state TEXT NOT NULL,
11
+ consumed_at BIGINT NOT NULL,
12
+ expires_at BIGINT
13
+ );
14
+ CREATE INDEX IF NOT EXISTS ep_gate_consumption_expires_idx
15
+ ON ep_gate_consumption (expires_at) WHERE expires_at IS NOT NULL;
16
+
17
+ CREATE TABLE IF NOT EXISTS ep_gate_actions (
18
+ id TEXT PRIMARY KEY,
19
+ record JSONB NOT NULL,
20
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
21
+ );
22
+
23
+ INSERT INTO ep_gate_schema_migrations (version)
24
+ VALUES ('gate-e2e-v1') ON CONFLICT (version) DO NOTHING;
25
+
26
+ COMMIT;
@@ -0,0 +1,32 @@
1
+ -- Local E2E role split: migrations run as gate_owner; the service connects as
2
+ -- gate_runtime. The runtime can mutate replay/action state and can append
3
+ -- evidence only through the SECURITY DEFINER function installed by
4
+ -- packages/gate/deploy/sql/001-runtime.sql.
5
+
6
+ \set ON_ERROR_STOP on
7
+
8
+ SELECT format(
9
+ 'CREATE ROLE gate_runtime LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT NOREPLICATION PASSWORD %L',
10
+ :'runtime_password'
11
+ )
12
+ WHERE NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = 'gate_runtime')
13
+ \gexec
14
+
15
+ ALTER ROLE gate_runtime
16
+ LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT NOREPLICATION;
17
+ SELECT format('ALTER ROLE gate_runtime PASSWORD %L', :'runtime_password')
18
+ \gexec
19
+
20
+ GRANT CONNECT ON DATABASE gate_e2e TO gate_runtime;
21
+ GRANT USAGE ON SCHEMA public TO gate_runtime;
22
+ GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE ep_gate_consumption TO gate_runtime;
23
+ GRANT SELECT, INSERT, UPDATE ON TABLE ep_gate_actions TO gate_runtime;
24
+ REVOKE DELETE, TRUNCATE, REFERENCES, TRIGGER ON TABLE ep_gate_actions FROM gate_runtime;
25
+
26
+ GRANT emilia_gate_evidence_runtime TO gate_runtime;
27
+ SELECT emilia_gate_evidence.grant_runtime_scope(
28
+ 'gate_runtime',
29
+ 'gate-e2e-tenant',
30
+ 'gate-e2e-service',
31
+ 'gate-e2e'
32
+ );
@@ -0,0 +1,29 @@
1
+ import fs from 'node:fs';
2
+ import { createProductionGateConfig } from '/app/apps/gate-service/src/production-config.js';
3
+
4
+ function readSecret(variable) {
5
+ const file = process.env[variable];
6
+ if (!file) throw new Error(`${variable}_required`);
7
+ const value = fs.readFileSync(file, 'utf8').trim();
8
+ if (!value) throw new Error(`${variable}_empty`);
9
+ return value;
10
+ }
11
+
12
+ const password = readSecret('EMILIA_GATE_RUNTIME_POSTGRES_PASSWORD_FILE');
13
+ const githubToken = readSecret('GITHUB_TOKEN_FILE');
14
+ const apiToken = readSecret('EMILIA_GATE_API_TOKEN_FILE');
15
+ const trust = readSecret('EP_GATE_ISSUER_ROOTS_FILE');
16
+
17
+ export default await createProductionGateConfig({
18
+ environment: {
19
+ EMILIA_GATE_DATABASE_URL: `postgresql://gate_runtime:${encodeURIComponent(password)}@postgres:5432/gate_e2e`,
20
+ EMILIA_GATE_API_TOKEN: apiToken,
21
+ EMILIA_GATE_PRINCIPAL_ID: 'operator:e2e',
22
+ EMILIA_GATE_TENANT_ID: 'gate-e2e-tenant',
23
+ EMILIA_GATE_ID: 'gate-e2e-service',
24
+ EMILIA_GATE_EVIDENCE_STREAM_ID: 'gate-e2e',
25
+ EMILIA_GATE_TRUST_JSON: trust,
26
+ EMILIA_GATE_ALLOWED_REPOSITORIES: 'emilia-e2e/disposable-repository',
27
+ GITHUB_TOKEN: githubToken,
28
+ },
29
+ });
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ chart_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ helm_bin="${HELM_BIN:-helm}"
6
+
7
+ if ! command -v "$helm_bin" >/dev/null 2>&1; then
8
+ echo "helm is required (or set HELM_BIN)" >&2
9
+ exit 127
10
+ fi
11
+ if ! command -v node >/dev/null 2>&1; then
12
+ echo "node is required to syntax-check the E2E operator config fixture" >&2
13
+ exit 127
14
+ fi
15
+
16
+ digest="sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
17
+ values=(
18
+ --set-string image.repository=registry.example.test/security/emilia-gate-service
19
+ --set-string image.digest="$digest"
20
+ --set-string configuration.existingSecret=gate-configuration
21
+ --set-string secrets.postgres.existingSecret=gate-postgres
22
+ --set-string migrations.postgres.existingSecret=gate-postgres-migrate
23
+ --set-string migrations.postgres.key=database-url
24
+ --set-string secrets.apiToken.existingSecret=gate-api-token
25
+ --set-string secrets.kms.existingSecret=gate-kms
26
+ --set-string secrets.issuerRoots.existingSecret=gate-issuer-roots
27
+ --set-string networkPolicy.egress.githubCidrs[0]=192.0.2.10/32
28
+ --set-string networkPolicy.egress.siemCidrs[0]=198.51.100.20/32
29
+ )
30
+
31
+ "$helm_bin" lint --strict "$chart_dir" "${values[@]}"
32
+ rendered="$("$helm_bin" template render-check "$chart_dir" --namespace gate-system "${values[@]}")"
33
+
34
+ assert_rendered() {
35
+ local expected="$1"
36
+ if ! grep -Fq -- "$expected" <<<"$rendered"; then
37
+ echo "render assertion failed: missing '$expected'" >&2
38
+ exit 1
39
+ fi
40
+ }
41
+
42
+ assert_rendered "kind: Deployment"
43
+ assert_rendered "kind: Job"
44
+ assert_rendered "kind: PodDisruptionBudget"
45
+ assert_rendered "kind: NetworkPolicy"
46
+ assert_rendered "replicas: 2"
47
+ assert_rendered "runAsNonRoot: true"
48
+ assert_rendered "readOnlyRootFilesystem: true"
49
+ assert_rendered "allowPrivilegeEscalation: false"
50
+ assert_rendered "automountServiceAccountToken: false"
51
+ assert_rendered "registry.example.test/security/emilia-gate-service@$digest"
52
+ assert_rendered "name: gate-postgres"
53
+ assert_rendered "name: gate-postgres-migrate"
54
+ assert_rendered "name: gate-api-token"
55
+ assert_rendered "name: gate-kms"
56
+ assert_rendered "name: gate-issuer-roots"
57
+ assert_rendered "secretName: gate-configuration"
58
+ assert_rendered "value: \"/app/config/gate.config.mjs\""
59
+ assert_rendered "path: /v1/live"
60
+ assert_rendered "path: /v1/ready"
61
+ assert_rendered "cidr: \"192.0.2.10/32\""
62
+ assert_rendered "cidr: \"198.51.100.20/32\""
63
+
64
+ if grep -Eq '^kind: Secret$' <<<"$rendered"; then
65
+ echo "chart must reference existing Secrets, never render one" >&2
66
+ exit 1
67
+ fi
68
+
69
+ if [[ "$(grep -Fc 'kind: NetworkPolicy' <<<"$rendered")" -ne 2 ]]; then
70
+ echo "chart must render separate base and service-only NetworkPolicies" >&2
71
+ exit 1
72
+ fi
73
+
74
+ if "$helm_bin" template missing-required "$chart_dir" >/dev/null 2>&1; then
75
+ echo "chart unexpectedly rendered without BYOC image and Secret references" >&2
76
+ exit 1
77
+ fi
78
+
79
+ if "$helm_bin" template mutable-image "$chart_dir" --namespace gate-system \
80
+ --set-string image.repository=registry.example.test/security/emilia-gate-service \
81
+ --set-string image.tag=latest \
82
+ --set-string configuration.existingSecret=gate-configuration \
83
+ --set-string secrets.postgres.existingSecret=gate-postgres \
84
+ --set-string migrations.postgres.existingSecret=gate-postgres-migrate \
85
+ --set-string migrations.postgres.key=database-url \
86
+ --set-string secrets.apiToken.existingSecret=gate-api-token \
87
+ --set-string secrets.issuerRoots.existingSecret=gate-issuer-roots >/dev/null 2>&1; then
88
+ echo "chart unexpectedly accepted image.tag=latest" >&2
89
+ exit 1
90
+ fi
91
+
92
+ if "$helm_bin" template missing-migration-secret "$chart_dir" --namespace gate-system \
93
+ --set-string image.repository=registry.example.test/security/emilia-gate-service \
94
+ --set-string image.digest="$digest" \
95
+ --set-string configuration.existingSecret=gate-configuration \
96
+ --set-string secrets.postgres.existingSecret=gate-postgres \
97
+ --set-string secrets.apiToken.existingSecret=gate-api-token \
98
+ --set-string secrets.issuerRoots.existingSecret=gate-issuer-roots >/dev/null 2>&1; then
99
+ echo "chart unexpectedly reused the runtime Postgres Secret for migrations" >&2
100
+ exit 1
101
+ fi
102
+
103
+ if "$helm_bin" template equal-migration-secret "$chart_dir" --namespace gate-system \
104
+ --set-string image.repository=registry.example.test/security/emilia-gate-service \
105
+ --set-string image.digest="$digest" \
106
+ --set-string configuration.existingSecret=gate-configuration \
107
+ --set-string secrets.postgres.existingSecret=gate-postgres \
108
+ --set-string migrations.postgres.existingSecret=gate-postgres \
109
+ --set-string migrations.postgres.key=database-url \
110
+ --set-string secrets.apiToken.existingSecret=gate-api-token \
111
+ --set-string secrets.issuerRoots.existingSecret=gate-issuer-roots >/dev/null 2>&1; then
112
+ echo "chart unexpectedly accepted the runtime Postgres Secret for migrations" >&2
113
+ exit 1
114
+ fi
115
+
116
+ migrations_disabled="$($helm_bin template migrations-disabled "$chart_dir" --namespace gate-system \
117
+ --set-string image.repository=registry.example.test/security/emilia-gate-service \
118
+ --set-string image.digest="$digest" \
119
+ --set-string configuration.existingSecret=gate-configuration \
120
+ --set-string secrets.postgres.existingSecret=gate-postgres \
121
+ --set-string secrets.apiToken.existingSecret=gate-api-token \
122
+ --set-string secrets.issuerRoots.existingSecret=gate-issuer-roots \
123
+ --set migrations.enabled=false)"
124
+ if grep -Fq 'kind: Job' <<<"$migrations_disabled"; then
125
+ echo "chart rendered a migration Job while migrations were disabled" >&2
126
+ exit 1
127
+ fi
128
+
129
+ without_kms="$($helm_bin template no-kms "$chart_dir" --namespace gate-system \
130
+ --set-string image.repository=registry.example.test/security/emilia-gate-service \
131
+ --set-string image.digest="$digest" \
132
+ --set-string configuration.existingSecret=gate-configuration \
133
+ --set-string secrets.postgres.existingSecret=gate-postgres \
134
+ --set-string migrations.postgres.existingSecret=gate-postgres-migrate \
135
+ --set-string migrations.postgres.key=database-url \
136
+ --set-string secrets.apiToken.existingSecret=gate-api-token \
137
+ --set-string secrets.issuerRoots.existingSecret=gate-issuer-roots)"
138
+ if grep -Fq -- "EP_KMS_KEY_ID" <<<"$without_kms"; then
139
+ echo "chart rendered a KMS dependency even though no KMS extension was configured" >&2
140
+ exit 1
141
+ fi
142
+
143
+ node --check "$chart_dir/tests/fixtures/gate.config.mjs"
144
+
145
+ echo "Helm lint and render assertions passed"
@@ -0,0 +1,145 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft-07/schema#",
3
+ "title": "EMILIA Gate service Helm values",
4
+ "type": "object",
5
+ "required": ["image", "configuration", "secrets"],
6
+ "properties": {
7
+ "replicaCount": { "type": "integer", "minimum": 1 },
8
+ "image": {
9
+ "type": "object",
10
+ "required": ["repository", "tag", "digest", "pullPolicy"],
11
+ "properties": {
12
+ "repository": { "type": "string" },
13
+ "tag": {
14
+ "type": "string",
15
+ "pattern": "^(?:$|[A-Za-z0-9_][A-Za-z0-9._-]{0,127})$",
16
+ "not": { "pattern": "^[Ll][Aa][Tt][Ee][Ss][Tt]$" }
17
+ },
18
+ "digest": { "type": "string" },
19
+ "pullPolicy": { "type": "string", "enum": ["Always", "IfNotPresent", "Never"] }
20
+ },
21
+ "additionalProperties": false
22
+ },
23
+ "imagePullSecrets": { "type": "array" },
24
+ "nameOverride": { "type": "string" },
25
+ "fullnameOverride": { "type": "string" },
26
+ "service": {
27
+ "type": "object",
28
+ "required": ["type", "port"],
29
+ "properties": {
30
+ "type": { "type": "string", "enum": ["ClusterIP", "NodePort", "LoadBalancer"] },
31
+ "port": { "type": "integer", "minimum": 1, "maximum": 65535 },
32
+ "annotations": { "type": "object" }
33
+ },
34
+ "additionalProperties": false
35
+ },
36
+ "runtime": {
37
+ "type": "object",
38
+ "required": ["port", "logLevel", "terminationGracePeriodSeconds", "extraEnv"],
39
+ "properties": {
40
+ "port": { "type": "integer", "minimum": 1, "maximum": 65535 },
41
+ "logLevel": { "type": "string", "enum": ["debug", "info", "warn", "error"] },
42
+ "terminationGracePeriodSeconds": { "type": "integer", "minimum": 1 },
43
+ "extraEnv": { "type": "array" }
44
+ },
45
+ "additionalProperties": false
46
+ },
47
+ "configuration": {
48
+ "type": "object",
49
+ "required": ["existingSecret", "configKey", "migrationKey", "mountPath"],
50
+ "properties": {
51
+ "existingSecret": { "type": "string" },
52
+ "configKey": { "type": "string", "minLength": 1 },
53
+ "migrationKey": { "type": "string", "minLength": 1 },
54
+ "mountPath": { "type": "string", "pattern": "^/" }
55
+ },
56
+ "additionalProperties": false
57
+ },
58
+ "secrets": {
59
+ "type": "object",
60
+ "required": ["postgres", "apiToken", "kms", "issuerRoots"],
61
+ "properties": {
62
+ "postgres": { "$ref": "#/definitions/secretReference" },
63
+ "apiToken": { "$ref": "#/definitions/secretReference" },
64
+ "kms": { "$ref": "#/definitions/secretReference" },
65
+ "issuerRoots": { "$ref": "#/definitions/secretReference" }
66
+ },
67
+ "additionalProperties": false
68
+ },
69
+ "migrations": {
70
+ "type": "object",
71
+ "required": ["enabled", "postgres", "command", "activeDeadlineSeconds", "backoffLimit", "ttlSecondsAfterFinished", "resources"],
72
+ "properties": {
73
+ "enabled": { "type": "boolean" },
74
+ "postgres": {
75
+ "type": "object",
76
+ "required": ["existingSecret", "key"],
77
+ "properties": {
78
+ "existingSecret": { "type": "string" },
79
+ "key": { "type": "string" }
80
+ },
81
+ "additionalProperties": false
82
+ },
83
+ "command": { "type": "array", "minItems": 1, "items": { "type": "string" } },
84
+ "activeDeadlineSeconds": { "type": "integer", "minimum": 1 },
85
+ "backoffLimit": { "type": "integer", "minimum": 0 },
86
+ "ttlSecondsAfterFinished": { "type": "integer", "minimum": 0 },
87
+ "resources": { "type": "object" }
88
+ },
89
+ "additionalProperties": false
90
+ },
91
+ "probes": {
92
+ "type": "object",
93
+ "required": ["startup", "liveness", "readiness"],
94
+ "properties": {
95
+ "startup": { "$ref": "#/definitions/probe" },
96
+ "liveness": { "$ref": "#/definitions/probe" },
97
+ "readiness": { "$ref": "#/definitions/probe" }
98
+ },
99
+ "additionalProperties": false
100
+ },
101
+ "resources": { "type": "object" },
102
+ "podDisruptionBudget": {
103
+ "type": "object",
104
+ "required": ["enabled", "minAvailable"],
105
+ "properties": {
106
+ "enabled": { "type": "boolean" },
107
+ "minAvailable": { "oneOf": [{ "type": "integer", "minimum": 0 }, { "type": "string" }] }
108
+ },
109
+ "additionalProperties": false
110
+ },
111
+ "podSecurityContext": { "type": "object" },
112
+ "containerSecurityContext": { "type": "object" },
113
+ "networkPolicy": { "type": "object" },
114
+ "topologySpreadConstraints": { "type": "array" },
115
+ "podAnnotations": { "type": "object" },
116
+ "podLabels": { "type": "object" },
117
+ "nodeSelector": { "type": "object" },
118
+ "tolerations": { "type": "array" },
119
+ "affinity": { "type": "object" }
120
+ },
121
+ "additionalProperties": false,
122
+ "definitions": {
123
+ "secretReference": {
124
+ "type": "object",
125
+ "required": ["existingSecret", "key", "envName"],
126
+ "properties": {
127
+ "existingSecret": { "type": "string" },
128
+ "key": { "type": "string", "minLength": 1 },
129
+ "envName": { "type": "string", "pattern": "^[A-Z_][A-Z0-9_]*$" }
130
+ },
131
+ "additionalProperties": false
132
+ },
133
+ "probe": {
134
+ "type": "object",
135
+ "required": ["path", "periodSeconds", "timeoutSeconds", "failureThreshold"],
136
+ "properties": {
137
+ "path": { "type": "string", "pattern": "^/" },
138
+ "periodSeconds": { "type": "integer", "minimum": 1 },
139
+ "timeoutSeconds": { "type": "integer", "minimum": 1 },
140
+ "failureThreshold": { "type": "integer", "minimum": 1 }
141
+ },
142
+ "additionalProperties": false
143
+ }
144
+ }
145
+ }