From a779f9a22c687fbcb2004824e025fc7355e04986 Mon Sep 17 00:00:00 2001 From: eding Date: Tue, 15 Sep 2026 19:34:30 +0200 Subject: [PATCH] feat: add secret E2E runtime chart --- .gitignore | 2 + Dockerfile | 17 +++++++ charts/maidn-e2e-secret/Chart.yaml | 4 ++ .../templates/deployment.yaml | 44 +++++++++++++++++++ .../forgejo-registry-credentials.yaml | 20 +++++++++ .../maidn-e2e-secret/templates/httproute.yaml | 17 +++++++ .../maidn-e2e-secret/templates/service.yaml | 12 +++++ charts/maidn-e2e-secret/values.yaml | 16 +++++++ package.json | 7 ++- src/health.ts | 22 ++++++++++ src/index.ts | 9 ++-- test/health.test.js | 26 +++++++++++ 12 files changed, 190 insertions(+), 6 deletions(-) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 charts/maidn-e2e-secret/Chart.yaml create mode 100644 charts/maidn-e2e-secret/templates/deployment.yaml create mode 100644 charts/maidn-e2e-secret/templates/forgejo-registry-credentials.yaml create mode 100644 charts/maidn-e2e-secret/templates/httproute.yaml create mode 100644 charts/maidn-e2e-secret/templates/service.yaml create mode 100644 charts/maidn-e2e-secret/values.yaml create mode 100644 src/health.ts create mode 100644 test/health.test.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1eae0cf --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +dist/ +node_modules/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..850ff5f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM node:22-alpine AS build +WORKDIR /app +COPY package.json . +RUN npm install --package-lock=false +COPY tsconfig.json . +COPY src ./src +RUN npm run build + +FROM node:22-alpine +WORKDIR /app +ENV NODE_ENV=production PORT=8080 +COPY package.json . +RUN npm install --omit=dev --package-lock=false && npm cache clean --force +COPY --from=build /app/dist ./dist +USER node +EXPOSE 8080 +CMD ["npm", "start"] diff --git a/charts/maidn-e2e-secret/Chart.yaml b/charts/maidn-e2e-secret/Chart.yaml new file mode 100644 index 0000000..a35a7d2 --- /dev/null +++ b/charts/maidn-e2e-secret/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v2 +name: maidn-e2e-secret +description: Maidn E2E secret runtime fixture +version: 0.1.0 diff --git a/charts/maidn-e2e-secret/templates/deployment.yaml b/charts/maidn-e2e-secret/templates/deployment.yaml new file mode 100644 index 0000000..9ac26f2 --- /dev/null +++ b/charts/maidn-e2e-secret/templates/deployment.yaml @@ -0,0 +1,44 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Chart.Name }} + labels: + app: {{ .Chart.Name }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app: {{ .Chart.Name }} + template: + metadata: + labels: + app: {{ .Chart.Name }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- range . }} + - name: {{ . | quote }} + {{- end }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 8080 + env: + - name: E2E_SECRET + valueFrom: + secretKeyRef: + name: maidn-e2e-secret-runtime-staging + key: {{ .Values.runtimeSecret.key | quote }} + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: maidn-e2e-secret-app + key: uri + readinessProbe: + httpGet: + path: / + port: http diff --git a/charts/maidn-e2e-secret/templates/forgejo-registry-credentials.yaml b/charts/maidn-e2e-secret/templates/forgejo-registry-credentials.yaml new file mode 100644 index 0000000..6b1bf2b --- /dev/null +++ b/charts/maidn-e2e-secret/templates/forgejo-registry-credentials.yaml @@ -0,0 +1,20 @@ +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: forgejo-registry-credentials +spec: + secretStoreRef: + kind: ClusterSecretStore + name: openbao + target: + name: forgejo-registry-credentials + creationPolicy: Owner + template: + type: kubernetes.io/dockerconfigjson + data: + .dockerconfigjson: "{{ `{{ .dockerconfigjson }}` }}" + data: + - secretKey: dockerconfigjson + remoteRef: + key: cicd/forgejo-registry + property: dockerconfigjson diff --git a/charts/maidn-e2e-secret/templates/httproute.yaml b/charts/maidn-e2e-secret/templates/httproute.yaml new file mode 100644 index 0000000..bc586e8 --- /dev/null +++ b/charts/maidn-e2e-secret/templates/httproute.yaml @@ -0,0 +1,17 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ .Chart.Name }} +spec: + parentRefs: + - name: {{ .Values.gateway.name }} + namespace: {{ .Values.gateway.namespace }} + sectionName: http + {{- with .Values.gateway.hostname }} + hostnames: + - {{ . | quote }} + {{- end }} + rules: + - backendRefs: + - name: {{ .Chart.Name }} + port: 8080 diff --git a/charts/maidn-e2e-secret/templates/service.yaml b/charts/maidn-e2e-secret/templates/service.yaml new file mode 100644 index 0000000..ccfd114 --- /dev/null +++ b/charts/maidn-e2e-secret/templates/service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Chart.Name }} +spec: + type: ClusterIP + selector: + app: {{ .Chart.Name }} + ports: + - name: http + port: 8080 + targetPort: http diff --git a/charts/maidn-e2e-secret/values.yaml b/charts/maidn-e2e-secret/values.yaml new file mode 100644 index 0000000..83276be --- /dev/null +++ b/charts/maidn-e2e-secret/values.yaml @@ -0,0 +1,16 @@ +replicaCount: 1 + +image: + repository: "" + tag: dev + pullPolicy: IfNotPresent +imagePullSecrets: + - forgejo-registry-credentials + +runtimeSecret: + key: e2e-secret + +gateway: + name: public + namespace: gateway-system + hostname: "" diff --git a/package.json b/package.json index 9db7c86..3045552 100644 --- a/package.json +++ b/package.json @@ -3,14 +3,17 @@ "private": true, "scripts": { "build": "tsc", - "start": "node dist/index.js" + "start": "node dist/index.js", + "test": "npm run build && node --test test/health.test.js" }, "dependencies": { - "express": "^5.1.0" + "express": "^5.1.0", + "pg": "^8.16.3" }, "devDependencies": { "@types/express": "^5.0.1", "@types/node": "^22.15.0", + "@types/pg": "^8.15.5", "typescript": "~5.8.0" } } diff --git a/src/health.ts b/src/health.ts new file mode 100644 index 0000000..67fae02 --- /dev/null +++ b/src/health.ts @@ -0,0 +1,22 @@ +import { Client } from 'pg'; + +export async function isReady(selectOne = databaseSelectOne) { + if (!process.env.E2E_SECRET || !process.env.DATABASE_URL) return false; + + try { + await selectOne(); + return true; + } catch { + return false; + } +} + +async function databaseSelectOne() { + const client = new Client({ connectionString: process.env.DATABASE_URL }); + await client.connect(); + try { + await client.query('SELECT 1'); + } finally { + await client.end(); + } +} diff --git a/src/index.ts b/src/index.ts index bff5619..7245740 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,11 @@ import express from 'express'; +import { isReady } from './health.js'; -const secretConfigured = Boolean(process.env.E2E_SECRET); const app = express(); -app.get('/', (_request, response) => { - response.status(secretConfigured ? 200 : 503).send(secretConfigured ? 'maidn-e2e-secret-ready' : 'maidn-e2e-secret-missing'); +app.get('/', async (_request, response) => { + const ready = await isReady(); + response.status(ready ? 200 : 503).send(ready ? 'maidn-e2e-secret-ready' : 'maidn-e2e-secret-not-ready'); }); -app.listen(process.env.PORT ?? 3000); +app.listen(process.env.PORT ?? 8080); diff --git a/test/health.test.js b/test/health.test.js new file mode 100644 index 0000000..6477a51 --- /dev/null +++ b/test/health.test.js @@ -0,0 +1,26 @@ +const assert = require('node:assert/strict'); +const test = require('node:test'); +const { isReady } = require('../dist/health.js'); + +test('readiness requires the secret and a successful database query', async () => { + const secret = process.env.E2E_SECRET; + const databaseUrl = process.env.DATABASE_URL; + + try { + delete process.env.E2E_SECRET; + assert.equal(await isReady(async () => assert.fail('query should not run')), false); + + process.env.E2E_SECRET = 'configured'; + delete process.env.DATABASE_URL; + assert.equal(await isReady(async () => assert.fail('query should not run')), false); + + process.env.DATABASE_URL = 'postgres://example'; + assert.equal(await isReady(async () => {}), true); + assert.equal(await isReady(async () => { throw new Error('unavailable'); }), false); + } finally { + if (secret === undefined) delete process.env.E2E_SECRET; + else process.env.E2E_SECRET = secret; + if (databaseUrl === undefined) delete process.env.DATABASE_URL; + else process.env.DATABASE_URL = databaseUrl; + } +});