diff --git a/.github/workflows/job-build.yml b/.github/workflows/job-build.yml index 4d2690d4c5..5f5d7c96e0 100644 --- a/.github/workflows/job-build.yml +++ b/.github/workflows/job-build.yml @@ -187,6 +187,9 @@ jobs: matrix: images: ${{ fromJSON(needs.matrix.outputs.build-matrix) }} steps: + - name: Checks-out repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Download artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -220,7 +223,8 @@ jobs: - name: Create manifest list and push working-directory: /tmp/digests/${{ matrix.images.name }} run: | - docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + "$GITHUB_WORKSPACE/ci/scripts/create-image-manifest.sh" \ + docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ $(printf '${{ inputs.REGISTRY }}/${{ inputs.NAMESPACE }}/${{ matrix.images.name }}@sha256:%s ' *) - name: Inspect image diff --git a/.github/workflows/job-lint.yml b/.github/workflows/job-lint.yml index bbbae077d6..8ce5aa96f7 100644 --- a/.github/workflows/job-lint.yml +++ b/.github/workflows/job-lint.yml @@ -5,16 +5,7 @@ permissions: on: workflow_call: - inputs: - NODE_VERSION: - required: false - type: string workflow_dispatch: - inputs: - NODE_VERSION: - description: Nodejs version used to run tests - required: true - type: string jobs: lint: @@ -24,16 +15,12 @@ jobs: - name: Checks-out repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Install pnpm - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 - id: pnpm-install + - name: Set up mise tools + uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 with: - run_install: false - - - name: Setup Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - with: - node-version: "${{ inputs.NODE_VERSION }}" + version: 2026.5.15 + install: true + cache: true - name: Get pnpm store directory id: pnpm-store @@ -53,8 +40,19 @@ jobs: run: pnpm install --frozen-lockfile - name: Check lint error + run: mise -E ci run lint + + - name: Check environment-file boundary + run: ./ci/scripts/check-environment-files.sh + + - name: Test environment launchers and configuration run: | - ./ci/scripts/run-tests.sh -l + ./scripts/with-nestjs-env.spec.sh + ./scripts/bootstrap-secrets.spec.sh + ./ci/scripts/run-unit-tests.spec.sh + ./ci/scripts/check-workflow-environment.spec.sh + ./ci/scripts/init-env.sh + ./ci/scripts/check-compose-environment.spec.sh - name: Validate nginx-strangler config run: | diff --git a/.github/workflows/job-npm.yml b/.github/workflows/job-npm.yml index 51e4f3eedc..ba5a7f9fe6 100644 --- a/.github/workflows/job-npm.yml +++ b/.github/workflows/job-npm.yml @@ -6,9 +6,6 @@ permissions: on: workflow_call: inputs: - NODE_VERSION: - required: true - type: string PUBLISH_APPS: required: true type: boolean @@ -23,10 +20,6 @@ on: type: boolean workflow_dispatch: inputs: - NODE_VERSION: - description: Nodejs version used - required: true - type: string PUBLISH_APPS: description: Publish apps modules required: true @@ -100,11 +93,12 @@ jobs: - name: Checks-out repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Install pnpm - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 - id: pnpm-install + - name: Set up mise tools + uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 with: - run_install: false + version: 2026.5.15 + install: true + cache: true - name: Check changes id: check @@ -177,11 +171,12 @@ jobs: with: ref: ${{ github.event.ref }} - - name: Install pnpm - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 - id: pnpm-install + - name: Set up mise tools + uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 with: - run_install: false + version: 2026.5.15 + install: true + cache: true - name: Check for new package version id: check-version @@ -199,13 +194,6 @@ jobs: fi echo "REMOTE=$REMOTE" >> $GITHUB_OUTPUT - - name: Setup Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - if: ${{ steps.check-version.outputs.REMOTE == 'false' }} - with: - node-version: "${{ inputs.NODE_VERSION }}" - registry-url: "https://registry.npmjs.org" - - name: Get pnpm store directory if: ${{ steps.check-version.outputs.REMOTE == 'false' }} id: pnpm-store diff --git a/.github/workflows/job-playwright.yml b/.github/workflows/job-playwright.yml index 8b3ac0a3df..4774d5ff01 100644 --- a/.github/workflows/job-playwright.yml +++ b/.github/workflows/job-playwright.yml @@ -6,18 +6,11 @@ permissions: on: workflow_call: inputs: - NODE_VERSION: - required: true - type: string TAG: required: true type: string workflow_dispatch: inputs: - NODE_VERSION: - description: Nodejs version used to run tests - required: true - type: string TAG: description: Container images version to use for console client and server/server-nestjs required: true @@ -37,6 +30,13 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Set up mise tools + uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 + with: + version: 2026.5.15 + install: true + cache: true + - name: Ensure Playwright browsers run: pnpm --dir playwright exec playwright install chromium firefox @@ -49,24 +49,25 @@ jobs: docker pull "${GHCR_CACHE}/cloud-pi-native/console/${img}:${{ inputs.TAG }}" docker tag "${GHCR_CACHE}/cloud-pi-native/console/${img}:${{ inputs.TAG }}" "dso-console/${img}:ci" done - docker compose -f ./docker/docker-compose.ci.yml up --no-build -d --remove-orphans + mise -E ci exec -- fnox exec -- docker compose -f ./docker/docker-compose.ci.yml up --no-build -d --remove-orphans # See https://playwright.dev/docs/test-sharding for how Playwright handles test sharding - name: Run Playwright tests - run: pnpm --dir playwright exec playwright test --grep @e2e --shard=${{ matrix.shard }}/4 --reporter=blob + run: mise -E ci run playwright:test -- --shard=${{ matrix.shard }}/4 --reporter=blob - name: Dump docker compose state on failure if: ${{ failure() }} run: | - docker compose -f ./docker/docker-compose.ci.yml ps -a + mise -E ci exec -- fnox exec -- docker compose -f ./docker/docker-compose.ci.yml ps -a for service in keycloak postgres server server-nestjs client nginx-strangler opencds-mockoon; do echo -e "\nInspecting ${service}..." - docker compose -f ./docker/docker-compose.ci.yml logs --no-color ${service} + mise -E ci exec -- fnox exec -- docker compose -f ./docker/docker-compose.ci.yml logs --no-color ${service} docker inspect dso-console_${service} --format '{{json .State}}' done - name: Clean up docker resources (containers, volumes) - run: docker compose -f ./docker/docker-compose.ci.yml down -v --remove-orphans + if: ${{ always() }} + run: mise -E ci exec -- fnox exec -- docker compose -f ./docker/docker-compose.ci.yml down -v --remove-orphans - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 if: ${{ !cancelled() }} diff --git a/.github/workflows/job-tests-unit.yml b/.github/workflows/job-tests-unit.yml index 993ff302cf..aed7911519 100644 --- a/.github/workflows/job-tests-unit.yml +++ b/.github/workflows/job-tests-unit.yml @@ -5,10 +5,6 @@ permissions: on: workflow_call: - inputs: - NODE_VERSION: - required: true - type: string secrets: SONAR_HOST_URL: required: false @@ -17,11 +13,6 @@ on: SONAR_PROJECT_KEY: required: false workflow_dispatch: - inputs: - NODE_VERSION: - description: Nodejs version used to run tests - required: true - type: string jobs: unit-tests: @@ -31,16 +22,12 @@ jobs: - name: Checks-out repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Install pnpm - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 - id: pnpm-install + - name: Set up mise tools + uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 with: - run_install: false - - - name: Setup Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - with: - node-version: "${{ inputs.NODE_VERSION }}" + version: 2026.5.15 + install: true + cache: true - name: Get pnpm store directory id: pnpm-store @@ -68,7 +55,7 @@ jobs: - name: Run unit tests run: | ./ci/scripts/init-env.sh - ./ci/scripts/run-tests.sh -u + mise -E ci run test:unit - name: Upload vitest coverage artifacts uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 @@ -115,16 +102,12 @@ jobs: name: unit-tests-coverage path: ./coverage - - name: Install pnpm - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 - id: pnpm-install - with: - run_install: false - - - name: Setup Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + - name: Set up mise tools + uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 with: - node-version: "${{ inputs.NODE_VERSION }}" + version: 2026.5.15 + install: true + cache: true - name: Get pnpm store directory id: pnpm-store diff --git a/.github/workflows/workflow-continuous-integration.yml b/.github/workflows/workflow-continuous-integration.yml index 09b5b24d26..9508609375 100644 --- a/.github/workflows/workflow-continuous-integration.yml +++ b/.github/workflows/workflow-continuous-integration.yml @@ -19,7 +19,6 @@ on: workflow_dispatch: env: - NODE_VERSION: 26.7.0 REGISTRY: ghcr.io NAMESPACE: "${{ github.repository }}" BUILD_AMD64: true @@ -56,7 +55,6 @@ jobs: expose-vars: runs-on: ubuntu-latest outputs: - NODE_VERSION: ${{ env.NODE_VERSION }} REGISTRY: ${{ env.REGISTRY }} NAMESPACE: ${{ env.NAMESPACE }} BUILD_AMD64: ${{ env.BUILD_AMD64 }} @@ -70,8 +68,6 @@ jobs: uses: ./.github/workflows/job-lint.yml needs: - expose-vars - with: - NODE_VERSION: ${{ needs.expose-vars.outputs.NODE_VERSION }} unit-tests: uses: ./.github/workflows/job-tests-unit.yml @@ -79,8 +75,6 @@ jobs: needs: - path-filter - expose-vars - with: - NODE_VERSION: ${{ needs.expose-vars.outputs.NODE_VERSION }} secrets: SONAR_HOST_URL: "${{ secrets.SONAR_HOST_URL }}" SONAR_TOKEN: "${{ secrets.SONAR_TOKEN }}" diff --git a/.github/workflows/workflow-handle-hotfix.yml b/.github/workflows/workflow-handle-hotfix.yml index bebe71ef42..493b60986e 100644 --- a/.github/workflows/workflow-handle-hotfix.yml +++ b/.github/workflows/workflow-handle-hotfix.yml @@ -19,7 +19,6 @@ env: BUILD_AMD64: true BUILD_ARM64: true USE_QEMU: false - NODE_VERSION: 26.7.0 jobs: expose-vars: @@ -30,7 +29,6 @@ jobs: BUILD_AMD64: ${{ env.BUILD_AMD64 }} BUILD_ARM64: ${{ env.BUILD_ARM64 }} USE_QEMU: ${{ env.USE_QEMU }} - NODE_VERSION: ${{ env.NODE_VERSION }} steps: - name: Exposing env vars run: echo "Exposing env vars" diff --git a/.github/workflows/workflow-handle-main.yml b/.github/workflows/workflow-handle-main.yml index 91721239d0..d7b7df2cfd 100644 --- a/.github/workflows/workflow-handle-main.yml +++ b/.github/workflows/workflow-handle-main.yml @@ -14,7 +14,6 @@ env: NAMESPACE: "${{ github.repository }}" BUILD_AMD64: true USE_QEMU: false - NODE_VERSION: 26.7.0 jobs: expose-vars: @@ -24,7 +23,6 @@ jobs: NAMESPACE: ${{ env.NAMESPACE }} BUILD_AMD64: ${{ env.BUILD_AMD64 }} USE_QEMU: ${{ env.USE_QEMU }} - NODE_VERSION: ${{ env.NODE_VERSION }} steps: - name: Exposing env vars run: echo "Exposing env vars" @@ -37,10 +35,10 @@ jobs: REGISTRY: ${{ needs.expose-vars.outputs.REGISTRY }} NAMESPACE: ${{ needs.expose-vars.outputs.NAMESPACE }} RELEASE_CREATED: false - TAG: '' - MAJOR_TAG: '' - MINOR_TAG: '' - PATCH_TAG: '' + TAG: "" + MAJOR_TAG: "" + MINOR_TAG: "" + PATCH_TAG: "" BUILD_AMD64: ${{ needs.expose-vars.outputs.BUILD_AMD64 == 'true' }} BUILD_ARM64: false USE_QEMU: ${{ needs.expose-vars.outputs.USE_QEMU == 'true' }} diff --git a/.github/workflows/workflow-handle-releases.yml b/.github/workflows/workflow-handle-releases.yml index 528648c9bd..45bcabb12e 100644 --- a/.github/workflows/workflow-handle-releases.yml +++ b/.github/workflows/workflow-handle-releases.yml @@ -17,7 +17,6 @@ env: BUILD_AMD64: true BUILD_ARM64: true USE_QEMU: false - NODE_VERSION: 26.7.0 jobs: expose-vars: @@ -28,7 +27,6 @@ jobs: BUILD_AMD64: ${{ env.BUILD_AMD64 }} BUILD_ARM64: ${{ env.BUILD_ARM64 }} USE_QEMU: ${{ env.USE_QEMU }} - NODE_VERSION: ${{ env.NODE_VERSION }} steps: - name: Exposing env vars run: echo "Exposing env vars" diff --git a/.github/workflows/workflow-merge-queue.yml b/.github/workflows/workflow-merge-queue.yml index cc7673b447..25e73a5d39 100644 --- a/.github/workflows/workflow-merge-queue.yml +++ b/.github/workflows/workflow-merge-queue.yml @@ -18,7 +18,6 @@ on: types: [checks_requested] env: - NODE_VERSION: 26.7.0 REGISTRY: ghcr.io NAMESPACE: "${{ github.repository }}" BUILD_AMD64: true @@ -60,7 +59,6 @@ jobs: expose-vars: runs-on: ubuntu-latest outputs: - NODE_VERSION: ${{ env.NODE_VERSION }} REGISTRY: ${{ env.REGISTRY }} NAMESPACE: ${{ env.NAMESPACE }} BUILD_AMD64: ${{ env.BUILD_AMD64 }} @@ -76,8 +74,6 @@ jobs: uses: ./.github/workflows/job-lint.yml needs: - expose-vars - with: - NODE_VERSION: ${{ needs.expose-vars.outputs.NODE_VERSION }} unit-tests: uses: ./.github/workflows/job-tests-unit.yml @@ -85,8 +81,6 @@ jobs: needs: - path-filter - expose-vars - with: - NODE_VERSION: ${{ needs.expose-vars.outputs.NODE_VERSION }} secrets: SONAR_HOST_URL: "${{ secrets.SONAR_HOST_URL }}" SONAR_TOKEN: "${{ secrets.SONAR_TOKEN }}" @@ -125,7 +119,6 @@ jobs: - expose-vars - build with: - NODE_VERSION: ${{ needs.expose-vars.outputs.NODE_VERSION }} TAG: pr-${{ github.event.inputs.PR_NUMBER || github.event.number || needs.expose-vars.outputs.MERGE_GROUP_HEAD_SHA }} scan-vuln: diff --git a/.github/workflows/workflow-post-merge.yml b/.github/workflows/workflow-post-merge.yml index 3e6190088c..3d54964d4a 100644 --- a/.github/workflows/workflow-post-merge.yml +++ b/.github/workflows/workflow-post-merge.yml @@ -5,9 +5,6 @@ on: branches: - main -env: - NODE_VERSION: 26.7.0 - permissions: contents: read id-token: write @@ -33,22 +30,11 @@ jobs: - 'packages/**' - 'plugins/**' - expose-vars: - runs-on: ubuntu-latest - if: ${{ !github.event.pull_request.draft }} - outputs: - NODE_VERSION: ${{ env.NODE_VERSION }} - steps: - - name: Exposing env vars - run: echo "Exposing env vars." - npm-publish: uses: ./.github/workflows/job-npm.yml needs: - path-filter - - expose-vars with: - NODE_VERSION: ${{ needs.expose-vars.outputs.NODE_VERSION }} PUBLISH_PACKAGES: true PUBLISH_PLUGINS: true CHECK_CHANGES: true diff --git a/.gitignore b/.gitignore index 090b202522..4c0dc0ff06 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,12 @@ node_modules .env.codespace .env.docker +# local mise/fnox overrides +mise.local.toml +mise.*.local.toml +fnox.local.toml +.fnox.local.toml + # init-db apps/server/src/init/db/imports/data diff --git a/AGENTS.md b/AGENTS.md index 513c0cd4b5..8b4384c4c0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,11 +53,11 @@ always filter by concrete id. `ProjectRole`/`Repository` foreign keys do not cas ## Environment config -- Files: `.env`, `.env.docker`, `.env.integ` in `apps/client/`, `apps/server/`, `apps/server-nestjs/` -- Templates: `*-example` suffix (not `.example`; git-tracked), active files gitignored -- Override chain (weakest to strongest): `.env` < `.env.docker` (if DOCKER=true) < `.env.integ` (if INTEGRATION=true) < explicit env vars -- Server loading: `apps/server/src/utils/env.ts` | Client: `apps/client/vite.config.ts` -- `ci/scripts/init-env.sh` copies `*-example` to active equivalents (non-destructive) +- `mise` owns tool versions, supported tasks and public configuration; `fnox` resolves secrets for child processes only. +- `apps/client/` and `apps/server-nestjs/` must not use `.env*` files or dotenv loaders. +- `apps/server/` is a frozen exception: its `.env*`, loaders and Compose `env_file` remain until its removal. +- `ci/scripts/init-env.sh` initializes that legacy exception only and must be deleted with it. +- The variable classification and profiles are in `docs/environment-variables.md`. ## Testing diff --git a/ENVIRONMENTS.md b/ENVIRONMENTS.md index 6c8cd9c397..85a2428125 100644 --- a/ENVIRONMENTS.md +++ b/ENVIRONMENTS.md @@ -53,11 +53,25 @@ Le mécanisme de surcharge des différentes configurations fonctionne de cette m var d'env settée explicitement -> fichier .env.docker (si contexte docker) -> fichier .env.integ (si INTEGRATION=true) -> fichier .env ``` -## Prégénération des fichiers .env, .env.docker, et .env.integ +## Transition du legacy Fastify -Un script permet de copier facilement les fichiers `.env*-example` en leur équivalent `.env*`: [`./ci/scripts/init-env.sh`](./ci/scripts/init-env.sh). +`ci/scripts/init-env.sh` ne sert qu'à `apps/server`, le backend Fastify gelé. Il conserve temporairement ses `.env*` jusqu'à la suppression de ce service. -> Il faut ensuite remplir ces fichiers, car ils ne sont là que simplement copiés avec les valeurs par défaut +Le client et `server-nestjs` migrent vers `mise` pour la configuration publique et `fnox` pour les secrets : leurs commandes ne génèrent pas de fichier `.env`. + +La décision et le registre de variables sont documentés dans [`docs/adr/0001-mise-fnox-environment-management.md`](docs/adr/0001-mise-fnox-environment-management.md) et [`docs/environment-variables.md`](docs/environment-variables.md). + +| Cas | Commande | +| --- | --- | +| Initialisation locale | `mise run setup` | +| Infrastructure locale | `mise run dev` | +| NestJS local | `mise run server-nestjs:dev` | +| Client local | `mise run client:dev` | +| Stack Docker locale | `mise run docker:dev` | +| Intégration hybride | `mise run integ` | +| CI | `mise -E ci run ` | + +`apps/server` reste la seule exception transitoire à ces commandes, jusqu'à sa suppression. ## Configuration pour le développement entièrement en local diff --git a/README.md b/README.md index 5eda2efb68..6a166c6e77 100644 --- a/README.md +++ b/README.md @@ -111,17 +111,14 @@ Le développement s'effectue à l'aide de Docker *(le client et le serveur peuve Une fois le projet cloné, lancez les commandes suivantes dans votre terminal pour installer le projet et construire l'ensemble de l'application `console` : ```shell -# Installer les dépendances du projet -pnpm install +# Installer les outils déclarés par le projet (Node, pnpm et fnox) +mise install -# Créer les fichiers d'environnement exemples -./ci/scripts/init-env.sh - -# Générer le client Prisma côté serveur -pnpm --filter @cpn-console/server run db:generate +# Initialiser les secrets locaux de démonstration et le bridge legacy apps/server +mise run setup # Construire tous les paquets applicatifs -pnpm build +mise exec -- pnpm build ``` #### Lancer l'application diff --git a/apps/client/.env-example b/apps/client/.env-example deleted file mode 100644 index 2f23f06f97..0000000000 --- a/apps/client/.env-example +++ /dev/null @@ -1,24 +0,0 @@ -# Mode d'exécution Node.js (development | production | test) -NODE_ENV=development -# Hôte du serveur backend vers lequel le client proxifie les appels API -SERVER_HOST=localhost -# Port d'écoute du serveur backend -# En développement local avec le nginx-strangler (pnpm dev -# + docker-compose.local.yml), il vaut mieux mettre SERVER_PORT=4000 pour que -# le proxy Vite passe par nginx-strangler plutôt que directement par server (4001) -# ou server-nestjs (3001). -SERVER_PORT=4000 -# Domaine (host:port) du serveur Keycloak pour l'authentification -KEYCLOAK_DOMAIN=localhost:8090 -# Nom du royaume Keycloak utilisé par la console -KEYCLOAK_REALM=dso -# Protocole de communication avec Keycloak (http | https) -KEYCLOAK_PROTOCOL=http -# Identifiant du client Keycloak côté frontend -KEYCLOAK_CLIENT_ID=dso-console-frontend -# URL de redirection après authentification Keycloak -KEYCLOAK_REDIRECT_URI=http://localhost:8080 -# Active/désactive le module OpenCDS (true | false) -OPENCDS_ENABLED=false -# Adresse e-mail de contact affichée dans l'interface -CONTACT_EMAIL=cloudpinative-relations@interieur.gouv.fr diff --git a/apps/client/.env.docker-example b/apps/client/.env.docker-example deleted file mode 100644 index 1f05a462c6..0000000000 --- a/apps/client/.env.docker-example +++ /dev/null @@ -1,21 +0,0 @@ -# Spécifique à ce ficher. Permet de dire que la stack est conteneurisée -DOCKER=true -# Mode d'exécution Node.js (development | production | test) -NODE_ENV=development -# Hôte du nginx vers lequel le client proxifie les appels API -SERVER_HOST=nginx-strangler -SERVER_PORT=8080 -# Domaine (host:port) du serveur Keycloak pour l'authentification -KEYCLOAK_DOMAIN=localhost:8090 -# Nom du royaume Keycloak utilisé par la console -KEYCLOAK_REALM=dso -# Protocole de communication avec Keycloak (http | https) -KEYCLOAK_PROTOCOL=http -# Identifiant du client Keycloak côté frontend -KEYCLOAK_CLIENT_ID=dso-console-frontend -# URL de redirection après authentification Keycloak -KEYCLOAK_REDIRECT_URI=http://localhost:8080 -# Active/désactive le module OpenCDS (true | false) -OPENCDS_ENABLED=false -# Adresse e-mail de contact affichée dans l'interface -CONTACT_EMAIL=cloudpinative-relations@interieur.gouv.fr diff --git a/apps/client/.env.integ-example b/apps/client/.env.integ-example deleted file mode 100644 index 4f870dfd2f..0000000000 --- a/apps/client/.env.integ-example +++ /dev/null @@ -1,11 +0,0 @@ -# Surcharges pour le branchement à un environnement d'intégration distant. -# Ces valeurs écrasent celles de .env et .env.docker quand INTEGRATION=true. - -# Domaine du Keycloak de l'environnement d'intégration (ex: keycloak.dso.example.fr) -KEYCLOAK_DOMAIN= -# Royaume Keycloak de l'environnement d'intégration -KEYCLOAK_REALM= -# En intégration, Keycloak est généralement exposé en HTTPS -KEYCLOAK_PROTOCOL=https -# Identifiant du client Keycloak frontend sur l'environnement d'intégration -KEYCLOAK_CLIENT_ID= diff --git a/apps/client/nginx/entrypoint.sh b/apps/client/nginx/entrypoint.sh index 98dfe3bcd9..e52d3198ae 100755 --- a/apps/client/nginx/entrypoint.sh +++ b/apps/client/nginx/entrypoint.sh @@ -11,9 +11,11 @@ make_envsubst_vars() { printf '%s\n' "$fmt" } +# Browser-visible configuration only; never add a secret here. ENV_VARS=$(make_envsubst_vars \ SERVER_HOST \ SERVER_PORT \ + CLIENT_PORT \ OPENCDS_ENABLED \ KEYCLOAK_PROTOCOL \ KEYCLOAK_DOMAIN \ diff --git a/apps/client/vite.config.ts b/apps/client/vite.config.ts index 817f1306c6..42ff334a6c 100644 --- a/apps/client/vite.config.ts +++ b/apps/client/vite.config.ts @@ -1,4 +1,3 @@ -import fs from 'node:fs' import { fileURLToPath, URL } from 'node:url' import { vueDsfrAutoimportPreset, @@ -11,19 +10,27 @@ import Components from 'unplugin-vue-components/vite' import { defineConfig } from 'vite' import { VitePWA } from 'vite-plugin-pwa' -if (process.env.INTEGRATION === 'true' && fs.existsSync('.env.integ')) - process.loadEnvFile('.env.integ') - -if (process.env.DOCKER !== 'true' && fs.existsSync('.env')) - process.loadEnvFile('.env') - const serverHost = process.env.SERVER_HOST ?? 'localhost' const serverPort = process.env.SERVER_PORT ?? 4000 const clientPort = process.env.CLIENT_PORT ?? 8080 +const publicClientEnv = { + NODE_ENV: process.env.NODE_ENV, + SERVER_HOST: process.env.SERVER_HOST, + SERVER_PORT: process.env.SERVER_PORT, + CLIENT_PORT: process.env.CLIENT_PORT, + KEYCLOAK_PROTOCOL: process.env.KEYCLOAK_PROTOCOL, + KEYCLOAK_DOMAIN: process.env.KEYCLOAK_DOMAIN, + KEYCLOAK_REALM: process.env.KEYCLOAK_REALM, + KEYCLOAK_CLIENT_ID: process.env.KEYCLOAK_CLIENT_ID, + KEYCLOAK_REDIRECT_URI: process.env.KEYCLOAK_REDIRECT_URI, + OPENCDS_ENABLED: process.env.OPENCDS_ENABLED, + CONTACT_EMAIL: process.env.CONTACT_EMAIL, +} + const define = process.env.NODE_ENV === 'production' ? { 'process.env': { APP_VERSION: process.env.APP_VERSION } } - : { 'process.env': process.env } + : { 'process.env': publicClientEnv } // https://vitejs.dev/config/ export default defineConfig({ diff --git a/apps/server-nestjs/.env-example b/apps/server-nestjs/.env-example deleted file mode 100644 index c1a05eaea5..0000000000 --- a/apps/server-nestjs/.env-example +++ /dev/null @@ -1,55 +0,0 @@ -# Active le mode développement (initialise les données de démo au démarrage) -DEV_SETUP="true" -# Mode d'exécution Node.js (development | production | test) -NODE_ENV=development -# HOME=/home/node -# Secret utilisé pour signer les cookies de session (min. 32 caractères) -SESSION_SECRET=a-very-strong-secret-with-more-than-32-char - -# --- Configuration Keycloak --- -# Domaine (host:port) du serveur Keycloak pour l'authentification -KEYCLOAK_DOMAIN=localhost:8090 -# Nom du royaume Keycloak utilisé par la console -KEYCLOAK_REALM=dso -# Protocole de communication avec Keycloak (http | https) -KEYCLOAK_PROTOCOL=http -# Identifiant du client Keycloak côté backend -KEYCLOAK_CLIENT_ID=dso-console-backend -# Secret du client Keycloak backend (confidentiel) -KEYCLOAK_CLIENT_SECRET=client-secret-backend -# Identifiant de l'administrateur Keycloak sur le royaume KEYCLOAK_REALM -KEYCLOAK_ADMIN=admin -# Mot de passe de l'administrateur Keycloak sur le royaume KEYCLOAK_REALM -KEYCLOAK_ADMIN_PASSWORD=admin -# Client utilisé par le client admin pour s'authentifier sur le royaume master (requis) -KEYCLOAK_ADMIN_CLIENT_ID=admin-cli -# URL de redirection après authentification Keycloak -KEYCLOAK_REDIRECT_URI=http://localhost:8080 -# JWKS cache TTL in milliseconds (default 300000 = 5 min) -KEYCLOAK_JWKS_CACHE_TTL_MS=300000 -# JWKS fetch timeout in milliseconds (default 1000 = 1 s) -KEYCLOAK_JWKS_TIMEOUT_MS=1000 -# Interface d'écoute du serveur backend -SERVER_HOST=localhost -# Port d'écoute du serveur backend -SERVER_PORT=3001 -# URL de connexion à la base de données PostgreSQL (format Prisma) -DB_URL=postgresql://admin:admin@localhost:5432/dso-console-db?schema=public -# Adresse e-mail de contact affichée dans l'interface -CONTACT_EMAIL=cloudpinative-relations@interieur.gouv.fr - -# --- Activation des plugins externes --- -# Mettez "true" et renseignez les variables ci-dessus pour activer chaque plugin. -USE_ARGOCD=false -USE_GITLAB=false -USE_NEXUS=false -USE_HARBOR=false -USE_SONARQUBE=false -USE_VAULT=false -USE_OBSERVABILITY=false - -# --- Configuration Observability (Grafana) --- -# URL publique de Grafana -GRAFANA_URL=https://grafana.example.com -# Version du chart Helm dso-observability -DSO_OBSERVABILITY_CHART_VERSION=dso-observability-0.1.7 diff --git a/apps/server-nestjs/.env.docker-example b/apps/server-nestjs/.env.docker-example deleted file mode 100644 index 22aa728b7a..0000000000 --- a/apps/server-nestjs/.env.docker-example +++ /dev/null @@ -1,67 +0,0 @@ -# Indique que l'application tourne dans un conteneur Docker -DOCKER=true -# Active le mode développement (initialise les données de démo au démarrage) -DEV_SETUP="true" -# Mode d'exécution Node.js (development | production | test) -NODE_ENV=development -# Secret utilisé pour signer les cookies de session (min. 32 caractères) -SESSION_SECRET=a-very-strong-secret-with-more-than-32-char - -# --- Configuration Keycloak --- -# Domaine du Keycloak (nom du service Docker Compose + port interne) -KEYCLOAK_DOMAIN=keycloak:8080 -# Nom du royaume Keycloak utilisé par la console -KEYCLOAK_REALM=dso -# Protocole de communication avec Keycloak (http en Docker local) -KEYCLOAK_PROTOCOL=http -# Identifiant du client Keycloak côté backend -KEYCLOAK_CLIENT_ID=dso-console-backend -# Secret du client Keycloak backend (confidentiel) -KEYCLOAK_CLIENT_SECRET=client-secret-backend -# Identifiant de l'administrateur Keycloak -KEYCLOAK_ADMIN=admin -# Mot de passe de l'administrateur Keycloak -KEYCLOAK_ADMIN_PASSWORD=admin -# Client utilisé par le client admin pour s'authentifier sur le royaume master (requis) -KEYCLOAK_ADMIN_CLIENT_ID=admin-cli -# URL de redirection après authentification Keycloak -KEYCLOAK_REDIRECT_URI=http://localhost:8080 -# JWKS cache TTL in milliseconds (default 300000 = 5 min) -KEYCLOAK_JWKS_CACHE_TTL_MS=300000 -# JWKS fetch timeout in milliseconds (default 1000 = 1 s) -KEYCLOAK_JWKS_TIMEOUT_MS=1000 -# Interface d'écoute du serveur backend -SERVER_HOST=0.0.0.0 -# Port d'écoute du serveur dans le réseau Docker -SERVER_PORT=3001 -# URL de connexion PostgreSQL (utilise le nom de service Docker 'postgres') -DB_URL=postgresql://admin:admin@postgres:5432/dso-console-db?schema=public -# Adresse e-mail de contact affichée dans l'interface -CONTACT_EMAIL=cloudpinative-relations@interieur.gouv.fr - -# --- Configuration OpenCDS --- -# Token d'authentification pour l'API OpenCDS -OPENCDS_API_TOKEN=token -# Vérification du certificat TLS de l'API OpenCDS (true | false) -OPENCDS_API_TLS_REJECT_UNAUTHORIZED=false -# URL accessible à l'extérieur du cluster -OPENCDS_URL=http://dso-console_opencds-mockoon:3100/api/v1/health -# URL accessible seulement à l'interieur du cluster -OPENCDS_INTERNAL_URL=http://dso-console_opencds-mockoon:3100/api/v1/health - -# --- Activation des plugins externes --- -# Mettez "true" et renseignez les variables ci-dessus pour activer chaque plugin. -USE_ARGOCD=false -USE_GITLAB=false -USE_NEXUS=false -USE_HARBOR=false -USE_SONARQUBE=false -USE_SERVICE_CHAIN=true -USE_VAULT=false -USE_OBSERVABILITY=false - -# --- Configuration Observability (Grafana) --- -# URL publique de Grafana -GRAFANA_URL=https://grafana.example.com -# Version du chart Helm dso-observability -DSO_OBSERVABILITY_CHART_VERSION=dso-observability-0.1.7 diff --git a/apps/server-nestjs/.env.integ-example b/apps/server-nestjs/.env.integ-example deleted file mode 100644 index a56c2ff131..0000000000 --- a/apps/server-nestjs/.env.integ-example +++ /dev/null @@ -1,132 +0,0 @@ -# Surcharges pour le branchement à un environnement d'intégration distant. -# Ces valeurs écrasent celles de .env et .env.docker quand INTEGRATION=true. - -# Désactive l'initialisation des données de démo (l'intégration a ses propres données) -DEV_SETUP="false" -# Active le mode intégration (charge ce fichier en surcharge) -INTEGRATION=true - -# --- Keycloak --- -# Protocole (généralement https en intégration) -KEYCLOAK_PROTOCOL=https -# Identifiant du client Keycloak backend sur l'environnement d'intégration -KEYCLOAK_CLIENT_ID= -# Secret du client Keycloak backend sur l'environnement d'intégration -KEYCLOAK_CLIENT_SECRET= -# Domaine du Keycloak d'intégration (ex: keycloak.dso.example.fr) -KEYCLOAK_DOMAIN= -# Royaume Keycloak d'intégration -KEYCLOAK_REALM= - -# JWKS cache TTL in milliseconds (default 300000 = 5 min) -KEYCLOAK_JWKS_CACHE_TTL_MS=300000 -# JWKS fetch timeout in milliseconds (default 1000 = 1 s) -KEYCLOAK_JWKS_TIMEOUT_MS=1000 - -# --- ArgoCD --- -# Namespace Kubernetes dans lequel ArgoCD est déployé -ARGO_NAMESPACE= -# URL de l'API ArgoCD -ARGOCD_URL= -# URL interne de l'API ArgoCD -ARGOCD_INTERNAL_URL= -# Sources autorisées pour tous les projets, séparées par des virgules. -# est remplacé par le slug du projet. -# Ex. pour le registre Harbor : oci://registry.example.com//** -ARGOCD_SHARED_SOURCE_REPOSITORIES= - -# --- GitLab --- -# Token d'accès personnel GitLab (scope api) -GITLAB_TOKEN= -# URL de l'instance GitLab -GITLAB_URL= -# URL interne de l'API Gitlab -GITLAB_INTERNAL_URL= -# Durée de validité (en jours) des tokens d'accès groupe utilisés pour le miroir -GITLAB_MIRROR_TOKEN_EXPIRATION_DAYS=365 -# Seuil (en jours) de rotation préventive des tokens, strictement inférieur à GITLAB_MIRROR_TOKEN_EXPIRATION_DAYS -GITLAB_MIRROR_TOKEN_ROTATION_THRESHOLD_DAYS=250 - -# --- Harbor --- -# Nom d'utilisateur admin Harbor -HARBOR_ADMIN= -# Mot de passe admin Harbor -HARBOR_ADMIN_PASSWORD= -# URL de l'instance Harbor -HARBOR_URL= -# URL interne de l'API Harbor -HARBOR_INTERNAL_URL= -# Durée de validité (en jours) des robots Harbor -HARBOR_ROBOT_EXPIRATION_DAYS=90 -# Seuil (en jours) de rotation préventive des secrets, strictement inférieur à HARBOR_ROBOT_EXPIRATION_DAYS -HARBOR_ROBOT_ROTATION_THRESHOLD_DAYS=60 - -# --- Keycloak (admin) --- -# Utilisateur admin Keycloak pour la gestion des royaumes/clients -KEYCLOAK_ADMIN= -# Mot de passe admin Keycloak -KEYCLOAK_ADMIN_PASSWORD= -# Client utilisé par le client admin pour s'authentifier sur le royaume master (requis) -KEYCLOAK_ADMIN_CLIENT_ID=admin-cli -# URL complète de Keycloak (peut différer de KEYCLOAK_DOMAIN) -KEYCLOAK_URL= - -# --- Nexus --- -# Nom d'utilisateur admin Nexus -NEXUS_ADMIN= -# Mot de passe admin Nexus -NEXUS_ADMIN_PASSWORD= -# URL de l'instance Nexus -NEXUS_URL= -# URL interne de l'API Nexus -NEXUS_INTERNAL_URL= - -# --- Divers --- -# Répertoire racine des projets dans Gitlab -PROJECTS_ROOT_DIR=forge-mi/projects - -# --- SonarQube --- -# Token d'API SonarQube -SONAR_API_TOKEN= -# URL de l'instance SonarQube -SONARQUBE_URL= -# URL interne de l'API SonarQube -SONARQUBE_INTERNAL_URL= - -# --- Vault --- -# Token d'authentification Vault -VAULT_TOKEN= -# URL de l'instance Vault -VAULT_URL= -# URL interne de l'API Vault -VAULT_INTERNAL_URL= - -# --- Kubeconfig --- -# Chemin vers le kubeconfig sur la machine hôte (monté en volume dans Docker) -KUBECONFIG_HOST_PATH= -# Chemin vers le kubeconfig à l'intérieur du conteneur -KUBECONFIG_PATH=$HOME/.kube/config -# Contexte Kubernetes à utiliser -KUBECONFIG_CTX= - -# Répertoire contenant les plugins externes (monté en volume dans Docker) -EXTERNAL_PLUGINS_DIR_HOST_PATH=/path/to/plugins - -# --- Configuration Observability (Grafana) --- -# Activer le plugin observabilité -USE_OBSERVABILITY=false -# URL publique de Grafana -GRAFANA_URL= -# Version du chart Helm dso-observability -DSO_OBSERVABILITY_CHART_VERSION=dso-observability-0.1.7 - -# --- Configuration proxy (si nécessaire pour accéder aux services externes) --- -# HTTP_PROXY= -# HTTPS_PROXY= -# NO_PROXY= - -# Configuration OpenCDS -OPENCDS_URL= -OPENCDS_INTERNAL_URL= -OPENCDS_API_TOKEN=token -OPENCDS_API_TLS_REJECT_UNAUTHORIZED=false diff --git a/apps/server-nestjs/prisma.config.ts b/apps/server-nestjs/prisma.config.ts index ff5aaf9186..057121c97b 100644 --- a/apps/server-nestjs/prisma.config.ts +++ b/apps/server-nestjs/prisma.config.ts @@ -1,20 +1,6 @@ -import fs from 'node:fs' import path from 'node:path' -import { parseEnv } from 'node:util' import { defineConfig } from 'prisma/config' -if (process.env.DOCKER !== 'true' && fs.existsSync('.env')) { - Object.assign(process.env, Object.fromEntries( - Object.entries(parseEnv(fs.readFileSync(path.resolve('.env'), 'utf-8'))).filter(([_k, v]) => Boolean(v)), - )) -} - -if (process.env.INTEGRATION === 'true' && fs.existsSync('.env.integ')) { - Object.assign(process.env, Object.fromEntries( - Object.entries(parseEnv(fs.readFileSync(path.resolve('.env.integ'), 'utf-8'))).filter(([_k, v]) => Boolean(v)), - )) -} - export default defineConfig({ schema: path.join('src', 'prisma', 'schema'), migrations: { diff --git a/apps/server-nestjs/src/main.module.ts b/apps/server-nestjs/src/main.module.ts index d1325a939b..3dd20c9a9b 100644 --- a/apps/server-nestjs/src/main.module.ts +++ b/apps/server-nestjs/src/main.module.ts @@ -25,16 +25,14 @@ import { ServiceMonitorModule } from './modules/service-monitor/service-monitor. import { StageModule } from './modules/stage/stage.module' import { SystemConfigModule } from './modules/system-config/system-config.module' import { SystemSettingsModule } from './modules/system-settings/system-settings.module' -import { UserModule } from './modules/user/user.module' import { UserTokensModule } from './modules/user-tokens/user-tokens.module' +import { UserModule } from './modules/user/user.module' import { VersionModule } from './modules/version/version.module' import { ZoneModule } from './modules/zone/zone.module' -import { getDotenvPaths } from './utils/dotenv.utils' @Module({ imports: [ ConfigModule.forRoot({ - envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory], }), diff --git a/apps/server-nestjs/src/modules/gitlab/gitlab.service.ts b/apps/server-nestjs/src/modules/gitlab/gitlab.service.ts index a5b0f91611..345be3da7a 100644 --- a/apps/server-nestjs/src/modules/gitlab/gitlab.service.ts +++ b/apps/server-nestjs/src/modules/gitlab/gitlab.service.ts @@ -6,13 +6,13 @@ import type { MirrorUserSecret, VaultSecret } from '../vault/vault-client.servic import type { GroupSchemaWith } from './gitlab-client.service' import type { ProjectWithDetails } from './gitlab-datastore.service' import { specificallyEnabled } from '@cpn-console/hooks' +import { daysAgo } from '@cpn-console/shared' import { AccessLevel } from '@gitbeaker/core' import { Inject, Injectable, Logger } from '@nestjs/common' import { OnEvent } from '@nestjs/event-emitter' import { trace } from '@opentelemetry/api' import { gitlabConfigFactory } from '../../config/gitlab.config' import { getAll } from '../../utils/iterable.utils' -import { daysAgo } from '@cpn-console/shared' import { StartActiveSpan } from '../infrastructure/telemetry/telemetry.decorator' import { capturePluginResult } from '../plugin/plugin.utils' import { VaultClientService } from '../vault/vault-client.service' diff --git a/apps/server-nestjs/src/modules/keycloak/keycloak.service.spec.ts b/apps/server-nestjs/src/modules/keycloak/keycloak.service.spec.ts index 9201c78f40..15c3078004 100644 --- a/apps/server-nestjs/src/modules/keycloak/keycloak.service.spec.ts +++ b/apps/server-nestjs/src/modules/keycloak/keycloak.service.spec.ts @@ -8,7 +8,6 @@ import { KeycloakClientService } from './keycloak-client.service' import { KeycloakDatastoreService } from './keycloak-datastore.service' import { makeGroupRepresentation, - makeProjectEnvironment, makeProjectMember, makeProjectRole, makeProjectUser, diff --git a/apps/server-nestjs/src/modules/keycloak/keycloak.service.ts b/apps/server-nestjs/src/modules/keycloak/keycloak.service.ts index 3602bd0592..62cd7b2178 100644 --- a/apps/server-nestjs/src/modules/keycloak/keycloak.service.ts +++ b/apps/server-nestjs/src/modules/keycloak/keycloak.service.ts @@ -3,7 +3,7 @@ import type { AdminRoleEventPayload } from '../events/app-events.service' import type { RequiredPluginResult } from '../plugin/plugin.utils' import type { AdminRoleWithDetails, ProjectWithDetails, UserWithAdminRoles } from './keycloak-datastore.service' import type { GroupRepresentationWith, GroupRepresentationWithIdNamePath } from './keycloak.utils' -import { getBaseRoleType, getPermsByUserRoles, isExternalRoleType, ProjectAuthorized, resourceListToDict } from '@cpn-console/shared' +import { getBaseRoleType, isExternalRoleType } from '@cpn-console/shared' import { Inject, Injectable, Logger } from '@nestjs/common' import { OnEvent } from '@nestjs/event-emitter' import { trace } from '@opentelemetry/api' diff --git a/apps/server-nestjs/src/modules/plugin/plugin.module.spec.ts b/apps/server-nestjs/src/modules/plugin/plugin.module.spec.ts index cb6e009682..817b1c0059 100644 --- a/apps/server-nestjs/src/modules/plugin/plugin.module.spec.ts +++ b/apps/server-nestjs/src/modules/plugin/plugin.module.spec.ts @@ -5,9 +5,16 @@ import { PluginModule } from './plugin.module' import { PluginService } from './plugin.service' describe('pluginModule', () => { - afterEach(() => vi.unstubAllEnvs()) + afterEach(() => { + vi.unstubAllEnvs() + vi.unstubAllGlobals() + }) it('resolves PluginService with all plugins enabled', async () => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(JSON.stringify({ + issuer: 'https://keycloak.test/realms/dso', + jwks_uri: 'https://keycloak.test/realms/dso/protocol/openid-connect/certs', + })))) vi.stubEnv('USE_ARGOCD', 'true') vi.stubEnv('USE_GITLAB', 'true') vi.stubEnv('USE_HARBOR', 'true') diff --git a/apps/server-nestjs/src/modules/registry/registry.service.ts b/apps/server-nestjs/src/modules/registry/registry.service.ts index b70843bef5..700b58d97d 100644 --- a/apps/server-nestjs/src/modules/registry/registry.service.ts +++ b/apps/server-nestjs/src/modules/registry/registry.service.ts @@ -43,7 +43,6 @@ import { PROJECT_DEVELOPER_GROUP_PATH_SUFFIXES_PLUGIN_KEY, PROJECT_GUEST_GROUP_PATH_SUFFIXES_PLUGIN_KEY, PROJECT_MAINTAINER_GROUP_PATH_SUFFIXES_PLUGIN_KEY, - REGISTRY_CONFIG_KEY_PUBLISH_PROJECT_ROBOT, REGISTRY_CONFIG_KEY_QUOTA_HARD_LIMIT, ROBOT_NAME_PROJECT, ROBOT_NAME_RO, diff --git a/apps/server-nestjs/src/modules/sonarqube/sonarqube-client.service.spec.ts b/apps/server-nestjs/src/modules/sonarqube/sonarqube-client.service.spec.ts index dc9680e339..e80938895d 100644 --- a/apps/server-nestjs/src/modules/sonarqube/sonarqube-client.service.spec.ts +++ b/apps/server-nestjs/src/modules/sonarqube/sonarqube-client.service.spec.ts @@ -1,11 +1,11 @@ import type { ConfigType } from '@nestjs/config' +import type { DeepMockProxy } from 'vitest-mock-extended' import type { AddPermissionGroupParams, CreatePermissionTemplateParams, CreateProjectParams, CreateUserGroupParams, CreateUserParams, DeactivateUserParams, RevokeUserTokenParams } from './sonarqube-client.service' import { faker } from '@faker-js/faker' import { Test } from '@nestjs/testing' import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest' -import type { DeepMockProxy } from 'vitest-mock-extended' import { mockDeep } from 'vitest-mock-extended' import { sonarqubeConfigFactory } from '../../config/sonarqube.config' import { getAll } from '../../utils/iterable.utils' diff --git a/apps/server-nestjs/src/modules/zone/zone.service.spec.ts b/apps/server-nestjs/src/modules/zone/zone.service.spec.ts index 427af6e1de..8b67aaf2e1 100644 --- a/apps/server-nestjs/src/modules/zone/zone.service.spec.ts +++ b/apps/server-nestjs/src/modules/zone/zone.service.spec.ts @@ -1,12 +1,12 @@ import type { Cluster, Prisma } from '@prisma/client' import type { DeepMockProxy } from 'vitest-mock-extended' +import type { Zone } from './zone-queries.utils' import { faker } from '@faker-js/faker' import { Test } from '@nestjs/testing' import { beforeEach, describe, expect, it } from 'vitest' import { mock, mockDeep } from 'vitest-mock-extended' import { AppEventsService } from '../events/app-events.service' import { PrismaService } from '../infrastructure/database/prisma.service' -import type { Zone } from './zone-queries.utils' import { makeZone } from './zone-testing.utils' import { ZoneService } from './zone.service' diff --git a/apps/server-nestjs/src/utils/dotenv.utils.spec.ts b/apps/server-nestjs/src/utils/dotenv.utils.spec.ts deleted file mode 100644 index b5c7014c46..0000000000 --- a/apps/server-nestjs/src/utils/dotenv.utils.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { afterEach, describe, expect, it, vi } from 'vitest' -import { getDotenvPaths, getExistingDotenvPaths } from './dotenv.utils' - -describe('dotenv.utils', () => { - afterEach(() => vi.unstubAllEnvs()) - - describe('getDotenvPaths', () => { - it('returns [".env"] when no env vars set', () => { - expect(getDotenvPaths()).toEqual(['.env']) - }) - - it('returns [".env", ".env.integ"] when INTEGRATION=true', () => { - vi.stubEnv('INTEGRATION', 'true') - expect(getDotenvPaths()).toEqual(['.env', '.env.integ']) - }) - - it('returns [".env.docker"] when DOCKER=true', () => { - vi.stubEnv('DOCKER', 'true') - expect(getDotenvPaths()).toEqual(['.env.docker']) - }) - - it('returns [".env.integ", ".env.docker"] when DOCKER=true and INTEGRATION=true', () => { - vi.stubEnv('DOCKER', 'true') - vi.stubEnv('INTEGRATION', 'true') - expect(getDotenvPaths()).toEqual(['.env.integ', '.env.docker']) - }) - }) - - describe('getExistingDotenvPaths', () => { - it('filters to only existing files', () => { - const paths = getExistingDotenvPaths() - expect(paths.every(p => typeof p === 'string')).toBe(true) - }) - }) -}) diff --git a/apps/server-nestjs/src/utils/dotenv.utils.ts b/apps/server-nestjs/src/utils/dotenv.utils.ts deleted file mode 100644 index 7d3e0a0501..0000000000 --- a/apps/server-nestjs/src/utils/dotenv.utils.ts +++ /dev/null @@ -1,26 +0,0 @@ -import fs from 'node:fs' - -export function getDotenvPaths(): string[] { - const paths: string[] = [] - - // Load .env unless DOCKER=true - if (process.env.DOCKER !== 'true') { - paths.push('.env') - } - - // Load .env.integ if INTEGRATION=true - if (process.env.INTEGRATION === 'true') { - paths.push('.env.integ') - } - - // Load .env.docker if DOCKER=true - if (process.env.DOCKER === 'true') { - paths.push('.env.docker') - } - - return paths -} - -export function getExistingDotenvPaths(): string[] { - return getDotenvPaths().filter(path => fs.existsSync(path)) -} diff --git a/apps/server-nestjs/test/admin-role.e2e-spec.ts b/apps/server-nestjs/test/admin-role.e2e-spec.ts index d9062f50fe..0d72f2fd47 100644 --- a/apps/server-nestjs/test/admin-role.e2e-spec.ts +++ b/apps/server-nestjs/test/admin-role.e2e-spec.ts @@ -8,7 +8,6 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { baseConfigFactory } from '../src/config/base.config' import { AdminRoleModule } from '../src/modules/admin-role/admin-role.module' import { PrismaService } from '../src/modules/infrastructure/database/prisma.service' -import { getDotenvPaths } from '../src/utils/dotenv.utils' const canRunAdminRoleE2E = Boolean(process.env.E2E) @@ -25,7 +24,7 @@ describeWithAdminRole('AdminRole HTTP (e2e)', () => { beforeAll(async () => { const moduleRef: TestingModule = await Test.createTestingModule({ - imports: [ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), AdminRoleModule], + imports: [ConfigModule.forRoot({ isGlobal: true, load: [baseConfigFactory] }), AdminRoleModule], }).compile() await moduleRef.init() diff --git a/apps/server-nestjs/test/admin-token.e2e-spec.ts b/apps/server-nestjs/test/admin-token.e2e-spec.ts index b5c9ec6459..7ed95bbae3 100644 --- a/apps/server-nestjs/test/admin-token.e2e-spec.ts +++ b/apps/server-nestjs/test/admin-token.e2e-spec.ts @@ -8,7 +8,6 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { baseConfigFactory } from '../src/config/base.config' import { AdminTokenModule } from '../src/modules/admin-token/admin-token.module' import { PrismaService } from '../src/modules/infrastructure/database/prisma.service' -import { getDotenvPaths } from '../src/utils/dotenv.utils' const canRunAdminTokenE2E = Boolean(process.env.E2E) @@ -23,7 +22,7 @@ describeWithAdminToken('AdminToken HTTP (e2e)', () => { beforeAll(async () => { const moduleRef: TestingModule = await Test.createTestingModule({ - imports: [ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), AdminTokenModule], + imports: [ConfigModule.forRoot({ isGlobal: true, load: [baseConfigFactory] }), AdminTokenModule], }).compile() await moduleRef.init() diff --git a/apps/server-nestjs/test/argocd.e2e-spec.ts b/apps/server-nestjs/test/argocd.e2e-spec.ts index dbd0fdc449..d48d993034 100644 --- a/apps/server-nestjs/test/argocd.e2e-spec.ts +++ b/apps/server-nestjs/test/argocd.e2e-spec.ts @@ -19,7 +19,6 @@ import { EventsModule } from '../src/modules/infrastructure/events/events.module import { LoggerModule } from '../src/modules/infrastructure/logger/logger.module' import { PermissionModule } from '../src/modules/infrastructure/permission/permission.module' import { VaultClientService } from '../src/modules/vault/vault-client.service' -import { getDotenvPaths } from '../src/utils/dotenv.utils' import { ARGOCD_RECONCILE_TIMEOUT, GITLAB_SYNC_TIMEOUT } from './constants' const canRunArgoCDE2E @@ -57,7 +56,7 @@ describeWithArgoCD('ArgoCDService (e2e)', () => { beforeAll(async () => { moduleRef = await Test.createTestingModule({ - imports: [ArgoCDModule, ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule], + imports: [ArgoCDModule, ConfigModule.forRoot({ isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule], }).compile() await moduleRef.init() diff --git a/apps/server-nestjs/test/gitlab.e2e-spec.ts b/apps/server-nestjs/test/gitlab.e2e-spec.ts index 2c1c2c501e..73e0f344cc 100644 --- a/apps/server-nestjs/test/gitlab.e2e-spec.ts +++ b/apps/server-nestjs/test/gitlab.e2e-spec.ts @@ -19,7 +19,6 @@ import { EventsModule } from '../src/modules/infrastructure/events/events.module import { LoggerModule } from '../src/modules/infrastructure/logger/logger.module' import { PermissionModule } from '../src/modules/infrastructure/permission/permission.module' import { VaultClientService } from '../src/modules/vault/vault-client.service' -import { getDotenvPaths } from '../src/utils/dotenv.utils' import { getAll } from '../src/utils/iterable.utils' import { GITLAB_PURGE_SYNC_TIMEOUT, GITLAB_SYNC_TIMEOUT } from './constants' @@ -44,7 +43,7 @@ describeWithGitLab('GitlabService (e2e)', () => { beforeAll(async () => { moduleRef = await Test.createTestingModule({ - imports: [GitlabModule, ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule], + imports: [GitlabModule, ConfigModule.forRoot({ isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule], }).compile() await moduleRef.init() diff --git a/apps/server-nestjs/test/keycloak.e2e-spec.ts b/apps/server-nestjs/test/keycloak.e2e-spec.ts index 5abcb02576..d96d881aad 100644 --- a/apps/server-nestjs/test/keycloak.e2e-spec.ts +++ b/apps/server-nestjs/test/keycloak.e2e-spec.ts @@ -17,7 +17,6 @@ import { PermissionModule } from '../src/modules/infrastructure/permission/permi import { KEYCLOAK_ADMIN_CLIENT, KeycloakClientService } from '../src/modules/keycloak/keycloak-client.service' import { projectSelect } from '../src/modules/keycloak/keycloak-datastore.service' import { KeycloakModule } from '../src/modules/keycloak/keycloak.module' -import { getDotenvPaths } from '../src/utils/dotenv.utils' import { KEYCLOAK_GROUP_SYNC_TIMEOUT } from './constants' const canRunKeycloakE2E @@ -40,7 +39,7 @@ describeWithKeycloak('KeycloakService (e2e)', () => { beforeAll(async () => { moduleRef = await Test.createTestingModule({ - imports: [KeycloakModule, ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule], + imports: [KeycloakModule, ConfigModule.forRoot({ isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule], }).compile() await moduleRef.init() diff --git a/apps/server-nestjs/test/log.e2e-spec.ts b/apps/server-nestjs/test/log.e2e-spec.ts index 4d81a4e725..bfc1eff161 100644 --- a/apps/server-nestjs/test/log.e2e-spec.ts +++ b/apps/server-nestjs/test/log.e2e-spec.ts @@ -12,7 +12,6 @@ import { LoggerModule } from '../src/modules/infrastructure/logger/logger.module import { PermissionModule } from '../src/modules/infrastructure/permission/permission.module' import { LogModule } from '../src/modules/log/log.module' import { LogService } from '../src/modules/log/log.service' -import { getDotenvPaths } from '../src/utils/dotenv.utils' const canRunLogE2E = Boolean(process.env.E2E) @@ -32,7 +31,7 @@ describeWithLog('LogService (e2e)', () => { beforeAll(async () => { moduleRef = await Test.createTestingModule({ - imports: [ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), LogModule, AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule], + imports: [ConfigModule.forRoot({ isGlobal: true, load: [baseConfigFactory] }), LogModule, AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule], }).compile() await moduleRef.init() diff --git a/apps/server-nestjs/test/nexus.e2e-spec.ts b/apps/server-nestjs/test/nexus.e2e-spec.ts index 3c0d31f7ef..03a4a57e2d 100644 --- a/apps/server-nestjs/test/nexus.e2e-spec.ts +++ b/apps/server-nestjs/test/nexus.e2e-spec.ts @@ -21,7 +21,6 @@ import { NexusModule } from '../src/modules/nexus/nexus.module' import { generateNexusCredPath } from '../src/modules/nexus/nexus.utils' import { VaultClientService } from '../src/modules/vault/vault-client.service' import { VaultModule } from '../src/modules/vault/vault.module' -import { getDotenvPaths } from '../src/utils/dotenv.utils' import { NEXUS_SYNC_TIMEOUT } from './constants' const canRunNexusE2E @@ -43,7 +42,7 @@ describeWithNexus('NexusService (e2e)', () => { beforeAll(async () => { moduleRef = await Test.createTestingModule({ - imports: [NexusModule, VaultModule, ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule], + imports: [NexusModule, VaultModule, ConfigModule.forRoot({ isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule], }).compile() await moduleRef.init() diff --git a/apps/server-nestjs/test/observability.e2e-spec.ts b/apps/server-nestjs/test/observability.e2e-spec.ts index 37a62410b2..bd63043fce 100644 --- a/apps/server-nestjs/test/observability.e2e-spec.ts +++ b/apps/server-nestjs/test/observability.e2e-spec.ts @@ -27,7 +27,6 @@ import { GRAFANA_SUBGROUP_PROD_RW, } from '../src/modules/observability/observability.constants' import { ObservabilityModule } from '../src/modules/observability/observability.module' -import { getDotenvPaths } from '../src/utils/dotenv.utils' import { KEYCLOAK_GROUP_SYNC_TIMEOUT } from './constants' const canRunObservabilityE2E @@ -88,7 +87,7 @@ describeWithObservability('ObservabilityService (e2e)', () => { } moduleRef = await Test.createTestingModule({ - imports: [ObservabilityModule, ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule], + imports: [ObservabilityModule, ConfigModule.forRoot({ isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule], }) .overrideProvider(GitlabClientService) .useValue(gitlabStub) diff --git a/apps/server-nestjs/test/project-bulk.e2e-spec.ts b/apps/server-nestjs/test/project-bulk.e2e-spec.ts index 8548971e55..0886364b58 100644 --- a/apps/server-nestjs/test/project-bulk.e2e-spec.ts +++ b/apps/server-nestjs/test/project-bulk.e2e-spec.ts @@ -13,7 +13,6 @@ import { LoggerModule } from '../src/modules/infrastructure/logger/logger.module import { PermissionModule } from '../src/modules/infrastructure/permission/permission.module' import { ProjectBulkModule } from '../src/modules/project-bulk/project-bulk.module' import { ProjectBulkService } from '../src/modules/project-bulk/project-bulk.service' -import { getDotenvPaths } from '../src/utils/dotenv.utils' const canRunProjectBulkE2E = Boolean(process.env.E2E) @@ -31,7 +30,7 @@ describeWithProjectBulk('ProjectBulkService (e2e)', () => { beforeAll(async () => { moduleRef = await Test.createTestingModule({ - imports: [ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule, ProjectBulkModule], + imports: [ConfigModule.forRoot({ isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule, ProjectBulkModule], }).compile() await moduleRef.init() diff --git a/apps/server-nestjs/test/project-hooks.e2e-spec.ts b/apps/server-nestjs/test/project-hooks.e2e-spec.ts index 91d56e504f..b73d9f8d56 100644 --- a/apps/server-nestjs/test/project-hooks.e2e-spec.ts +++ b/apps/server-nestjs/test/project-hooks.e2e-spec.ts @@ -17,7 +17,6 @@ import { ProjectHooksModule } from '../src/modules/project-hooks/project-hooks.m import { ProjectHooksService } from '../src/modules/project-hooks/project-hooks.service' import { VaultClientService } from '../src/modules/vault/vault-client.service' import { VaultService } from '../src/modules/vault/vault.service' -import { getDotenvPaths } from '../src/utils/dotenv.utils' const canRunProjectHooksE2E = Boolean(process.env.E2E) @@ -41,7 +40,7 @@ describeWithProjectHooks('ProjectHooksService (e2e)', () => { vaultClient = mockDeep() moduleRef = await Test.createTestingModule({ - imports: [ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule, ProjectHooksModule], + imports: [ConfigModule.forRoot({ isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule, ProjectHooksModule], providers: [ { provide: VaultService, useValue: vaultService }, { provide: VaultClientService, useValue: vaultClient }, diff --git a/apps/server-nestjs/test/project-members.e2e-spec.ts b/apps/server-nestjs/test/project-members.e2e-spec.ts index 315bdba2dd..47c592b1c3 100644 --- a/apps/server-nestjs/test/project-members.e2e-spec.ts +++ b/apps/server-nestjs/test/project-members.e2e-spec.ts @@ -17,7 +17,6 @@ import { PermissionModule } from '../src/modules/infrastructure/permission/permi import { KeycloakClientService } from '../src/modules/keycloak/keycloak-client.service' import { ProjectMembersModule } from '../src/modules/project-members/project-members.module' import { ProjectMembersService } from '../src/modules/project-members/project-members.service' -import { getDotenvPaths } from '../src/utils/dotenv.utils' const canRunProjectMembersE2E = Boolean(process.env.E2E) @@ -38,7 +37,7 @@ describeWithProjectMembers('ProjectMembersService (e2e)', () => { keycloakClient.getUserByEmail.mockResolvedValue(undefined) moduleRef = await Test.createTestingModule({ - imports: [ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule, ProjectMembersModule], + imports: [ConfigModule.forRoot({ isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule, ProjectMembersModule], providers: [ { provide: KeycloakClientService, useValue: keycloakClient }, ], diff --git a/apps/server-nestjs/test/project-roles.e2e-spec.ts b/apps/server-nestjs/test/project-roles.e2e-spec.ts index 3f180330d0..edaf97febe 100644 --- a/apps/server-nestjs/test/project-roles.e2e-spec.ts +++ b/apps/server-nestjs/test/project-roles.e2e-spec.ts @@ -14,7 +14,6 @@ import { LoggerModule } from '../src/modules/infrastructure/logger/logger.module import { PermissionModule } from '../src/modules/infrastructure/permission/permission.module' import { ProjectRolesModule } from '../src/modules/project-roles/project-roles.module' import { ProjectRolesService } from '../src/modules/project-roles/project-roles.service' -import { getDotenvPaths } from '../src/utils/dotenv.utils' const canRunProjectRolesE2E = Boolean(process.env.E2E) @@ -32,7 +31,7 @@ describeWithProjectRoles('ProjectRolesService (e2e)', () => { beforeAll(async () => { moduleRef = await Test.createTestingModule({ - imports: [ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule, ProjectRolesModule], + imports: [ConfigModule.forRoot({ isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule, ProjectRolesModule], }).compile() await moduleRef.init() diff --git a/apps/server-nestjs/test/project-secrets.e2e-spec.ts b/apps/server-nestjs/test/project-secrets.e2e-spec.ts index 6a3165c448..45009044ff 100644 --- a/apps/server-nestjs/test/project-secrets.e2e-spec.ts +++ b/apps/server-nestjs/test/project-secrets.e2e-spec.ts @@ -17,7 +17,6 @@ import { ProjectSecretsModule } from '../src/modules/project-secrets/project-sec import { ProjectSecretsService } from '../src/modules/project-secrets/project-secrets.service' import { VaultClientService } from '../src/modules/vault/vault-client.service' import { generateProjectPath } from '../src/modules/vault/vault.utils' -import { getDotenvPaths } from '../src/utils/dotenv.utils' const canRunProjectSecretsE2E = Boolean(process.env.E2E) @@ -47,7 +46,7 @@ describeWithProjectSecrets('ProjectSecretsService (e2e)', () => { vi.stubEnv('USE_VAULT', 'true') moduleRef = await Test.createTestingModule({ - imports: [ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule, ProjectSecretsModule], + imports: [ConfigModule.forRoot({ isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule, ProjectSecretsModule], }).compile() await moduleRef.init() diff --git a/apps/server-nestjs/test/project-services.e2e-spec.ts b/apps/server-nestjs/test/project-services.e2e-spec.ts index 95df8901a6..cee441d1f3 100644 --- a/apps/server-nestjs/test/project-services.e2e-spec.ts +++ b/apps/server-nestjs/test/project-services.e2e-spec.ts @@ -14,7 +14,6 @@ import { PermissionModule } from '../src/modules/infrastructure/permission/permi import { NEXUS_CONFIG_KEY_ACTIVATE_NPM_REPO, PLUGIN_NAME } from '../src/modules/nexus/nexus.constants' import { ProjectServicesModule } from '../src/modules/project-services/project-services.module' import { ProjectServicesService } from '../src/modules/project-services/project-services.service' -import { getDotenvPaths } from '../src/utils/dotenv.utils' const canRunProjectServicesE2E = Boolean(process.env.E2E) const describeWithProjectServices = describe.runIf(canRunProjectServicesE2E) @@ -32,7 +31,7 @@ describeWithProjectServices('ProjectServicesService (e2e)', () => { vi.stubEnv('USE_NEXUS', 'true') moduleRef = await Test.createTestingModule({ - imports: [ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule, ProjectServicesModule], + imports: [ConfigModule.forRoot({ isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule, ProjectServicesModule], }).compile() await moduleRef.init() diff --git a/apps/server-nestjs/test/project.e2e-spec.ts b/apps/server-nestjs/test/project.e2e-spec.ts index 0a670aa45a..eb28668c78 100644 --- a/apps/server-nestjs/test/project.e2e-spec.ts +++ b/apps/server-nestjs/test/project.e2e-spec.ts @@ -16,7 +16,6 @@ import { PermissionModule } from '../src/modules/infrastructure/permission/permi import { makeCreateProjectBody } from '../src/modules/project/project-testing.utils' import { ProjectModule } from '../src/modules/project/project.module' import { ProjectService } from '../src/modules/project/project.service' -import { getDotenvPaths } from '../src/utils/dotenv.utils' const canRunProjectE2E = Boolean(process.env.E2E) @@ -32,7 +31,7 @@ describeWithProject('ProjectService (e2e)', () => { beforeAll(async () => { moduleRef = await Test.createTestingModule({ - imports: [ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule, ProjectModule], + imports: [ConfigModule.forRoot({ isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule, ProjectModule], }).compile() await moduleRef.init() diff --git a/apps/server-nestjs/test/registry.e2e-spec.ts b/apps/server-nestjs/test/registry.e2e-spec.ts index 5f0b982883..38f9315a2c 100644 --- a/apps/server-nestjs/test/registry.e2e-spec.ts +++ b/apps/server-nestjs/test/registry.e2e-spec.ts @@ -16,7 +16,6 @@ import { RegistryModule } from '../src/modules/registry/registry.module' import { RegistryService } from '../src/modules/registry/registry.service' import { getHostFromUrl, getProjectVaultPath } from '../src/modules/registry/registry.utils' import { VaultClientService } from '../src/modules/vault/vault-client.service' -import { getDotenvPaths } from '../src/utils/dotenv.utils' import { getAll } from '../src/utils/iterable.utils' const canRunRegistryE2E @@ -36,7 +35,7 @@ describeWithRegistry('RegistryService (e2e)', () => { beforeAll(async () => { moduleRef = await Test.createTestingModule({ - imports: [ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory, harborConfigFactory] }), RegistryModule, EventsModule], + imports: [ConfigModule.forRoot({ isGlobal: true, load: [baseConfigFactory, harborConfigFactory] }), RegistryModule, EventsModule], }) .compile() diff --git a/apps/server-nestjs/test/sonarqube.e2e-spec.ts b/apps/server-nestjs/test/sonarqube.e2e-spec.ts index 5daf1616c2..5451d06589 100644 --- a/apps/server-nestjs/test/sonarqube.e2e-spec.ts +++ b/apps/server-nestjs/test/sonarqube.e2e-spec.ts @@ -23,7 +23,6 @@ import { SonarqubeModule } from '../src/modules/sonarqube/sonarqube.module' import { SonarqubeService } from '../src/modules/sonarqube/sonarqube.service' import { VaultClientService } from '../src/modules/vault/vault-client.service' import { VaultModule } from '../src/modules/vault/vault.module' -import { getDotenvPaths } from '../src/utils/dotenv.utils' import { getAll } from '../src/utils/iterable.utils' import { SONARQUBE_PROJECT_TIMEOUT } from './constants' @@ -49,7 +48,7 @@ describeWithSonarqube('SonarqubeService (e2e)', () => { beforeAll(async () => { moduleRef = await Test.createTestingModule({ - imports: [SonarqubeModule, GitlabModule, VaultModule, ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory, gitlabConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule], + imports: [SonarqubeModule, GitlabModule, VaultModule, ConfigModule.forRoot({ isGlobal: true, load: [baseConfigFactory, gitlabConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule], }).compile() await moduleRef.init() diff --git a/apps/server-nestjs/test/user-tokens.e2e-spec.ts b/apps/server-nestjs/test/user-tokens.e2e-spec.ts index bb7f98e826..a3bfa41eba 100644 --- a/apps/server-nestjs/test/user-tokens.e2e-spec.ts +++ b/apps/server-nestjs/test/user-tokens.e2e-spec.ts @@ -13,7 +13,6 @@ import { PermissionModule } from '../src/modules/infrastructure/permission/permi import { UserTokensModule } from '../src/modules/user-tokens/user-tokens.module' import { UserTokensService } from '../src/modules/user-tokens/user-tokens.service' import { hashToken } from '../src/utils/crypto.utils' -import { getDotenvPaths } from '../src/utils/dotenv.utils' const canRunUserTokensE2E = Boolean(process.env.E2E) @@ -31,7 +30,7 @@ describeWithUserTokens('UserTokensService (e2e)', () => { beforeAll(async () => { moduleRef = await Test.createTestingModule({ - imports: [ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, LoggerModule, PermissionModule, UserTokensModule], + imports: [ConfigModule.forRoot({ isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, LoggerModule, PermissionModule, UserTokensModule], }).compile() await moduleRef.init() diff --git a/apps/server-nestjs/test/vault.e2e-spec.ts b/apps/server-nestjs/test/vault.e2e-spec.ts index 8404cbcdcf..010dec4e72 100644 --- a/apps/server-nestjs/test/vault.e2e-spec.ts +++ b/apps/server-nestjs/test/vault.e2e-spec.ts @@ -15,7 +15,6 @@ import { VaultClientService } from '../src/modules/vault/vault-client.service' import { projectSelect } from '../src/modules/vault/vault-datastore.service' import { makeProjectWithDetails } from '../src/modules/vault/vault-testing.utils' import { VaultModule } from '../src/modules/vault/vault.module' -import { getDotenvPaths } from '../src/utils/dotenv.utils' import { VAULT_PROVISION_TIMEOUT } from './constants' const canRunVaultE2E @@ -35,7 +34,7 @@ describeWithVault('VaultService (e2e)', () => { beforeAll(async () => { moduleRef = await Test.createTestingModule({ - imports: [VaultModule, ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule], + imports: [VaultModule, ConfigModule.forRoot({ isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule], }).compile() await moduleRef.init() diff --git a/apps/server-nestjs/test/zone.e2e-spec.ts b/apps/server-nestjs/test/zone.e2e-spec.ts index f13978f7f1..8b6b8fcc41 100644 --- a/apps/server-nestjs/test/zone.e2e-spec.ts +++ b/apps/server-nestjs/test/zone.e2e-spec.ts @@ -14,7 +14,6 @@ import { PermissionModule } from '../src/modules/infrastructure/permission/permi import { VaultClientService } from '../src/modules/vault/vault-client.service' import { VaultModule } from '../src/modules/vault/vault.module' import { VaultService } from '../src/modules/vault/vault.service' -import { getDotenvPaths } from '../src/utils/dotenv.utils' import { VAULT_PROVISION_TIMEOUT } from './constants' const canRunZoneE2E = Boolean(process.env.E2E) @@ -33,7 +32,7 @@ describeWithZone('Zone lifecycle (e2e)', () => { beforeAll(async () => { moduleRef = await Test.createTestingModule({ - imports: [VaultModule, ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule], + imports: [VaultModule, ConfigModule.forRoot({ isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule], }).compile() await moduleRef.init() diff --git a/ci/scripts/check-compose-environment.spec.sh b/ci/scripts/check-compose-environment.spec.sh new file mode 100755 index 0000000000..268472b23f --- /dev/null +++ b/ci/scripts/check-compose-environment.spec.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +set -euo pipefail + +cd "$(git rev-parse --show-toplevel)" + +# Use demonstration CI secrets even for dev/integ to avoid reading local pass entries. +for profile in docker ci integ; do + compose_profile="$profile" + [ "$profile" != docker ] || compose_profile=dev + KUBECONFIG_HOST_PATH=/tmp/console-test-kubeconfig EXTERNAL_PLUGINS_DIR_HOST_PATH=/tmp/console-test-plugins \ + mise -E "$profile" exec -- fnox -P ci exec -- \ + docker compose -f "docker/docker-compose.$compose_profile.yml" config --format json | + node --input-type=module -e ' + import assert from "node:assert/strict" + let input = "" + for await (const chunk of process.stdin) input += chunk + const environment = JSON.parse(input).services["server-nestjs"].environment + for (const plugin of ["ARGOCD", "GITLAB", "HARBOR", "NEXUS", "OBSERVABILITY", "SONARQUBE", "VAULT"]) { + assert.equal(environment["USE_" + plugin], "false", "USE_" + plugin + " must be explicitly disabled") + } + assert.equal(environment.USE_SERVICE_CHAIN, process.argv[1] === "integ" ? "false" : "true") + if (process.argv[1] === "ci") { + assert.equal(environment.NODE_ENV, "production") + assert.equal(environment.KEYCLOAK_DOMAIN, "keycloak:8080") + assert.equal(new URL(environment.DB_URL).hostname, "postgres") + assert.equal(JSON.parse(input).services.client.environment.SERVER_HOST, "nginx-strangler") + } + ' "$profile" +done +printf 'Compose plugin environment regression tests passed.\n' diff --git a/ci/scripts/check-environment-files.sh b/ci/scripts/check-environment-files.sh new file mode 100755 index 0000000000..096e4bd9d6 --- /dev/null +++ b/ci/scripts/check-environment-files.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -euo pipefail + +unexpected="$(git ls-files | grep -E '(^|/)\.env[^/]*$' | grep -v '^apps/server/\.env' || true)" +if [ -n "$unexpected" ]; then + printf 'Unexpected tracked environment files outside apps/server:\n%s\n' "$unexpected" >&2 + exit 1 +fi + +unexpected_loaders="$(grep -RInE 'getDotenvPaths|dotenv\.utils|loadEnvFile|envFilePath' \ + apps/server-nestjs/src apps/server-nestjs/test apps/client/vite.config.ts playwright/playwright.config.ts || true)" +if [ -n "$unexpected_loaders" ]; then + printf 'Unexpected dotenv consumers outside apps/server:\n%s\n' "$unexpected_loaders" >&2 + exit 1 +fi diff --git a/ci/scripts/check-workflow-environment.spec.sh b/ci/scripts/check-workflow-environment.spec.sh new file mode 100755 index 0000000000..de20bba05d --- /dev/null +++ b/ci/scripts/check-workflow-environment.spec.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +set -euo pipefail + +cd "$(git rev-parse --show-toplevel)" + +workflow=.github/workflows/job-playwright.yml +if grep -n 'docker compose' "$workflow" | grep -v 'name:' | grep -vF 'mise -E ci exec -- fnox exec -- docker compose'; then + printf 'Every Playwright Compose command must resolve CI configuration and secrets.\n' >&2 + exit 1 +fi +test "$(grep -cF 'mise -E ci exec -- fnox exec -- docker compose' "$workflow")" -eq 4 +grep -A 2 'name: Clean up docker resources' "$workflow" | grep -Fq "if: \${{ always() }}" +if grep -Rn 'NODE_VERSION' .github/workflows; then + printf 'Node must be pinned only by mise, not by unused workflow inputs.\n' >&2 + exit 1 +fi +printf 'Workflow environment regression tests passed.\n' diff --git a/ci/scripts/create-image-manifest.sh b/ci/scripts/create-image-manifest.sh new file mode 100755 index 0000000000..d3a88dbe4f --- /dev/null +++ b/ci/scripts/create-image-manifest.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -euo pipefail + +retry_delays=(10 60 300) + +for delay in "${retry_delays[@]}"; do + if "$@"; then + exit 0 + fi + + printf 'Image manifest creation failed; retrying in %s seconds.\n' "$delay" >&2 + sleep "$delay" +done + +exec "$@" diff --git a/ci/scripts/create-image-manifest.spec.sh b/ci/scripts/create-image-manifest.spec.sh new file mode 100755 index 0000000000..26ab286fea --- /dev/null +++ b/ci/scripts/create-image-manifest.spec.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +set -euo pipefail + +project_dir="$(git rev-parse --show-toplevel)" +tmp_dir="$(mktemp -d)" +trap 'rm -rf "$tmp_dir"' EXIT +mkdir -p "$tmp_dir/bin" + +cat > "$tmp_dir/bin/docker" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +printf '%s\n' "$*" >> "$DOCKER_CALLS" +attempts="$(wc -l < "$DOCKER_CALLS")" +if [ "$attempts" -le "$DOCKER_FAILURES" ]; then + exit 1 +fi +EOF + +cat > "$tmp_dir/bin/sleep" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +printf '%s\n' "$*" >> "$SLEEP_CALLS" +EOF + +chmod +x "$tmp_dir/bin/docker" "$tmp_dir/bin/sleep" +export PATH="$tmp_dir/bin:$PATH" + +run_manifest() { + local failures="$1" + export DOCKER_FAILURES="$failures" + export DOCKER_CALLS="$tmp_dir/docker-$failures.calls" + export SLEEP_CALLS="$tmp_dir/sleep-$failures.calls" + + "$project_dir/ci/scripts/create-image-manifest.sh" docker buildx imagetools create image:tag image@sha256:digest +} + +run_manifest 3 + +test "$(wc -l < "$tmp_dir/docker-3.calls")" -eq 4 +grep -Fqx '10' "$tmp_dir/sleep-3.calls" +grep -Fqx '60' "$tmp_dir/sleep-3.calls" +grep -Fqx '300' "$tmp_dir/sleep-3.calls" + +if run_manifest 4; then + printf 'Manifest creation unexpectedly succeeded after all retries.\n' >&2 + exit 1 +fi + +test "$(wc -l < "$tmp_dir/docker-4.calls")" -eq 4 +test "$(wc -l < "$tmp_dir/sleep-4.calls")" -eq 3 diff --git a/ci/scripts/init-env.sh b/ci/scripts/init-env.sh index 0c6c5d3feb..0b69a9288a 100755 --- a/ci/scripts/init-env.sh +++ b/ci/scripts/init-env.sh @@ -12,7 +12,8 @@ i=1 PROJECT_DIR="$(git rev-parse --show-toplevel)" # Declare script helper -TEXT_HELPER="\nThis script aims to copy .env-example files into .env files at project initialization. +TEXT_HELPER="\nTemporary bridge for the frozen apps/server legacy: copies its .env-example files into .env files. +Remove this script when apps/server is removed from the repository. Following flags are available: -h Print script help\n\n" @@ -32,12 +33,14 @@ do done -find $PROJECT_DIR -type f -name ".env*-example" -or -name "*-example.yaml" | while read f; do - if [ ! -f "${f/-example/}" ]; then - printf "\n${red}Copy${no_color}: '$f' - ${red}to${no_color}: '${f/-example/}'\n" - cp "$f" "${f/-example/}" +# Temporary bridge for the frozen Fastify legacy only. +# Remove this script when apps/server is removed from the repository. +find "$PROJECT_DIR/apps/server" -type f -name ".env*-example" -print0 | while IFS= read -r -d '' file; do + target="${file/-example/}" + if [ ! -f "$target" ]; then + printf "\n${red}Copy${no_color}: '%s'\n${red}to${no_color}: '%s'\n" "$file" "$target" + cp "$file" "$target" else - printf "\nFile '${f/-example/}' already exists\n" + printf "\nFile '%s' already exists\n" "$target" fi done diff --git a/ci/scripts/run-unit-tests.sh b/ci/scripts/run-unit-tests.sh new file mode 100755 index 0000000000..7da22a5888 --- /dev/null +++ b/ci/scripts/run-unit-tests.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -euo pipefail + +project_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +exec "$project_dir/scripts/with-nestjs-env.sh" pnpm test:cov diff --git a/ci/scripts/run-unit-tests.spec.sh b/ci/scripts/run-unit-tests.spec.sh new file mode 100755 index 0000000000..d12b817ab7 --- /dev/null +++ b/ci/scripts/run-unit-tests.spec.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash + +set -euo pipefail + +project_dir="$(git rev-parse --show-toplevel)" +tmp_dir="$(mktemp -d)" +trap 'rm -rf "$tmp_dir"' EXIT +mkdir -p "$tmp_dir/bin" + +cat > "$tmp_dir/bin/fnox" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +[ "$1" = "exec" ] +shift +exec "$@" +EOF + +cat > "$tmp_dir/bin/pnpm" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +printf '%s\n' "$*" > "$PNPM_CALL" +env | sort > "$PNPM_ENV" +EOF + +chmod +x "$tmp_dir/bin/fnox" "$tmp_dir/bin/pnpm" +export PATH="$tmp_dir/bin:$PATH" +export PNPM_CALL="$tmp_dir/pnpm.call" +export PNPM_ENV="$tmp_dir/pnpm.env" + +unset KEYCLOAK_PROTOCOL KEYCLOAK_DOMAIN KEYCLOAK_REALM KEYCLOAK_CLIENT_ID KEYCLOAK_REDIRECT_URI CONTACT_EMAIL || true +export NESTJS_SERVER_HOST='0.0.0.0' +export NESTJS_SERVER_PORT='3001' +export NESTJS_KEYCLOAK_PROTOCOL='http' +export NESTJS_KEYCLOAK_DOMAIN='keycloak:8080' +export NESTJS_KEYCLOAK_REALM='dso' +export NESTJS_KEYCLOAK_CLIENT_ID='dso-console-backend' +export NESTJS_KEYCLOAK_REDIRECT_URI='http://localhost:8080' +export NESTJS_CONTACT_EMAIL='cloudpinative-relations@interieur.gouv.fr' +export NESTJS_GRAFANA_URL='https://grafana.example.com' +export NESTJS_DSO_OBSERVABILITY_CHART_VERSION='dso-observability-0.1.7' +export NESTJS_FUTURE_PUBLIC_VALUE='available' + +"$project_dir/ci/scripts/run-unit-tests.sh" + +grep -Fqx 'test:cov' "$PNPM_CALL" +grep -Fqx 'SERVER_HOST=0.0.0.0' "$PNPM_ENV" +grep -Fqx 'SERVER_PORT=3001' "$PNPM_ENV" +grep -Fqx 'KEYCLOAK_PROTOCOL=http' "$PNPM_ENV" +grep -Fqx 'KEYCLOAK_DOMAIN=keycloak:8080' "$PNPM_ENV" +grep -Fqx 'KEYCLOAK_REALM=dso' "$PNPM_ENV" +grep -Fqx 'KEYCLOAK_CLIENT_ID=dso-console-backend' "$PNPM_ENV" +grep -Fqx 'KEYCLOAK_REDIRECT_URI=http://localhost:8080' "$PNPM_ENV" +grep -Fqx 'CONTACT_EMAIL=cloudpinative-relations@interieur.gouv.fr' "$PNPM_ENV" +grep -Fqx 'GRAFANA_URL=https://grafana.example.com' "$PNPM_ENV" +grep -Fqx 'DSO_OBSERVABILITY_CHART_VERSION=dso-observability-0.1.7' "$PNPM_ENV" +grep -Fqx 'FUTURE_PUBLIC_VALUE=available' "$PNPM_ENV" diff --git a/docker/docker-compose.ci.yml b/docker/docker-compose.ci.yml index 10ce41a23b..48a44535fd 100644 --- a/docker/docker-compose.ci.yml +++ b/docker/docker-compose.ci.yml @@ -86,15 +86,35 @@ services: condition: service_healthy opencds-mockoon: condition: service_healthy - env_file: - - ../apps/server-nestjs/.env.docker environment: - NODE_ENV: 'production' - CI: 'true' - DEV_SETUP: 'true' - OPENCDS_URL: 'http://dso-console_opencds-mockoon:3100/api/v1' - OPENCDS_API_TOKEN: 'token' - OPENCDS_API_TLS_REJECT_UNAUTHORIZED: 'false' + NODE_ENV: ${NODE_ENV:?NODE_ENV is required} + CI: ${CI:-false} + DEV_SETUP: ${DEV_SETUP:-false} + SERVER_HOST: ${NESTJS_SERVER_HOST:?NESTJS_SERVER_HOST is required} + SERVER_PORT: ${NESTJS_SERVER_PORT:?NESTJS_SERVER_PORT is required} + DB_URL: ${DB_URL:?DB_URL is required} + SESSION_SECRET: ${SESSION_SECRET:?SESSION_SECRET is required} + KEYCLOAK_PROTOCOL: ${NESTJS_KEYCLOAK_PROTOCOL:?NESTJS_KEYCLOAK_PROTOCOL is required} + KEYCLOAK_DOMAIN: ${NESTJS_KEYCLOAK_DOMAIN:?NESTJS_KEYCLOAK_DOMAIN is required} + KEYCLOAK_REALM: ${NESTJS_KEYCLOAK_REALM:?NESTJS_KEYCLOAK_REALM is required} + KEYCLOAK_CLIENT_ID: ${NESTJS_KEYCLOAK_CLIENT_ID:?NESTJS_KEYCLOAK_CLIENT_ID is required} + KEYCLOAK_CLIENT_SECRET: ${KEYCLOAK_CLIENT_SECRET:?KEYCLOAK_CLIENT_SECRET is required} + KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN:?KEYCLOAK_ADMIN is required} + KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:?KEYCLOAK_ADMIN_PASSWORD is required} + KEYCLOAK_REDIRECT_URI: ${NESTJS_KEYCLOAK_REDIRECT_URI:?NESTJS_KEYCLOAK_REDIRECT_URI is required} + CONTACT_EMAIL: ${NESTJS_CONTACT_EMAIL:?NESTJS_CONTACT_EMAIL is required} + USE_ARGOCD: ${NESTJS_USE_ARGOCD:-false} + USE_GITLAB: ${NESTJS_USE_GITLAB:-false} + USE_HARBOR: ${NESTJS_USE_HARBOR:-false} + USE_NEXUS: ${NESTJS_USE_NEXUS:-false} + USE_OBSERVABILITY: ${NESTJS_USE_OBSERVABILITY:-false} + USE_SERVICE_CHAIN: ${NESTJS_USE_SERVICE_CHAIN:-false} + USE_SONARQUBE: ${NESTJS_USE_SONARQUBE:-false} + USE_VAULT: ${NESTJS_USE_VAULT:-false} + OPENCDS_URL: ${NESTJS_OPENCDS_URL:-} + OPENCDS_INTERNAL_URL: ${NESTJS_OPENCDS_INTERNAL_URL:-} + OPENCDS_API_TOKEN: ${OPENCDS_API_TOKEN:?OPENCDS_API_TOKEN is required} + OPENCDS_API_TLS_REJECT_UNAUTHORIZED: ${NESTJS_OPENCDS_API_TLS_REJECT_UNAUTHORIZED:-true} networks: - dso-network @@ -131,10 +151,17 @@ services: user: root volumes: - ../apps/client/nginx/default.docker.conf:/etc/nginx/conf.d/default.conf - env_file: - - ../apps/client/.env.docker environment: - OPENCDS_ENABLED: 'true' + SERVER_HOST: ${CLIENT_SERVER_HOST:?CLIENT_SERVER_HOST is required} + SERVER_PORT: ${CLIENT_SERVER_PORT:?CLIENT_SERVER_PORT is required} + CLIENT_PORT: ${CLIENT_PORT:-8080} + KEYCLOAK_PROTOCOL: ${CLIENT_KEYCLOAK_PROTOCOL:?CLIENT_KEYCLOAK_PROTOCOL is required} + KEYCLOAK_DOMAIN: ${CLIENT_KEYCLOAK_DOMAIN:?CLIENT_KEYCLOAK_DOMAIN is required} + KEYCLOAK_REALM: ${CLIENT_KEYCLOAK_REALM:?CLIENT_KEYCLOAK_REALM is required} + KEYCLOAK_CLIENT_ID: ${CLIENT_KEYCLOAK_CLIENT_ID:?CLIENT_KEYCLOAK_CLIENT_ID is required} + KEYCLOAK_REDIRECT_URI: ${CLIENT_KEYCLOAK_REDIRECT_URI:?CLIENT_KEYCLOAK_REDIRECT_URI is required} + OPENCDS_ENABLED: ${CLIENT_OPENCDS_ENABLED:-false} + CONTACT_EMAIL: ${CLIENT_CONTACT_EMAIL:?CLIENT_CONTACT_EMAIL is required} networks: - dso-network diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index f09196aaf0..bf8970c30c 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -134,8 +134,33 @@ services: condition: service_healthy opencds-mockoon: condition: service_healthy - env_file: - - ../apps/server-nestjs/.env.docker + environment: + NODE_ENV: ${NODE_ENV:?NODE_ENV is required} + SERVER_HOST: ${NESTJS_SERVER_HOST:?NESTJS_SERVER_HOST is required} + SERVER_PORT: ${NESTJS_SERVER_PORT:?NESTJS_SERVER_PORT is required} + DB_URL: ${DB_URL:?DB_URL is required} + SESSION_SECRET: ${SESSION_SECRET:?SESSION_SECRET is required} + KEYCLOAK_PROTOCOL: ${NESTJS_KEYCLOAK_PROTOCOL:?NESTJS_KEYCLOAK_PROTOCOL is required} + KEYCLOAK_DOMAIN: ${NESTJS_KEYCLOAK_DOMAIN:?NESTJS_KEYCLOAK_DOMAIN is required} + KEYCLOAK_REALM: ${NESTJS_KEYCLOAK_REALM:?NESTJS_KEYCLOAK_REALM is required} + KEYCLOAK_CLIENT_ID: ${NESTJS_KEYCLOAK_CLIENT_ID:?NESTJS_KEYCLOAK_CLIENT_ID is required} + KEYCLOAK_CLIENT_SECRET: ${KEYCLOAK_CLIENT_SECRET:?KEYCLOAK_CLIENT_SECRET is required} + KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN:?KEYCLOAK_ADMIN is required} + KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:?KEYCLOAK_ADMIN_PASSWORD is required} + KEYCLOAK_REDIRECT_URI: ${NESTJS_KEYCLOAK_REDIRECT_URI:?NESTJS_KEYCLOAK_REDIRECT_URI is required} + CONTACT_EMAIL: ${NESTJS_CONTACT_EMAIL:?NESTJS_CONTACT_EMAIL is required} + USE_ARGOCD: ${NESTJS_USE_ARGOCD:-false} + USE_GITLAB: ${NESTJS_USE_GITLAB:-false} + USE_HARBOR: ${NESTJS_USE_HARBOR:-false} + USE_NEXUS: ${NESTJS_USE_NEXUS:-false} + USE_OBSERVABILITY: ${NESTJS_USE_OBSERVABILITY:-false} + USE_SERVICE_CHAIN: ${NESTJS_USE_SERVICE_CHAIN:-false} + USE_SONARQUBE: ${NESTJS_USE_SONARQUBE:-false} + USE_VAULT: ${NESTJS_USE_VAULT:-false} + OPENCDS_URL: ${NESTJS_OPENCDS_URL:-} + OPENCDS_INTERNAL_URL: ${NESTJS_OPENCDS_INTERNAL_URL:-} + OPENCDS_API_TOKEN: ${OPENCDS_API_TOKEN:?OPENCDS_API_TOKEN is required} + OPENCDS_API_TLS_REJECT_UNAUTHORIZED: ${NESTJS_OPENCDS_API_TLS_REJECT_UNAUTHORIZED:-true} develop: watch: - action: sync @@ -202,8 +227,17 @@ services: condition: service_started ports: - 8080:8080 - env_file: - - ../apps/client/.env.docker + environment: + SERVER_HOST: ${CLIENT_SERVER_HOST:?CLIENT_SERVER_HOST is required} + SERVER_PORT: ${CLIENT_SERVER_PORT:?CLIENT_SERVER_PORT is required} + CLIENT_PORT: ${CLIENT_PORT:-8080} + KEYCLOAK_PROTOCOL: ${CLIENT_KEYCLOAK_PROTOCOL:?CLIENT_KEYCLOAK_PROTOCOL is required} + KEYCLOAK_DOMAIN: ${CLIENT_KEYCLOAK_DOMAIN:?CLIENT_KEYCLOAK_DOMAIN is required} + KEYCLOAK_REALM: ${CLIENT_KEYCLOAK_REALM:?CLIENT_KEYCLOAK_REALM is required} + KEYCLOAK_CLIENT_ID: ${CLIENT_KEYCLOAK_CLIENT_ID:?CLIENT_KEYCLOAK_CLIENT_ID is required} + KEYCLOAK_REDIRECT_URI: ${CLIENT_KEYCLOAK_REDIRECT_URI:?CLIENT_KEYCLOAK_REDIRECT_URI is required} + OPENCDS_ENABLED: ${CLIENT_OPENCDS_ENABLED:-false} + CONTACT_EMAIL: ${CLIENT_CONTACT_EMAIL:?CLIENT_CONTACT_EMAIL is required} develop: watch: - action: sync diff --git a/docker/docker-compose.integ.yml b/docker/docker-compose.integ.yml index d023ad3698..be2ed30106 100644 --- a/docker/docker-compose.integ.yml +++ b/docker/docker-compose.integ.yml @@ -110,9 +110,33 @@ services: volumes: - ${KUBECONFIG_HOST_PATH}:${KUBECONFIG_PATH:-/home/node/.kube/config} - ${EXTERNAL_PLUGINS_DIR_HOST_PATH}:/plugins - env_file: - - ../apps/server-nestjs/.env.docker - - ../apps/server-nestjs/.env.integ + environment: + NODE_ENV: ${NODE_ENV:?NODE_ENV is required} + SERVER_HOST: ${NESTJS_SERVER_HOST:?NESTJS_SERVER_HOST is required} + SERVER_PORT: ${NESTJS_SERVER_PORT:?NESTJS_SERVER_PORT is required} + DB_URL: ${DB_URL:?DB_URL is required} + SESSION_SECRET: ${SESSION_SECRET:?SESSION_SECRET is required} + KEYCLOAK_PROTOCOL: ${NESTJS_KEYCLOAK_PROTOCOL:?NESTJS_KEYCLOAK_PROTOCOL is required} + KEYCLOAK_DOMAIN: ${NESTJS_KEYCLOAK_DOMAIN:?NESTJS_KEYCLOAK_DOMAIN is required} + KEYCLOAK_REALM: ${NESTJS_KEYCLOAK_REALM:?NESTJS_KEYCLOAK_REALM is required} + KEYCLOAK_CLIENT_ID: ${NESTJS_KEYCLOAK_CLIENT_ID:?NESTJS_KEYCLOAK_CLIENT_ID is required} + KEYCLOAK_CLIENT_SECRET: ${KEYCLOAK_CLIENT_SECRET:?KEYCLOAK_CLIENT_SECRET is required} + KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN:?KEYCLOAK_ADMIN is required} + KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:?KEYCLOAK_ADMIN_PASSWORD is required} + KEYCLOAK_REDIRECT_URI: ${NESTJS_KEYCLOAK_REDIRECT_URI:?NESTJS_KEYCLOAK_REDIRECT_URI is required} + CONTACT_EMAIL: ${NESTJS_CONTACT_EMAIL:?NESTJS_CONTACT_EMAIL is required} + USE_ARGOCD: ${NESTJS_USE_ARGOCD:-false} + USE_GITLAB: ${NESTJS_USE_GITLAB:-false} + USE_HARBOR: ${NESTJS_USE_HARBOR:-false} + USE_NEXUS: ${NESTJS_USE_NEXUS:-false} + USE_OBSERVABILITY: ${NESTJS_USE_OBSERVABILITY:-false} + USE_SERVICE_CHAIN: ${NESTJS_USE_SERVICE_CHAIN:-false} + USE_SONARQUBE: ${NESTJS_USE_SONARQUBE:-false} + USE_VAULT: ${NESTJS_USE_VAULT:-false} + OPENCDS_URL: ${NESTJS_OPENCDS_URL:-} + OPENCDS_INTERNAL_URL: ${NESTJS_OPENCDS_INTERNAL_URL:-} + OPENCDS_API_TOKEN: ${OPENCDS_API_TOKEN:?OPENCDS_API_TOKEN is required} + OPENCDS_API_TLS_REJECT_UNAUTHORIZED: ${NESTJS_OPENCDS_API_TLS_REJECT_UNAUTHORIZED:-true} develop: watch: - action: sync @@ -179,9 +203,17 @@ services: condition: service_started ports: - 8080:8080 - env_file: - - ../apps/client/.env.docker - - ../apps/client/.env.integ + environment: + SERVER_HOST: ${CLIENT_SERVER_HOST:?CLIENT_SERVER_HOST is required} + SERVER_PORT: ${CLIENT_SERVER_PORT:?CLIENT_SERVER_PORT is required} + CLIENT_PORT: ${CLIENT_PORT:-8080} + KEYCLOAK_PROTOCOL: ${CLIENT_KEYCLOAK_PROTOCOL:?CLIENT_KEYCLOAK_PROTOCOL is required} + KEYCLOAK_DOMAIN: ${CLIENT_KEYCLOAK_DOMAIN:?CLIENT_KEYCLOAK_DOMAIN is required} + KEYCLOAK_REALM: ${CLIENT_KEYCLOAK_REALM:?CLIENT_KEYCLOAK_REALM is required} + KEYCLOAK_CLIENT_ID: ${CLIENT_KEYCLOAK_CLIENT_ID:?CLIENT_KEYCLOAK_CLIENT_ID is required} + KEYCLOAK_REDIRECT_URI: ${CLIENT_KEYCLOAK_REDIRECT_URI:?CLIENT_KEYCLOAK_REDIRECT_URI is required} + OPENCDS_ENABLED: ${CLIENT_OPENCDS_ENABLED:-false} + CONTACT_EMAIL: ${CLIENT_CONTACT_EMAIL:?CLIENT_CONTACT_EMAIL is required} develop: watch: - action: sync diff --git a/docs/adr/0001-mise-fnox-environment-management.md b/docs/adr/0001-mise-fnox-environment-management.md new file mode 100644 index 0000000000..13ed95ce1f --- /dev/null +++ b/docs/adr/0001-mise-fnox-environment-management.md @@ -0,0 +1,24 @@ +# ADR 0001 — mise et fnox pour la configuration et les secrets + +## Statut + +Proposé pour validation en séance plénière. + +## Contexte + +La Console mélange configuration et secrets dans des fichiers `.env*` lus par les applications, Docker Compose, Prisma et la CI. Les sources et les précédences diffèrent selon le mode d’exécution, rendant la configuration difficile à auditer et à reproduire. + +## Décision + +`mise` est l’unique point d’entrée supporté et fournit les outils, tâches et variables publiques. `fnox` injecte les secrets uniquement dans le processus enfant de `fnox exec`. Le fournisseur local par défaut est `pass`, sous `fnox/console/`. Les profils CI lisent uniquement les secrets nécessaires au processus qui les reçoit depuis GitHub Actions et n’installent pas `pass`. + +Les fichiers `.env*` sont interdits hors de `apps/server`. Le legacy Fastify est gelé : ses fichiers, chargeurs et `env_file` Compose restent inchangés jusqu’à sa suppression du dépôt. + +## Conséquences + +Les appels directs à `pnpm` et `docker compose` ne sont plus des points d’entrée documentés. Le client ne reçoit qu’une liste blanche de variables publiques. Un changement de fournisseur fnox reste localisé aux profils fnox et ne change pas les tâches applicatives. + +## Alternatives écartées + +- Conserver les `.env*` : ne résout ni la multiplicité des sources, ni la confusion entre secrets et configuration. +- Nix et SecretSpec : alternative retenue seulement si `mise` et `fnox` ne répondent plus au besoin ; elle est hors périmètre de cette migration. diff --git a/docs/environment-variables.md b/docs/environment-variables.md new file mode 100644 index 0000000000..ec5bf337e9 --- /dev/null +++ b/docs/environment-variables.md @@ -0,0 +1,25 @@ +# Registre des variables d’environnement + +Ce registre est la source de vérité de la classification des variables hors legacy. Une nouvelle variable doit être ajoutée ici avant d’être exposée par `mise`, `fnox`, Docker Compose ou GitHub Actions. Les noms du tableau sont les contrats des processus ; `mise` préfixe les valeurs propres à un consommateur (`CLIENT_*` ou `NESTJS_*`) et les tâches ou Compose les traduisent explicitement afin d’éviter les collisions frontend/backend. + +| Classe | Variables | Source | Consommateurs | +| --- | --- | --- | --- | +| Configuration publique commune | `NODE_ENV`, `CI`, `APP_VERSION`, `SERVER_HOST`, `SERVER_PORT`, `NESTJS_SERVER_HOST`, `NESTJS_SERVER_PORT`, `CONTACT_EMAIL`, `PROJECTS_ROOT_DIR`, `LOG_LEVEL` | `mise` | client, server-nestjs, Docker Compose, CI | +| Configuration publique navigateur | `SERVER_HOST`, `SERVER_PORT`, `CLIENT_PORT`, `KEYCLOAK_PROTOCOL`, `KEYCLOAK_DOMAIN`, `KEYCLOAK_REALM`, `KEYCLOAK_CLIENT_ID`, `KEYCLOAK_REDIRECT_URI`, `OPENCDS_ENABLED`, `CONTACT_EMAIL` | `mise` | client | +| Activation et endpoints de services | `USE_ARGOCD`, `USE_GITLAB`, `USE_NEXUS`, `USE_HARBOR`, `USE_SONARQUBE`, `USE_SERVICE_CHAIN`, `USE_VAULT`, `USE_OBSERVABILITY`, `ARGO_NAMESPACE`, `ARGOCD_URL`, `ARGOCD_INTERNAL_URL`, `ARGOCD_EXTRA_REPOSITORIES`, `ARGOCD_SHARED_SOURCE_REPOSITORIES`, `GITLAB_URL`, `GITLAB_INTERNAL_URL`, `GITLAB_MIRROR_TOKEN_EXPIRATION_DAYS`, `GITLAB_MIRROR_TOKEN_ROTATION_THRESHOLD_DAYS`, `GITLAB__SECRET_EXPOSE_INTERNAL_URL`, `HARBOR_URL`, `HARBOR_INTERNAL_URL`, `HARBOR_PROJECT_SLUG_CACHE_TTL_MS`, `HARBOR_RETENTION_CRON`, `HARBOR_ROBOT_EXPIRATION_DAYS`, `HARBOR_ROBOT_ROTATION_THRESHOLD_DAYS`, `HARBOR_RULE_COUNT`, `HARBOR_RULE_TEMPLATE`, `NEXUS_URL`, `NEXUS_INTERNAL_URL`, `NEXUS__SECRET_EXPOSE_INTERNAL_URL`, `SONARQUBE_URL`, `SONARQUBE_INTERNAL_URL`, `GRAFANA_URL`, `DSO_OBSERVABILITY_CHART_VERSION`, `VAULT_URL`, `VAULT_INTERNAL_URL`, `VAULT_KV_NAME`, `VAULT__DEPLOY_VAULT_CONNECTION_IN_NS`, `OPENCDS_URL`, `OPENCDS_INTERNAL_URL`, `OPENCDS_API_TLS_REJECT_UNAUTHORIZED`, `DSO_ENV_CHART_VERSION`, `DSO_NS_CHART_VERSION`, `KUBECONFIG_HOST_PATH`, `KUBECONFIG_PATH`, `KUBECONFIG_CTX`, `EXTERNAL_PLUGINS_DIR_HOST_PATH` | `mise` | server-nestjs, Docker Compose | +| Secrets locaux | `SESSION_SECRET`, `DB_URL`, `KEYCLOAK_CLIENT_SECRET`, `KEYCLOAK_ADMIN`, `KEYCLOAK_ADMIN_PASSWORD`, `OPENCDS_API_TOKEN` | `fnox` / `pass` | server-nestjs, Docker Compose | +| Secrets d’intégration | `GITLAB_TOKEN`, `HARBOR_ADMIN`, `HARBOR_ADMIN_PASSWORD`, `NEXUS_ADMIN`, `NEXUS_ADMIN_PASSWORD`, `SONAR_API_TOKEN`, `VAULT_TOKEN` | `fnox` / fournisseur du profil | server-nestjs, Docker Compose | +| Secrets CI | secrets GitHub Actions déjà déclarés par le workflow (`SONAR_*`, `ARGOCD_TOKEN`, clés GitHub App et Helm) | GitHub Actions | étape consommatrice uniquement | + +## Profils + +| Profil | Usage | Source des secrets | +| --- | --- | --- | +| défaut | développement local natif | `pass` sous `fnox/console/local/` | +| `docker` | stack Docker locale | `pass` sous `fnox/console/local/` | +| `integ` | environnement d’intégration | `pass` ou futur fournisseur fnox du profil | +| `ci` | tests déterministes | valeurs de fixture fnox ; secrets GitHub Actions restent limités à leur étape | + +## Exception legacy + +`apps/server` reste hors de ce registre pendant sa période de décommissionnement. Ses `.env*`, templates et chargeurs sont conservés inchangés et `ci/scripts/init-env.sh` ne sert plus qu’à eux. diff --git a/fnox.ci.toml b/fnox.ci.toml new file mode 100644 index 0000000000..e11673dd6e --- /dev/null +++ b/fnox.ci.toml @@ -0,0 +1,7 @@ +[secrets] +SESSION_SECRET = { default = "a-very-strong-secret-with-more-than-32-char" } +DB_URL = { default = "postgresql://admin:admin@postgres:5432/dso-console-db?schema=public" } +KEYCLOAK_CLIENT_SECRET = { default = "client-secret-backend" } +KEYCLOAK_ADMIN = { default = "admin" } +KEYCLOAK_ADMIN_PASSWORD = { default = "admin" } +OPENCDS_API_TOKEN = { default = "token" } diff --git a/fnox.integ.toml b/fnox.integ.toml new file mode 100644 index 0000000000..0c5b595d07 --- /dev/null +++ b/fnox.integ.toml @@ -0,0 +1,8 @@ +[secrets] +GITLAB_TOKEN = { provider = "pass", value = "integ/GITLAB_TOKEN" } +HARBOR_ADMIN = { provider = "pass", value = "integ/HARBOR_ADMIN" } +HARBOR_ADMIN_PASSWORD = { provider = "pass", value = "integ/HARBOR_ADMIN_PASSWORD" } +NEXUS_ADMIN = { provider = "pass", value = "integ/NEXUS_ADMIN" } +NEXUS_ADMIN_PASSWORD = { provider = "pass", value = "integ/NEXUS_ADMIN_PASSWORD" } +SONAR_API_TOKEN = { provider = "pass", value = "integ/SONAR_API_TOKEN" } +VAULT_TOKEN = { provider = "pass", value = "integ/VAULT_TOKEN" } diff --git a/fnox.toml b/fnox.toml new file mode 100644 index 0000000000..ad532a28fb --- /dev/null +++ b/fnox.toml @@ -0,0 +1,16 @@ +#:schema https://fnox.jdx.dev/schema.json +root = true +env = "exec" +if_missing = "error" + +[providers.pass] +type = "password-store" +prefix = "fnox/console/" + +[secrets] +SESSION_SECRET = { provider = "pass", value = "local/SESSION_SECRET" } +DB_URL = { provider = "pass", value = "local/DB_URL" } +KEYCLOAK_CLIENT_SECRET = { provider = "pass", value = "local/KEYCLOAK_CLIENT_SECRET" } +KEYCLOAK_ADMIN = { provider = "pass", value = "local/KEYCLOAK_ADMIN" } +KEYCLOAK_ADMIN_PASSWORD = { provider = "pass", value = "local/KEYCLOAK_ADMIN_PASSWORD" } +OPENCDS_API_TOKEN = { provider = "pass", value = "local/OPENCDS_API_TOKEN" } diff --git a/mise.ci.toml b/mise.ci.toml new file mode 100644 index 0000000000..c4b1a32553 --- /dev/null +++ b/mise.ci.toml @@ -0,0 +1,14 @@ +[env] +CI = "true" +FNOX_PROFILE = "ci" +NODE_ENV = "production" +DEV_SETUP = "true" +CLIENT_SERVER_HOST = "nginx-strangler" +CLIENT_SERVER_PORT = "8080" +CLIENT_OPENCDS_ENABLED = "true" +NESTJS_SERVER_HOST = "0.0.0.0" +NESTJS_SERVER_PORT = "3001" +NESTJS_KEYCLOAK_DOMAIN = "keycloak:8080" +NESTJS_USE_SERVICE_CHAIN = "true" +NESTJS_OPENCDS_URL = "http://dso-console_opencds-mockoon:3100/api/v1" +NESTJS_OPENCDS_INTERNAL_URL = "http://dso-console_opencds-mockoon:3100/api/v1/health" diff --git a/mise.docker.toml b/mise.docker.toml new file mode 100644 index 0000000000..7f51151a76 --- /dev/null +++ b/mise.docker.toml @@ -0,0 +1,11 @@ +[env] +DOCKER = "true" +CLIENT_SERVER_HOST = "nginx-strangler" +CLIENT_SERVER_PORT = "8080" +CLIENT_OPENCDS_ENABLED = "true" +NESTJS_SERVER_HOST = "0.0.0.0" +NESTJS_SERVER_PORT = "3001" +NESTJS_KEYCLOAK_DOMAIN = "keycloak:8080" +NESTJS_USE_SERVICE_CHAIN = "true" +NESTJS_OPENCDS_URL = "http://dso-console_opencds-mockoon:3100/api/v1" +NESTJS_OPENCDS_INTERNAL_URL = "http://dso-console_opencds-mockoon:3100/api/v1/health" diff --git a/mise.integ.toml b/mise.integ.toml new file mode 100644 index 0000000000..6fc7add2cc --- /dev/null +++ b/mise.integ.toml @@ -0,0 +1,5 @@ +[env] +INTEGRATION = "true" +DEV_SETUP = "false" +NESTJS_SERVER_HOST = "0.0.0.0" +NESTJS_SERVER_PORT = "3001" diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000000..e3718c8d56 --- /dev/null +++ b/mise.toml @@ -0,0 +1,105 @@ +min_version = "2026.5.15" + +[tools] +node = "26.7.0" +"npm:pnpm" = "11.8.0" +fnox = "1.36.0" + +[env] +NODE_ENV = "development" +DEV_SETUP = "true" +CLIENT_SERVER_HOST = "localhost" +CLIENT_SERVER_PORT = "4000" +CLIENT_PORT = "8080" +CLIENT_KEYCLOAK_PROTOCOL = "http" +CLIENT_KEYCLOAK_DOMAIN = "localhost:8090" +CLIENT_KEYCLOAK_REALM = "dso" +CLIENT_KEYCLOAK_CLIENT_ID = "dso-console-frontend" +CLIENT_KEYCLOAK_REDIRECT_URI = "http://localhost:8080" +CLIENT_OPENCDS_ENABLED = "false" +CLIENT_CONTACT_EMAIL = "cloudpinative-relations@interieur.gouv.fr" +NESTJS_SERVER_HOST = "localhost" +NESTJS_SERVER_PORT = "3001" +NESTJS_KEYCLOAK_PROTOCOL = "http" +NESTJS_KEYCLOAK_DOMAIN = "localhost:8090" +NESTJS_KEYCLOAK_REALM = "dso" +NESTJS_KEYCLOAK_CLIENT_ID = "dso-console-backend" +NESTJS_KEYCLOAK_REDIRECT_URI = "http://localhost:8080" +NESTJS_CONTACT_EMAIL = "cloudpinative-relations@interieur.gouv.fr" +NESTJS_PROJECTS_ROOT_DIR = "" +NESTJS_KEYCLOAK_ADMIN_CLIENT_ID = "admin-cli" +NESTJS_KEYCLOAK_JWKS_CACHE_TTL_MS = "300000" +NESTJS_KEYCLOAK_JWKS_TIMEOUT_MS = "1000" +NESTJS_KEYCLOAK_OPENID_CONFIGURATION_CACHE_TTL_MS = "300000" +NESTJS_USE_ARGOCD = "false" +NESTJS_USE_GITLAB = "false" +NESTJS_USE_HARBOR = "false" +NESTJS_USE_NEXUS = "false" +NESTJS_USE_OBSERVABILITY = "false" +NESTJS_USE_SERVICE_CHAIN = "false" +NESTJS_USE_SONARQUBE = "false" +NESTJS_USE_VAULT = "false" +NESTJS_OPENCDS_URL = "" +NESTJS_OPENCDS_INTERNAL_URL = "" +NESTJS_OPENCDS_API_TLS_REJECT_UNAUTHORIZED = "false" +NESTJS_GRAFANA_URL = "https://grafana.example.com" +NESTJS_DSO_OBSERVABILITY_CHART_VERSION = "dso-observability-0.1.7" + +[tasks.setup] +run = "./ci/scripts/init-env.sh && ./scripts/bootstrap-secrets.sh" + +[tasks.dev] +run = "docker compose -f docker/docker-compose.local.yml up -d --remove-orphans" + +[tasks."server-nestjs:dev"] +run = "./scripts/with-nestjs-env.sh pnpm --filter server-nestjs run start:dev" + +[tasks."server-nestjs:test"] +run = "mise -E ci run server-nestjs:test-inner" + +[tasks."server-nestjs:test-inner"] +hide = true +run = "NODE_ENV=test ./scripts/with-nestjs-env.sh pnpm --filter server-nestjs test" + +[tasks."client:dev"] +run = "SERVER_HOST=$CLIENT_SERVER_HOST SERVER_PORT=$CLIENT_SERVER_PORT CLIENT_PORT=$CLIENT_PORT KEYCLOAK_PROTOCOL=$CLIENT_KEYCLOAK_PROTOCOL KEYCLOAK_DOMAIN=$CLIENT_KEYCLOAK_DOMAIN KEYCLOAK_REALM=$CLIENT_KEYCLOAK_REALM KEYCLOAK_CLIENT_ID=$CLIENT_KEYCLOAK_CLIENT_ID KEYCLOAK_REDIRECT_URI=$CLIENT_KEYCLOAK_REDIRECT_URI OPENCDS_ENABLED=$CLIENT_OPENCDS_ENABLED CONTACT_EMAIL=$CLIENT_CONTACT_EMAIL pnpm --filter client run dev" + +[tasks."client:build"] +run = "mise -E ci run client:build-inner" + +[tasks."client:build-inner"] +hide = true +run = "APP_VERSION=${APP_VERSION:-dev} pnpm --filter client run build" + +[tasks.format] +run = "pnpm format" + +[tasks.lint] +run = "pnpm lint" + +[tasks."test:unit"] +run = "./ci/scripts/run-unit-tests.sh" + +[tasks."playwright:test"] +run = "KEYCLOAK_PROTOCOL=$CLIENT_KEYCLOAK_PROTOCOL KEYCLOAK_DOMAIN=$CLIENT_KEYCLOAK_DOMAIN KEYCLOAK_REALM=$CLIENT_KEYCLOAK_REALM KEYCLOAK_REDIRECT_URI=$CLIENT_KEYCLOAK_REDIRECT_URI fnox -P ci exec -- pnpm --dir playwright exec playwright test --grep @e2e" + +[tasks."docker:dev"] +run = "mise -E docker run docker:dev-inner" + +[tasks."docker:dev-inner"] +hide = true +run = "fnox exec -- docker compose -f docker/docker-compose.dev.yml up -d --remove-orphans" + +[tasks.integ] +run = "mise -E integ run integ-inner" + +[tasks."integ-inner"] +hide = true +run = "fnox -P integ exec -- docker compose -f docker/docker-compose.local.yml up -d --remove-orphans postgres pgadmin" + +[tasks."docker:integ"] +run = "mise -E integ run docker:integ-inner" + +[tasks."docker:integ-inner"] +hide = true +run = "fnox -P integ exec -- docker compose -f docker/docker-compose.integ.yml up -d --remove-orphans" diff --git a/playwright/playwright.config.ts b/playwright/playwright.config.ts index ec4838c6da..c6aa2c62f8 100644 --- a/playwright/playwright.config.ts +++ b/playwright/playwright.config.ts @@ -1,8 +1,5 @@ -import path from 'node:path' import { defineConfig, devices } from '@playwright/test' -process.loadEnvFile(path.resolve(__dirname, '..', 'apps/client', '.env.docker')) - const isIntegration = process.env.INTEGRATION === 'true' /** diff --git a/scripts/bootstrap-secrets.sh b/scripts/bootstrap-secrets.sh new file mode 100755 index 0000000000..4b741c8e88 --- /dev/null +++ b/scripts/bootstrap-secrets.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if ! command -v pass >/dev/null 2>&1; then + printf 'pass is required; install and initialize it with your GPG identity before running this command.\n' >&2 + exit 1 +fi + +if ! command -v fnox >/dev/null 2>&1; then + printf 'fnox is required; run mise install before running this command.\n' >&2 + exit 1 +fi + +printf 'Initialize missing local demonstration secrets in pass? [y/N] ' +read -r confirmation +if [ "$confirmation" != "y" ] && [ "$confirmation" != "Y" ]; then + printf 'Secret initialization cancelled.\n' + exit 0 +fi + +# fnox set rewrites its config even when the secret reference already exists. +# Write through a disposable copy so contributor edits are never overwritten. +project_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +tmp_dir="$(mktemp -d)" +trap 'rm -rf "$tmp_dir"' EXIT +cp "$project_dir/fnox.toml" "$tmp_dir/fnox.toml" + +while IFS='|' read -r name value; do + [ -n "$name" ] || continue + fnox get "$name" >/dev/null 2>&1 || printf '%s' "$value" | fnox --config "$tmp_dir/fnox.toml" set "$name" --provider pass +done <<'SECRETS' +SESSION_SECRET|a-very-strong-secret-with-more-than-32-char +DB_URL|postgresql://admin:admin@localhost:5432/dso-console-db?schema=public +KEYCLOAK_CLIENT_SECRET|client-secret-backend +KEYCLOAK_ADMIN|admin +KEYCLOAK_ADMIN_PASSWORD|admin +OPENCDS_API_TOKEN|token +SECRETS diff --git a/scripts/bootstrap-secrets.spec.sh b/scripts/bootstrap-secrets.spec.sh new file mode 100755 index 0000000000..334ad0878c --- /dev/null +++ b/scripts/bootstrap-secrets.spec.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash + +set -euo pipefail + +project_dir="$(git rev-parse --show-toplevel)" +tmp_dir="$(mktemp -d)" +trap 'rm -rf "$tmp_dir"' EXIT +mkdir -p "$tmp_dir/bin" "$tmp_dir/project/scripts" "$tmp_dir/temporary-configs" +cp "$project_dir/scripts/bootstrap-secrets.sh" "$tmp_dir/project/scripts/" +cp "$project_dir/fnox.toml" "$tmp_dir/project/fnox.toml" +printf '\n# Preserve contributor edits too.\n' >> "$tmp_dir/project/fnox.toml" +cp "$tmp_dir/project/fnox.toml" "$tmp_dir/expected.toml" + +cat > "$tmp_dir/bin/pass" <<'EOF' +#!/usr/bin/env bash +exit 0 +EOF + +cat > "$tmp_dir/bin/fnox" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +config=fnox.toml +if [ "$1" = --config ]; then + config="$2" + shift 2 +fi +if [ "$1" = get ]; then + [ "${EXISTING_SECRETS:-false}" = true ] + exit +fi +printf '%s\n' "$*" >> "$FNOX_CALLS" +[ "$1" = set ] +name="$2" +shift 2 +# Simulate fnox rewriting the selected config, including when storage fails. +printf '\n# fnox rewrote config\n' >> "$config" +[ "$1" = --provider ] || { printf 'Secrets must not appear in argv\n' >&2; exit 1; } +[ "$2" = pass ] +value="$(> "$FNOX_VALUES" +exit "${FNOX_EXIT:-0}" +EOF + +chmod +x "$tmp_dir/bin/"* +export FNOX_CALLS="$tmp_dir/fnox.calls" +export FNOX_VALUES="$tmp_dir/fnox.values" +export TMPDIR="$tmp_dir/temporary-configs" +export PATH="$tmp_dir/bin:$PATH" +cd "$tmp_dir/project" + +printf 'y\n' | ./scripts/bootstrap-secrets.sh +cmp fnox.toml "$tmp_dir/expected.toml" +test "$(grep -c '^set ' "$FNOX_CALLS")" -eq 6 +grep -Fqx 'set SESSION_SECRET --provider pass' "$FNOX_CALLS" +grep -Fqx 'SESSION_SECRET|a-very-strong-secret-with-more-than-32-char' "$FNOX_VALUES" +grep -Fqx 'OPENCDS_API_TOKEN|token' "$FNOX_VALUES" +test -z "$(ls -A "$TMPDIR")" + +rm "$FNOX_CALLS" +printf 'y\n' | EXISTING_SECRETS=true ./scripts/bootstrap-secrets.sh +test ! -e "$FNOX_CALLS" +printf 'n\n' | ./scripts/bootstrap-secrets.sh +test ! -e "$FNOX_CALLS" + +if printf 'y\n' | FNOX_EXIT=42 ./scripts/bootstrap-secrets.sh; then + printf 'Bootstrap swallowed a provider failure\n' >&2 + exit 1 +else + test "$?" -eq 42 +fi +cmp fnox.toml "$tmp_dir/expected.toml" +test -z "$(ls -A "$TMPDIR")" +printf 'Secret bootstrap regression tests passed.\n' diff --git a/scripts/with-nestjs-env.sh b/scripts/with-nestjs-env.sh new file mode 100755 index 0000000000..d4d70acdb4 --- /dev/null +++ b/scripts/with-nestjs-env.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -euo pipefail + +for name in "${!NESTJS_@}"; do + export "${name#NESTJS_}=${!name}" +done + +exec fnox exec -- "$@" diff --git a/scripts/with-nestjs-env.spec.sh b/scripts/with-nestjs-env.spec.sh new file mode 100755 index 0000000000..ed4ec082ba --- /dev/null +++ b/scripts/with-nestjs-env.spec.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash + +set -euo pipefail + +project_dir="$(git rev-parse --show-toplevel)" +cd "$project_dir" +tmp_dir="$(mktemp -d)" +trap 'rm -rf "$tmp_dir"' EXIT +mkdir -p "$tmp_dir/bin" + +cat > "$tmp_dir/bin/fnox" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +[ "$1" != -P ] || shift 2 +[ "$1" = exec ] +shift +[ "$1" = -- ] && shift +exec "$@" +EOF + +cat > "$tmp_dir/bin/pnpm" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +printf '%s\n' "$*" > "$PNPM_CALL" +env > "$PNPM_ENV" +exit "${PNPM_EXIT:-0}" +EOF +chmod +x "$tmp_dir/bin/"* + +for task in server-nestjs:dev server-nestjs:test-inner test:unit playwright:test; do + command="$(mise tasks info "$task" --json | node --input-type=module -e ' + let input = "" + for await (const chunk of process.stdin) input += chunk + process.stdout.write(JSON.parse(input).run.join("\n")) + ')" + env -i PATH="$tmp_dir/bin:$PATH" HOME="$HOME" PNPM_CALL="$tmp_dir/call" PNPM_ENV="$tmp_dir/env" \ + NESTJS_SERVER_HOST=localhost NESTJS_SERVER_PORT=3001 \ + NESTJS_KEYCLOAK_PROTOCOL=http NESTJS_KEYCLOAK_DOMAIN=localhost:8090 \ + NESTJS_KEYCLOAK_REALM=dso NESTJS_KEYCLOAK_CLIENT_ID=backend \ + NESTJS_KEYCLOAK_REDIRECT_URI=http://localhost:8080 NESTJS_CONTACT_EMAIL=demo@example.com \ + NESTJS_USE_VAULT=false NESTJS_USE_GITLAB=false NESTJS_PROJECTS_ROOT_DIR='' \ + NESTJS_FUTURE_PUBLIC_VALUE='value with spaces' \ + CLIENT_KEYCLOAK_PROTOCOL=https CLIENT_KEYCLOAK_DOMAIN=browser.example.com \ + CLIENT_KEYCLOAK_REALM=browser-realm CLIENT_KEYCLOAK_REDIRECT_URI=https://console.example.com \ + bash -c "$command" + if [ "$task" = playwright:test ]; then + grep -Fqx 'KEYCLOAK_PROTOCOL=https' "$tmp_dir/env" + grep -Fqx 'KEYCLOAK_DOMAIN=browser.example.com' "$tmp_dir/env" + grep -Fqx 'KEYCLOAK_REALM=browser-realm' "$tmp_dir/env" + grep -Fqx 'KEYCLOAK_REDIRECT_URI=https://console.example.com' "$tmp_dir/env" + else + grep -Fqx 'USE_VAULT=false' "$tmp_dir/env" + grep -Fqx 'USE_GITLAB=false' "$tmp_dir/env" + grep -Fqx 'PROJECTS_ROOT_DIR=' "$tmp_dir/env" + grep -Fqx 'FUTURE_PUBLIC_VALUE=value with spaces' "$tmp_dir/env" + fi + case "$task" in + server-nestjs:dev) grep -Fqx -- '--filter server-nestjs run start:dev' "$tmp_dir/call" ;; + server-nestjs:test-inner) + grep -Fqx -- '--filter server-nestjs test' "$tmp_dir/call" + grep -Fqx 'NODE_ENV=test' "$tmp_dir/env" + ;; + test:unit) grep -Fqx 'test:cov' "$tmp_dir/call" ;; + playwright:test) grep -Fqx -- '--dir playwright exec playwright test --grep @e2e' "$tmp_dir/call" ;; + esac + if env PATH="$tmp_dir/bin:$PATH" PNPM_CALL="$tmp_dir/call" PNPM_ENV="$tmp_dir/env" PNPM_EXIT=42 bash -c "$command"; then + printf 'Task %s swallowed the child failure\n' "$task" >&2 + exit 1 + else + test "$?" -eq 42 + fi +done +printf 'Application task environment regression tests passed.\n'