fix: rafraîchir les manifests du dashboard après déploiement
All checks were successful
Validation Dashboard / Tests backend et build frontend (push) Successful in 40s
All checks were successful
Validation Dashboard / Tests backend et build frontend (push) Successful in 40s
This commit is contained in:
@@ -13,6 +13,7 @@ const CATEGORIES = new Set(['ITINOVA', 'SANTINOVA', 'INFRA']);
|
|||||||
const ENVIRONMENTS = new Set(['recette', 'prod']);
|
const ENVIRONMENTS = new Set(['recette', 'prod']);
|
||||||
const AI_BILLING = new Set(['free', 'paid']);
|
const AI_BILLING = new Set(['free', 'paid']);
|
||||||
const AI_SOURCES = new Set(['application', 'referential']);
|
const AI_SOURCES = new Set(['application', 'referential']);
|
||||||
|
const DEFAULT_MANIFEST_REFRESH_INTERVAL_MS = 15_000;
|
||||||
|
|
||||||
function isHttpsUrl(value) {
|
function isHttpsUrl(value) {
|
||||||
if (typeof value !== 'string' || value.length === 0) return false;
|
if (typeof value !== 'string' || value.length === 0) return false;
|
||||||
@@ -171,13 +172,14 @@ function discoverApps({ appsBasePath, environment }) {
|
|||||||
* Toute valeur obsolète est remplacée : seules les applications réellement
|
* Toute valeur obsolète est remplacée : seules les applications réellement
|
||||||
* déployées et correctement déclarées restent disponibles.
|
* déployées et correctement déclarées restent disponibles.
|
||||||
*/
|
*/
|
||||||
function refreshApps(config, logger = console) {
|
function refreshApps(config, logger = console, now = Date.now()) {
|
||||||
const result = discoverApps({
|
const result = discoverApps({
|
||||||
appsBasePath: config.appsBasePath,
|
appsBasePath: config.appsBasePath,
|
||||||
environment: config.environment,
|
environment: config.environment,
|
||||||
});
|
});
|
||||||
|
|
||||||
config.apps.splice(0, config.apps.length, ...result.apps);
|
config.apps.splice(0, config.apps.length, ...result.apps);
|
||||||
|
config.appsRefreshedAt = now;
|
||||||
for (const issue of result.errors) {
|
for (const issue of result.errors) {
|
||||||
logger.warn(`[app-registry] Manifeste ignoré (${issue.directory}) : ${issue.error}`);
|
logger.warn(`[app-registry] Manifeste ignoré (${issue.directory}) : ${issue.error}`);
|
||||||
}
|
}
|
||||||
@@ -186,6 +188,24 @@ function refreshApps(config, logger = console) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rafraîchit le registre après expiration, sans parcourir les applications à
|
||||||
|
* chaque requête. Les déploiements deviennent ainsi visibles sans redémarrage.
|
||||||
|
*/
|
||||||
|
function refreshAppsIfStale(config, {
|
||||||
|
logger = console,
|
||||||
|
now = Date.now(),
|
||||||
|
maxAgeMs = config.appManifestRefreshIntervalMs || DEFAULT_MANIFEST_REFRESH_INTERVAL_MS,
|
||||||
|
} = {}) {
|
||||||
|
const hasFreshCache = Array.isArray(config.apps) && config.apps.length > 0 &&
|
||||||
|
Number.isFinite(config.appsRefreshedAt) &&
|
||||||
|
now - config.appsRefreshedAt < maxAgeMs;
|
||||||
|
if (hasFreshCache) {
|
||||||
|
return { apps: config.apps, errors: [], refreshed: false };
|
||||||
|
}
|
||||||
|
return { ...refreshApps(config, logger, now), refreshed: true };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Écrit un manifeste de manière atomique puis le valide avant publication.
|
* Écrit un manifeste de manière atomique puis le valide avant publication.
|
||||||
*/
|
*/
|
||||||
@@ -216,6 +236,7 @@ function findAppByRepo(config, repositoryName) {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
discoverApps,
|
discoverApps,
|
||||||
refreshApps,
|
refreshApps,
|
||||||
|
refreshAppsIfStale,
|
||||||
writeAppManifest,
|
writeAppManifest,
|
||||||
findAppDirectoryByRepo,
|
findAppDirectoryByRepo,
|
||||||
findAppByRepo,
|
findAppByRepo,
|
||||||
|
|||||||
@@ -10,12 +10,24 @@ const { checkAllApps, addDeploymentLog, updateDeploymentLog, getDeploymentLogs,
|
|||||||
const { createApplication, getAvailableStacks, initDynamicApps } = require('./app-creator');
|
const { createApplication, getAvailableStacks, initDynamicApps } = require('./app-creator');
|
||||||
const { readCiAlerts } = require('./ci-alerts');
|
const { readCiAlerts } = require('./ci-alerts');
|
||||||
const config = require('./config');
|
const config = require('./config');
|
||||||
|
const { refreshAppsIfStale } = require('./app-registry');
|
||||||
|
|
||||||
// Nettoyer les déploiements orphelins au démarrage (entrées "running" restantes d'un crash précédent)
|
// Nettoyer les déploiements orphelins au démarrage (entrées "running" restantes d'un crash précédent)
|
||||||
cleanupOrphanedDeployments();
|
cleanupOrphanedDeployments();
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Les manifests sont la source de vérité ; leur cache est rafraîchi après un
|
||||||
|
// court délai pour refléter les déploiements sans alourdir chaque requête API.
|
||||||
|
router.use((req, res, next) => {
|
||||||
|
try {
|
||||||
|
refreshAppsIfStale(config);
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Configuration multer pour l'upload de fichiers ZIP
|
// Configuration multer pour l'upload de fichiers ZIP
|
||||||
const uploadDir = '/tmp/dashboard-uploads';
|
const uploadDir = '/tmp/dashboard-uploads';
|
||||||
if (!fs.existsSync(uploadDir)) {
|
if (!fs.existsSync(uploadDir)) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const fs = require('node:fs');
|
|||||||
const os = require('node:os');
|
const os = require('node:os');
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
|
|
||||||
const { discoverApps, refreshApps } = require('../src/app-registry');
|
const { discoverApps, refreshApps, refreshAppsIfStale } = require('../src/app-registry');
|
||||||
|
|
||||||
function writeManifest(basePath, directory, manifest) {
|
function writeManifest(basePath, directory, manifest) {
|
||||||
const appPath = path.join(basePath, directory);
|
const appPath = path.join(basePath, directory);
|
||||||
@@ -113,6 +113,33 @@ try {
|
|||||||
'app.json doit prévaloir sur toute définition statique'
|
'app.json doit prévaloir sur toute définition statique'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Un déploiement met à jour app.json : le dashboard doit le détecter sans
|
||||||
|
// redémarrage, mais sans relire les manifests à chaque requête.
|
||||||
|
refreshApps(config, console, 1_000);
|
||||||
|
writeManifest(sandbox, 'contacts', {
|
||||||
|
id: 'itinova-contacts',
|
||||||
|
name: 'Itinova Contacts mis à jour',
|
||||||
|
category: 'ITINOVA',
|
||||||
|
urls: {
|
||||||
|
recette: 'https://contacts-itinova.recette.santinova-soft.org',
|
||||||
|
prod: 'https://contacts-itinova.santinova-soft.org',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const cachedRefresh = refreshAppsIfStale(config, { now: 1_999, maxAgeMs: 1_000 });
|
||||||
|
assert.equal(cachedRefresh.refreshed, false, 'le cache reste utilisable avant son expiration');
|
||||||
|
assert.equal(
|
||||||
|
config.apps.find((candidate) => candidate.id === 'itinova-contacts').name,
|
||||||
|
'Itinova Contacts',
|
||||||
|
'le cache ne doit pas relire les manifests avant son expiration'
|
||||||
|
);
|
||||||
|
const expiredRefresh = refreshAppsIfStale(config, { now: 2_000, maxAgeMs: 1_000 });
|
||||||
|
assert.equal(expiredRefresh.refreshed, true, 'le manifeste est relu lorsque le cache expire');
|
||||||
|
assert.equal(
|
||||||
|
config.apps.find((candidate) => candidate.id === 'itinova-contacts').name,
|
||||||
|
'Itinova Contacts mis à jour',
|
||||||
|
'la modification de manifeste doit apparaître sans redémarrage du dashboard'
|
||||||
|
);
|
||||||
|
|
||||||
console.log('OK app-registry');
|
console.log('OK app-registry');
|
||||||
} finally {
|
} finally {
|
||||||
fs.rmSync(sandbox, { recursive: true, force: true });
|
fs.rmSync(sandbox, { recursive: true, force: true });
|
||||||
|
|||||||
Reference in New Issue
Block a user