diff --git a/.gitignore b/.gitignore index 37a2be33..0cc83087 100644 --- a/.gitignore +++ b/.gitignore @@ -97,4 +97,7 @@ typings/ # End of https://www.gitignore.io/api/node # Ignore loki file -.store \ No newline at end of file +.store + +# Ignore TypeScript build output +app/dist \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index e913655c..ab7b0f8f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -49,14 +49,17 @@ script: - (cd e2e && npm run lint) - (cd ui && npm run lint) + # Package app + - (cd app && npm run build) + + # Package ui + - (cd ui && npm run build) + # Run UT - (cd app && npm test) # Report to Code Climate - - (cd app && ./cc-test-reporter after-build -t lcov --debug --exit-code $TRAVIS_TEST_RESULT) - - # Package ui - - (cd ui && npm run build) +# - (cd app && ./cc-test-reporter after-build -t lcov --debug --exit-code $TRAVIS_TEST_RESULT) # Build wud docker image - docker build -t wud --build-arg WUD_VERSION=$IMAGE_VERSION . diff --git a/Dockerfile b/Dockerfile index 2471e662..fd665d20 100644 --- a/Dockerfile +++ b/Dockerfile @@ -43,7 +43,7 @@ CMD ["node", "index"] COPY --from=dependencies /home/node/app/node_modules ./node_modules # Copy app -COPY app/ ./ +COPY app/dist ./ # Copy ui COPY ui/dist/ ./ui diff --git a/app/api/api.js b/app/api/api.ts similarity index 59% rename from app/api/api.js rename to app/api/api.ts index 6da173c3..63924de3 100644 --- a/app/api/api.js +++ b/app/api/api.ts @@ -1,21 +1,20 @@ -const express = require('express'); -const passport = require('passport'); -const appRouter = require('./app'); -const containerRouter = require('./container'); -const watcherRouter = require('./watcher'); -const triggerRouter = require('./trigger'); -const registryRouter = require('./registry'); -const authenticationRouter = require('./authentication'); -const logRouter = require('./log'); -const storeRouter = require('./store'); -const serverRouter = require('./server'); -const auth = require('./auth'); +import express, { Request, Response } from 'express'; +import passport from 'passport'; +import * as appRouter from './app'; +import * as containerRouter from './container'; +import * as watcherRouter from './watcher'; +import * as triggerRouter from './trigger'; +import * as registryRouter from './registry'; +import * as authenticationRouter from './authentication'; +import * as logRouter from './log'; +import * as storeRouter from './store'; +import * as serverRouter from './server'; +import * as auth from './auth'; /** * Init the API router. - * @returns {*|Router} */ -function init() { +export function init() { const router = express.Router(); // Mount app router @@ -49,11 +48,8 @@ function init() { router.use('/authentications', authenticationRouter.init()); // All other API routes => 404 - router.get('/*', (req, res) => res.sendStatus(404)); + router.get('/{*any}', (_req: Request, res: Response) => { res.sendStatus(404) }); return router; } -module.exports = { - init, -}; diff --git a/app/api/app.js b/app/api/app.ts similarity index 56% rename from app/api/app.js rename to app/api/app.ts index 98ac0e6b..03a2ea4f 100644 --- a/app/api/app.js +++ b/app/api/app.ts @@ -1,10 +1,10 @@ -const express = require('express'); -const nocache = require('nocache'); -const storeApp = require('../store/app'); +import express from 'express'; +import nocache from 'nocache'; +import * as storeApp from '../store/app'; +import { Request, Response } from 'express'; /** * App infos router. - * @type {Router} */ const router = express.Router(); @@ -13,19 +13,15 @@ const router = express.Router(); * @param req the request * @param res the response */ -function getAppInfos(req, res) { +function getAppInfos(_req: Request, res: Response) { res.status(200).json(storeApp.getAppInfos()); } /** * Init Router. - * @returns {*} */ -function init() { +export function init() { router.use(nocache()); router.get('/', getAppInfos); return router; } -module.exports = { - init, -}; diff --git a/app/api/auth.js b/app/api/auth.ts similarity index 72% rename from app/api/auth.js rename to app/api/auth.ts index 049910f8..9a383aab 100644 --- a/app/api/auth.js +++ b/app/api/auth.ts @@ -1,18 +1,21 @@ -const express = require('express'); -const session = require('express-session'); -const LokiStore = require('connect-loki')(session); -const passport = require('passport'); -const { v5: uuidV5 } = require('uuid'); -const getmac = require('getmac').default; -const store = require('../store'); -const registry = require('../registry'); -const log = require('../log'); -const { getVersion } = require('../configuration'); - +import express, { Request, Response, Express } from 'express'; +import session from 'express-session'; +import connect from 'connect-loki'; +import passport from 'passport'; +import { v5 as uuidV5 } from 'uuid'; +import getmac from 'getmac'; +import * as store from '../store'; +import * as registry from '../registry'; +import * as states from '../registry/states'; +import log from '../log'; +import { getVersion } from '../configuration'; +import { Authentication, StrategyDescription } from '../authentications/providers/Authentication'; + +const LokiStore = connect(session); const router = express.Router(); // The configured strategy ids. -const STRATEGY_IDS = []; +const STRATEGY_IDS: string[] = []; // Constant WUD namespace for uuid v5 bound sessions. const WUD_NAMESPACE = 'dee41e92-5fc4-460e-beec-528c9ea7d760'; @@ -21,22 +24,20 @@ const WUD_NAMESPACE = 'dee41e92-5fc4-460e-beec-528c9ea7d760'; * Get all strategies id. * @returns {[]} */ -function getAllIds() { +export function getAllIds() { return STRATEGY_IDS; } /** * Get cookie max age. * @param days - * @returns {number} */ -function getCookieMaxAge(days) { +function getCookieMaxAge(days: number) { return 3600 * 1000 * 24 * days; } /** * Get session secret key (bound to wud version). - * @returns {string} */ function getSessionSecretKey() { const stringToHash = `wud.${getVersion()}.${getmac()}`; @@ -48,12 +49,12 @@ function getSessionSecretKey() { * @param authentication * @param app */ -function useStrategy(authentication, app) { +function useStrategy(authentication: Authentication, app: Express) { try { const strategy = authentication.getStrategy(app); passport.use(authentication.getId(), strategy); STRATEGY_IDS.push(authentication.getId()); - } catch (e) { + } catch (e: any) { log.warn( `Unable to apply authentication ${authentication.getId()} (${e.message})`, ); @@ -61,10 +62,10 @@ function useStrategy(authentication, app) { } function getUniqueStrategies() { - const strategies = Object.values(registry.getState().authentication).map( + const strategies = Object.values(states.getState().authentication).map( (authentication) => authentication.getStrategyDescription(), ); - const uniqueStrategies = []; + const uniqueStrategies: StrategyDescription[] = []; strategies.forEach((strategy) => { if ( !uniqueStrategies.find( @@ -83,7 +84,7 @@ function getUniqueStrategies() { * @param req * @param res */ -function getStrategies(req, res) { +function getStrategies(req: Request, res: Response) { res.json(getUniqueStrategies()); } @@ -102,7 +103,7 @@ function getLogoutRedirectUrl() { * @param req * @param res */ -function getUser(req, res) { +function getUser(req: Request, res: Response) { const user = req.user || { username: 'anonymous' }; res.status(200).json(user); } @@ -112,7 +113,7 @@ function getUser(req, res) { * @param req * @param res */ -function login(req, res) { +function login(req: Request, res: Response) { return getUser(req, res); } @@ -121,8 +122,8 @@ function login(req, res) { * @param req * @param res */ -function logout(req, res) { - req.logout(() => {}); +function logout(req: Request, res: Response) { + req.logout(() => { }); res.status(200).json({ logoutUrl: getLogoutRedirectUrl(), }); @@ -130,9 +131,8 @@ function logout(req, res) { /** * Init auth (passport.js). - * @returns {*} */ -function init(app) { +export function init(app: Express) { // Init express session app.use( session({ @@ -155,7 +155,7 @@ function init(app) { app.use(passport.session()); // Register all authentications - Object.values(registry.getState().authentication).forEach( + Object.values(states.getState().authentication).forEach( (authentication) => useStrategy(authentication, app), ); @@ -163,7 +163,7 @@ function init(app) { done(null, JSON.stringify(user)); }); - passport.deserializeUser((user, done) => { + passport.deserializeUser((user, done) => { done(null, JSON.parse(user)); }); @@ -182,8 +182,3 @@ function init(app) { app.use('/auth', router); } - -module.exports = { - init, - getAllIds, -}; diff --git a/app/api/authentication.js b/app/api/authentication.js deleted file mode 100644 index b8cb027e..00000000 --- a/app/api/authentication.js +++ /dev/null @@ -1,13 +0,0 @@ -const component = require('./component'); - -/** - * Init Router. - * @returns {*} - */ -function init() { - return component.init('authentication'); -} - -module.exports = { - init, -}; diff --git a/app/api/authentication.ts b/app/api/authentication.ts new file mode 100644 index 00000000..d1b6ed3b --- /dev/null +++ b/app/api/authentication.ts @@ -0,0 +1,8 @@ +import * as component from './component'; + +/** + * Init Router. + */ +export function init() { + return component.init('authentication'); +} diff --git a/app/api/component.js b/app/api/component.js deleted file mode 100644 index 6fd44e64..00000000 --- a/app/api/component.js +++ /dev/null @@ -1,81 +0,0 @@ -const { byValues, byString } = require('sort-es'); - -const express = require('express'); -const nocache = require('nocache'); -const registry = require('../registry'); - -/** - * Map a Component to a displayable (api/ui) item. - * @param key - * @param component - * @returns {{id: *}} - */ -function mapComponentToItem(key, component) { - return { - id: key, - type: component.type, - name: component.name, - configuration: component.maskConfiguration(), - }; -} - -/** - * Return a list instead of a map. - * @param listFunction - * @returns {{id: string}[]} - */ -function mapComponentsToList(components) { - return Object.keys(components) - .map((key) => mapComponentToItem(key, components[key])) - .sort( - byValues([ - [(x) => x.type, byString()], - [(x) => x.name, byString()], - ]), - ); -} - -/** - * Get all components. - * @param req - * @param res - */ -function getAll(req, res, kind) { - res.status(200).json(mapComponentsToList(registry.getState()[kind])); -} - -/** - * Get a component by id. - * @param req - * @param res - * @param listFunction - */ -function getById(req, res, kind) { - const { type, name } = req.params; - const id = `${type}.${name}`; - const component = registry.getState()[kind][id]; - if (component) { - res.status(200).json(mapComponentToItem(id, component)); - } else { - res.sendStatus(404); - } -} - -/** - * Init the component router. - * @param kind - * @returns {*|Router} - */ -function init(kind) { - const router = express.Router(); - router.use(nocache()); - router.get('/', (req, res) => getAll(req, res, kind)); - router.get('/:type/:name', (req, res) => getById(req, res, kind)); - return router; -} - -module.exports = { - init, - mapComponentsToList, - getById, -}; diff --git a/app/api/component.ts b/app/api/component.ts new file mode 100644 index 00000000..f108e273 --- /dev/null +++ b/app/api/component.ts @@ -0,0 +1,83 @@ +import { byValues, byString } from 'sort-es'; + +import express, { Request, Response } from 'express'; +import nocache from 'nocache'; +import * as registry from '../registry'; +import * as states from '../registry/states'; +import { BaseConfig, Component, ComponentKind } from '../registry/Component'; + +export interface ComponentItem { + id: string; + type: string; + name: string; + configuration: T; +} + +/** + * Map a Component to a displayable (api/ui) item. + * @param key + * @param component + */ +function mapComponentToItem, TConfig extends BaseConfig>(key: string, component: T): + ComponentItem { + return { + id: key, + type: component.type, + name: component.name, + configuration: component.maskConfiguration(), + }; +} + +/** + * Return a list instead of a map. + * @param listFunction + */ +export function mapComponentsToList, TConfig extends BaseConfig>(components: { [key: string]: T }) + : ComponentItem[] { + return Object.keys(components) + .map((key) => mapComponentToItem(key, components[key])) + .sort( + byValues([ + [(x) => x.type, byString()], + [(x) => x.name, byString()], + ]), + ); +} + +/** + * Get all components. + * @param req + * @param res + */ +function getAll(req: Request, res: Response, kind: ComponentKind) { + res.status(200).json(mapComponentsToList, BaseConfig>(states.getState()[kind])); +} + +/** + * Get a component by id. + * @param req + * @param res + * @param listFunction + */ +export function getById(req: Request, res: Response, kind: ComponentKind) { + const { type, name } = req.params; + const id = `${type}.${name}`; + const component = states.getState()[kind][id]; + if (component) { + res.status(200).json(mapComponentToItem, BaseConfig>(id, component)); + } else { + res.sendStatus(404); + } +} + +/** + * Init the component router. + * @param kind + */ +export function init(kind: ComponentKind) { + const router = express.Router(); + router.use(nocache()); + router.get('/', (req, res) => getAll(req, res, kind)); + router.get('/:type/:name', (req, res) => getById(req, res, kind)); + return router; +} \ No newline at end of file diff --git a/app/api/container.js b/app/api/container.ts similarity index 76% rename from app/api/container.js rename to app/api/container.ts index 11537abc..be23502c 100644 --- a/app/api/container.js +++ b/app/api/container.ts @@ -1,30 +1,33 @@ -const express = require('express'); -const nocache = require('nocache'); -const storeContainer = require('../store/container'); -const registry = require('../registry'); -const { getServerConfiguration } = require('../configuration'); -const { mapComponentsToList } = require('./component'); -const Trigger = require('../triggers/providers/Trigger'); -const log = require('../log').child({ component: 'container' }); +import express from 'express'; +import nocache from 'nocache'; +import * as storeContainer from '../store/container'; +import * as registry from '../registry'; +import * as states from '../registry/states'; +import { getServerConfiguration } from '../configuration'; +import { ComponentItem, mapComponentsToList } from './component'; +import { Trigger, TriggerConfiguration } from '../triggers/providers/Trigger'; +import logger from '../log'; +import { Request, Response } from 'express'; +const log = logger.child({ component: 'container' }); const router = express.Router(); const serverConfiguration = getServerConfiguration(); + + /** * Return registered watchers. - * @returns {{id: string}[]} */ function getWatchers() { - return registry.getState().watcher; + return states.getState().watcher; } /** * Return registered triggers. - * @returns {{id: string}[]} */ -function getTriggers() { - return registry.getState().trigger; +function getTriggers(): { [key: string]: Trigger } { + return states.getState().trigger; } /** @@ -32,7 +35,7 @@ function getTriggers() { * @param query * @returns {*} */ -function getContainersFromStore(query) { +export function getContainersFromStore(query: storeContainer.Query) { return storeContainer.getContainers(query); } @@ -41,7 +44,7 @@ function getContainersFromStore(query) { * @param req * @param res */ -function getContainers(req, res) { +function getContainers(req: Request, res: Response) { const { query } = req; res.status(200).json(getContainersFromStore(query)); } @@ -51,7 +54,7 @@ function getContainers(req, res) { * @param req * @param res */ -function getContainer(req, res) { +function getContainer(req: Request, res: Response) { const { id } = req.params; const container = storeContainer.getContainer(id); if (container) { @@ -66,7 +69,7 @@ function getContainer(req, res) { * @param req * @param res */ -function deleteContainer(req, res) { +function deleteContainer(req: Request, res: Response) { if (!serverConfiguration.feature.delete) { res.sendStatus(403); } else { @@ -85,46 +88,45 @@ function deleteContainer(req, res) { * Watch all containers. * @param req * @param res - * @returns {Promise} */ -async function watchContainers(req, res) { +async function watchContainers(req: Request, res: Response) { try { await Promise.all( Object.values(getWatchers()).map((watcher) => watcher.watch()), ); getContainers(req, res); - } catch (e) { + } catch (e: any) { res.status(500).json({ error: `Error when watching images (${e.message})`, }); } } -async function getContainerTriggers(req, res) { +async function getContainerTriggers(req: Request, res: Response) { const { id } = req.params; const container = storeContainer.getContainer(id); if (container) { - const allTriggers = mapComponentsToList(getTriggers()); + const allTriggers = mapComponentsToList(getTriggers()); const includedTriggers = container.triggerInclude ? container.triggerInclude - .split(/\s*,\s*/) - .map((includedTrigger) => - Trigger.parseIncludeOrIncludeTriggerString( - includedTrigger, - ), - ) + .split(/\s*,\s*/) + .map((includedTrigger) => + Trigger.parseIncludeOrIncludeTriggerString( + includedTrigger, + ), + ) : undefined; const excludedTriggers = container.triggerExclude ? container.triggerExclude - .split(/\s*,\s*/) - .map((excludedTrigger) => - Trigger.parseIncludeOrIncludeTriggerString( - excludedTrigger, - ), - ) + .split(/\s*,\s*/) + .map((excludedTrigger) => + Trigger.parseIncludeOrIncludeTriggerString( + excludedTrigger, + ), + ) : undefined; - const associatedTriggers = []; + const associatedTriggers: ComponentItem[] = []; allTriggers.forEach((trigger) => { const triggerToAssociate = { ...trigger }; let associated = true; @@ -162,7 +164,7 @@ async function getContainerTriggers(req, res) { * @param {*} req * @param {*} res */ -async function runTrigger(req, res) { +async function runTrigger(req: Request, res: Response) { const { id, triggerType, triggerName } = req.params; const containerToTrigger = storeContainer.getContainer(id); @@ -175,7 +177,7 @@ async function runTrigger(req, res) { `Trigger executed with success (type=${triggerType}, name=${triggerName}, container=${JSON.stringify(containerToTrigger)})`, ); res.status(200).json({}); - } catch (e) { + } catch (e: any) { log.warn( `Error when running trigger (type=${triggerType}, name=${triggerName}) (${e.message})`, ); @@ -199,9 +201,8 @@ async function runTrigger(req, res) { * Watch an image. * @param req * @param res - * @returns {Promise} */ -async function watchContainer(req, res) { +async function watchContainer(req: Request, res: Response) { const { id } = req.params; const container = storeContainer.getContainer(id); @@ -228,7 +229,7 @@ async function watchContainer(req, res) { await watcher.watchContainer(container); res.status(200).json(containerReport.container); } - } catch (e) { + } catch (e: any) { res.status(500).json({ error: `Error when watching container ${id} (${e.message})`, }); @@ -241,9 +242,8 @@ async function watchContainer(req, res) { /** * Init Router. - * @returns {*} */ -function init() { +export function init() { router.use(nocache()); router.get('/', getContainers); router.post('/watch', watchContainers); @@ -254,8 +254,3 @@ function init() { router.post('/:id/watch', watchContainer); return router; } - -module.exports = { - init, - getContainersFromStore, -}; diff --git a/app/api/health.js b/app/api/health.js deleted file mode 100644 index 2c65b623..00000000 --- a/app/api/health.js +++ /dev/null @@ -1,23 +0,0 @@ -const express = require('express'); -const nocache = require('nocache'); -const healthcheck = require('express-healthcheck'); - -/** - * Healthcheck router. - * @type {Router} - */ -const router = express.Router(); - -/** - * Init Router. - * @returns {*} - */ -function init() { - router.use(nocache()); - router.get('/', healthcheck()); - return router; -} - -module.exports = { - init, -}; diff --git a/app/api/health.ts b/app/api/health.ts new file mode 100644 index 00000000..91c31b4a --- /dev/null +++ b/app/api/health.ts @@ -0,0 +1,17 @@ +import express from 'express'; +import nocache from 'nocache'; +import expressHealthCheck from 'express-healthcheck'; + +/** + * HealthCheck router. + */ +const router = express.Router(); + +/** + * Init Router. + */ +export function init() { + router.use(nocache()); + router.get('/', expressHealthCheck()); + return router; +} diff --git a/app/api/index.js b/app/api/index.ts similarity index 80% rename from app/api/index.js rename to app/api/index.ts index b2720e4b..d079b8ab 100644 --- a/app/api/index.js +++ b/app/api/index.ts @@ -1,15 +1,16 @@ -const fs = require('fs'); -const https = require('https'); -const express = require('express'); -const cors = require('cors'); -const bodyParser = require('body-parser'); -const log = require('../log').child({ component: 'api' }); -const auth = require('./auth'); -const apiRouter = require('./api'); -const uiRouter = require('./ui'); -const prometheusRouter = require('./prometheus'); -const healthRouter = require('./health'); -const { getServerConfiguration } = require('../configuration'); +import fs from 'fs'; +import https from 'https'; +import express from 'express'; +import cors from 'cors'; +import bodyParser from 'body-parser'; +import logger from '../log'; +const log = logger.child({ component: 'api' }); +import * as auth from './auth'; +import * as apiRouter from './api'; +import * as uiRouter from './ui'; +import * as prometheusRouter from './prometheus'; +import * as healthRouter from './health'; +import { getServerConfiguration } from '../configuration'; const configuration = getServerConfiguration(); @@ -17,7 +18,7 @@ const configuration = getServerConfiguration(); * Init Http API. * @returns {Promise} */ -async function init() { +export async function init() { // Start API if enabled if (configuration.enabled) { log.debug( @@ -31,7 +32,7 @@ async function init() { app.set('trust proxy', true); // Replace undefined values by null to prevent them from being removed from json responses - app.set('json replacer', (key, value) => + app.set('json replacer', (_key: string, value: any) => value === undefined ? null : value, ); @@ -69,16 +70,16 @@ async function init() { let serverKey; let serverCert; try { - serverKey = fs.readFileSync(configuration.tls.key); - } catch (e) { + serverKey = fs.readFileSync(configuration.tls.key!); + } catch (e: any) { log.error( `Unable to read the key file under ${configuration.tls.key} (${e.message})`, ); throw e; } try { - serverCert = fs.readFileSync(configuration.tls.cert); - } catch (e) { + serverCert = fs.readFileSync(configuration.tls.cert!); + } catch (e: any) { log.error( `Unable to read the cert file under ${configuration.tls.cert} (${e.message})`, ); @@ -104,6 +105,3 @@ async function init() { } } -module.exports = { - init, -}; diff --git a/app/api/log.js b/app/api/log.ts similarity index 53% rename from app/api/log.js rename to app/api/log.ts index 49910854..5beaba36 100644 --- a/app/api/log.js +++ b/app/api/log.ts @@ -1,6 +1,6 @@ -const express = require('express'); -const nocache = require('nocache'); -const { getLogLevel } = require('../configuration'); +import express, { Request, Response } from 'express'; +import nocache from 'nocache'; +import { getLogLevel } from '../configuration'; const router = express.Router(); @@ -9,7 +9,7 @@ const router = express.Router(); * @param req * @param res */ -function getLog(req, res) { +function getLog(req: Request, res: Response) { res.status(200).json({ level: getLogLevel(), }); @@ -17,14 +17,9 @@ function getLog(req, res) { /** * Init Router. - * @returns {*} */ -function init() { +export function init() { router.use(nocache()); router.get('/', getLog); return router; } - -module.exports = { - init, -}; diff --git a/app/api/prometheus.js b/app/api/prometheus.ts similarity index 58% rename from app/api/prometheus.js rename to app/api/prometheus.ts index aef286bd..e724763f 100644 --- a/app/api/prometheus.js +++ b/app/api/prometheus.ts @@ -1,12 +1,11 @@ -const express = require('express'); -const passport = require('passport'); -const nocache = require('nocache'); -const { output } = require('../prometheus'); -const auth = require('./auth'); +import express, { Request, Response } from 'express'; +import passport from 'passport'; +import nocache from 'nocache'; +import { output } from '../prometheus'; +import * as auth from './auth'; /** * Prometheus Metrics router. - * @type {Router} */ const router = express.Router(); @@ -15,7 +14,7 @@ const router = express.Router(); * @param req * @param res */ -async function outputMetrics(req, res) { +async function outputMetrics(req: Request, res: Response) { res.status(200) .type('text') .send(await output()); @@ -23,9 +22,8 @@ async function outputMetrics(req, res) { /** * Init Router. - * @returns {*} */ -function init() { +export function init() { router.use(nocache()); // Routes to protect after this line @@ -35,6 +33,3 @@ function init() { return router; } -module.exports = { - init, -}; diff --git a/app/api/registry.js b/app/api/registry.js deleted file mode 100644 index e3983849..00000000 --- a/app/api/registry.js +++ /dev/null @@ -1,14 +0,0 @@ -const component = require('./component'); - -/** - * Init Router. - * @returns {*} - */ -function init() { - const router = component.init('registry'); - return router; -} - -module.exports = { - init, -}; diff --git a/app/api/registry.ts b/app/api/registry.ts new file mode 100644 index 00000000..2ae68e92 --- /dev/null +++ b/app/api/registry.ts @@ -0,0 +1,10 @@ +import * as component from './component'; + +/** + * Init Router. + */ +export function init() { + const router = component.init('registry'); + return router; +} + diff --git a/app/api/server.js b/app/api/server.ts similarity index 54% rename from app/api/server.js rename to app/api/server.ts index 834fc933..6c4c766e 100644 --- a/app/api/server.js +++ b/app/api/server.ts @@ -1,6 +1,6 @@ -const express = require('express'); -const nocache = require('nocache'); -const { getServerConfiguration } = require('../configuration'); +import express, { Request, Response } from 'express'; +import nocache from 'nocache'; +import { getServerConfiguration } from '../configuration'; const router = express.Router(); @@ -9,7 +9,7 @@ const router = express.Router(); * @param req * @param res */ -function getServer(req, res) { +function getServer(req: Request, res: Response) { res.status(200).json({ configuration: getServerConfiguration(), }); @@ -17,14 +17,9 @@ function getServer(req, res) { /** * Init Router. - * @returns {*} */ -function init() { +export function init() { router.use(nocache()); router.get('/', getServer); return router; } - -module.exports = { - init, -}; diff --git a/app/api/store.js b/app/api/store.ts similarity index 58% rename from app/api/store.js rename to app/api/store.ts index 22faf0c9..1ae58289 100644 --- a/app/api/store.js +++ b/app/api/store.ts @@ -1,6 +1,6 @@ -const express = require('express'); -const nocache = require('nocache'); -const store = require('../store'); +import express, { Request, Response } from 'express'; +import nocache from 'nocache'; +import * as store from '../store'; const router = express.Router(); @@ -9,7 +9,7 @@ const router = express.Router(); * @param req * @param res */ -function getStore(req, res) { +function getStore(req: Request, res: Response) { res.status(200).json({ configuration: store.getConfiguration(), }); @@ -17,14 +17,10 @@ function getStore(req, res) { /** * Init Router. - * @returns {*} */ -function init() { +export function init() { router.use(nocache()); router.get('/', getStore); return router; } -module.exports = { - init, -}; diff --git a/app/api/trigger.js b/app/api/trigger.ts similarity index 75% rename from app/api/trigger.js rename to app/api/trigger.ts index 75a100a5..cbc21008 100644 --- a/app/api/trigger.js +++ b/app/api/trigger.ts @@ -1,20 +1,22 @@ -const component = require('./component'); -const registry = require('../registry'); -const log = require('../log').child({ component: 'trigger' }); +import { Request, Response } from 'express'; +import * as component from './component'; +import * as registry from '../registry'; +import * as states from '../registry/states'; +import logger from '../log'; +const log = logger.child({ component: 'trigger' }); /** * Run a specific trigger on a specific container provided in the payload. * @param {*} req * @param {*} res - * @returns */ -async function runTrigger(req, res) { +async function runTrigger(req: Request, res: Response) { const triggerType = req.params.type; const triggerName = req.params.name; const containerToTrigger = req.body; const triggerToRun = - registry.getState().trigger[`${triggerType}.${triggerName}`]; + states.getState().trigger[`${triggerType}.${triggerName}`]; if (!triggerToRun) { log.warn(`No trigger found(type=${triggerType}, name=${triggerName})`); res.status(404).json({ @@ -38,7 +40,7 @@ async function runTrigger(req, res) { `Trigger executed with success (type=${triggerType}, name=${triggerName}, container=${JSON.stringify(containerToTrigger)})`, ); res.status(200).json({}); - } catch (e) { + } catch (e: any) { log.warn( `Error when running trigger ${triggerType}.${triggerName} (${e.message})`, ); @@ -50,14 +52,9 @@ async function runTrigger(req, res) { /** * Init Router. - * @returns {*} */ -function init() { +export function init() { const router = component.init('trigger'); - router.post('/:type/:name', (req, res) => runTrigger(req, res)); + router.post('/:type/:name', runTrigger); return router; } - -module.exports = { - init, -}; diff --git a/app/api/ui.js b/app/api/ui.ts similarity index 62% rename from app/api/ui.js rename to app/api/ui.ts index 4b46de98..94793cd3 100644 --- a/app/api/ui.js +++ b/app/api/ui.ts @@ -1,21 +1,17 @@ -const path = require('path'); -const express = require('express'); +import path from 'path'; +import express from 'express'; /** * Init the UI router. - * @returns {*|Router} */ -function init() { +export function init() { const router = express.Router(); router.use(express.static(path.join(__dirname, '..', 'ui'))); // Redirect all 404 to index.html (for vue history mode) - router.get('*', (req, res) => { + router.get('/{*any}', (_req, res) => { res.sendFile(path.join(__dirname, '..', 'ui', 'index.html')); }); return router; } -module.exports = { - init, -}; diff --git a/app/api/watcher.js b/app/api/watcher.js deleted file mode 100644 index efbdef07..00000000 --- a/app/api/watcher.js +++ /dev/null @@ -1,13 +0,0 @@ -const component = require('./component'); - -/** - * Init Router. - * @returns {*} - */ -function init() { - return component.init('watcher'); -} - -module.exports = { - init, -}; diff --git a/app/api/watcher.ts b/app/api/watcher.ts new file mode 100644 index 00000000..56db2f52 --- /dev/null +++ b/app/api/watcher.ts @@ -0,0 +1,9 @@ +import * as component from './component'; + +/** + * Init Router. + */ +export function init() { + return component.init('watcher'); +} + diff --git a/app/authentications/providers/Authentication.js b/app/authentications/providers/Authentication.js deleted file mode 100644 index 0d189864..00000000 --- a/app/authentications/providers/Authentication.js +++ /dev/null @@ -1,30 +0,0 @@ -const Component = require('../../registry/Component'); - -class Authentication extends Component { - /** - * Init the Trigger. - */ - async init() { - await this.initAuthentication(); - } - - /** - * Init Trigger. Can be overridden in trigger implementation class. - */ - initAuthentication() { - // do nothing by default - } - - /** - * Return passport strategy. - */ - getStrategy() { - throw new Error('getStrategy must be implemented'); - } - - getStrategyDescription() { - throw new Error('getStrategyDescription must be implemented'); - } -} - -module.exports = Authentication; diff --git a/app/authentications/providers/Authentication.test.js b/app/authentications/providers/Authentication.test.ts similarity index 72% rename from app/authentications/providers/Authentication.test.js rename to app/authentications/providers/Authentication.test.ts index 99f43302..59506828 100644 --- a/app/authentications/providers/Authentication.test.js +++ b/app/authentications/providers/Authentication.test.ts @@ -1,4 +1,5 @@ -const Authentication = require('./Authentication'); +import { Authentication } from './Authentication'; +import { Express } from 'express'; test('init should call initAuthentication', async () => { const authentication = new Authentication(); @@ -9,7 +10,7 @@ test('init should call initAuthentication', async () => { test('getStrategy throw an error by default', async () => { const authentication = new Authentication(); - expect(() => authentication.getStrategy()).toThrow( + expect(() => authentication.getStrategy({} as Express)).toThrow( 'getStrategy must be implemented', ); }); diff --git a/app/authentications/providers/Authentication.ts b/app/authentications/providers/Authentication.ts new file mode 100644 index 00000000..0b31e2cb --- /dev/null +++ b/app/authentications/providers/Authentication.ts @@ -0,0 +1,36 @@ +import { Strategy } from 'passport'; +import { BaseConfig, Component } from '../../registry/Component'; +import { Express } from 'express'; + +export class Authentication extends Component { + /** + * Init the Trigger. + */ + async init() { + await this.initAuthentication(); + } + + /** + * Init Trigger. Can be overridden in trigger implementation class. + */ + async initAuthentication() { + // do nothing by default + } + + /** + * Return passport strategy. + */ + getStrategy(app: Express): Strategy { + throw new Error('getStrategy must be implemented'); + } + + getStrategyDescription(): StrategyDescription { + throw new Error('getStrategyDescription must be implemented'); + } +} + +export interface StrategyDescription { + type: string; + name: string; + logoutUrl?: string; +} diff --git a/app/authentications/providers/anonymous/Anonymous.test.js b/app/authentications/providers/anonymous/Anonymous.test.ts similarity index 92% rename from app/authentications/providers/anonymous/Anonymous.test.js rename to app/authentications/providers/anonymous/Anonymous.test.ts index a0db4fbb..d2474020 100644 --- a/app/authentications/providers/anonymous/Anonymous.test.js +++ b/app/authentications/providers/anonymous/Anonymous.test.ts @@ -1,4 +1,4 @@ -const Anonymous = require('./Anonymous'); +import { Anonymous } from './Anonymous'; const anonymous = new Anonymous(); diff --git a/app/authentications/providers/anonymous/Anonymous.js b/app/authentications/providers/anonymous/Anonymous.ts similarity index 59% rename from app/authentications/providers/anonymous/Anonymous.js rename to app/authentications/providers/anonymous/Anonymous.ts index da309245..44379d4e 100644 --- a/app/authentications/providers/anonymous/Anonymous.js +++ b/app/authentications/providers/anonymous/Anonymous.ts @@ -1,11 +1,11 @@ -const AnonymousStrategy = require('passport-anonymous').Strategy; -const Authentication = require('../Authentication'); -const log = require('../../../log'); +import { Strategy } from "passport-anonymous"; +import { Authentication } from '../Authentication'; +import log from '../../../log'; /** * Anonymous authentication. */ -class Anonymous extends Authentication { +export class Anonymous extends Authentication { /** * Return passport strategy. */ @@ -13,7 +13,7 @@ class Anonymous extends Authentication { log.warn( 'Anonymous authentication is enabled; please make sure that the app is not exposed to unsecure networks', ); - return new AnonymousStrategy(); + return new Strategy(); } getStrategyDescription() { @@ -24,4 +24,3 @@ class Anonymous extends Authentication { } } -module.exports = Anonymous; diff --git a/app/authentications/providers/basic/Basic.test.js b/app/authentications/providers/basic/Basic.test.ts similarity index 92% rename from app/authentications/providers/basic/Basic.test.js rename to app/authentications/providers/basic/Basic.test.ts index 7d79ee74..11578ffc 100644 --- a/app/authentications/providers/basic/Basic.test.js +++ b/app/authentications/providers/basic/Basic.test.ts @@ -1,5 +1,5 @@ -const { ValidationError } = require('joi'); -const Basic = require('./Basic'); +import { ValidationError } from 'joi'; +import { Basic, BasicConfiguration } from './Basic'; const configurationValid = { user: 'john', @@ -20,7 +20,7 @@ test('validateConfiguration should return validated configuration when valid', ( }); test('validateConfiguration should throw error when invalid', () => { - const configuration = {}; + const configuration = {} as BasicConfiguration; expect(() => { basic.validateConfiguration(configuration); }).toThrowError(ValidationError); diff --git a/app/authentications/providers/basic/Basic.js b/app/authentications/providers/basic/Basic.ts similarity index 75% rename from app/authentications/providers/basic/Basic.js rename to app/authentications/providers/basic/Basic.ts index f23e1235..a2bec34f 100644 --- a/app/authentications/providers/basic/Basic.js +++ b/app/authentications/providers/basic/Basic.ts @@ -1,11 +1,16 @@ -const passJs = require('pass'); -const BasicStrategy = require('./BasicStrategy'); -const Authentication = require('../Authentication'); +import passJs from 'pass'; +import { BasicStrategy } from './BasicStrategy'; +import { Authentication } from '../Authentication'; + +export interface BasicConfiguration { + user: string; + hash: string; +} /** * Htpasswd authentication. */ -class Basic extends Authentication { +export class Basic extends Authentication { /** * Get the Trigger configuration schema. * @returns {*} @@ -44,7 +49,7 @@ class Basic extends Authentication { }; } - authenticate(user, pass, done) { + authenticate(user: string, pass: string, done: (error: Error | null, user?: false | { username: string }) => void) { // No user or different user? => reject if (!user || user !== this.configuration.user) { done(null, false); @@ -58,10 +63,9 @@ class Basic extends Authentication { username: this.configuration.user, }); } else { - done(null, false); + done(err, false); } }); } } -module.exports = Basic; diff --git a/app/authentications/providers/basic/BasicStrategy.test.js b/app/authentications/providers/basic/BasicStrategy.test.ts similarity index 86% rename from app/authentications/providers/basic/BasicStrategy.test.js rename to app/authentications/providers/basic/BasicStrategy.test.ts index 187f3eef..c4a52c41 100644 --- a/app/authentications/providers/basic/BasicStrategy.test.js +++ b/app/authentications/providers/basic/BasicStrategy.test.ts @@ -1,6 +1,6 @@ -const BasicStrategy = require('./BasicStrategy'); +import { BasicStrategy } from './BasicStrategy'; -const basicStrategy = new BasicStrategy({}, () => {}); +const basicStrategy = new BasicStrategy({}, () => { }) as any; beforeEach(() => { basicStrategy.success = jest.fn(); diff --git a/app/authentications/providers/basic/BasicStrategy.js b/app/authentications/providers/basic/BasicStrategy.ts similarity index 58% rename from app/authentications/providers/basic/BasicStrategy.js rename to app/authentications/providers/basic/BasicStrategy.ts index a120f2bc..fbdfed7d 100644 --- a/app/authentications/providers/basic/BasicStrategy.js +++ b/app/authentications/providers/basic/BasicStrategy.ts @@ -1,21 +1,20 @@ -const { BasicStrategy: HttpBasicStrategy } = require('passport-http'); +import { BasicStrategy as HttpBasicStrategy } from 'passport-http'; +import { Request } from 'express'; /** * Inherit from Basic Strategy including Session support. - * @type {module.MyStrategy} */ -module.exports = class BasicStrategy extends HttpBasicStrategy { - authenticate(req) { +export class BasicStrategy extends HttpBasicStrategy { + authenticate(req: Request) { // Already authenticated (thanks to session) => ok if (req.isAuthenticated()) { - return this.success(req.user); + return (this as any).success(req.user); } return super.authenticate(req); } /** * Override challenge to avoid browser popup on 401 errrors. - * @returns {string} * @private */ _challenge() { diff --git a/app/authentications/providers/oidc/Oidc.test.js b/app/authentications/providers/oidc/Oidc.test.ts similarity index 86% rename from app/authentications/providers/oidc/Oidc.test.js rename to app/authentications/providers/oidc/Oidc.test.ts index d6985add..37cfe6da 100644 --- a/app/authentications/providers/oidc/Oidc.test.js +++ b/app/authentications/providers/oidc/Oidc.test.ts @@ -1,7 +1,7 @@ -const { ValidationError } = require('joi'); -const express = require('express'); -const { Issuer } = require('openid-client'); -const Oidc = require('./Oidc'); +import { ValidationError } from 'joi'; +import express from 'express'; +import { Issuer } from 'openid-client'; +import { Oidc, OidcConfiguration } from './Oidc'; const app = express(); @@ -31,7 +31,7 @@ test('validateConfiguration should return validated configuration when valid', ( }); test('validateConfiguration should throw error when invalid', () => { - const configuration = {}; + const configuration = {} as OidcConfiguration; expect(() => { oidc.validateConfiguration(configuration); }).toThrowError(ValidationError); diff --git a/app/authentications/providers/oidc/Oidc.js b/app/authentications/providers/oidc/Oidc.ts similarity index 75% rename from app/authentications/providers/oidc/Oidc.js rename to app/authentications/providers/oidc/Oidc.ts index 9078401f..e85b9965 100644 --- a/app/authentications/providers/oidc/Oidc.js +++ b/app/authentications/providers/oidc/Oidc.ts @@ -1,13 +1,25 @@ -const { Issuer, generators, custom } = require('openid-client'); -const { v4: uuid } = require('uuid'); -const Authentication = require('../Authentication'); -const OidcStrategy = require('./OidcStrategy'); -const { getPublicUrl } = require('../../../configuration'); +import { Issuer, generators, custom, Client, TokenSet } from 'openid-client'; +import { v4 as uuid } from 'uuid'; +import { Authentication } from '../Authentication'; +import { OidcStrategy, User } from './OidcStrategy'; +import { getPublicUrl } from '../../../configuration'; +import { Express, Request } from 'express'; + +export interface OidcConfiguration { + discovery: string; + clientid: string; + clientsecret: string; + redirect: boolean; + timeout: number; +} /** - * Htpasswd authentication. + * OIDC authentication. */ -class Oidc extends Authentication { +export class Oidc extends Authentication { + public client?: Client; + private logoutUrl?: string; + /** * Get the Trigger configuration schema. * @returns {*} @@ -24,7 +36,6 @@ class Oidc extends Authentication { /** * Sanitize sensitive data - * @returns {*} */ maskConfiguration() { return { @@ -52,7 +63,7 @@ class Oidc extends Authentication { }); try { this.logoutUrl = this.client.endSessionUrl(); - } catch (e) { + } catch (e: any) { this.log.warn(` End session url is not supported (${e.message})`); } } @@ -61,7 +72,7 @@ class Oidc extends Authentication { * Return passport strategy. * @param app */ - getStrategy(app) { + getStrategy(app: Express) { app.get(`/auth/oidc/${this.name}/redirect`, async (req, res) => this.redirect(req, res), ); @@ -70,7 +81,7 @@ class Oidc extends Authentication { ); const strategy = new OidcStrategy( { - client: this.client, + client: this.client!, params: { scope: 'openid email profile', }, @@ -91,16 +102,15 @@ class Oidc extends Authentication { }; } - async redirect(req, res) { + async redirect(req: Request, res: any) { const codeVerifier = generators.codeVerifier(); const codeChallenge = generators.codeChallenge(codeVerifier); const state = uuid(); - - req.session.oidc = { + (req.session as any).oidc = { codeVerifier, state, }; - const authUrl = `${this.client.authorizationUrl({ + const authUrl = `${this.client!.authorizationUrl({ redirect_uri: `${getPublicUrl(req)}/auth/oidc/${this.name}/cb`, scope: 'openid email profile', code_challenge_method: 'S256', @@ -113,13 +123,13 @@ class Oidc extends Authentication { }); } - async callback(req, res) { + async callback(req: Request, res: any) { try { this.log.debug('Validate callback data'); - const params = this.client.callbackParams(req); - const oidcChecks = req.session.oidc; + const params = this.client!.callbackParams(req); + const oidcChecks = (req.session as any).oidc; - const tokenSet = await this.client.callback( + const tokenSet = await this.client!.callback( `${getPublicUrl(req)}/auth/oidc/${this.name}/cb`, params, { @@ -129,9 +139,7 @@ class Oidc extends Authentication { }, ); this.log.debug('Get user info'); - const user = await this.getUserFromAccessToken( - tokenSet.access_token, - ); + const user = await this.getUserFromAccessToken(tokenSet); this.log.debug('Perform passport login'); req.login(user, (err) => { @@ -145,30 +153,29 @@ class Oidc extends Authentication { res.redirect(`${getPublicUrl(req)}`); } }); - } catch (err) { + } catch (err: any) { this.log.warn(`Error when logging the user [${err.message}]`); res.status(401).send(err.message); } } - async verify(accessToken, done) { + async verify(accessToken: TokenSet, done: (err: any, user?: User | undefined) => void) { try { const user = await this.getUserFromAccessToken(accessToken); done(null, user); - } catch (e) { + } catch (e: any) { this.log.warn( `Error when validating the user access token (${e.message})`, ); - done(null, false); + done(null, undefined); } } - async getUserFromAccessToken(accessToken) { - const userInfo = await this.client.userinfo(accessToken); + async getUserFromAccessToken(accessToken: TokenSet) { + const userInfo = await this.client!.userinfo(accessToken); return { username: userInfo.email || 'unknown', }; } } -module.exports = Oidc; diff --git a/app/authentications/providers/oidc/OidcStrategy.test.js b/app/authentications/providers/oidc/OidcStrategy.test.ts similarity index 83% rename from app/authentications/providers/oidc/OidcStrategy.test.js rename to app/authentications/providers/oidc/OidcStrategy.test.ts index 7972cb8b..ad22edfd 100644 --- a/app/authentications/providers/oidc/OidcStrategy.test.js +++ b/app/authentications/providers/oidc/OidcStrategy.test.ts @@ -1,10 +1,10 @@ -const { Issuer } = require('openid-client'); -const OidcStrategy = require('./OidcStrategy'); -const log = require('../../../log'); +import { Issuer } from 'openid-client'; +import { OidcStrategy } from './OidcStrategy'; +import log from '../../../log'; const { Client } = new Issuer({ issuer: 'issuer' }); const client = new Client({ client_id: '123456789' }); -const oidcStrategy = new OidcStrategy({ client }, () => {}, log); +const oidcStrategy = new OidcStrategy({ client }, () => { }, log) as any; beforeEach(() => { oidcStrategy.success = jest.fn(); diff --git a/app/authentications/providers/oidc/OidcStrategy.js b/app/authentications/providers/oidc/OidcStrategy.ts similarity index 69% rename from app/authentications/providers/oidc/OidcStrategy.js rename to app/authentications/providers/oidc/OidcStrategy.ts index 9fa416ed..c6cabea2 100644 --- a/app/authentications/providers/oidc/OidcStrategy.js +++ b/app/authentications/providers/oidc/OidcStrategy.ts @@ -1,13 +1,22 @@ -const { Strategy } = require('openid-client'); +import Logger from 'bunyan'; +import { Client, Strategy, StrategyOptions, StrategyVerifyCallback, TokenSet } from 'openid-client'; +import { Request } from 'express'; -module.exports = class OidcStrategy extends Strategy { +export interface User { + username: string +} + +export class OidcStrategy extends Strategy { + private log: Logger; + private verify: StrategyVerifyCallback; + public name: string = 'oidc'; /** * Constructor. * @param options * @param verify * @param log */ - constructor(options, verify, log) { + constructor(options: StrategyOptions, verify: StrategyVerifyCallback, log: Logger) { super(options, verify); this.log = log; this.verify = verify; @@ -17,7 +26,7 @@ module.exports = class OidcStrategy extends Strategy { * Authenticate method. * @param req */ - authenticate(req) { + authenticate(req: Request) { // Already authenticated (thanks to session) => ok this.log.debug('Executing oidc strategy'); if (req.isAuthenticated()) { @@ -30,7 +39,7 @@ module.exports = class OidcStrategy extends Strategy { if (authSplit.length === 2) { this.log.debug('Bearer token found => validate it'); const accessToken = authSplit[1]; - this.verify(accessToken, (err, user) => { + this.verify(accessToken as unknown as TokenSet, (err, user) => { if (err || !user) { this.log.warn('Bearer token is invalid'); this.fail(401); diff --git a/app/configuration/index.test.js b/app/configuration/index.test.ts similarity index 98% rename from app/configuration/index.test.js rename to app/configuration/index.test.ts index 67dd4d34..779ce77e 100644 --- a/app/configuration/index.test.js +++ b/app/configuration/index.test.ts @@ -1,4 +1,4 @@ -const configuration = require('./index'); +import * as configuration from './index'; test('getVersion should return wud version', () => { configuration.wudEnvVars.WUD_VERSION = 'x.y.z'; diff --git a/app/configuration/index.js b/app/configuration/index.ts similarity index 72% rename from app/configuration/index.js rename to app/configuration/index.ts index 7b3cbe00..5b9da946 100644 --- a/app/configuration/index.js +++ b/app/configuration/index.ts @@ -1,6 +1,7 @@ -const fs = require('fs'); -const joi = require('joi'); -const setValue = require('set-value'); +import joi from "joi"; +import Logger from 'bunyan'; +import fs from 'fs'; +import setValue from 'set-value'; const VAR_FILE_SUFFIX = '__FILE'; @@ -9,8 +10,8 @@ const VAR_FILE_SUFFIX = '__FILE'; * @param prop * @returns {{}} */ -function get(prop, env = process.env) { - const object = {}; +export function get(prop: string, env = process.env) { + const object: T = {} as T; const envVarPattern = prop.replace(/\./g, '_').toUpperCase(); const matchingEnvVars = Object.keys(env).filter((envKey) => envKey.startsWith(envVarPattern), @@ -33,7 +34,7 @@ function get(prop, env = process.env) { * Lookup external secrets defined in files. * @param wudEnvVars */ -function replaceSecrets(wudEnvVars) { +export function replaceSecrets(wudEnvVars: Record) { const secretFileEnvVars = Object.keys(wudEnvVars).filter((wudEnvVar) => wudEnvVar.toUpperCase().endsWith(VAR_FILE_SUFFIX), ); @@ -46,35 +47,39 @@ function replaceSecrets(wudEnvVars) { }); } +export const wudEnvVars: Record = {}; // 1. Get a copy of all wud related env vars -const wudEnvVars = {}; Object.keys(process.env) .filter((envVar) => envVar.toUpperCase().startsWith('WUD')) .forEach((wudEnvVar) => { - wudEnvVars[wudEnvVar] = process.env[wudEnvVar]; + wudEnvVars[wudEnvVar] = process.env[wudEnvVar]!; }); // 2. Replace all secret files referenced by their secret values replaceSecrets(wudEnvVars); -function getVersion() { +export function getVersion() { return wudEnvVars.WUD_VERSION || 'unknown'; } -function getLogLevel() { - return wudEnvVars.WUD_LOG_LEVEL || 'info'; +export function getLogLevel(): Logger.LogLevel { + const logLevel = wudEnvVars.WUD_LOG_LEVEL + if (logLevel) { + return Object.keys(Logger.levelFromName).includes(logLevel) ? logLevel as Logger.LogLevel : 'info' + } + return 'info'; } /** * Get watcher configuration. */ -function getWatcherConfigurations() { +export function getWatcherConfigurations() { return get('wud.watcher', wudEnvVars); } /** * Get trigger configurations. */ -function getTriggerConfigurations() { +export function getTriggerConfigurations() { return get('wud.trigger', wudEnvVars); } @@ -82,7 +87,7 @@ function getTriggerConfigurations() { * Get registry configurations. * @returns {*} */ -function getRegistryConfigurations() { +export function getRegistryConfigurations() { return get('wud.registry', wudEnvVars); } @@ -90,23 +95,23 @@ function getRegistryConfigurations() { * Get authentication configurations. * @returns {*} */ -function getAuthenticationConfigurations() { +export function getAuthenticationConfigurations() { return get('wud.auth', wudEnvVars); } /** * Get Input configurations. */ -function getStoreConfiguration() { +export function getStoreConfiguration() { return get('wud.store', wudEnvVars); } /** * Get Server configurations. */ -function getServerConfiguration() { +export function getServerConfiguration() { const configurationFromEnv = get('wud.server', wudEnvVars); - const configurationSchema = joi.object().keys({ + const configurationSchema = joi.object().keys({ enabled: joi.boolean().default(true), port: joi.number().default(3000).integer().min(0).max(65535), tls: joi @@ -150,7 +155,7 @@ function getServerConfiguration() { return configurationToValidate.value; } -function getPublicUrl(req) { +export function getPublicUrl(req: any) { const publicUrl = wudEnvVars.WUD_PUBLIC_URL; if (publicUrl) { return publicUrl; @@ -159,17 +164,23 @@ function getPublicUrl(req) { return `${req.protocol}://${req.hostname}`; } -module.exports = { - wudEnvVars, - get, - replaceSecrets, - getVersion, - getLogLevel, - getStoreConfiguration, - getWatcherConfigurations, - getTriggerConfigurations, - getRegistryConfigurations, - getAuthenticationConfigurations, - getServerConfiguration, - getPublicUrl, -}; + +export interface ServerConfiguration { + enabled: boolean; + port: number; + tls: { + enabled: boolean; + key?: string; + cert?: string; + }; + cors: { + enabled: boolean; + origin: string; + methods: string; + }; + feature: { + delete: boolean; + }; +} + +export type BaseConfiguration = Record>; \ No newline at end of file diff --git a/app/event/index.test.js b/app/event/index.test.js deleted file mode 100644 index 244192d3..00000000 --- a/app/event/index.test.js +++ /dev/null @@ -1,46 +0,0 @@ -const event = require('./index'); - -const eventTestCases = [ - { - emitter: event.emitContainerReports, - register: event.registerContainerReports, - }, - { - emitter: event.emitContainerReport, - register: event.registerContainerReport, - }, - { - emitter: event.emitContainerAdded, - register: event.registerContainerAdded, - }, - { - emitter: event.emitContainerUpdated, - register: event.registerContainerUpdated, - }, - { - emitter: event.emitContainerRemoved, - register: event.registerContainerRemoved, - }, - { - emitter: event.emitWatcherStart, - register: event.registerWatcherStart, - }, - { - emitter: event.emitWatcherStop, - register: event.registerWatcherStop, - }, -]; -test.each(eventTestCases)( - 'the registered $register.name function must execute the handler when the $emitter.name emitter function is called', - async ({ register, emitter }) => { - // Register an handler - const handlerMock = jest.fn((item) => item); - register(handlerMock); - - // Emit the event - emitter(); - - // Ensure handler is called - expect(handlerMock.mock.calls.length === 1); - }, -); diff --git a/app/event/index.test.ts b/app/event/index.test.ts new file mode 100644 index 00000000..7ab9651c --- /dev/null +++ b/app/event/index.test.ts @@ -0,0 +1,49 @@ +import * as event from './index'; + +const eventTestCases: { + emitter: (it: any) => void; + register: (handler: (item: any) => void) => void; +}[] = [ + { + emitter: event.emitContainerReports, + register: event.registerContainerReports, + }, + { + emitter: event.emitContainerReport, + register: event.registerContainerReport, + }, + { + emitter: event.emitContainerAdded, + register: event.registerContainerAdded, + }, + { + emitter: event.emitContainerUpdated, + register: event.registerContainerUpdated, + }, + { + emitter: event.emitContainerRemoved, + register: event.registerContainerRemoved, + }, + { + emitter: event.emitWatcherStart, + register: event.registerWatcherStart, + }, + { + emitter: event.emitWatcherStop, + register: event.registerWatcherStop, + }, + ]; +test.each(eventTestCases)( + 'the registered $register.name function must execute the handler when the $emitter.name emitter function is called', + async ({ register, emitter }) => { + // Register an handler + const handlerMock = jest.fn((item) => item); + register(handlerMock); + + // Emit the event + emitter({}); + + // Ensure handler is called + expect(handlerMock.mock.calls.length === 1); + }, +); diff --git a/app/event/index.js b/app/event/index.ts similarity index 54% rename from app/event/index.js rename to app/event/index.ts index a6878bc1..84d4b467 100644 --- a/app/event/index.js +++ b/app/event/index.ts @@ -1,7 +1,22 @@ -const events = require('events'); +import { EventEmitter } from 'events'; +import { Container } from '../model/container'; +import { Watcher } from '../watchers/Watcher'; + +export interface ContainerReport { + container: Container, + changed?: boolean +} // Build EventEmitter -const eventEmitter = new events.EventEmitter(); +const eventEmitter = new EventEmitter<{ + [WUD_CONTAINER_ADDED]: [Container]; + [WUD_CONTAINER_UPDATED]: [Container]; + [WUD_CONTAINER_REMOVED]: [Container]; + [WUD_CONTAINER_REPORT]: [ContainerReport]; + [WUD_CONTAINER_REPORTS]: [ContainerReport[]]; + [WUD_WATCHER_START]: [Watcher]; + [WUD_WATCHER_STOP]: [Watcher]; +}>(); // Container related events const WUD_CONTAINER_ADDED = 'wud:container-added'; @@ -18,7 +33,7 @@ const WUD_WATCHER_STOP = 'wud:watcher-stop'; * Emit ContainerReports event. * @param containerReports */ -function emitContainerReports(containerReports) { +export function emitContainerReports(containerReports: ContainerReport[]) { eventEmitter.emit(WUD_CONTAINER_REPORTS, containerReports); } @@ -26,7 +41,7 @@ function emitContainerReports(containerReports) { * Register to ContainersResult event. * @param handler */ -function registerContainerReports(handler) { +export function registerContainerReports(handler: (reports: ContainerReport[]) => void) { eventEmitter.on(WUD_CONTAINER_REPORTS, handler); } @@ -34,7 +49,7 @@ function registerContainerReports(handler) { * Emit ContainerReport event. * @param containerReport */ -function emitContainerReport(containerReport) { +export function emitContainerReport(containerReport: ContainerReport) { eventEmitter.emit(WUD_CONTAINER_REPORT, containerReport); } @@ -42,7 +57,7 @@ function emitContainerReport(containerReport) { * Register to ContainerReport event. * @param handler */ -function registerContainerReport(handler) { +export function registerContainerReport(handler: (report: ContainerReport) => void) { eventEmitter.on(WUD_CONTAINER_REPORT, handler); } @@ -50,7 +65,7 @@ function registerContainerReport(handler) { * Emit container added. * @param containerAdded */ -function emitContainerAdded(containerAdded) { +export function emitContainerAdded(containerAdded: Container) { eventEmitter.emit(WUD_CONTAINER_ADDED, containerAdded); } @@ -58,7 +73,7 @@ function emitContainerAdded(containerAdded) { * Register to container added event. * @param handler */ -function registerContainerAdded(handler) { +export function registerContainerAdded(handler: (container: Container) => void) { eventEmitter.on(WUD_CONTAINER_ADDED, handler); } @@ -66,7 +81,7 @@ function registerContainerAdded(handler) { * Emit container added. * @param containerUpdated */ -function emitContainerUpdated(containerUpdated) { +export function emitContainerUpdated(containerUpdated: Container) { eventEmitter.emit(WUD_CONTAINER_UPDATED, containerUpdated); } @@ -74,7 +89,7 @@ function emitContainerUpdated(containerUpdated) { * Register to container updated event. * @param handler */ -function registerContainerUpdated(handler) { +export function registerContainerUpdated(handler: (container: Container) => void) { eventEmitter.on(WUD_CONTAINER_UPDATED, handler); } @@ -82,7 +97,7 @@ function registerContainerUpdated(handler) { * Emit container removed. * @param containerRemoved */ -function emitContainerRemoved(containerRemoved) { +export function emitContainerRemoved(containerRemoved: Container) { eventEmitter.emit(WUD_CONTAINER_REMOVED, containerRemoved); } @@ -90,38 +105,23 @@ function emitContainerRemoved(containerRemoved) { * Register to container removed event. * @param handler */ -function registerContainerRemoved(handler) { +export function registerContainerRemoved(handler: (removed: Container) => void) { eventEmitter.on(WUD_CONTAINER_REMOVED, handler); } -function emitWatcherStart(watcher) { +export function emitWatcherStart(watcher: Watcher) { eventEmitter.emit(WUD_WATCHER_START, watcher); } -function registerWatcherStart(handler) { +export function registerWatcherStart(handler: (watcher: Watcher) => void) { eventEmitter.on(WUD_WATCHER_START, handler); } -function emitWatcherStop(watcher) { +export function emitWatcherStop(watcher: Watcher) { eventEmitter.emit(WUD_WATCHER_STOP, watcher); } -function registerWatcherStop(handler) { +export function registerWatcherStop(handler: (watcher: Watcher) => void) { eventEmitter.on(WUD_WATCHER_STOP, handler); } -module.exports = { - emitContainerReports, - registerContainerReports, - emitContainerReport, - registerContainerReport, - emitContainerAdded, - registerContainerAdded, - emitContainerUpdated, - registerContainerUpdated, - emitContainerRemoved, - registerContainerRemoved, - emitWatcherStart, - registerWatcherStart, - emitWatcherStop, - registerWatcherStop, -}; + diff --git a/app/index.js b/app/index.ts similarity index 55% rename from app/index.js rename to app/index.ts index 26ec2d15..0d83ca91 100644 --- a/app/index.js +++ b/app/index.ts @@ -1,9 +1,9 @@ -const { getVersion } = require('./configuration'); -const log = require('./log'); -const store = require('./store'); -const registry = require('./registry'); -const api = require('./api'); -const prometheus = require('./prometheus'); +import { getVersion } from './configuration'; +import log from './log'; +import * as store from './store'; +import * as registry from './registry'; +import * as api from './api'; +import * as prometheus from './prometheus'; async function main() { log.info(`WUD is starting (version = ${getVersion()})`); diff --git a/app/jest.config.js b/app/jest.config.js new file mode 100644 index 00000000..d914c9d4 --- /dev/null +++ b/app/jest.config.js @@ -0,0 +1,8 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} **/ +module.exports = { + testEnvironment: 'node', + transform: { + '^.+\.tsx?$': ['ts-jest', {}], + }, + testMatch: ['**/*.test.ts'], +}; diff --git a/app/log/index.js b/app/log/index.js deleted file mode 100644 index dafb6dc1..00000000 --- a/app/log/index.js +++ /dev/null @@ -1,8 +0,0 @@ -const bunyan = require('bunyan'); -const { getLogLevel } = require('../configuration'); - -// Init Bunyan logger -module.exports = bunyan.createLogger({ - name: 'whats-up-docker', - level: getLogLevel(), -}); diff --git a/app/log/index.ts b/app/log/index.ts new file mode 100644 index 00000000..8ffb9867 --- /dev/null +++ b/app/log/index.ts @@ -0,0 +1,8 @@ +import { createLogger } from 'bunyan'; +import { getLogLevel } from '../configuration/index'; + +// Init Bunyan logger +export default createLogger({ + name: 'whats-up-docker', + level: getLogLevel(), +}); diff --git a/app/model/container.test.js b/app/model/container.test.ts similarity index 89% rename from app/model/container.test.js rename to app/model/container.test.ts index 3b0df99d..7c3769da 100644 --- a/app/model/container.test.js +++ b/app/model/container.test.ts @@ -1,4 +1,4 @@ -const container = require('./container'); +import * as container from './container'; test('model should be validated when compliant', () => { const containerValidated = container.validate({ @@ -31,7 +31,7 @@ test('model should be validated when compliant', () => { }, }); - expect(containerValidated.resultChanged.name).toEqual( + expect(containerValidated.resultChanged?.name).toEqual( 'resultChangedFunction', ); delete containerValidated.resultChanged; @@ -81,7 +81,7 @@ test('model should be validated when compliant', () => { test('model should not be validated when invalid', () => { expect(() => { - container.validate({}); + container.validate({} as container.Container); }).toThrow(); }); @@ -215,9 +215,9 @@ test('model should flag updateAvailable when created is different', () => { const containerDifferent = container.validate({ ...containerValidated, }); - containerDifferent.result.tag = 'y'; - expect(containerValidated.resultChanged(containerEquals)).toBeFalsy(); - expect(containerValidated.resultChanged(containerDifferent)).toBeTruthy(); + containerDifferent.result!.tag = 'y'; + expect(containerValidated.resultChanged!(containerEquals)).toBeFalsy(); + expect(containerValidated.resultChanged!(containerDifferent)).toBeTruthy(); }); test('model should support transforms for links', () => { @@ -322,12 +322,12 @@ test('fullName should build an id with watcher name & container name when called container.fullName({ watcher: 'watcher', name: 'container_name', - }), + } as container.Container), ).toEqual('watcher_container_name'); }); test('getLink should render link templates when called', () => { - const getLink = container.__get__('getLink'); + const getLink = container.getLink; expect( getLink( { @@ -336,21 +336,22 @@ test('getLink should render link templates when called', () => { image: { tag: { semver: true, + value: '', }, }, - }, + } as container.Container, '10.5.2', ), ).toEqual('https://test-10.5.2.acme.com'); }); test('getLink should render undefined when template is missing', () => { - const getLink = container.__get__('getLink'); - expect(getLink(undefined)).toBeUndefined(); + const getLink = container.getLink; + expect(getLink(undefined as unknown as container.Container, '')).toBeUndefined(); }); test('addUpdateKindProperty should detect major update', () => { - const addUpdateKindProperty = container.__get__('addUpdateKindProperty'); + const addUpdateKindProperty = container.addUpdateKindProperty; const containerObject = { updateAvailable: true, image: { @@ -362,7 +363,7 @@ test('addUpdateKindProperty should detect major update', () => { result: { tag: '2.0.0', }, - }; + } as container.Container; addUpdateKindProperty(containerObject); expect(containerObject.updateKind).toEqual({ kind: 'tag', @@ -373,7 +374,7 @@ test('addUpdateKindProperty should detect major update', () => { }); test('addUpdateKindProperty should detect minor update', () => { - const addUpdateKindProperty = container.__get__('addUpdateKindProperty'); + const addUpdateKindProperty = container.addUpdateKindProperty; const containerObject = { updateAvailable: true, image: { @@ -385,7 +386,7 @@ test('addUpdateKindProperty should detect minor update', () => { result: { tag: '1.1.0', }, - }; + } as container.Container; addUpdateKindProperty(containerObject); expect(containerObject.updateKind).toEqual({ kind: 'tag', @@ -396,7 +397,7 @@ test('addUpdateKindProperty should detect minor update', () => { }); test('addUpdateKindProperty should detect patch update', () => { - const addUpdateKindProperty = container.__get__('addUpdateKindProperty'); + const addUpdateKindProperty = container.addUpdateKindProperty; const containerObject = { updateAvailable: true, image: { @@ -408,7 +409,7 @@ test('addUpdateKindProperty should detect patch update', () => { result: { tag: '1.0.1', }, - }; + } as container.Container; addUpdateKindProperty(containerObject); expect(containerObject.updateKind).toEqual({ kind: 'tag', @@ -419,7 +420,7 @@ test('addUpdateKindProperty should detect patch update', () => { }); test('addUpdateKindProperty should support transforms', () => { - const addUpdateKindProperty = container.__get__('addUpdateKindProperty'); + const addUpdateKindProperty = container.addUpdateKindProperty; const containerObject = { transformTags: '^(\\d+\\.\\d+)-.*-(\\d+) => $1.$2', updateAvailable: true, @@ -432,7 +433,7 @@ test('addUpdateKindProperty should support transforms', () => { result: { tag: '1.2-bar-4', }, - }; + } as container.Container; addUpdateKindProperty(containerObject); expect(containerObject.updateKind).toEqual({ kind: 'tag', @@ -443,7 +444,7 @@ test('addUpdateKindProperty should support transforms', () => { }); test('addUpdateKindProperty should detect prerelease semver update', () => { - const addUpdateKindProperty = container.__get__('addUpdateKindProperty'); + const addUpdateKindProperty = container.addUpdateKindProperty; const containerObject = { updateAvailable: true, image: { @@ -455,7 +456,7 @@ test('addUpdateKindProperty should detect prerelease semver update', () => { result: { tag: '1.0.0-test2', }, - }; + } as container.Container; addUpdateKindProperty(containerObject); expect(containerObject.updateKind).toEqual({ kind: 'tag', @@ -466,7 +467,7 @@ test('addUpdateKindProperty should detect prerelease semver update', () => { }); test('addUpdateKindProperty should detect digest update', () => { - const addUpdateKindProperty = container.__get__('addUpdateKindProperty'); + const addUpdateKindProperty = container.addUpdateKindProperty; const containerObject = { updateAvailable: true, image: { @@ -482,7 +483,7 @@ test('addUpdateKindProperty should detect digest update', () => { tag: 'latest', digest: 'sha256:987654321', }, - }; + } as container.Container; addUpdateKindProperty(containerObject); expect(containerObject.updateKind).toEqual({ kind: 'digest', @@ -492,8 +493,8 @@ test('addUpdateKindProperty should detect digest update', () => { }); test('addUpdateKindProperty should return unknown when no image or result', () => { - const addUpdateKindProperty = container.__get__('addUpdateKindProperty'); - const containerObject = {}; + const addUpdateKindProperty = container.addUpdateKindProperty; + const containerObject = {} as container.Container; addUpdateKindProperty(containerObject); expect(containerObject.updateKind).toEqual({ kind: 'unknown', @@ -501,12 +502,15 @@ test('addUpdateKindProperty should return unknown when no image or result', () = }); test('addUpdateKindProperty should return unknown when no update available', () => { - const addUpdateKindProperty = container.__get__('addUpdateKindProperty'); + const addUpdateKindProperty = container.addUpdateKindProperty; const containerObject = { - image: 'image', + image: {}, result: {}, updateAvailable: false, - }; + id: "", + name: "", + watcher: "", + } as container.Container; addUpdateKindProperty(containerObject); expect(containerObject.updateKind).toEqual({ kind: 'unknown', diff --git a/app/model/container.js b/app/model/container.ts similarity index 69% rename from app/model/container.js rename to app/model/container.ts index 6bb3b868..cd49ab42 100644 --- a/app/model/container.js +++ b/app/model/container.ts @@ -1,11 +1,10 @@ -const joi = require('joi'); -const flat = require('flat'); -const { snakeCase } = require('snake-case'); -const { parse: parseSemver, diff: diffSemver } = require('../tag'); -const { transform: transformTag } = require('../tag'); +import joi from 'joi'; +import flat from 'flat'; +import { snakeCase } from 'snake-case'; +import { parse as parseSemver, diff as diffSemver, transform as transformTag } from '../tag'; // Container data schema -const schema = joi.object({ +const schema = joi.object({ id: joi.string().min(1).required(), name: joi.string().min(1).required(), displayName: joi.string().default(joi.ref('name')), @@ -77,7 +76,7 @@ const schema = joi.object({ * @param container * @returns {undefined|*} */ -function getLink(container, originalTagValue) { +export function getLink(container: Container, originalTagValue: string) { if (!container || !container.linkTemplate) { return undefined; } @@ -94,24 +93,23 @@ function getLink(container, originalTagValue) { let prerelease = ''; if (container.image.tag.semver) { - const versionSemver = parseSemver(transformed); - major = versionSemver.major; - minor = versionSemver.minor; - patch = versionSemver.patch; + const versionSemver = parseSemver(transformed)!; + major = versionSemver.major.toString(); + minor = versionSemver.minor.toString(); + patch = versionSemver.patch.toString(); prerelease = versionSemver.prerelease && versionSemver.prerelease.length > 0 - ? prerelease[0] + ? versionSemver.prerelease.toString() : ''; } - return eval('`' + container.linkTemplate + '`'); + return eval('`' + container.linkTemplate + '`') as string; } /** * Computed function to check whether there is an update. * @param container - * @returns {boolean} */ -function addUpdateAvailableProperty(container) { +function addUpdateAvailableProperty(container: Container) { Object.defineProperty(container, 'updateAvailable', { enumerable: true, get() { @@ -163,7 +161,7 @@ function addUpdateAvailableProperty(container) { * @param container * @returns {undefined|*} */ -function addLinkProperty(container) { +function addLinkProperty(container: Container) { if (container.linkTemplate) { Object.defineProperty(container, 'link', { enumerable: true, @@ -176,7 +174,7 @@ function addLinkProperty(container) { Object.defineProperty(container.result, 'link', { enumerable: true, get() { - return getLink(container, container.result.tag); + return getLink(container, container.result!.tag!); }, }); } @@ -186,13 +184,12 @@ function addLinkProperty(container) { /** * Computed updateKind property. * @param container - * @returns {{semverDiff: undefined, kind: string, remoteValue: undefined, localValue: undefined}} */ -function addUpdateKindProperty(container) { +export function addUpdateKindProperty(container: Container) { Object.defineProperty(container, 'updateKind', { enumerable: true, get() { - const updateKind = { + const updateKind: UpdateKind = { kind: 'unknown', localValue: undefined, remoteValue: undefined, @@ -215,7 +212,7 @@ function addUpdateKindProperty(container) { ) { if (container.image.tag.value !== container.result.tag) { updateKind.kind = 'tag'; - let semverDiffWud = 'unknown'; + let semverDiffWud: SimpleReleaseType = 'unknown'; const isSemver = container.image.tag.semver; if (isSemver) { const semverDiff = diffSemver( @@ -225,7 +222,7 @@ function addUpdateKindProperty(container) { ), transformTag( container.transformTags, - container.result.tag, + container.result!.tag!, ), ); switch (semverDiff) { @@ -270,21 +267,21 @@ function addUpdateKindProperty(container) { * @param otherContainer * @returns {boolean} */ -function resultChangedFunction(otherContainer) { +function resultChangedFunction(this: Container, otherContainer: Container): boolean { return ( otherContainer === undefined || - this.result.tag !== otherContainer.result.tag || - this.result.digest !== otherContainer.result.digest || - this.result.created !== otherContainer.result.created + this.result === undefined || + this.result.tag !== otherContainer.result!.tag || + this.result.digest !== otherContainer.result!.digest || + this.result.created !== otherContainer.result!.created ); } /** * Add computed function to check whether the result is different. * @param container - * @returns {*} */ -function addResultChangedFunction(container) { +function addResultChangedFunction(container: Container) { const containerWithResultChanged = container; containerWithResultChanged.resultChanged = resultChangedFunction; return containerWithResultChanged; @@ -293,9 +290,8 @@ function addResultChangedFunction(container) { /** * Apply validation to the container object. * @param container - * @returns {*} */ -function validate(container) { +export function validate(container: Container) { const validation = schema.validate(container); if (validation.error) { throw new Error( @@ -317,10 +313,9 @@ function validate(container) { /** * Flatten the container object (useful for k/v based integrations). * @param container - * @returns {*} */ -function flatten(container) { - const containerFlatten = flat(container, { +export function flatten(container: Container): FlattenedContainer { + const containerFlatten = flat(container, { delimiter: '_', transformKey: (key) => snakeCase(key), }); @@ -333,12 +328,109 @@ function flatten(container) { * @param container * @returns {string} */ -function fullName(container) { +export function fullName(container: Container) { return `${container.watcher}_${container.name}`; } -module.exports = { - validate, - flatten, - fullName, + +export type ContainerImage = { + id: string; + registry: { + name?: string; + url: string; + }; + name: string; + tag: { + value: string; + semver?: boolean; + }; + digest: { + watch?: boolean; + value?: string; + repo?: string; + }; + architecture: string; + os: string; + variant?: string; + created?: string; // ISO date string }; + +export type Container = { + id: string; + name: string; + displayName?: string; + displayIcon?: string; + status?: string; + watcher: string; + includeTags?: string; + excludeTags?: string; + transformTags?: string; + linkTemplate?: string; + link?: string; + triggerInclude?: string; + triggerExclude?: string; + image: ContainerImage; + result?: { + tag?: string; + digest?: string; + created?: string; // ISO date string + link?: string; + }; + error?: { + message: string; + }; + updateAvailable?: boolean; + updateKind?: UpdateKind; + resultChanged?: (container: Container) => boolean; + labels?: Record; +} + +export type UpdateKind = { + kind: 'tag' | 'digest' | 'unknown'; + localValue?: string; + remoteValue?: string; + semverDiff?: SimpleReleaseType; +} + +export type SimpleReleaseType = "major" | "minor" | "patch" | "prerelease" | "unknown"; + +export type FlattenedContainer = { + id: string; + name: string; + display_name?: string; + display_icon?: string; + status?: string; + watcher: string; + include_tags?: string; + exclude_tags?: string; + transform_tags?: string; + link_template?: string; + link?: string; + trigger_include?: string; + trigger_exclude?: string; + image_id: string; + image_registry_name: string; + image_registry_url: string; + image_name: string; + image_tag_value: string; + image_tag_semver?: boolean; + image_digest_watch?: boolean; + image_digest_value?: string; + image_digest_repo?: string; + image_architecture: string; + image_os: string; + image_variant?: string; + image_created?: string; // ISO date string + result_tag?: string; + result_digest?: string; + result_created?: string; // ISO date string + result_link?: string; + error_message?: string; + update_available?: boolean; + update_kind_kind?: 'tag' | 'digest' | 'unknown'; + update_kind_local_value?: string; + update_kind_remote_value?: string; + update_kind_semver_diff?: SimpleReleaseType; + result_changed?: (container: Container) => boolean; + labels?: Record; +} \ No newline at end of file diff --git a/app/package-lock.json b/app/package-lock.json index 36a7f107..e41608b7 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -9,15 +9,15 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@slack/web-api": "7.8.0", + "@slack/web-api": "7.9.1", "aws-sdk": "2.1692.0", "body-parser": "1.20.3", "bunyan": "1.8.15", "capitalize": "2.0.4", "connect-loki": "1.2.0", "cors": "2.8.5", - "dockerode": "4.0.2", - "express": "4.21.2", + "dockerode": "4.0.5", + "express": "5.1.0", "express-healthcheck": "0.1.0", "express-session": "1.18.1", "flat": "5.0.2", @@ -28,7 +28,7 @@ "just-debounce": "1.1.0", "kafkajs": "2.2.4", "lokijs": "1.5.12", - "mqtt": "5.10.3", + "mqtt": "5.10.4", "nocache": "4.0.0", "node-cron": "3.0.3", "node-telegram-bot-api": "0.66.0", @@ -43,24 +43,51 @@ "pushover-notifications": "1.2.3", "request": "2.88.2", "request-promise-native": "1.0.9", - "semver": "7.6.3", + "semver": "7.7.1", "set-value": "4.1.0", "snake-case": "3.0.4", - "sort-es": "1.7.13", - "uuid": "11.0.3", - "yaml": "2.6.1" + "sort-es": "1.7.14", + "uuid": "^11.1.0", + "yaml": "2.7.1" }, "devDependencies": { + "@types/aws-sdk": "^0.0.42", + "@types/body-parser": "^1.19.5", + "@types/bunyan": "^1.8.11", + "@types/cors": "^2.8.17", + "@types/dockerode": "^3.3.37", + "@types/express": "^5.0.1", + "@types/express-session": "^1.18.1", + "@types/flat": "^5.0.5", + "@types/hapi__joi": "^17.1.15", + "@types/jest": "^29.5.14", + "@types/lokijs": "^1.5.14", + "@types/mqtt": "^0.0.34", + "@types/node": "^22.14.0", + "@types/node-cron": "^3.0.11", + "@types/node-telegram-bot-api": "^0.64.8", + "@types/nodemailer": "^6.4.17", + "@types/passport": "^1.0.17", + "@types/passport-anonymous": "^1.0.5", + "@types/passport-http": "^0.3.11", + "@types/request": "^2.48.12", + "@types/request-promise-native": "^1.0.21", + "@types/semver": "^7.7.0", + "@types/set-value": "^4.0.3", + "@types/yaml": "^1.9.6", "babel-jest": "29.7.0", "babel-plugin-rewire": "1.2.0", - "eslint": "9.17.0", - "eslint-config-prettier": "9.1.0", + "eslint": "9.24.0", + "eslint-config-prettier": "10.1.1", "eslint-plugin-import": "2.31.0", - "eslint-plugin-jest": "28.10.0", - "eslint-plugin-prettier": "5.2.1", + "eslint-plugin-jest": "28.11.0", + "eslint-plugin-prettier": "5.2.6", "jest": "29.7.0", - "nodemon": "3.1.9", - "prettier": "3.4.2" + "nodemon": "^3.1.9", + "prettier": "3.5.3", + "ts-jest": "^29.3.2", + "ts-node": "^10.9.2", + "typescript": "^5.8.3" } }, "node_modules/@ampproject/remapping": { @@ -601,6 +628,30 @@ "dev": true, "license": "MIT" }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@cypress/request": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.7.tgz", @@ -730,13 +781,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz", - "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.5", + "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -744,10 +795,20 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz", + "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/core": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.1.tgz", - "integrity": "sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", + "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -758,9 +819,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", - "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "dev": true, "license": "MIT", "dependencies": { @@ -815,9 +876,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.17.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz", - "integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==", + "version": "9.24.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.24.0.tgz", + "integrity": "sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==", "dev": true, "license": "MIT", "engines": { @@ -825,9 +886,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz", - "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -835,18 +896,63 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.4.tgz", - "integrity": "sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==", + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", + "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", "dev": true, "license": "Apache-2.0", "dependencies": { + "@eslint/core": "^0.13.0", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.2.tgz", + "integrity": "sha512-nnR5nmL6lxF8YBqb6gWvEgLdLh/Fn+kvAdX5hUOnt48sNSb0riz/93ASd2E5gvanPA41X6Yp25bIfGRp1SMb2g==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.13", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", + "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", @@ -915,9 +1021,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", - "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", + "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1317,6 +1423,16 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1374,9 +1490,9 @@ } }, "node_modules/@pkgr/core": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", - "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.0.tgz", + "integrity": "sha512-vsJDAkYR6qCPu+ioGScGiMYR7LvZYIXh/dlQeviqoTWNCVfKTLYD/LkNWH4Mxsv2a5vpIRc77FN5DnmK1eBggQ==", "dev": true, "license": "MIT", "engines": { @@ -1386,6 +1502,70 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1477,16 +1657,16 @@ } }, "node_modules/@slack/web-api": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@slack/web-api/-/web-api-7.8.0.tgz", - "integrity": "sha512-d4SdG+6UmGdzWw38a4sN3lF/nTEzsDxhzU13wm10ejOpPehtmRoqBKnPztQUfFiWbNvSb4czkWYJD4kt+5+Fuw==", + "version": "7.9.1", + "resolved": "https://registry.npmjs.org/@slack/web-api/-/web-api-7.9.1.tgz", + "integrity": "sha512-qMcb1oWw3Y/KlUIVJhkI8+NcQXq1lNymwf+ewk93ggZsGd6iuz9ObQsOEbvlqlx1J+wd8DmIm3DORGKs0fcKdg==", "license": "MIT", "dependencies": { "@slack/logger": "^4.0.0", "@slack/types": "^2.9.0", "@types/node": ">=18.0.0", "@types/retry": "0.12.0", - "axios": "^1.7.8", + "axios": "^1.8.3", "eventemitter3": "^5.0.1", "form-data": "^4.0.0", "is-electron": "2.2.2", @@ -1512,6 +1692,44 @@ "node": ">=10" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/aws-sdk": { + "version": "0.0.42", + "resolved": "https://registry.npmjs.org/@types/aws-sdk/-/aws-sdk-0.0.42.tgz", + "integrity": "sha512-zIgLukZrf0/s+oAKxLMHgZFDDjDpuJ95hbE9DiNGrmNGNM7odIt99rHLWVwnOYdF0TNjF0reQeL/mcadAIqljg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1557,6 +1775,27 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bunyan": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/@types/bunyan/-/bunyan-1.8.11.tgz", + "integrity": "sha512-758fRH7umIMk5qt5ELmRMff4mLDlN+xyYzC+dkPTdKwbSkJFvz6xwyScrytPU0QIBbRRwbiE8/BIg8bpajerNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/cacheable-request": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", @@ -1569,6 +1808,56 @@ "@types/responselike": "^1.0.0" } }, + "node_modules/@types/caseless": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", + "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/docker-modem": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz", + "integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/ssh2": "*" + } + }, + "node_modules/@types/dockerode": { + "version": "3.3.37", + "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.37.tgz", + "integrity": "sha512-r+IoKpE5MLKaeD8CvoEh39ckWMLHR/+WBMoRQxrkL+apJqEWLMhBHh+93KIfyPWGd6gK7Q21jpoULKgNoRI0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/docker-modem": "*", + "@types/node": "*", + "@types/ssh2": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -1576,6 +1865,48 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.1.tgz", + "integrity": "sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/express-session": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.18.1.tgz", + "integrity": "sha512-S6TkD/lljxDlQ2u/4A70luD8/ZxZcrU5pQwI1rVXCiaVIywoFgbA+PIUNDjPhQpPdK0dGleLtYc/y7XWBfclBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/flat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@types/flat/-/flat-5.0.5.tgz", + "integrity": "sha512-nPLljZQKSnac53KDUDzuzdRfGI0TDb5qPrb+SrQyN3MtdQrOnGsKniHN1iYZsJEBIVQve94Y6gNz22sgISZq+Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -1586,12 +1917,26 @@ "@types/node": "*" } }, + "node_modules/@types/hapi__joi": { + "version": "17.1.15", + "resolved": "https://registry.npmjs.org/@types/hapi__joi/-/hapi__joi-17.1.15.tgz", + "integrity": "sha512-Ehq/YQB0ZqZGObrGngztxtThTiShrG0jlqyUSsNK3cebJSoyYgE/hdZvYt6lH4Wimi28RowDwnr87XseiemqAg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/http-cache-semantics": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", "license": "MIT" }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -1619,6 +1964,17 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1642,15 +1998,112 @@ "@types/node": "*" } }, + "node_modules/@types/lokijs": { + "version": "1.5.14", + "resolved": "https://registry.npmjs.org/@types/lokijs/-/lokijs-1.5.14.tgz", + "integrity": "sha512-4Fic47BX3Qxr8pd12KT6/T1XWU8dOlJBIp1jGoMbaDbiEvdv50rAii+B3z1b/J2pvMywcVP+DBPGP5/lgLOKGA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mqtt": { + "version": "0.0.34", + "resolved": "https://registry.npmjs.org/@types/mqtt/-/mqtt-0.0.34.tgz", + "integrity": "sha512-wABv2ml0o7ggNzTI89ZXbdstRfQNvsMOJcR2NDg/Euq67XIb1qQwFqRDB9ilMfNBY1O10myafRh2sWGFfBEuyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node": { - "version": "22.10.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", - "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", + "version": "22.14.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz", + "integrity": "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==", "license": "MIT", "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~6.21.0" } }, + "node_modules/@types/node-cron": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.11.tgz", + "integrity": "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node-telegram-bot-api": { + "version": "0.64.8", + "resolved": "https://registry.npmjs.org/@types/node-telegram-bot-api/-/node-telegram-bot-api-0.64.8.tgz", + "integrity": "sha512-1c1RF6iWdPfuzknnJTrTT+JeIqpw2KcY2sxvXBq7Ycf7AEMK3dhV7uFNQbqPSrvWfSCshE2HnivI8THeFtwQpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/request": "*" + } + }, + "node_modules/@types/nodemailer": { + "version": "6.4.17", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz", + "integrity": "sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/passport": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", + "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/passport-anonymous": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/passport-anonymous/-/passport-anonymous-1.0.5.tgz", + "integrity": "sha512-RCn+UFc13LKmABj+GGGCGRIA2m2UcdjWQamFllOhIWXJGzbC2rDhVRU6/y8wgRalbXlpt/OXI/EyVNQTRDIJ4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/passport": "*" + } + }, + "node_modules/@types/passport-http": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@types/passport-http/-/passport-http-0.3.11.tgz", + "integrity": "sha512-FO0rDRYtuha9m2ZgRx5+jrgrrkAnUzgzdItFI0dwKBC6k9pArK677Gtan67u6+Qah2nXVP3M1uZ5p90SpBT5Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.9.18", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", + "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/readable-stream": { "version": "4.0.18", "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.18.tgz", @@ -1667,6 +2120,46 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, + "node_modules/@types/request": { + "version": "2.48.12", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.12.tgz", + "integrity": "sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + } + }, + "node_modules/@types/request-promise-native": { + "version": "1.0.21", + "resolved": "https://registry.npmjs.org/@types/request-promise-native/-/request-promise-native-1.0.21.tgz", + "integrity": "sha512-NJ1M6iqWTEUT+qdP+OmXsRZ6tSdkoBdblHKatIWTVP1HdYpHU3IkfpLPf4MWb0+CC4Nl3TtLpYhDlhjZxytDIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/request": "*" + } + }, + "node_modules/@types/request/node_modules/form-data": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.3.tgz", + "integrity": "sha512-XHIrMD0NpDrNM/Ckf7XJiBbLl57KEhT3+i3yY+eWm+cqYZJQTZrKo8Y8AWKnuV5GT4scfuUGt9LzNoIx3dU1nQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, "node_modules/@types/responselike": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", @@ -1682,6 +2175,70 @@ "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", "license": "MIT" }, + "node_modules/@types/semver": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", + "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/set-value": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/set-value/-/set-value-4.0.3.tgz", + "integrity": "sha512-tSuUcLl6kMzI+l0gG7FZ04xbIcynxNIYgWFj91LPAvRcn7W3L1EveXNdVjqFDgAZPjY1qCOsm8Sb1C70SxAPHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ssh2": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.5.tgz", + "integrity": "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^18.11.18" + } + }, + "node_modules/@types/ssh2/node_modules/@types/node": { + "version": "18.19.86", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.86.tgz", + "integrity": "sha512-fifKayi175wLyKyc5qUfyENhQ1dCNI1UNjp653d8kuYcPQN5JhX3dGuP/XmvPTg/xRBn1VTLpbmi+H/Mr7tLfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/ssh2/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -1689,15 +2246,32 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/ws": { - "version": "8.5.13", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", - "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", "license": "MIT", "dependencies": { "@types/node": "*" } }, + "node_modules/@types/yaml": { + "version": "1.9.6", + "resolved": "https://registry.npmjs.org/@types/yaml/-/yaml-1.9.6.tgz", + "integrity": "sha512-VKOCuDN57wngmyQnRqcn4vuGWCXViISHv+UCCjrKcf1yt4zyfMmOGlZDI2ucTHK72V8ki+sd7h21OZL6O5S52A==", + "dev": true, + "license": "MIT", + "dependencies": { + "yaml": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -1716,14 +2290,14 @@ "license": "MIT" }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.18.1.tgz", - "integrity": "sha512-HxfHo2b090M5s2+/9Z3gkBhI6xBH8OJCFjH9MhQ+nnoZqxU3wNxkLT+VWXWSFWc3UF3Z+CfPAyqdCTdoXtDPCQ==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.29.0.tgz", + "integrity": "sha512-aO1PVsq7Gm+tcghabUpzEnVSFMCU4/nYIgC2GOatJcllvWfnhrgW0ZEbnTxm36QsikmCN1K/6ZgM7fok2I7xNw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.18.1", - "@typescript-eslint/visitor-keys": "8.18.1" + "@typescript-eslint/types": "8.29.0", + "@typescript-eslint/visitor-keys": "8.29.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1734,9 +2308,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.18.1.tgz", - "integrity": "sha512-7uoAUsCj66qdNQNpH2G8MyTFlgerum8ubf21s3TSM3XmKXuIn+H2Sifh/ES2nPOPiYSRJWAk0fDkW0APBWcpfw==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.29.0.tgz", + "integrity": "sha512-wcJL/+cOXV+RE3gjCyl/V2G877+2faqvlgtso/ZRbTCnZazh0gXhe+7gbAnfubzN2bNsBtZjDvlh7ero8uIbzg==", "dev": true, "license": "MIT", "engines": { @@ -1748,20 +2322,20 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.1.tgz", - "integrity": "sha512-z8U21WI5txzl2XYOW7i9hJhxoKKNG1kcU4RzyNvKrdZDmbjkmLBo8bgeiOJmA06kizLI76/CCBAAGlTlEeUfyg==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.29.0.tgz", + "integrity": "sha512-yOfen3jE9ISZR/hHpU/bmNvTtBW1NjRbkSFdZOksL1N+ybPEE7UVGMwqvS6CP022Rp00Sb0tdiIkhSCe6NI8ow==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.18.1", - "@typescript-eslint/visitor-keys": "8.18.1", + "@typescript-eslint/types": "8.29.0", + "@typescript-eslint/visitor-keys": "8.29.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1771,7 +2345,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { @@ -1801,16 +2375,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.18.1.tgz", - "integrity": "sha512-8vikiIj2ebrC4WRdcAdDcmnu9Q/MXXwg+STf40BVfT8exDqBCUPdypvzcUPxEqRGKg9ALagZ0UWcYCtn+4W2iQ==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.29.0.tgz", + "integrity": "sha512-gX/A0Mz9Bskm8avSWFcK0gP7cZpbY4AIo6B0hWYFCaIsz750oaiWR4Jr2CI+PQhfW1CpcQr9OlfPS+kMFegjXA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.18.1", - "@typescript-eslint/types": "8.18.1", - "@typescript-eslint/typescript-estree": "8.18.1" + "@typescript-eslint/scope-manager": "8.29.0", + "@typescript-eslint/types": "8.29.0", + "@typescript-eslint/typescript-estree": "8.29.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1821,17 +2395,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.1.tgz", - "integrity": "sha512-Vj0WLm5/ZsD013YeUKn+K0y8p1M0jPpxOkKdbD1wB0ns53a5piVY02zjf072TblEweAbcYiFiPoSMF3kp+VhhQ==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.29.0.tgz", + "integrity": "sha512-Sne/pVz8ryR03NFK21VpN88dZ2FdQXOlq3VIklbrTYEt8yXtRFr9tvUhqvCeKjqYk5FSim37sHbooT6vzBTZcg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.18.1", + "@typescript-eslint/types": "8.29.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -1855,13 +2429,34 @@ } }, "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", "license": "MIT", "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "mime-db": "^1.54.0" }, "engines": { "node": ">= 0.6" @@ -1890,6 +2485,19 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -1939,7 +2547,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -1949,7 +2556,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -1975,6 +2581,13 @@ "node": ">= 8" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -2001,12 +2614,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, "node_modules/array-includes": { "version": "3.1.8", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", @@ -2143,6 +2750,13 @@ "node": ">=0.8" } }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2211,9 +2825,9 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.7.9", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", - "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", + "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -2502,12 +3116,49 @@ "ms": "2.0.0" } }, + "node_modules/body-parser/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/body-parser/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/body-parser/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2565,6 +3216,19 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/bser": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", @@ -2887,7 +3551,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -2932,7 +3595,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -2945,7 +3607,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/combined-stream": { @@ -2998,9 +3659,9 @@ } }, "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" @@ -3035,10 +3696,13 @@ } }, "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "license": "MIT" + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } }, "node_modules/core-util-is": { "version": "1.0.2", @@ -3095,6 +3759,13 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cron-parser": { "version": "2.18.0", "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-2.18.0.tgz", @@ -3343,6 +4014,16 @@ "node": ">=8" } }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -3354,9 +4035,9 @@ } }, "node_modules/docker-modem": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.3.tgz", - "integrity": "sha512-89zhop5YVhcPEt5FpUFGr3cDyceGhq/F9J+ZndQ4KfqNvfbJpPMfgeixFgUj5OjCYAboElqODxY5Z1EBsSa6sg==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.6.tgz", + "integrity": "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ==", "license": "Apache-2.0", "dependencies": { "debug": "^4.1.1", @@ -3369,19 +4050,36 @@ } }, "node_modules/dockerode": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.2.tgz", - "integrity": "sha512-9wM1BVpVMFr2Pw3eJNXrYYt6DT9k0xMcsSCjtPvyQ+xa1iPg/Mo3T/gUcwI0B2cczqCeCYRPF8yFYDwtFXT0+w==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.5.tgz", + "integrity": "sha512-ZPmKSr1k1571Mrh7oIBS/j0AqAccoecY2yH420ni5j1KyNMgnoTh4Nu4FWunh0HZIJmRSmSysJjBIpa/zyWUEA==", "license": "Apache-2.0", "dependencies": { "@balena/dockerignore": "^1.0.2", - "docker-modem": "^5.0.3", - "tar-fs": "~2.0.1" + "@grpc/grpc-js": "^1.11.1", + "@grpc/proto-loader": "^0.7.13", + "docker-modem": "^5.0.6", + "protobufjs": "^7.3.2", + "tar-fs": "~2.1.2", + "uuid": "^10.0.0" }, "engines": { "node": ">= 8.0" } }, + "node_modules/dockerode/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -3449,6 +4147,22 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.75", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.75.tgz", @@ -3473,7 +4187,6 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/encodeurl": { @@ -3596,14 +4309,15 @@ } }, "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.4", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -3639,7 +4353,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -3665,22 +4378,23 @@ } }, "node_modules/eslint": { - "version": "9.17.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.17.0.tgz", - "integrity": "sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==", + "version": "9.24.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.24.0.tgz", + "integrity": "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.9.0", - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.17.0", - "@eslint/plugin-kit": "^0.2.3", + "@eslint/config-array": "^0.20.0", + "@eslint/config-helpers": "^0.2.0", + "@eslint/core": "^0.12.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.24.0", + "@eslint/plugin-kit": "^0.2.7", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.1", + "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", @@ -3688,7 +4402,7 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.2.0", + "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", @@ -3725,9 +4439,9 @@ } }, "node_modules/eslint-config-prettier": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", - "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.1.tgz", + "integrity": "sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw==", "dev": true, "license": "MIT", "bin": { @@ -3842,9 +4556,9 @@ } }, "node_modules/eslint-plugin-jest": { - "version": "28.10.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.10.0.tgz", - "integrity": "sha512-hyMWUxkBH99HpXT3p8hc7REbEZK3D+nk8vHXGgpB+XXsi0gO4PxMSP+pjfUzb67GnV9yawV9a53eUmcde1CCZA==", + "version": "28.11.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.11.0.tgz", + "integrity": "sha512-QAfipLcNCWLVocVbZW8GimKn5p5iiMcgGbRzz8z/P5q7xw+cNEpYqyzFMtIF/ZgF2HLOyy+dYBut+DoYolvqig==", "dev": true, "license": "MIT", "dependencies": { @@ -3868,14 +4582,14 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", - "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.6.tgz", + "integrity": "sha512-mUcf7QG2Tjk7H055Jk0lGBjbgDnfrvqjhXh9t2xLMSCjZVcw9Rb1V6sVNXO0th3jgeO7zllWPTNRil3JW94TnQ==", "dev": true, "license": "MIT", "dependencies": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.9.1" + "synckit": "^0.11.0" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -3886,7 +4600,7 @@ "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", - "eslint-config-prettier": "*", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "peerDependenciesMeta": { @@ -3899,9 +4613,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", - "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -4139,45 +4853,41 @@ } }, "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" }, "funding": { "type": "opencollective", @@ -4239,21 +4949,74 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/express/node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/express/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/express/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", "license": "MIT", "dependencies": { - "ms": "2.0.0" + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" } }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -4283,9 +5046,9 @@ "license": "Apache-2.0" }, "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, "license": "MIT", "dependencies": { @@ -4293,7 +5056,7 @@ "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "micromatch": "^4.0.8" }, "engines": { "node": ">=8.6.0" @@ -4339,9 +5102,9 @@ } }, "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "dev": true, "license": "ISC", "dependencies": { @@ -4380,6 +5143,39 @@ "node": ">=0.10.0" } }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -4394,38 +5190,22 @@ } }, "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", "license": "MIT", "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" }, "engines": { "node": ">= 0.8" } }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, "node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -4532,12 +5312,12 @@ } }, "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/fs-constants": { @@ -4620,7 +5400,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -5040,9 +5819,9 @@ "license": "ISC" }, "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5341,7 +6120,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5460,6 +6238,12 @@ "node": ">=0.10.0" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -5723,6 +6507,25 @@ "node": ">=8" } }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", @@ -6552,6 +7355,19 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -6565,6 +7381,12 @@ "integrity": "sha512-Q5ALD6JiS6xAUWCwX3taQmgwxyveCtIIuL08+ml0nHwT3k0S/GIFJN+Hd38b1qYIMaE5X++iqsqWVksz7SYW+Q==", "license": "MIT" }, + "node_modules/long": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.1.tgz", + "integrity": "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng==", + "license": "Apache-2.0" + }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -6635,19 +7457,22 @@ } }, "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", "license": "MIT", + "engines": { + "node": ">=18" + }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } @@ -6669,15 +7494,6 @@ "node": ">= 8" } }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -6807,27 +7623,27 @@ } }, "node_modules/mqtt": { - "version": "5.10.3", - "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.10.3.tgz", - "integrity": "sha512-hA/6YrUS4fywhBGCjH/XXUuLeueJiPqruVVWjK2A24Ma4KcWfZ/x8x07aoesBV+HXDWBC08tbT4IWfSXNW0Jtw==", + "version": "5.10.4", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.10.4.tgz", + "integrity": "sha512-wN+SuhT2/ZaG6NPxca0N6YtRivnMxk6VflxQUEeqDH4erKdj+wPAGhHmcTLzvqfE4sJRxrEJ+XJxUc0No0E7eQ==", "license": "MIT", "dependencies": { - "@types/readable-stream": "^4.0.5", - "@types/ws": "^8.5.9", + "@types/readable-stream": "^4.0.18", + "@types/ws": "^8.5.14", "commist": "^3.2.0", "concat-stream": "^2.0.0", - "debug": "^4.3.4", + "debug": "^4.4.0", "help-me": "^5.0.0", - "lru-cache": "^10.0.1", + "lru-cache": "^10.4.3", "minimist": "^1.2.8", "mqtt-packet": "^9.0.1", "number-allocator": "^1.0.14", - "readable-stream": "^4.4.2", + "readable-stream": "^4.7.0", "reinterval": "^1.1.0", - "rfdc": "^1.3.0", + "rfdc": "^1.4.1", "split2": "^4.2.0", - "worker-timers": "^7.1.4", - "ws": "^8.17.1" + "worker-timers": "^7.1.8", + "ws": "^8.18.0" }, "bin": { "mqtt": "build/bin/mqtt.js", @@ -6909,9 +7725,9 @@ "license": "ISC" }, "node_modules/mqtt/node_modules/readable-stream": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.6.0.tgz", - "integrity": "sha512-cbAdYt0VcnpN2Bekq7PU+k363ZRsPwJoEEJOEtSJQlJXzwaxt3FIo/uL+KeDSGIjJqtkwyge4KQgD2S2kd+CQw==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", "license": "MIT", "dependencies": { "abort-controller": "^3.0.0", @@ -6970,9 +7786,9 @@ } }, "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -7704,10 +8520,13 @@ "license": "MIT" }, "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "license": "MIT" + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } }, "node_modules/pause": { "version": "0.0.1", @@ -7783,9 +8602,9 @@ } }, "node_modules/prettier": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", - "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "dev": true, "license": "MIT", "bin": { @@ -7881,6 +8700,30 @@ "node": ">= 6" } }, + "node_modules/protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -7964,12 +8807,12 @@ } }, "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -8318,7 +9161,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8412,9 +9254,9 @@ } }, "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, "license": "MIT", "engines": { @@ -8460,6 +9302,22 @@ "node": "*" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -8566,9 +9424,9 @@ "license": "ISC" }, "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -8578,66 +9436,61 @@ } }, "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", "license": "MIT", "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" }, "engines": { - "node": ">= 0.8.0" + "node": ">= 18" } }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/send/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", - "dependencies": { - "ms": "2.0.0" + "engines": { + "node": ">= 0.6" } }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "node_modules/send/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, "engines": { - "node": ">= 0.8" + "node": ">= 0.6" } }, "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", "license": "MIT", "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" }, "engines": { - "node": ">= 0.8.0" + "node": ">= 18" } }, "node_modules/set-function-length": { @@ -8839,9 +9692,9 @@ } }, "node_modules/sort-es": { - "version": "1.7.13", - "resolved": "https://registry.npmjs.org/sort-es/-/sort-es-1.7.13.tgz", - "integrity": "sha512-B4pX8saLSZloycBfy68iwcOS1/UJanyufNBWXeqjiWLPTibAkEEEEQ0m/H/TLbbl8ndjF7/yBwHdbwbQ9lg/LQ==", + "version": "1.7.14", + "resolved": "https://registry.npmjs.org/sort-es/-/sort-es-1.7.14.tgz", + "integrity": "sha512-EvSaXKPk+ylUD9ynUwgiuh+11bE2b8jdTjnCZAOSVCX1Ud9JEY4zdTPRDKldzu8ge9vcGIJKWBVb54ANalh/cQ==", "license": "MIT" }, "node_modules/source-map": { @@ -8997,7 +9850,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -9068,7 +9920,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -9137,32 +9988,32 @@ } }, "node_modules/synckit": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", - "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.2.tgz", + "integrity": "sha512-1IUffI8zZ8qUMB3NUJIjk0RpLroG/8NkQDAWH1NbB2iJ0/5pn3M8rxfNzMz4GH9OnYaGYn31LEDSXJp/qIlxgA==", "dev": true, "license": "MIT", "dependencies": { - "@pkgr/core": "^0.1.0", - "tslib": "^2.6.2" + "@pkgr/core": "^0.2.0", + "tslib": "^2.8.1" }, "engines": { "node": "^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/unts" + "url": "https://opencollective.com/synckit" } }, "node_modules/tar-fs": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz", - "integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", + "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", "license": "MIT", "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", - "tar-stream": "^2.0.0" + "tar-stream": "^2.1.4" } }, "node_modules/tar-fs/node_modules/pump": { @@ -9320,16 +10171,123 @@ } }, "node_modules/ts-api-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", - "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-jest": { + "version": "29.3.2", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.2.tgz", + "integrity": "sha512-bJJkrWc6PjFVz5g2DGCNUo8z7oFEYaz1xP1NpeDU7KNLMWPpEyV8Chbpkn8xjzgRDpQhnGMyvyldoL7h8JXyug==", "dev": true, "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.1", + "type-fest": "^4.39.1", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.40.0.tgz", + "integrity": "sha512-ABHZ2/tS2JkvH1PEjxFDTUWC8dB5OsIGZP4IFLhR293GqT5Y5qB1WwL2kMPYhQW9DVgVD8Hd7I8gjwPIf5GFkw==", + "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, "peerDependencies": { - "typescript": ">=4.2.0" + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } } }, "node_modules/tsconfig-paths": { @@ -9429,13 +10387,35 @@ } }, "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "license": "MIT", "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" }, "engines": { "node": ">= 0.6" @@ -9522,12 +10502,11 @@ "license": "MIT" }, "node_modules/typescript": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", - "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9574,9 +10553,9 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, "node_modules/universalify": { @@ -9692,9 +10671,9 @@ } }, "node_modules/uuid": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz", - "integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -9704,6 +10683,13 @@ "uuid": "dist/esm/bin/uuid" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -9906,7 +10892,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -9987,7 +10972,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -10001,9 +10985,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", - "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", + "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", "license": "ISC", "bin": { "yaml": "bin.mjs" @@ -10016,7 +11000,6 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -10035,12 +11018,21 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, "license": "ISC", "engines": { "node": ">=12" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/app/package.json b/app/package.json index c66ac89c..c94b4dea 100644 --- a/app/package.json +++ b/app/package.json @@ -2,28 +2,31 @@ "name": "wud-app", "version": "1.0.0", "description": "What'up Docker? the app", - "main": "index.js", + "main": "index.ts", "scripts": { - "start": "nodemon index.js | bunyan -L -o short", + "build": "tsc --build", + "build:dev": "tsc --watch", + "start": "nodemon index.ts | bunyan -L -o short", "doc": ".docsify serve ./docs", "format": "prettier --write .", "lint:fix": "prettier --write . && eslint '**/*.js'", "lint": "eslint '**/*.js'", - "test": "jest --coverage" + "test": "jest", + "test:coverage": "jest --coverage" }, "author": "fmartinou", "repository": "getwud/wud", "license": "MIT", "dependencies": { - "@slack/web-api": "7.8.0", + "@slack/web-api": "7.9.1", "aws-sdk": "2.1692.0", "body-parser": "1.20.3", "bunyan": "1.8.15", "capitalize": "2.0.4", "connect-loki": "1.2.0", "cors": "2.8.5", - "dockerode": "4.0.2", - "express": "4.21.2", + "dockerode": "4.0.5", + "express": "5.1.0", "express-healthcheck": "0.1.0", "express-session": "1.18.1", "flat": "5.0.2", @@ -34,7 +37,7 @@ "just-debounce": "1.1.0", "kafkajs": "2.2.4", "lokijs": "1.5.12", - "mqtt": "5.10.3", + "mqtt": "5.10.4", "nocache": "4.0.0", "node-cron": "3.0.3", "node-telegram-bot-api": "0.66.0", @@ -49,23 +52,50 @@ "pushover-notifications": "1.2.3", "request": "2.88.2", "request-promise-native": "1.0.9", - "semver": "7.6.3", + "semver": "7.7.1", "set-value": "4.1.0", "snake-case": "3.0.4", - "sort-es": "1.7.13", - "uuid": "11.0.3", - "yaml": "2.6.1" + "sort-es": "1.7.14", + "uuid": "^11.1.0", + "yaml": "2.7.1" }, "devDependencies": { + "@types/aws-sdk": "^0.0.42", + "@types/body-parser": "^1.19.5", + "@types/bunyan": "^1.8.11", + "@types/cors": "^2.8.17", + "@types/dockerode": "^3.3.37", + "@types/express": "^5.0.1", + "@types/express-session": "^1.18.1", + "@types/flat": "^5.0.5", + "@types/hapi__joi": "^17.1.15", + "@types/jest": "^29.5.14", + "@types/lokijs": "^1.5.14", + "@types/mqtt": "^0.0.34", + "@types/node": "^22.14.0", + "@types/node-cron": "^3.0.11", + "@types/node-telegram-bot-api": "^0.64.8", + "@types/nodemailer": "^6.4.17", + "@types/passport": "^1.0.17", + "@types/passport-anonymous": "^1.0.5", + "@types/passport-http": "^0.3.11", + "@types/request": "^2.48.12", + "@types/request-promise-native": "^1.0.21", + "@types/semver": "^7.7.0", + "@types/set-value": "^4.0.3", + "@types/yaml": "^1.9.6", "babel-jest": "29.7.0", "babel-plugin-rewire": "1.2.0", - "eslint": "9.17.0", - "eslint-config-prettier": "9.1.0", + "eslint": "9.24.0", + "eslint-config-prettier": "10.1.1", "eslint-plugin-import": "2.31.0", - "eslint-plugin-jest": "28.10.0", - "eslint-plugin-prettier": "5.2.1", + "eslint-plugin-jest": "28.11.0", + "eslint-plugin-prettier": "5.2.6", "jest": "29.7.0", - "nodemon": "3.1.9", - "prettier": "3.4.2" + "nodemon": "^3.1.9", + "prettier": "3.5.3", + "ts-jest": "^29.3.2", + "ts-node": "^10.9.2", + "typescript": "^5.8.3" } -} +} \ No newline at end of file diff --git a/app/prometheus/container.test.js b/app/prometheus/container.test.ts similarity index 88% rename from app/prometheus/container.test.js rename to app/prometheus/container.test.ts index 7fc1a133..f44dda7f 100644 --- a/app/prometheus/container.test.js +++ b/app/prometheus/container.test.ts @@ -1,13 +1,13 @@ jest.mock('../store/container'); jest.mock('../log'); -const store = require('../store/container'); -const container = require('./container'); -const log = require('../log'); +import * as store from '../store/container'; +import * as container from './container'; +import log from '../log'; test('gauge must be populated when containers are in the store', () => { jest.useFakeTimers(); - store.getContainers = () => [ + jest.spyOn(store, 'getContainers').mockReturnValue([ { id: 'container-123456789', name: 'test', @@ -35,7 +35,7 @@ test('gauge must be populated when containers are in the store', () => { tag: 'version', }, }, - ]; + ]); const gauge = container.init(); const spySet = jest.spyOn(gauge, 'set'); jest.runOnlyPendingTimers(); @@ -62,11 +62,11 @@ test('gauge must be populated when containers are in the store', () => { }); test("gauge must warn when data don't match expected labels", () => { - store.getContainers = () => [ + jest.spyOn(store, 'getContainers').mockReturnValue([ { extra: 'extra', - }, - ]; + } as any, + ]); const spyLog = jest.spyOn(log, 'warn'); container.init(); expect(spyLog).toHaveBeenCalled(); diff --git a/app/prometheus/container.js b/app/prometheus/container.ts similarity index 79% rename from app/prometheus/container.js rename to app/prometheus/container.ts index 1badf12e..bc3c3277 100644 --- a/app/prometheus/container.js +++ b/app/prometheus/container.ts @@ -1,26 +1,26 @@ -const { Gauge, register } = require('prom-client'); -const storeContainer = require('../store/container'); -const log = require('../log'); -const { flatten } = require('../model/container'); - -let gaugeContainer; +import { Gauge, register } from 'prom-client'; +import { getContainers } from '../store/container'; +import log from '../log'; +import { flatten, FlattenedContainer } from '../model/container'; +let gaugeContainer: Gauge; +const name = 'wud_containers' /** * Populate gauge. */ function populateGauge() { gaugeContainer.reset(); - storeContainer.getContainers().forEach((container) => { + getContainers().forEach((container) => { try { const flatContainer = flatten(container); const flatContainerWithoutLabels = Object.keys(flatContainer) .filter((key) => !key.startsWith('labels_')) .reduce((obj, key) => { - obj[key] = flatContainer[key]; + obj[key] = flatContainer[key as keyof FlattenedContainer]; return obj; - }, {}); + }, {} as Record); gaugeContainer.set(flatContainerWithoutLabels, 1); - } catch (e) { + } catch (e: any) { log.warn( `${container.id} - Error when adding container to the metrics (${e.message})`, ); @@ -33,13 +33,13 @@ function populateGauge() { * Init Container prometheus gauge. * @returns {Gauge} */ -function init() { +export function init() { // Replace gauge if init is called more than once if (gaugeContainer) { - register.removeSingleMetric(gaugeContainer.name); + register.removeSingleMetric(name); } gaugeContainer = new Gauge({ - name: 'wud_containers', + name: name, help: 'The watched containers', labelNames: [ 'display_icon', @@ -86,7 +86,3 @@ function init() { populateGauge(); return gaugeContainer; } - -module.exports = { - init, -}; diff --git a/app/prometheus/index.js b/app/prometheus/index.js deleted file mode 100644 index a35e81a7..00000000 --- a/app/prometheus/index.js +++ /dev/null @@ -1,32 +0,0 @@ -const { collectDefaultMetrics, register } = require('prom-client'); - -const log = require('../log').child({ component: 'prometheus' }); -const container = require('./container'); -const trigger = require('./trigger'); -const watcher = require('./watcher'); -const registry = require('./registry'); - -/** - * Start the Prometheus registry. - */ -function init() { - log.info('Init Prometheus module'); - collectDefaultMetrics(); - container.init(); - registry.init(); - trigger.init(); - watcher.init(); -} - -/** - * Return all metrics as string for Prometheus scrapping. - * @returns {string} - */ -async function output() { - return register.metrics(); -} - -module.exports = { - init, - output, -}; diff --git a/app/prometheus/index.ts b/app/prometheus/index.ts new file mode 100644 index 00000000..4e09532e --- /dev/null +++ b/app/prometheus/index.ts @@ -0,0 +1,28 @@ +import { collectDefaultMetrics, register } from 'prom-client'; + +import logger from '../log'; +import * as container from './container'; +import * as trigger from './trigger'; +import * as watcher from './watcher'; +import * as registry from './registry'; + +const log = logger.child({ component: 'prometheus' }); +/** + * Start the Prometheus registry. + */ +export function init() { + log.info('Init Prometheus module'); + collectDefaultMetrics(); + container.init(); + registry.init(); + trigger.init(); + watcher.init(); +} + +/** + * Return all metrics as string for Prometheus scrapping. + */ +export async function output() { + return register.metrics(); +} + diff --git a/app/prometheus/registry.js b/app/prometheus/registry.js deleted file mode 100644 index c1303461..00000000 --- a/app/prometheus/registry.js +++ /dev/null @@ -1,24 +0,0 @@ -const { Summary, register } = require('prom-client'); - -let summaryGetTags; - -function init() { - // Replace summary if init is called more than once - if (summaryGetTags) { - register.removeSingleMetric(summaryGetTags.name); - } - summaryGetTags = new Summary({ - name: 'wud_registry_response', - help: 'The Registry response time (in second)', - labelNames: ['type', 'name'], - }); -} - -function getSummaryTags() { - return summaryGetTags; -} - -module.exports = { - init, - getSummaryTags, -}; diff --git a/app/prometheus/registry.test.js b/app/prometheus/registry.test.ts similarity index 100% rename from app/prometheus/registry.test.js rename to app/prometheus/registry.test.ts diff --git a/app/prometheus/registry.ts b/app/prometheus/registry.ts new file mode 100644 index 00000000..f0353e77 --- /dev/null +++ b/app/prometheus/registry.ts @@ -0,0 +1,19 @@ +import { Summary, register } from 'prom-client'; + +let summaryGetTags: Summary | null = null; +const name = 'wud_registry_response' +export function init() { + // Replace summary if init is called more than once + if (summaryGetTags) { + register.removeSingleMetric(name); + } + summaryGetTags = new Summary({ + name: name, + help: 'The Registry response time (in second)', + labelNames: ['type', 'name'], + }); +} + +export function getSummaryTags() { + return summaryGetTags!; +} \ No newline at end of file diff --git a/app/prometheus/trigger.js b/app/prometheus/trigger.js deleted file mode 100644 index f8437c8d..00000000 --- a/app/prometheus/trigger.js +++ /dev/null @@ -1,24 +0,0 @@ -const { Counter, register } = require('prom-client'); - -let triggerCounter; - -function init() { - // Replace counter if init is called more than once - if (triggerCounter) { - register.removeSingleMetric(triggerCounter.name); - } - triggerCounter = new Counter({ - name: 'wud_trigger_count', - help: 'Total count of trigger events', - labelNames: ['type', 'name', 'status'], - }); -} - -function getTriggerCounter() { - return triggerCounter; -} - -module.exports = { - init, - getTriggerCounter, -}; diff --git a/app/prometheus/trigger.test.js b/app/prometheus/trigger.test.ts similarity index 100% rename from app/prometheus/trigger.test.js rename to app/prometheus/trigger.test.ts diff --git a/app/prometheus/trigger.ts b/app/prometheus/trigger.ts new file mode 100644 index 00000000..e79b8bdf --- /dev/null +++ b/app/prometheus/trigger.ts @@ -0,0 +1,20 @@ +import { Counter, register } from 'prom-client'; + +let triggerCounter: Counter; +let name = 'wud_trigger_count'; + +export function init() { + // Replace counter if init is called more than once + if (triggerCounter) { + register.removeSingleMetric(name); + } + triggerCounter = new Counter({ + name: name, + help: 'Total count of trigger events', + labelNames: ['type', 'name', 'status'], + }); +} + +export function getTriggerCounter() { + return triggerCounter; +} \ No newline at end of file diff --git a/app/prometheus/watcher.js b/app/prometheus/watcher.js deleted file mode 100644 index 2ffa2d87..00000000 --- a/app/prometheus/watcher.js +++ /dev/null @@ -1,24 +0,0 @@ -const { Gauge, register } = require('prom-client'); - -let watchContainerGauge; - -function init() { - // Replace gauge if init is called more than once - if (watchContainerGauge) { - register.removeSingleMetric(watchContainerGauge.name); - } - watchContainerGauge = new Gauge({ - name: 'wud_watcher_total', - help: 'The number of watched containers', - labelNames: ['type', 'name'], - }); -} - -function getWatchContainerGauge() { - return watchContainerGauge; -} - -module.exports = { - init, - getWatchContainerGauge, -}; diff --git a/app/prometheus/watcher.test.js b/app/prometheus/watcher.test.ts similarity index 100% rename from app/prometheus/watcher.test.js rename to app/prometheus/watcher.test.ts diff --git a/app/prometheus/watcher.ts b/app/prometheus/watcher.ts new file mode 100644 index 00000000..70fb6f9b --- /dev/null +++ b/app/prometheus/watcher.ts @@ -0,0 +1,20 @@ +import { Gauge, register } from 'prom-client'; + +let watchContainerGauge: Gauge | null = null; +const name = 'wud_watcher_total'; + +export function init() { + // Replace gauge if init is called more than once + if (watchContainerGauge) { + register.removeSingleMetric(name); + } + watchContainerGauge = new Gauge({ + name: name, + help: 'The number of watched containers', + labelNames: ['type', 'name'], + }); +} + +export function getWatchContainerGauge() { + return watchContainerGauge; +} diff --git a/app/registries/Registry.test.js b/app/registries/Registry.test.ts similarity index 82% rename from app/registries/Registry.test.js rename to app/registries/Registry.test.ts index c0bdba6f..91aa8e38 100644 --- a/app/registries/Registry.test.js +++ b/app/registries/Registry.test.ts @@ -1,13 +1,16 @@ -const log = require('../log'); +import request from 'request'; +import log from '../log'; +import { ContainerImage } from '../model/container'; +import { Registry } from './Registry'; +import { RequestPromiseOptions } from 'request-promise-native'; jest.mock('request-promise-native'); jest.mock('../prometheus/registry', () => ({ getSummaryTags: () => ({ - observe: () => {}, + observe: () => { }, }), })); -const Registry = require('./Registry'); const registry = new Registry(); registry.register('registry', 'hub', 'test', {}); @@ -23,15 +26,15 @@ test('getId should return registry type only', () => { }); test('match should return false when not overridden', () => { - expect(registry.match({})).toBeFalsy(); + expect(registry.match({} as ContainerImage)).toBeFalsy(); }); test('normalizeImage should return same image when not overridden', () => { - expect(registry.normalizeImage({ x: 'x' })).toStrictEqual({ x: 'x' }); + expect(registry.normalizeImage({ x: 'x' } as unknown as ContainerImage)).toStrictEqual({ x: 'x' }); }); test('authenticate should return same request options when not overridden', () => { - expect(registry.authenticate({}, { x: 'x' })).resolves.toStrictEqual({ + expect(registry.authenticate({} as ContainerImage, { x: 'x' } as RequestPromiseOptions)).resolves.toStrictEqual({ x: 'x', }); }); @@ -39,24 +42,24 @@ test('authenticate should return same request options when not overridden', () = test('getTags should sort tags z -> a', () => { const registryMocked = new Registry(); registryMocked.log = log; - registryMocked.callRegistry = () => ({ + registryMocked.callRegistry = () => Promise.resolve({ headers: {}, body: { tags: ['v1', 'v2', 'v3'] }, - }); + } as T); expect( - registryMocked.getTags({ name: 'test', registry: { url: 'test' } }), + registryMocked.getTags({ name: 'test', registry: { url: 'test' } } as ContainerImage), ).resolves.toStrictEqual(['v3', 'v2', 'v1']); }); test('getImageManifestDigest should return digest for application/vnd.docker.distribution.manifest.list.v2+json then application/vnd.docker.distribution.manifest.v2+json', () => { const registryMocked = new Registry(); registryMocked.log = log; - registryMocked.callRegistry = (options) => { + registryMocked.callRegistry = (options: any): Promise => { if ( options.headers.Accept === 'application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json' ) { - return { + return Promise.resolve({ schemaVersion: 2, mediaType: 'application/vnd.docker.distribution.manifest.list.v2+json', @@ -79,19 +82,19 @@ test('getImageManifestDigest should return digest for application/vnd.docker.dis mediaType: 'fail', }, ], - }; + } as T); } if ( options.headers.Accept === 'application/vnd.docker.distribution.manifest.v2+json' ) { - return { + return Promise.resolve({ headers: { 'docker-content-digest': '123456789', }, - }; + } as T); } - throw new Error('Boom!'); + return Promise.reject(new Error('Boom!')); }; expect( registryMocked.getImageManifestDigest({ @@ -104,7 +107,7 @@ test('getImageManifestDigest should return digest for application/vnd.docker.dis registry: { url: 'url', }, - }), + } as ContainerImage), ).resolves.toStrictEqual({ version: 2, digest: '123456789', @@ -114,12 +117,12 @@ test('getImageManifestDigest should return digest for application/vnd.docker.dis test('getImageManifestDigest should return digest for application/vnd.docker.distribution.manifest.list.v2+json then application/vnd.docker.container.image.v1+json', () => { const registryMocked = new Registry(); registryMocked.log = log; - registryMocked.callRegistry = (options) => { + registryMocked.callRegistry = (options: any): Promise => { if ( options.headers.Accept === 'application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json' ) { - return { + return Promise.resolve({ schemaVersion: 2, mediaType: 'application/vnd.docker.distribution.manifest.list.v2+json', @@ -142,9 +145,9 @@ test('getImageManifestDigest should return digest for application/vnd.docker.dis mediaType: 'fail', }, ], - }; + } as T); } - throw new Error('Boom!'); + return Promise.reject(new Error('Boom!')); }; expect( registryMocked.getImageManifestDigest({ @@ -157,7 +160,7 @@ test('getImageManifestDigest should return digest for application/vnd.docker.dis registry: { url: 'url', }, - }), + } as ContainerImage), ).resolves.toStrictEqual({ version: 1, digest: 'digest_x', @@ -167,12 +170,12 @@ test('getImageManifestDigest should return digest for application/vnd.docker.dis test('getImageManifestDigest should return digest for application/vnd.docker.distribution.manifest.v2+json', () => { const registryMocked = new Registry(); registryMocked.log = log; - registryMocked.callRegistry = (options) => { + registryMocked.callRegistry = (options: any): Promise => { if ( options.headers.Accept === 'application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json' ) { - return { + return Promise.resolve({ schemaVersion: 2, mediaType: 'application/vnd.docker.distribution.manifest.v2+json', @@ -181,19 +184,19 @@ test('getImageManifestDigest should return digest for application/vnd.docker.dis mediaType: 'application/vnd.docker.distribution.manifest.v2+json', }, - }; + } as T); } if ( options.headers.Accept === 'application/vnd.docker.distribution.manifest.v2+json' ) { - return { + return Promise.resolve({ headers: { 'docker-content-digest': '123456789', }, - }; + } as T); } - throw new Error('Boom!'); + return Promise.reject(new Error('Boom!')); }; expect( registryMocked.getImageManifestDigest({ @@ -206,7 +209,7 @@ test('getImageManifestDigest should return digest for application/vnd.docker.dis registry: { url: 'url', }, - }), + } as ContainerImage), ).resolves.toStrictEqual({ version: 2, digest: '123456789', @@ -216,12 +219,12 @@ test('getImageManifestDigest should return digest for application/vnd.docker.dis test('getImageManifestDigest should return digest for application/vnd.docker.container.image.v1+json', () => { const registryMocked = new Registry(); registryMocked.log = log; - registryMocked.callRegistry = (options) => { + registryMocked.callRegistry = (options: any): Promise => { if ( options.headers.Accept === 'application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json' ) { - return { + return Promise.resolve({ schemaVersion: 1, history: [ { @@ -232,9 +235,9 @@ test('getImageManifestDigest should return digest for application/vnd.docker.con }), }, ], - }; + } as T); } - throw new Error('Boom!'); + return Promise.reject(new Error('Boom!')); }; expect( registryMocked.getImageManifestDigest({ @@ -247,7 +250,7 @@ test('getImageManifestDigest should return digest for application/vnd.docker.con registry: { url: 'url', }, - }), + } as ContainerImage), ).resolves.toStrictEqual({ version: 1, digest: 'xxxxxxxxxx', @@ -258,7 +261,7 @@ test('getImageManifestDigest should return digest for application/vnd.docker.con test('getImageManifestDigest should throw when no digest found', () => { const registryMocked = new Registry(); registryMocked.log = log; - registryMocked.callRegistry = () => ({}); + registryMocked.callRegistry = () => ({} as unknown as Promise); expect( registryMocked.getImageManifestDigest({ name: 'image', @@ -270,7 +273,7 @@ test('getImageManifestDigest should throw when no digest found', () => { registry: { url: 'url', }, - }), + } as ContainerImage), ).rejects.toEqual(new Error('Unexpected error; no manifest found')); }); @@ -279,7 +282,7 @@ test('callRegistry should call authenticate', () => { registryMocked.log = log; const spyAuthenticate = jest.spyOn(registryMocked, 'authenticate'); registryMocked.callRegistry({ - image: {}, + image: {} as ContainerImage, url: 'url', method: 'get', }); diff --git a/app/registries/Registry.js b/app/registries/Registry.ts similarity index 73% rename from app/registries/Registry.js rename to app/registries/Registry.ts index e77ccc9f..193f57e4 100644 --- a/app/registries/Registry.js +++ b/app/registries/Registry.ts @@ -1,26 +1,25 @@ -const rp = require('request-promise-native'); -const log = require('../log'); -const Component = require('../registry/Component'); -const { getSummaryTags } = require('../prometheus/registry'); +import rp, { RequestPromiseOptions } from 'request-promise-native'; +import log from '../log'; +import { Component, ComponentKind, BaseConfig } from '../registry/Component'; +import { getSummaryTags } from '../prometheus/registry'; +import { ContainerImage } from '../model/container'; +import { RequestPart } from 'request'; /** * Docker Registry Abstract class. */ -class Registry extends Component { +export class Registry extends Component { /** * Encode Bse64(login:password) - * @param login - * @param token - * @returns {string} */ - static base64Encode(login, token) { + static base64Encode(login: string, token: string) { return Buffer.from(`${login}:${token}`, 'utf-8').toString('base64'); } /** * Override to apply custom format to the logger. */ - async register(kind, type, name, configuration) { + async register(kind: ComponentKind, type: string, name: string, configuration: any) { this.log = log.child({ component: `${kind}.${type}.${name}` }); this.kind = kind; this.type = type; @@ -28,7 +27,7 @@ class Registry extends Component { this.configuration = this.validateConfiguration(configuration); this.log.info( - `Register with configuration ${JSON.stringify(this.maskConfiguration(configuration))}`, + `Register with configuration ${JSON.stringify(this.maskConfiguration())}`, ); await this.init(); return this; @@ -36,32 +35,23 @@ class Registry extends Component { /** * If this registry is responsible for the image (to be overridden). - * @param image the image - * @returns {boolean} */ - match(image) { + match(image: ContainerImage) { return false; } /** * Normalize image according to Registry Custom characteristics (to be overridden). - * @param image - * @returns {*} */ - - normalizeImage(image) { + normalizeImage(image: ContainerImage): ContainerImage { return image; } /** * Authenticate and set authentication value to requestOptions. - * @param image - * @param requestOptions - * @returns {*} */ - - async authenticate(image, requestOptions) { + async authenticate(image: ContainerImage, requestOptions: RequestPromiseOptions) { return requestOptions; } @@ -70,12 +60,12 @@ class Registry extends Component { * @param image * @returns {*} */ - async getTags(image) { + async getTags(image: ContainerImage) { this.log.debug(`Get ${image.name} tags`); const tags = []; - let page; + let page: Request | undefined = undefined; let hasNext = true; - let link; + let link: string | undefined; while (hasNext) { const lastItem = page ? page.body.tags[page.body.tags.length - 1] @@ -83,8 +73,8 @@ class Registry extends Component { page = await this.getTagsPage(image, lastItem, link); const pageTags = page.body.tags ? page.body.tags : []; - link = page.headers.link; - hasNext = page.headers.link !== undefined; + link = page.headers['link']; + hasNext = link !== undefined; tags.push(...pageTags); } @@ -98,13 +88,12 @@ class Registry extends Component { * Get tags page * @param image * @param lastItem - * @returns {Promise<*>} */ - getTagsPage(image, lastItem = undefined) { + getTagsPage(image: ContainerImage, lastItem: string | undefined = undefined, _link: string | undefined = undefined) { // Default items per page (not honoured by all registries) const itemsPerPage = 1000; const last = lastItem ? `&last=${lastItem}` : ''; - return this.callRegistry({ + return this.callRegistry>({ image, url: `${image.registry.url}/${image.name}/tags/list?n=${itemsPerPage}${last}`, resolveWithFullResponse: true, @@ -115,16 +104,15 @@ class Registry extends Component { * Get image manifest for a remote tag. * @param image * @param digest (optional) - * @returns {Promise} */ - async getImageManifestDigest(image, digest) { + async getImageManifestDigest(image: ContainerImage, digest?: string) { const tagOrDigest = digest || image.tag.value; let manifestDigestFound; let manifestMediaType; this.log.debug( `${this.getId()} - Get ${image.name}:${tagOrDigest} manifest`, ); - const responseManifests = await this.callRegistry({ + const responseManifests = await this.callRegistry({ image, url: `${image.registry.url}/${image.name}/manifests/${tagOrDigest}`, headers: { @@ -140,9 +128,9 @@ class Registry extends Component { ); if ( responseManifests.mediaType === - 'application/vnd.docker.distribution.manifest.list.v2+json' || + 'application/vnd.docker.distribution.manifest.list.v2+json' || responseManifests.mediaType === - 'application/vnd.oci.image.index.v1+json' + 'application/vnd.oci.image.index.v1+json' ) { log.debug( `Filter manifest for [arch=${image.architecture}, os=${image.os}, variant=${image.variant}]`, @@ -151,7 +139,7 @@ class Registry extends Component { const manifestFounds = responseManifests.manifests.filter( (manifest) => manifest.platform.architecture === - image.architecture && + image.architecture && manifest.platform.os === image.os, ); @@ -183,9 +171,9 @@ class Registry extends Component { } } else if ( responseManifests.mediaType === - 'application/vnd.docker.distribution.manifest.v2+json' || + 'application/vnd.docker.distribution.manifest.v2+json' || responseManifests.mediaType === - 'application/vnd.oci.image.manifest.v1+json' + 'application/vnd.oci.image.manifest.v1+json' ) { log.debug( `Manifest found with [digest=${responseManifests.config.digest}, mediaType=${responseManifests.config.mediaType}]`, @@ -211,15 +199,15 @@ class Registry extends Component { if ( (manifestDigestFound && manifestMediaType === - 'application/vnd.docker.distribution.manifest.v2+json') || + 'application/vnd.docker.distribution.manifest.v2+json') || (manifestDigestFound && manifestMediaType === - 'application/vnd.oci.image.manifest.v1+json') + 'application/vnd.oci.image.manifest.v1+json') ) { log.debug( 'Calling registry to get docker-content-digest header', ); - const responseManifest = await this.callRegistry({ + const responseManifest = await this.callRegistry({ image, method: 'head', url: `${image.registry.url}/${image.name}/manifests/${manifestDigestFound}`, @@ -229,7 +217,7 @@ class Registry extends Component { resolveWithFullResponse: true, }); const manifestFound = { - digest: responseManifest.headers['docker-content-digest'], + digest: responseManifest.headers!['docker-content-digest'], version: 2, }; log.debug( @@ -240,10 +228,10 @@ class Registry extends Component { if ( (manifestDigestFound && manifestMediaType === - 'application/vnd.docker.container.image.v1+json') || + 'application/vnd.docker.container.image.v1+json') || (manifestDigestFound && manifestMediaType === - 'application/vnd.oci.image.config.v1+json') + 'application/vnd.oci.image.config.v1+json') ) { const manifestFound = { digest: manifestDigestFound, @@ -259,7 +247,7 @@ class Registry extends Component { throw new Error('Unexpected error; no manifest found'); } - async callRegistry({ + async callRegistry({ image, url, method = 'get', @@ -267,12 +255,17 @@ class Registry extends Component { Accept: 'application/json', }, resolveWithFullResponse = false, + }: { + image: ContainerImage; + url: string; + method?: string; + headers?: any; + resolveWithFullResponse?: boolean; }) { const start = new Date().getTime(); // Request options - const getRequestOptions = { - uri: url, + const getRequestOptions: RequestPromiseOptions = { method, headers, json: true, @@ -283,16 +276,16 @@ class Registry extends Component { image, getRequestOptions, ); - const response = await rp(getRequestOptionsWithAuth); + const response = await rp(url, getRequestOptionsWithAuth); const end = new Date().getTime(); getSummaryTags().observe( { type: this.type, name: this.name }, (end - start) / 1000, ); - return response; + return response as T; } - getImageFullName(image, tagOrDigest) { + getImageFullName(image: ContainerImage, tagOrDigest: string) { // digests are separated with @ whereas tags are separated with : const tagOrDigestWithSeparator = tagOrDigest.indexOf(':') !== -1 @@ -306,13 +299,58 @@ class Registry extends Component { } /** - * Return {username, pass } or undefined. + * Return {username, password } or undefined. * @returns {} */ - getAuthPull() { + getAuthPull(): Auth | undefined { return undefined; } } -module.exports = Registry; +export interface Auth { + username: string; + password: string; +} + +interface DockerManifestList { + schemaVersion: number; + mediaType: string; + manifests: DockerManifest[]; + config: DockerManifestConfig; + history: DockerManifestHistory[]; +} + +interface DockerManifestHistory { + v1Compatibility: string; +} + +interface DockerManifestConfig { + mediaType: string; + size: number; + digest: string; +} + +interface DockerManifest { + mediaType: string; + size: number; + digest: string; + platform: DockerPlatform; +} + +interface DockerPlatform { + architecture: string; + os: string; + variant: string; + features?: string[]; +} + +export interface Request { + headers: Record; + body: T; +} + +export interface DockerRegistryTags { + name: string; + tags: string[]; +} \ No newline at end of file diff --git a/app/registries/providers/acr/Acr.test.js b/app/registries/providers/acr/Acr.test.ts similarity index 83% rename from app/registries/providers/acr/Acr.test.js rename to app/registries/providers/acr/Acr.test.ts index fc85e7dd..34a43a56 100644 --- a/app/registries/providers/acr/Acr.test.js +++ b/app/registries/providers/acr/Acr.test.ts @@ -1,4 +1,5 @@ -const Acr = require('./Acr'); +import { ContainerImage } from '../../../model/container'; +import { Acr, AcrConfiguration } from './Acr'; const acr = new Acr(); acr.configuration = { @@ -20,7 +21,7 @@ test('validatedConfiguration should initialize when configuration is valid', () test('validatedConfiguration should throw error when configuration item is missing', () => { expect(() => { - acr.validateConfiguration({}); + acr.validateConfiguration({} as AcrConfiguration); }).toThrow('"clientid" is required'); }); @@ -37,7 +38,7 @@ test('match should return true when registry url is from acr', () => { registry: { url: 'test.azurecr.io', }, - }), + } as ContainerImage), ).toBeTruthy(); }); @@ -47,7 +48,7 @@ test('match should return false when registry url is not from acr', () => { registry: { url: 'est.notme.io', }, - }), + } as ContainerImage), ).toBeFalsy(); }); @@ -58,7 +59,7 @@ test('normalizeImage should return the proper registry v2 endpoint', () => { registry: { url: 'test.azurecr.io/test/image', }, - }), + } as ContainerImage), ).toStrictEqual({ name: 'test/image', registry: { @@ -68,7 +69,7 @@ test('normalizeImage should return the proper registry v2 endpoint', () => { }); test('authenticate should add basic auth', () => { - expect(acr.authenticate(undefined, { headers: {} })).resolves.toEqual({ + expect(acr.authenticate({} as ContainerImage, { headers: {} })).resolves.toEqual({ headers: { Authorization: 'Basic Y2xpZW50aWQ6Y2xpZW50c2VjcmV0', }, diff --git a/app/registries/providers/acr/Acr.js b/app/registries/providers/acr/Acr.ts similarity index 67% rename from app/registries/providers/acr/Acr.js rename to app/registries/providers/acr/Acr.ts index f76d15dd..4ffa250d 100644 --- a/app/registries/providers/acr/Acr.js +++ b/app/registries/providers/acr/Acr.ts @@ -1,9 +1,17 @@ -const Registry = require('../../Registry'); +import { RequestPromiseOptions } from 'request-promise-native'; +import { ContainerImage } from '../../../model/container'; +import { Registry } from '../../Registry'; + +export interface AcrConfiguration { + clientid: string; + clientsecret: string; +} + /** * Azure Container Registry integration. */ -class Acr extends Registry { +export class Acr extends Registry { getConfigurationSchema() { return this.joi.object().keys({ clientid: this.joi.string().required(), @@ -13,7 +21,6 @@ class Acr extends Registry { /** * Sanitize sensitive data - * @returns {*} */ maskConfiguration() { return { @@ -26,20 +33,18 @@ class Acr extends Registry { /** * Return true if image has not registryUrl. * @param image the image - * @returns {boolean} */ - match(image) { + match(image: ContainerImage) { return /^.*\.?azurecr.io$/.test(image.registry.url); } /** * Normalize image according to AWS ECR characteristics. * @param image - * @returns {*} */ - normalizeImage(image) { + normalizeImage(image: ContainerImage) { const imageNormalized = image; if (!imageNormalized.registry.url.startsWith('https://')) { imageNormalized.registry.url = `https://${imageNormalized.registry.url}/v2`; @@ -47,9 +52,9 @@ class Acr extends Registry { return imageNormalized; } - async authenticate(image, requestOptions) { + async authenticate(_image: ContainerImage, requestOptions: RequestPromiseOptions) { const requestOptionsWithAuth = requestOptions; - requestOptionsWithAuth.headers.Authorization = `Basic ${Acr.base64Encode(this.configuration.clientid, this.configuration.clientsecret)}`; + requestOptionsWithAuth.headers!.Authorization = `Basic ${Acr.base64Encode(this.configuration.clientid, this.configuration.clientsecret)}`; return requestOptionsWithAuth; } @@ -60,5 +65,3 @@ class Acr extends Registry { }; } } - -module.exports = Acr; diff --git a/app/registries/providers/custom/Custom.test.js b/app/registries/providers/custom/Custom.test.ts similarity index 86% rename from app/registries/providers/custom/Custom.test.js rename to app/registries/providers/custom/Custom.test.ts index ab79ef31..99fbf01e 100644 --- a/app/registries/providers/custom/Custom.test.js +++ b/app/registries/providers/custom/Custom.test.ts @@ -1,4 +1,5 @@ -const Custom = require('./Custom'); +import { ContainerImage } from '../../../model/container'; +import { Custom, CustomConfiguration } from './Custom'; const custom = new Custom(); custom.configuration = { @@ -45,7 +46,7 @@ test('match should return true when registry url is from custom', () => { registry: { url: 'localhost:5000', }, - }), + } as ContainerImage), ).toBeTruthy(); }); @@ -55,7 +56,7 @@ test('match should return false when registry url is not from custom', () => { registry: { url: 'est.notme.io', }, - }), + } as ContainerImage), ).toBeFalsy(); }); @@ -66,7 +67,7 @@ test('normalizeImage should return the proper registry v2 endpoint', () => { registry: { url: 'localhost:5000/test/image', }, - }), + } as ContainerImage), ).toStrictEqual({ name: 'test/image', registry: { @@ -76,7 +77,7 @@ test('normalizeImage should return the proper registry v2 endpoint', () => { }); test('authenticate should add basic auth', () => { - expect(custom.authenticate(undefined, { headers: {} })).resolves.toEqual({ + expect(custom.authenticate({} as ContainerImage, { headers: {} })).resolves.toEqual({ headers: { Authorization: 'Basic bG9naW46cGFzc3dvcmQ=', }, @@ -90,11 +91,11 @@ test('getAuthCredentials should return base64 creds when set in configuration', test('getAuthCredentials should return base64 creds when login/token set in configuration', () => { custom.configuration.login = 'username'; - custom.configuration.token = 'password'; + custom.configuration.password = 'password'; expect(custom.getAuthCredentials()).toEqual('dXNlcm5hbWU6cGFzc3dvcmQ='); }); test('getAuthCredentials should return undefined when no login/token/auth set in configuration', () => { - custom.configuration = {}; + custom.configuration = {} as CustomConfiguration; expect(custom.getAuthCredentials()).toBe(undefined); }); diff --git a/app/registries/providers/custom/Custom.js b/app/registries/providers/custom/Custom.ts similarity index 76% rename from app/registries/providers/custom/Custom.js rename to app/registries/providers/custom/Custom.ts index 8306d196..15fa378c 100644 --- a/app/registries/providers/custom/Custom.js +++ b/app/registries/providers/custom/Custom.ts @@ -1,10 +1,21 @@ -const Registry = require('../../Registry'); +import { RequestPromiseOptions } from 'request-promise-native'; +import { ContainerImage } from '../../../model/container'; +import { Registry } from '../../Registry'; +import { AnySchema } from 'joi'; +import { BaseConfig } from '../../../registry/Component'; + +export interface CustomConfiguration extends BaseConfig { + url: string; + login?: string; + password?: string; + auth?: string; +} /** * Docker Custom Registry V2 integration. */ -class Custom extends Registry { - getConfigurationSchema() { +export class Custom extends Registry { + getConfigurationSchema(): AnySchema { return this.joi.alternatives([ this.joi.string().allow(''), this.joi.object().keys({ @@ -35,7 +46,6 @@ class Custom extends Registry { /** * Sanitize sensitive data - * @returns {*} */ maskConfiguration() { return { @@ -48,18 +58,16 @@ class Custom extends Registry { /** * Return true if image has no registry url. * @param image the image - * @returns {boolean} */ - match(image) { + match(image: ContainerImage) { return this.configuration.url.indexOf(image.registry.url) !== -1; } /** * Normalize images according to Custom characteristics. * @param image - * @returns {*} */ - normalizeImage(image) { + normalizeImage(image: ContainerImage) { const imageNormalized = image; imageNormalized.registry.url = `${this.configuration.url}/v2`; return imageNormalized; @@ -69,13 +77,12 @@ class Custom extends Registry { * Authenticate to Registry. * @param image * @param requestOptions - * @returns {Promise<*>} */ - async authenticate(image, requestOptions) { + async authenticate(_image: ContainerImage, requestOptions: RequestPromiseOptions) { const requestOptionsWithAuth = requestOptions; const credentials = this.getAuthCredentials(); if (credentials) { - requestOptionsWithAuth.headers.Authorization = `Basic ${credentials}`; + requestOptionsWithAuth.headers!.Authorization = `Basic ${credentials}`; } return requestOptionsWithAuth; } @@ -101,11 +108,9 @@ class Custom extends Registry { if (this.configuration.login) { return { username: this.configuration.login, - password: this.configuration.password, + password: this.configuration.password!, }; } return undefined; } } - -module.exports = Custom; diff --git a/app/registries/providers/ecr/Ecr.test.js b/app/registries/providers/ecr/Ecr.test.ts similarity index 88% rename from app/registries/providers/ecr/Ecr.test.js rename to app/registries/providers/ecr/Ecr.test.ts index 201d41c3..5d759d04 100644 --- a/app/registries/providers/ecr/Ecr.test.js +++ b/app/registries/providers/ecr/Ecr.test.ts @@ -1,4 +1,5 @@ -const Ecr = require('./Ecr'); +import { ContainerImage } from '../../../model/container'; +import { Ecr, EcrConfiguration } from './Ecr'; jest.mock('aws-sdk/clients/ecr', () => jest.fn().mockImplementation(() => ({ @@ -39,7 +40,7 @@ test('validatedConfiguration should throw error when accessKey is missing', () = ecr.validateConfiguration({ secretaccesskey: 'secretaccesskey', region: 'region', - }); + } as EcrConfiguration); }).toThrow('"accesskeyid" is required'); }); @@ -48,7 +49,7 @@ test('validatedConfiguration should throw error when secretaccesskey is missing' ecr.validateConfiguration({ accesskeyid: 'accesskeyid', region: 'region', - }); + } as EcrConfiguration); }).toThrow('"secretaccesskey" is required'); }); @@ -57,7 +58,7 @@ test('validatedConfiguration should throw error when secretaccesskey is missing' ecr.validateConfiguration({ accesskeyid: 'accesskeyid', secretaccesskey: 'secretaccesskey', - }); + } as EcrConfiguration); }).toThrow('"region" is required'); }); @@ -67,7 +68,7 @@ test('match should return true when registry url is from ecr', () => { registry: { url: '123456789.dkr.ecr.eu-west-1.amazonaws.com', }, - }), + } as ContainerImage), ).toBeTruthy(); }); @@ -77,7 +78,7 @@ test('match should return false when registry url is not from ecr', () => { registry: { url: '123456789.dkr.ecr.eu-west-1.acme.com', }, - }), + } as ContainerImage), ).toBeFalsy(); }); @@ -96,7 +97,7 @@ test('normalizeImage should return the proper registry v2 endpoint', () => { registry: { url: '123456789.dkr.ecr.eu-west-1.amazonaws.com/test/image', }, - }), + } as ContainerImage), ).toStrictEqual({ name: 'test/image', registry: { @@ -106,7 +107,7 @@ test('normalizeImage should return the proper registry v2 endpoint', () => { }); test('authenticate should call ecr auth endpoint', () => { - expect(ecr.authenticate(undefined, { headers: {} })).resolves.toEqual({ + expect(ecr.authenticate({} as ContainerImage, { headers: {} })).resolves.toEqual({ headers: { Authorization: 'Basic xxxxx', }, diff --git a/app/registries/providers/ecr/Ecr.js b/app/registries/providers/ecr/Ecr.ts similarity index 70% rename from app/registries/providers/ecr/Ecr.js rename to app/registries/providers/ecr/Ecr.ts index a9ade139..e82398a8 100644 --- a/app/registries/providers/ecr/Ecr.js +++ b/app/registries/providers/ecr/Ecr.ts @@ -1,14 +1,23 @@ -const ECR = require('aws-sdk/clients/ecr'); -require('aws-sdk/lib/maintenance_mode_message').suppress = true; // Disable aws sdk maintenance mode message at startup -const rp = require('request-promise-native'); -const Registry = require('../../Registry'); +import ECR from 'aws-sdk/clients/ecr'; +import awsSdk from 'aws-sdk/lib/maintenance_mode_message'; +awsSdk.suppress = true; // Disable aws sdk maintenance mode message at startup +import rp, { RequestPromiseOptions } from 'request-promise-native'; +import { Registry } from '../../Registry'; +import { ContainerImage } from '../../../model/container'; const ECR_PUBLIC_GALLERY_HOSTNAME = 'public.ecr.aws'; +export interface EcrConfiguration { + accesskeyid: string; + secretaccesskey: string; + region: string; +} + + /** * Elastic Container Registry integration. */ -class Ecr extends Registry { +export class Ecr extends Registry { getConfigurationSchema() { return this.joi.alternatives([ this.joi.string().allow(''), @@ -22,7 +31,6 @@ class Ecr extends Registry { /** * Sanitize sensitive data - * @returns {*} */ maskConfiguration() { return { @@ -36,10 +44,9 @@ class Ecr extends Registry { /** * Return true if image has not registryUrl. * @param image the image - * @returns {boolean} */ - match(image) { + match(image: ContainerImage) { return ( /^.*\.dkr\.ecr\..*\.amazonaws\.com$/.test(image.registry.url) || image.registry.url === ECR_PUBLIC_GALLERY_HOSTNAME @@ -49,10 +56,9 @@ class Ecr extends Registry { /** * Normalize image according to AWS ECR characteristics. * @param image - * @returns {*} */ - normalizeImage(image) { + normalizeImage(image: ContainerImage) { const imageNormalized = image; if (!imageNormalized.registry.url.startsWith('https://')) { imageNormalized.registry.url = `https://${imageNormalized.registry.url}/v2`; @@ -60,7 +66,7 @@ class Ecr extends Registry { return imageNormalized; } - async authenticate(image, requestOptions) { + async authenticate(image: ContainerImage, requestOptions: RequestPromiseOptions) { const requestOptionsWithAuth = requestOptions; // Private registry if (this.configuration.accesskeyid) { @@ -75,9 +81,9 @@ class Ecr extends Registry { .getAuthorizationToken() .promise(); const tokenValue = - authorizationToken.authorizationData[0].authorizationToken; + authorizationToken.authorizationData![0].authorizationToken; - requestOptionsWithAuth.headers.Authorization = `Basic ${tokenValue}`; + requestOptionsWithAuth.headers!.Authorization = `Basic ${tokenValue}`; // Public ECR gallery } else if (image.registry.url.includes(ECR_PUBLIC_GALLERY_HOSTNAME)) { @@ -89,7 +95,7 @@ class Ecr extends Registry { }, json: true, }); - requestOptionsWithAuth.headers.Authorization = `Bearer ${response.token}`; + requestOptionsWithAuth.headers!.Authorization = `Bearer ${response.token}`; } return requestOptionsWithAuth; } @@ -97,11 +103,9 @@ class Ecr extends Registry { getAuthPull() { return this.configuration.accesskeyid ? { - username: this.configuration.accesskeyid, - password: this.configuration.secretaccesskey, - } + username: this.configuration.accesskeyid, + password: this.configuration.secretaccesskey, + } : undefined; } } - -module.exports = Ecr; diff --git a/app/registries/providers/forgejo/Forgejo.js b/app/registries/providers/forgejo/Forgejo.js deleted file mode 100644 index 6ddec6e3..00000000 --- a/app/registries/providers/forgejo/Forgejo.js +++ /dev/null @@ -1,8 +0,0 @@ -const Gitea = require('../gitea/Gitea'); - -/** - * Forgejo Container Registry integration. - */ -class Forgejo extends Gitea {} - -module.exports = Forgejo; diff --git a/app/registries/providers/forgejo/Forgejo.test.js b/app/registries/providers/forgejo/Forgejo.test.ts similarity index 80% rename from app/registries/providers/forgejo/Forgejo.test.js rename to app/registries/providers/forgejo/Forgejo.test.ts index 316a4a15..528141ea 100644 --- a/app/registries/providers/forgejo/Forgejo.test.js +++ b/app/registries/providers/forgejo/Forgejo.test.ts @@ -1,4 +1,5 @@ -const Forgejo = require('./Forgejo'); +import { ContainerImage } from '../../../model/container'; +import { Forgejo } from './Forgejo'; const forgejo = new Forgejo(); forgejo.configuration = { @@ -14,7 +15,7 @@ test('normalizeImage should return the proper registry v2 endpoint', () => { registry: { url: 'forgejo.acme.com/test/image', }, - }), + } as ContainerImage), ).toStrictEqual({ name: 'test/image', registry: { diff --git a/app/registries/providers/forgejo/Forgejo.ts b/app/registries/providers/forgejo/Forgejo.ts new file mode 100644 index 00000000..84870877 --- /dev/null +++ b/app/registries/providers/forgejo/Forgejo.ts @@ -0,0 +1,6 @@ +import { Gitea } from '../gitea/Gitea'; + +/** + * Forgejo Container Registry integration. + */ +export class Forgejo extends Gitea { } diff --git a/app/registries/providers/gcr/Gcr.test.js b/app/registries/providers/gcr/Gcr.test.ts similarity index 84% rename from app/registries/providers/gcr/Gcr.test.js rename to app/registries/providers/gcr/Gcr.test.ts index 32b7bd50..dcf1a54d 100644 --- a/app/registries/providers/gcr/Gcr.test.js +++ b/app/registries/providers/gcr/Gcr.test.ts @@ -1,4 +1,5 @@ -const Gcr = require('./Gcr'); +import { ContainerImage } from '../../../model/container'; +import { Gcr, GcrConfiguration } from './Gcr'; jest.mock('request-promise-native', () => jest.fn().mockImplementation(() => ({ @@ -28,7 +29,7 @@ test('validatedConfiguration should initialize when configuration is valid', () test('validatedConfiguration should throw error when configuration is missing', () => { expect(() => { - gcr.validateConfiguration({}); + gcr.validateConfiguration({} as GcrConfiguration); }).toThrow('"clientemail" is required'); }); @@ -45,28 +46,28 @@ test('match should return true when registry url is from gcr', () => { registry: { url: 'gcr.io', }, - }), + } as ContainerImage), ).toBeTruthy(); expect( gcr.match({ registry: { url: 'us.gcr.io', }, - }), + } as ContainerImage), ).toBeTruthy(); expect( gcr.match({ registry: { url: 'eu.gcr.io', }, - }), + } as ContainerImage), ).toBeTruthy(); expect( gcr.match({ registry: { url: 'asia.gcr.io', }, - }), + } as ContainerImage), ).toBeTruthy(); }); @@ -76,7 +77,7 @@ test('match should return false when registry url is not from gcr', () => { registry: { url: 'grr.io', }, - }), + } as ContainerImage), ).toBeFalsy(); }); @@ -87,7 +88,7 @@ test('normalizeImage should return the proper registry v2 endpoint', () => { registry: { url: 'eu.gcr.io/test/image', }, - }), + } as ContainerImage), ).toStrictEqual({ name: 'test/image', registry: { @@ -97,7 +98,7 @@ test('normalizeImage should return the proper registry v2 endpoint', () => { }); test('authenticate should call ecr auth endpoint', () => { - expect(gcr.authenticate({}, { headers: {} })).resolves.toEqual({ + expect(gcr.authenticate({} as ContainerImage, { headers: {} })).resolves.toEqual({ headers: { Authorization: 'Bearer xxxxx', }, diff --git a/app/registries/providers/gcr/Gcr.js b/app/registries/providers/gcr/Gcr.ts similarity index 78% rename from app/registries/providers/gcr/Gcr.js rename to app/registries/providers/gcr/Gcr.ts index 914c6e2e..0165fdfd 100644 --- a/app/registries/providers/gcr/Gcr.js +++ b/app/registries/providers/gcr/Gcr.ts @@ -1,10 +1,16 @@ -const rp = require('request-promise-native'); -const Registry = require('../../Registry'); +import rp, { RequestPromiseOptions } from 'request-promise-native'; +import { Registry } from '../../Registry'; +import { ContainerImage } from '../../../model/container'; + +export interface GcrConfiguration { + clientemail: string; + privatekey: string; +} /** * Google Container Registry integration. */ -class Gcr extends Registry { +export class Gcr extends Registry { getConfigurationSchema() { return this.joi.alternatives([ this.joi.string().allow(''), @@ -17,7 +23,6 @@ class Gcr extends Registry { /** * Sanitize sensitive data - * @returns {*} */ maskConfiguration() { return { @@ -30,20 +35,18 @@ class Gcr extends Registry { /** * Return true if image has not registry url. * @param image the image - * @returns {boolean} */ - match(image) { + match(image: ContainerImage) { return /^.*\.?gcr.io$/.test(image.registry.url); } /** * Normalize image according to AWS ECR characteristics. * @param image - * @returns {*} */ - normalizeImage(image) { + normalizeImage(image: ContainerImage) { const imageNormalized = image; if (!imageNormalized.registry.url.startsWith('https://')) { imageNormalized.registry.url = `https://${imageNormalized.registry.url}/v2`; @@ -51,7 +54,7 @@ class Gcr extends Registry { return imageNormalized; } - async authenticate(image, requestOptions) { + async authenticate(image: ContainerImage, requestOptions: RequestPromiseOptions) { if (!this.configuration.clientemail) { return requestOptions; } @@ -73,7 +76,7 @@ class Gcr extends Registry { const response = await rp(request); const requestOptionsWithAuth = requestOptions; - requestOptionsWithAuth.headers.Authorization = `Bearer ${response.token}`; + requestOptionsWithAuth.headers!.Authorization = `Bearer ${response.token}`; return requestOptionsWithAuth; } @@ -84,5 +87,3 @@ class Gcr extends Registry { }; } } - -module.exports = Gcr; diff --git a/app/registries/providers/ghcr/Ghcr.test.js b/app/registries/providers/ghcr/Ghcr.test.ts similarity index 84% rename from app/registries/providers/ghcr/Ghcr.test.js rename to app/registries/providers/ghcr/Ghcr.test.ts index bc0dc826..6ef8231d 100644 --- a/app/registries/providers/ghcr/Ghcr.test.js +++ b/app/registries/providers/ghcr/Ghcr.test.ts @@ -1,4 +1,5 @@ -const Ghcr = require('./Ghcr'); +import { ContainerImage } from '../../../model/container'; +import { Ghcr } from './Ghcr'; const ghcr = new Ghcr(); ghcr.configuration = { @@ -31,7 +32,7 @@ test('match should return true when registry url is from ghcr', () => { registry: { url: 'ghcr.io', }, - }), + } as ContainerImage), ).toBeTruthy(); }); @@ -41,7 +42,7 @@ test('match should return false when registry url is not from ghcr', () => { registry: { url: 'grr.io', }, - }), + } as ContainerImage), ).toBeFalsy(); }); @@ -52,7 +53,7 @@ test('normalizeImage should return the proper registry v2 endpoint', () => { registry: { url: 'ghcr.io/test/image', }, - }), + } as ContainerImage), ).toStrictEqual({ name: 'test/image', registry: { @@ -62,7 +63,7 @@ test('normalizeImage should return the proper registry v2 endpoint', () => { }); test('authenticate should populate header with base64 bearer', () => { - expect(ghcr.authenticate({}, { headers: {} })).resolves.toEqual({ + expect(ghcr.authenticate({} as ContainerImage, { headers: {} })).resolves.toEqual({ headers: { Authorization: 'Bearer dG9rZW4=', }, diff --git a/app/registries/providers/ghcr/Ghcr.js b/app/registries/providers/ghcr/Ghcr.ts similarity index 72% rename from app/registries/providers/ghcr/Ghcr.js rename to app/registries/providers/ghcr/Ghcr.ts index dd6789d2..3d233d0b 100644 --- a/app/registries/providers/ghcr/Ghcr.js +++ b/app/registries/providers/ghcr/Ghcr.ts @@ -1,10 +1,18 @@ -const Registry = require('../../Registry'); +import { RequestPromiseOptions } from 'request-promise-native'; +import { ContainerImage } from '../../../model/container'; +import { Registry } from '../../Registry'; +import { AnySchema } from 'joi'; + +export interface GhcrConfiguration { + username: string; + token: string; +} /** * Github Container Registry integration. */ -class Ghcr extends Registry { - getConfigurationSchema() { +export class Ghcr extends Registry { + getConfigurationSchema(): AnySchema { return this.joi.alternatives([ this.joi.string().allow(''), this.joi.object().keys({ @@ -16,7 +24,6 @@ class Ghcr extends Registry { /** * Sanitize sensitive data - * @returns {*} */ maskConfiguration() { return { @@ -29,20 +36,17 @@ class Ghcr extends Registry { /** * Return true if image has not registry url. * @param image the image - * @returns {boolean} */ - match(image) { + match(image: ContainerImage) { return /^.*\.?ghcr.io$/.test(image.registry.url); } /** * Normalize image according to Github Container Registry characteristics. * @param image - * @returns {*} */ - - normalizeImage(image) { + normalizeImage(image: ContainerImage) { const imageNormalized = image; if (!imageNormalized.registry.url.startsWith('https://')) { imageNormalized.registry.url = `https://${imageNormalized.registry.url}/v2`; @@ -50,13 +54,13 @@ class Ghcr extends Registry { return imageNormalized; } - async authenticate(image, requestOptions) { + async authenticate(image: ContainerImage, requestOptions: RequestPromiseOptions) { const requestOptionsWithAuth = requestOptions; const bearer = Buffer.from( this.configuration.token ? this.configuration.token : ':', 'utf-8', ).toString('base64'); - requestOptionsWithAuth.headers.Authorization = `Bearer ${bearer}`; + requestOptionsWithAuth.headers!.Authorization = `Bearer ${bearer}`; return requestOptionsWithAuth; } @@ -70,5 +74,3 @@ class Ghcr extends Registry { return undefined; } } - -module.exports = Ghcr; diff --git a/app/registries/providers/gitea/Gitea.test.js b/app/registries/providers/gitea/Gitea.test.ts similarity index 89% rename from app/registries/providers/gitea/Gitea.test.js rename to app/registries/providers/gitea/Gitea.test.ts index 1568f6ff..8dd21fc2 100644 --- a/app/registries/providers/gitea/Gitea.test.js +++ b/app/registries/providers/gitea/Gitea.test.ts @@ -1,4 +1,5 @@ -const Gitea = require('./Gitea'); +import { ContainerImage } from '../../../model/container'; +import { Gitea } from './Gitea'; const gitea = new Gitea(); gitea.configuration = { @@ -36,7 +37,7 @@ test('match should return true when registry url is from gitea', () => { registry: { url: 'gitea.acme.com', }, - }), + } as ContainerImage), ).toBeTruthy(); }); @@ -46,7 +47,7 @@ test('match should return false when registry url is not from custom', () => { registry: { url: 'gitea.notme.io', }, - }), + } as ContainerImage), ).toBeFalsy(); }); @@ -57,7 +58,7 @@ test('normalizeImage should return the proper registry v2 endpoint', () => { registry: { url: 'gitea.acme.com/test/image', }, - }), + } as ContainerImage), ).toStrictEqual({ name: 'test/image', registry: { diff --git a/app/registries/providers/gitea/Gitea.js b/app/registries/providers/gitea/Gitea.ts similarity index 84% rename from app/registries/providers/gitea/Gitea.js rename to app/registries/providers/gitea/Gitea.ts index 565b94c8..2d6ef21e 100644 --- a/app/registries/providers/gitea/Gitea.js +++ b/app/registries/providers/gitea/Gitea.ts @@ -1,9 +1,10 @@ -const Custom = require('../custom/Custom'); +import { ContainerImage } from '../../../model/container'; +import { Custom, CustomConfiguration } from '../custom/Custom'; /** * Gitea Container Registry integration. */ -class Gitea extends Custom { +export class Gitea extends Custom { getConfigurationSchema() { return this.joi.object().keys({ url: this.joi.string().uri().required(), @@ -43,14 +44,13 @@ class Gitea extends Custom { /** * Return true if image registry match gitea fqdn. * @param image the image - * @returns {boolean} */ - match(image) { + match(image: ContainerImage) { const fqdnConfigured = /(?:https?:\/\/)?(.*)/ - .exec(this.configuration.url)[1] + .exec(this.configuration.url)![1] .toLowerCase(); const imageRegistryFqdn = /(?:https?:\/\/)?(.*)/ - .exec(image.registry.url)[1] + .exec(image.registry.url)![1] .toLowerCase(); return fqdnConfigured === imageRegistryFqdn; } @@ -58,13 +58,10 @@ class Gitea extends Custom { /** * Normalize image according to Gitea Container Registry characteristics. * @param image - * @returns {*} */ - normalizeImage(image) { + normalizeImage(image: ContainerImage) { const imageNormalized = image; imageNormalized.registry.url = `${this.configuration.url}/v2`; return imageNormalized; } } - -module.exports = Gitea; diff --git a/app/registries/providers/gitlab/Gitlab.test.js b/app/registries/providers/gitlab/Gitlab.test.ts similarity index 85% rename from app/registries/providers/gitlab/Gitlab.test.js rename to app/registries/providers/gitlab/Gitlab.test.ts index d8023610..906d3934 100644 --- a/app/registries/providers/gitlab/Gitlab.test.js +++ b/app/registries/providers/gitlab/Gitlab.test.ts @@ -1,5 +1,7 @@ -const rp = require('request-promise-native'); -const Gitlab = require('./Gitlab'); +import rp, { RequestPromise } from 'request-promise-native'; +import { Gitlab, GitlabConfiguration } from './Gitlab'; +import { ContainerImage } from '../../../model/container'; + const gitlab = new Gitlab(); gitlab.configuration = { @@ -14,7 +16,7 @@ test('validatedConfiguration should initialize when configuration is valid', () expect( gitlab.validateConfiguration({ token: 'abcdef', - }), + } as GitlabConfiguration), ).toStrictEqual({ url: 'https://registry.gitlab.com', authurl: 'https://gitlab.com', @@ -35,7 +37,7 @@ test('validatedConfiguration should initialize when configuration is valid', () test('validatedConfiguration should throw error when no pam', () => { expect(() => { - gitlab.validateConfiguration({}); + gitlab.validateConfiguration({} as GitlabConfiguration); }).toThrow('"token" is required'); }); @@ -53,7 +55,7 @@ test('match should return true when registry url is from gitlab.com', () => { registry: { url: 'gitlab.com', }, - }), + } as ContainerImage), ).toBeTruthy(); }); @@ -69,17 +71,17 @@ test('match should return true when registry url is from custom gitlab', () => { registry: { url: 'custom.com', }, - }), + } as ContainerImage), ).toBeTruthy(); }); test('authenticate should perform authenticate request', () => { - rp.mockImplementation(() => ({ + (rp as unknown as jest.Mock).mockImplementation(() => ({ token: 'token', })); expect( gitlab.authenticate( - {}, + {} as ContainerImage, { headers: {}, }, @@ -94,7 +96,7 @@ test('normalizeImage should return the proper registry v2 endpoint', () => { registry: { url: 'registry.gitlab.com', }, - }), + } as ContainerImage), ).toStrictEqual({ name: 'test/image', registry: { diff --git a/app/registries/providers/gitlab/Gitlab.js b/app/registries/providers/gitlab/Gitlab.ts similarity index 77% rename from app/registries/providers/gitlab/Gitlab.js rename to app/registries/providers/gitlab/Gitlab.ts index 471c2917..56c5f497 100644 --- a/app/registries/providers/gitlab/Gitlab.js +++ b/app/registries/providers/gitlab/Gitlab.ts @@ -1,13 +1,19 @@ -const rp = require('request-promise-native'); -const Registry = require('../../Registry'); +import rp, { RequestPromiseOptions } from 'request-promise-native'; +import { Registry } from '../../Registry'; +import { ContainerImage } from '../../../model/container'; + +export interface GitlabConfiguration { + url: string; + authurl: string; + token: string; +} /** * Docker Gitlab integration. */ -class Gitlab extends Registry { +export class Gitlab extends Registry { /** * Get the Gitlab configuration schema. - * @returns {*} */ getConfigurationSchema() { return this.joi.object().keys({ @@ -19,7 +25,6 @@ class Gitlab extends Registry { /** * Sanitize sensitive data - * @returns {*} */ maskConfiguration() { return { @@ -33,19 +38,17 @@ class Gitlab extends Registry { /** * Return true if image has no registry url. * @param image the image - * @returns {boolean} */ - match(image) { + match(image: ContainerImage) { return this.configuration.url.indexOf(image.registry.url) !== -1; } /** * Normalize images according to Gitlab characteristics. * @param image - * @returns {*} */ - normalizeImage(image) { + normalizeImage(image: ContainerImage) { const imageNormalized = image; if (!imageNormalized.registry.url.startsWith('https://')) { imageNormalized.registry.url = `https://${imageNormalized.registry.url}/v2`; @@ -57,9 +60,8 @@ class Gitlab extends Registry { * Authenticate to Gitlab. * @param image * @param requestOptions - * @returns {Promise<*>} */ - async authenticate(image, requestOptions) { + async authenticate(image: ContainerImage, requestOptions: RequestPromiseOptions) { const request = { method: 'GET', uri: `${this.configuration.authurl}/jwt/auth?service=container_registry&scope=repository:${image.name}:pull`, @@ -71,13 +73,12 @@ class Gitlab extends Registry { }; const response = await rp(request); const requestOptionsWithAuth = requestOptions; - requestOptionsWithAuth.headers.Authorization = `Bearer ${response.token}`; + requestOptionsWithAuth.headers!.Authorization = `Bearer ${response.token}`; return requestOptionsWithAuth; } /** * Return empty username and personal access token value. - * @returns {{password: (string|undefined|*), username: string}} */ getAuthPull() { return { @@ -86,5 +87,3 @@ class Gitlab extends Registry { }; } } - -module.exports = Gitlab; diff --git a/app/registries/providers/hub/Hub.test.js b/app/registries/providers/hub/Hub.test.ts similarity index 79% rename from app/registries/providers/hub/Hub.test.js rename to app/registries/providers/hub/Hub.test.ts index bd9e049d..b617164f 100644 --- a/app/registries/providers/hub/Hub.test.js +++ b/app/registries/providers/hub/Hub.test.ts @@ -1,5 +1,6 @@ -const rp = require('request-promise-native'); -const Hub = require('./Hub'); +import rp from 'request-promise-native'; +import { Hub, HubConfiguration } from './Hub'; +import { ContainerImage } from '../../../model/container'; const hub = new Hub(); hub.configuration = { @@ -15,23 +16,23 @@ test('validatedConfiguration should initialize when configuration is valid', () hub.validateConfiguration({ login: 'login', password: 'password', - }), + } as HubConfiguration), ).toStrictEqual({ login: 'login', password: 'password', }); - expect(hub.validateConfiguration({ auth: 'auth' })).toStrictEqual({ + expect(hub.validateConfiguration({ auth: 'auth' } as HubConfiguration)).toStrictEqual({ auth: 'auth', }); - expect(hub.validateConfiguration({})).toStrictEqual({}); - expect(hub.validateConfiguration(undefined)).toStrictEqual({}); + expect(hub.validateConfiguration({} as HubConfiguration)).toStrictEqual({}); + expect(hub.validateConfiguration(undefined as unknown as HubConfiguration)).toStrictEqual({}); }); test('validatedConfiguration should throw error when auth is not base64', () => { expect(() => { hub.validateConfiguration({ auth: '°°°', - }); + } as HubConfiguration); }).toThrow('"auth" must be a valid base64 string'); }); @@ -48,7 +49,7 @@ test('match should return true when no registry on the image', () => { expect( hub.match({ registry: {}, - }), + } as ContainerImage), ).toBeTruthy(); }); @@ -58,7 +59,7 @@ test('match should return true when registry id docker.io on the image', () => { registry: { url: 'docker.io', }, - }), + } as ContainerImage), ).toBeTruthy(); }); @@ -68,7 +69,7 @@ test('match should return false when registry on the image', () => { registry: { url: 'registry', }, - }), + } as ContainerImage), ).toBeFalsy(); }); @@ -77,7 +78,7 @@ test('normalizeImage should prefix with library when no organization', () => { hub.normalizeImage({ name: 'test', registry: {}, - }), + } as ContainerImage), ).toStrictEqual({ name: 'library/test', registry: { @@ -91,7 +92,7 @@ test('normalizeImage should not prefix with library when existing organization', hub.normalizeImage({ name: 'myorga/test', registry: {}, - }), + } as ContainerImage), ).toStrictEqual({ name: 'myorga/test', registry: { @@ -101,12 +102,10 @@ test('normalizeImage should not prefix with library when existing organization', }); test('authenticate should perform authenticate request', () => { - rp.mockImplementation(() => ({ - token: 'token', - })); + (rp as unknown as jest.Mock).mockImplementation(() => Promise.resolve({ token: 'token' })); expect( hub.authenticate( - {}, + {} as ContainerImage, { headers: {}, }, @@ -126,6 +125,6 @@ test('getAuthCredentials should return base64 creds when login/token set in conf }); test('getAuthCredentials should return undefined when no login/token/auth set in configuration', () => { - hub.configuration = {}; + hub.configuration = {} as HubConfiguration; expect(hub.getAuthCredentials()).toBe(undefined); }); diff --git a/app/registries/providers/hub/Hub.js b/app/registries/providers/hub/Hub.ts similarity index 70% rename from app/registries/providers/hub/Hub.js rename to app/registries/providers/hub/Hub.ts index 3a894ce5..43ceec3f 100644 --- a/app/registries/providers/hub/Hub.js +++ b/app/registries/providers/hub/Hub.ts @@ -1,10 +1,19 @@ -const rp = require('request-promise-native'); -const Custom = require('../custom/Custom'); +import rp, { RequestPromiseOptions } from 'request-promise-native'; +import { Custom } from '../custom/Custom'; +import { ContainerImage } from '../../../model/container'; + +export interface HubConfiguration { + url: string; + login?: string; + password?: string; + token?: string; + auth?: string; +} /** * Docker Hub integration. */ -class Hub extends Custom { +export class Hub extends Custom { init() { this.configuration.url = 'https://registry-1.docker.io'; if (this.configuration.token) { @@ -14,7 +23,6 @@ class Hub extends Custom { /** * Get the Hub configuration schema. - * @returns {*} */ getConfigurationSchema() { return this.joi.alternatives([ @@ -30,7 +38,6 @@ class Hub extends Custom { /** * Sanitize sensitive data - * @returns {*} */ maskConfiguration() { return { @@ -46,10 +53,9 @@ class Hub extends Custom { /** * Return true if image has no registry url. * @param image the image - * @returns {boolean} */ - match(image) { + match(image: ContainerImage) { return ( !image.registry.url || /^.*\.?docker.io$/.test(image.registry.url) ); @@ -58,9 +64,8 @@ class Hub extends Custom { /** * Normalize images according to Hub characteristics. * @param image - * @returns {*} */ - normalizeImage(image) { + normalizeImage(image: ContainerImage) { const imageNormalized = super.normalizeImage(image); if (imageNormalized.name) { imageNormalized.name = imageNormalized.name.includes('/') @@ -74,12 +79,11 @@ class Hub extends Custom { * Authenticate to Hub. * @param image * @param requestOptions - * @returns {Promise<*>} */ - async authenticate(image, requestOptions) { - const request = { + async authenticate(image: ContainerImage, requestOptions: RequestPromiseOptions) { + const uri = `https://auth.docker.io/token?service=registry.docker.io&scope=repository:${image.name}:pull&grant_type=password`; + const request: RequestPromiseOptions = { method: 'GET', - uri: `https://auth.docker.io/token?service=registry.docker.io&scope=repository:${image.name}:pull&grant_type=password`, headers: { Accept: 'application/json', }, @@ -89,21 +93,19 @@ class Hub extends Custom { // Add Authorization if any const credentials = this.getAuthCredentials(); if (credentials) { - request.headers.Authorization = `Basic ${credentials}`; + request.headers!.Authorization = `Basic ${credentials}`; } - const response = await rp(request); + const response = await rp(uri, request); const requestOptionsWithAuth = requestOptions; - requestOptionsWithAuth.headers.Authorization = `Bearer ${response.token}`; + requestOptionsWithAuth.headers!.Authorization = `Bearer ${response.token}`; return requestOptionsWithAuth; } - getImageFullName(image, tagOrDigest) { + getImageFullName(image: ContainerImage, tagOrDigest: string) { let fullName = super.getImageFullName(image, tagOrDigest); fullName = fullName.replace(/registry-1.docker.io\//, ''); fullName = fullName.replace(/library\//, ''); return fullName; } } - -module.exports = Hub; diff --git a/app/registries/providers/lscr/Lscr.test.js b/app/registries/providers/lscr/Lscr.test.ts similarity index 83% rename from app/registries/providers/lscr/Lscr.test.js rename to app/registries/providers/lscr/Lscr.test.ts index 8e770c52..5c9995b2 100644 --- a/app/registries/providers/lscr/Lscr.test.js +++ b/app/registries/providers/lscr/Lscr.test.ts @@ -1,4 +1,6 @@ -const Lscr = require('./Lscr'); +import { ContainerImage } from '../../../model/container'; +import { GhcrConfiguration } from '../ghcr/Ghcr'; +import { Lscr } from './Lscr'; jest.mock('request-promise-native', () => jest.fn().mockImplementation(() => ({ @@ -28,7 +30,7 @@ test('validatedConfiguration should initialize when configuration is valid', () test('validatedConfiguration should throw error when configuration is missing', () => { expect(() => { - lscr.validateConfiguration({}); + lscr.validateConfiguration({} as GhcrConfiguration); }).toThrow('"username" is required'); }); @@ -38,7 +40,7 @@ test('match should return true when registry url is from lscr', () => { registry: { url: 'lscr.io', }, - }), + } as ContainerImage), ).toBeTruthy(); }); @@ -48,7 +50,7 @@ test('match should return false when registry url is not from lscr', () => { registry: { url: 'wrong.io', }, - }), + } as ContainerImage), ).toBeFalsy(); }); @@ -59,7 +61,7 @@ test('normalizeImage should return the proper registry v2 endpoint', () => { registry: { url: 'lscr.io/test/image', }, - }), + } as ContainerImage), ).toStrictEqual({ name: 'test/image', registry: { diff --git a/app/registries/providers/lscr/Lscr.js b/app/registries/providers/lscr/Lscr.ts similarity index 78% rename from app/registries/providers/lscr/Lscr.js rename to app/registries/providers/lscr/Lscr.ts index 056519a7..4ab01f08 100644 --- a/app/registries/providers/lscr/Lscr.js +++ b/app/registries/providers/lscr/Lscr.ts @@ -1,9 +1,10 @@ -const Ghcr = require('../ghcr/Ghcr'); +import { ContainerImage } from '../../../model/container'; +import { Ghcr } from '../ghcr/Ghcr'; /** * Linux-Server Container Registry integration. */ -class Lscr extends Ghcr { +export class Lscr extends Ghcr { getConfigurationSchema() { return this.joi.object().keys({ username: this.joi.string().required(), @@ -14,26 +15,21 @@ class Lscr extends Ghcr { /** * Return true if image has not registry url. * @param image the image - * @returns {boolean} */ - match(image) { + match(image: ContainerImage) { return /^.*\.?lscr.io$/.test(image.registry.url); } /** * Normalize image according to Github Container Registry characteristics. * @param image - * @returns {*} */ - - normalizeImage(image) { + normalizeImage(image: ContainerImage) { const imageNormalized = image; if (!imageNormalized.registry.url.startsWith('https://')) { imageNormalized.registry.url = `https://${imageNormalized.registry.url}/v2`; } return imageNormalized; } -} - -module.exports = Lscr; +} \ No newline at end of file diff --git a/app/registries/providers/quay/Quay.test.js b/app/registries/providers/quay/Quay.test.ts similarity index 76% rename from app/registries/providers/quay/Quay.test.js rename to app/registries/providers/quay/Quay.test.ts index 10125adf..42a4f120 100644 --- a/app/registries/providers/quay/Quay.test.js +++ b/app/registries/providers/quay/Quay.test.ts @@ -1,9 +1,10 @@ -const rp = require('request-promise-native'); -const Quay = require('./Quay'); -const log = require('../../../log'); +import rp, { RequestPromise } from 'request-promise-native'; +import { Quay, QuayConfiguration } from './Quay'; +import log from '../../../log'; +import { ContainerImage } from '../../../model/container'; jest.mock('request-promise-native'); -rp.mockImplementation(() => ({ +(rp as unknown as jest.Mock).mockImplementation((): Promise<{ token: string }> => Promise.resolve({ token: 'token', })); @@ -16,8 +17,8 @@ quay.configuration = { quay.log = log; test('validatedConfiguration should initialize when anonymous configuration is valid', () => { - expect(quay.validateConfiguration('')).toStrictEqual({}); - expect(quay.validateConfiguration(undefined)).toStrictEqual({}); + expect(quay.validateConfiguration('' as unknown as QuayConfiguration)).toStrictEqual({}); + expect(quay.validateConfiguration(undefined as unknown as QuayConfiguration)).toStrictEqual({}); }); test('validatedConfiguration should initialize when auth configuration is valid', () => { @@ -36,13 +37,13 @@ test('validatedConfiguration should initialize when auth configuration is valid' test('validatedConfiguration should throw error when configuration is missing', () => { expect(() => { - quay.validateConfiguration({}); + quay.validateConfiguration({} as QuayConfiguration); }).toThrow('"namespace" is required'); }); test('maskConfiguration should mask anonymous configuration secrets', () => { const quayInstance = new Quay(); - quayInstance.configuration = ''; + quayInstance.configuration = '' as unknown as QuayConfiguration; expect(quayInstance.maskConfiguration()).toEqual({}); }); @@ -60,7 +61,7 @@ test('match should return true when registry url is from quay.io', () => { registry: { url: 'quay.io', }, - }), + } as ContainerImage), ).toBeTruthy(); }); @@ -70,7 +71,7 @@ test('match should return false when registry url is not from quay.io', () => { registry: { url: 'error.io', }, - }), + } as ContainerImage), ).toBeFalsy(); }); @@ -81,7 +82,7 @@ test('normalizeImage should return the proper registry v2 endpoint', () => { registry: { url: 'quay.io/test/image', }, - }), + } as ContainerImage), ).toStrictEqual({ name: 'test/image', registry: { @@ -92,7 +93,7 @@ test('normalizeImage should return the proper registry v2 endpoint', () => { test('getAuthCredentials should return undefined when anonymous configuration', () => { const quayInstance = new Quay(); - quayInstance.configuration = {}; + quayInstance.configuration = {} as QuayConfiguration; expect(quayInstance.getAuthCredentials()).toEqual(undefined); }); @@ -110,7 +111,7 @@ test('getAuthCredentials should return base64 encode credentials when auth confi test('getAuthPull should return undefined when anonymous configuration', () => { const quayInstance = new Quay(); - quayInstance.configuration = {}; + quayInstance.configuration = {} as QuayConfiguration; expect(quayInstance.getAuthPull()).toEqual(undefined); }); @@ -128,7 +129,7 @@ test('getAuthPull should return credentials when auth configuration', () => { }); test('authenticate should populate header with base64 bearer', () => { - expect(quay.authenticate({}, { headers: {} })).resolves.toEqual({ + expect(quay.authenticate({} as ContainerImage, { headers: {} })).resolves.toEqual({ headers: { Authorization: 'Bearer token', }, @@ -137,8 +138,8 @@ test('authenticate should populate header with base64 bearer', () => { test('authenticate should not populate header with base64 bearer when anonymous', () => { const quayInstance = new Quay(); - quayInstance.configuration = {}; - expect(quayInstance.authenticate({}, { headers: {} })).resolves.toEqual({ + quayInstance.configuration = {} as QuayConfiguration; + expect(quayInstance.authenticate({} as ContainerImage, { headers: {} })).resolves.toEqual({ headers: {}, }); }); diff --git a/app/registries/providers/quay/Quay.js b/app/registries/providers/quay/Quay.ts similarity index 82% rename from app/registries/providers/quay/Quay.js rename to app/registries/providers/quay/Quay.ts index f0efd337..6745def5 100644 --- a/app/registries/providers/quay/Quay.js +++ b/app/registries/providers/quay/Quay.ts @@ -1,10 +1,17 @@ -const rp = require('request-promise-native'); -const Registry = require('../../Registry'); +import rp, { RequestPromiseOptions } from 'request-promise-native'; +import { DockerRegistryTags, Request, Registry } from '../../Registry'; +import { ContainerImage } from '../../../model/container'; + +export interface QuayConfiguration { + namespace: string; + account: string; + token: string; +} /** * Quay.io Registry integration. */ -class Quay extends Registry { +export class Quay extends Registry { getConfigurationSchema() { return this.joi.alternatives([ // Anonymous configuration @@ -21,7 +28,6 @@ class Quay extends Registry { /** * Sanitize sensitive data - * @returns {*} */ maskConfiguration() { return { @@ -35,20 +41,17 @@ class Quay extends Registry { /** * Return true if image has not registry url. * @param image the image - * @returns {boolean} */ - match(image) { + match(image: ContainerImage) { return /^.*\.?quay.io$/.test(image.registry.url); } /** * Normalize image according to Github Container Registry characteristics. * @param image - * @returns {*} */ - - normalizeImage(image) { + normalizeImage(image: ContainerImage) { const imageNormalized = image; if (!imageNormalized.registry.url.startsWith('https://')) { imageNormalized.registry.url = `https://${imageNormalized.registry.url}/v2`; @@ -56,7 +59,7 @@ class Quay extends Registry { return imageNormalized; } - async authenticate(image, requestOptions) { + async authenticate(image: ContainerImage, requestOptions: RequestPromiseOptions) { const requestOptionsWithAuth = requestOptions; let token; @@ -75,7 +78,7 @@ class Quay extends Registry { try { const response = await rp(request); token = response.token; - } catch (e) { + } catch (e: any) { this.log.warn( `Error when trying to get an access token (${e.message})`, ); @@ -84,14 +87,13 @@ class Quay extends Registry { // Token? Put it in authorization header if (token) { - requestOptionsWithAuth.headers.Authorization = `Bearer ${token}`; + requestOptionsWithAuth.headers!.Authorization = `Bearer ${token}`; } return requestOptionsWithAuth; } /** * Return Base64 credentials if any. - * @returns {string|undefined|*} */ getAuthCredentials() { if (this.configuration.namespace && this.configuration.account) { @@ -105,7 +107,6 @@ class Quay extends Registry { /** * Return username / password for Docker(+compose) triggers usage - * @return {{password: string, username: string}|undefined} */ getAuthPull() { if (this.configuration.namespace && this.configuration.account) { @@ -117,7 +118,7 @@ class Quay extends Registry { return undefined; } - getTagsPage(image, lastItem, link) { + getTagsPage(image: ContainerImage, _lastItem?: string | undefined, link?: string | undefined) { // Default items per page (not honoured by all registries) const itemsPerPage = 1000; let nextOrLast = ''; @@ -130,12 +131,10 @@ class Quay extends Registry { nextOrLast = `&last=${lastRegex[1]}`; } } - return this.callRegistry({ + return this.callRegistry>({ image, url: `${image.registry.url}/${image.name}/tags/list?n=${itemsPerPage}${nextOrLast}`, resolveWithFullResponse: true, }); } } - -module.exports = Quay; diff --git a/app/registry/Component.test.js b/app/registry/Component.test.ts similarity index 85% rename from app/registry/Component.test.js rename to app/registry/Component.test.ts index 2d796938..abf33e6f 100644 --- a/app/registry/Component.test.js +++ b/app/registry/Component.test.ts @@ -1,4 +1,4 @@ -const Component = require('./Component'); +import { Component } from './Component'; beforeEach(() => { jest.resetAllMocks(); @@ -26,7 +26,7 @@ test('mask should not fail when mask is longer than original string', () => { test('getId should return the concatenation $type.$name', () => { const component = new Component(); - component.register('kind', 'type', 'name', { x: 'x' }); + component.register('trigger', 'type', 'name', { x: 'x' }); expect(component.getId()).toEqual('type.name'); }); @@ -37,7 +37,7 @@ test('register should call validateConfiguration and init methods of the compone 'validateConfiguration', ); const spyInit = jest.spyOn(component, 'init'); - component.register('kind', 'type', 'name', { x: 'x' }); + component.register('trigger', 'type', 'name', { x: 'x' }); expect(spyValidateConsiguration).toHaveBeenCalledWith({ x: 'x' }); expect(spyInit).toHaveBeenCalledTimes(1); }); @@ -48,7 +48,7 @@ test('register should not call init when validateConfiguration fails', () => { throw new Error('validation failed'); }; const spyInit = jest.spyOn(component, 'init'); - expect(component.register('type', 'name', { x: 'x' })).rejects.toThrowError( + expect(component.register('trigger', 'name', '', { x: 'x' })).rejects.toThrowError( 'validation failed', ); expect(spyInit).toHaveBeenCalledTimes(0); @@ -59,7 +59,7 @@ test('register should throw when init fails', () => { component.init = () => { throw new Error('init failed'); }; - expect(component.register('type', 'name', { x: 'x' })).rejects.toThrowError( + expect(component.register('trigger', 'name', '', { x: 'x' })).rejects.toThrowError( 'init failed', ); }); diff --git a/app/registry/Component.js b/app/registry/Component.ts similarity index 64% rename from app/registry/Component.js rename to app/registry/Component.ts index de6c92b8..dee1e0f4 100644 --- a/app/registry/Component.js +++ b/app/registry/Component.ts @@ -1,10 +1,22 @@ -const joi = require('joi'); -const log = require('../log'); +import joi from 'joi'; +import log from '../log'; +import Logger from 'bunyan'; + +export type ComponentKind = 'trigger' | 'watcher' | 'registry' | 'authentication'; + +export interface BaseConfig { } /** * Base Component Class. */ -class Component { +export class Component { + joi: joi.Root; + log!: Logger; + + kind!: ComponentKind; + type!: string; + name!: string; + configuration!: TConfig; /** * Constructor. */ @@ -18,7 +30,7 @@ class Component { * @param name the name of the component * @param configuration the configuration of the component */ - async register(kind, type, name, configuration) { + async register(kind: ComponentKind, type: string, name: string, configuration: TConfig) { // Child log for the component this.log = log.child({ component: `${kind}.${type}.${name}` }); this.kind = kind; @@ -27,7 +39,7 @@ class Component { this.configuration = this.validateConfiguration(configuration); this.log.info( - `Register with configuration ${JSON.stringify(this.maskConfiguration(configuration))}`, + `Register with configuration ${JSON.stringify(this.maskConfiguration())}`, ); await this.init(); return this; @@ -35,17 +47,15 @@ class Component { /** * Deregister the component. - * @returns {Promise} */ async deregister() { - this.log.info('Deregister component'); + this.log?.info('Deregister component'); await this.deregisterComponent(); return this; } /** - * Deregistger the component (do nothing by default). - * @returns {Promise} + * Deregister the component (do nothing by default). */ async deregisterComponent() { @@ -56,24 +66,23 @@ class Component { * Validate the configuration of the component. * * @param configuration the configuration - * @returns {*} or throw a validation error + * @returns or throw a validation error */ - validateConfiguration(configuration) { + validateConfiguration(configuration: T): T { const schema = this.getConfigurationSchema(); const schemaValidated = schema.validate(configuration); if (schemaValidated.error) { throw schemaValidated.error; } - return schemaValidated.value ? schemaValidated.value : {}; + return schemaValidated.value ? schemaValidated.value as T : {} as T; } /** * Get the component configuration schema. * Can be overridden by the component implementation class - * @returns {*} */ - getConfigurationSchema() { - return this.joi.object(); + getConfigurationSchema(): joi.AnySchema { + return this.joi.object(); } /** @@ -81,11 +90,10 @@ class Component { * Can be overridden by the component implementation class */ - init() {} + init() { } /** * Sanitize sensitive data - * @returns {*} */ maskConfiguration() { return this.configuration; @@ -93,20 +101,21 @@ class Component { /** * Get Component ID. - * @returns {string} */ getId() { return `${this.type}.${this.name}`; } + static mask(value: string, nb?: number, char?: string): string; + static mask(value: null | undefined, nb?: number, char?: string): undefined; + static mask(value: string | undefined, nb?: number, char?: string): string | undefined; /** * Mask a String * @param value the value to mask * @param nb the number of chars to keep start/end * @param char the replacement char - * @returns {string|undefined} the masked string */ - static mask(value, nb = 1, char = '*') { + static mask(value: string | null | undefined, nb = 1, char = '*') { if (!value) { return undefined; } @@ -117,6 +126,4 @@ class Component { Math.max(0, value.length - nb * 2), )}${value.substring(value.length - nb, value.length)}`; } -} - -module.exports = Component; +} \ No newline at end of file diff --git a/app/registry/index.test.js b/app/registry/index.test.ts similarity index 63% rename from app/registry/index.test.js rename to app/registry/index.test.ts index e80b244d..5b8349ae 100644 --- a/app/registry/index.test.js +++ b/app/registry/index.test.ts @@ -1,25 +1,30 @@ -const configuration = require('../configuration'); -const Component = require('./Component'); -const prometheusWatcher = require('../prometheus/watcher'); - -jest.mock('../configuration'); - -configuration.getLogLevel = () => 'info'; - let registries = {}; let triggers = {}; let watchers = {}; let authentications = {}; -configuration.getRegistryConfigurations = () => registries; -configuration.getTriggerConfigurations = () => triggers; -configuration.getWatcherConfigurations = () => watchers; -configuration.getAuthenticationConfigurations = () => authentications; +jest.mock('../configuration', () => { + const originalModule = jest.requireActual('../configuration'); + return { + ...originalModule, + getLogLevel: jest.fn(() => 'info'), + getRegistryConfigurations: jest.fn(() => registries), + getTriggerConfigurations: jest.fn(() => triggers), + getWatcherConfigurations: jest.fn(() => watchers), + getAuthenticationConfigurations: jest.fn(() => authentications), + }; +}); + +import { Component } from './Component'; +import * as prometheusWatcher from '../prometheus/watcher'; -const registry = require('./index'); +import * as registry from './index'; +import { getState } from './states'; +import { Trigger } from '../triggers/providers/Trigger'; +import { Registry } from '../registries/Registry'; +import { Watcher } from '../watchers/Watcher'; beforeEach(() => { - jest.resetAllMocks(); prometheusWatcher.init(); registries = {}; triggers = {}; @@ -29,36 +34,25 @@ beforeEach(() => { afterEach(async () => { try { - await registry.__get__('deregisterRegistries')(); - await registry.__get__('deregisterTriggers')(); - await registry.__get__('deregisterWatchers')(); - await registry.__get__('deregisterAuthentications')(); + await registry.deregisterRegistries(); + await registry.deregisterTriggers(); + await registry.deregisterWatchers(); + await registry.deregisterAuthentications(); } catch (e) { // ignore error } }); -test('registerComponent should warn when component does not exist', () => { - const registerComponent = registry.__get__('registerComponent'); - expect( - registerComponent('kind', 'provider', 'name', {}, 'path'), - ).rejects.toThrow( - "Error when registering component provider (Cannot find module 'path/provider/Provider' from 'registry/index.js'", - ); -}); - test('registerComponents should return empty array if not components', () => { - const registerComponents = registry.__get__('registerComponents'); - expect(registerComponents('kind', undefined, 'path')).resolves.toEqual([]); + expect(registry.registerComponents('trigger', {}, '')).resolves.toEqual([]); }); test('deregisterComponent should throw when component fails to deregister', () => { - const deregisterComponent = registry.__get__('deregisterComponent'); const component = new Component(); component.deregister = () => { throw new Error('Error x'); }; - expect(deregisterComponent(component)).rejects.toThrowError( + expect(registry.deregisterComponent(component, 'trigger')).rejects.toThrowError( 'Error when deregistering component undefined.undefined', ); }); @@ -79,8 +73,8 @@ test('registerRegistries should register all registries', async () => { }, }, }; - await registry.__get__('registerRegistries')(); - expect(Object.keys(registry.getState().registry).sort()).toEqual([ + await registry.registerRegistries(); + expect(Object.keys(getState().registry).sort()).toEqual([ 'ecr.private', 'gcr.public', 'ghcr.public', @@ -90,8 +84,8 @@ test('registerRegistries should register all registries', async () => { }); test('registerRegistries should register all anonymous registries by default', async () => { - await registry.__get__('registerRegistries')(); - expect(Object.keys(registry.getState().registry).sort()).toEqual([ + await registry.registerRegistries(); + expect(Object.keys(getState().registry).sort()).toEqual([ 'ecr.public', 'gcr.public', 'ghcr.public', @@ -101,7 +95,7 @@ test('registerRegistries should register all anonymous registries by default', a }); test('registerRegistries should warn when registration errors occur', async () => { - const spyLog = jest.spyOn(registry.__get__('log'), 'warn'); + const spyLog = jest.spyOn(registry.log, 'warn'); registries = { hub: { private: { @@ -109,7 +103,7 @@ test('registerRegistries should warn when registration errors occur', async () = }, }, }; - await registry.__get__('registerRegistries')(); + await registry.registerRegistries(); expect(spyLog).toHaveBeenCalledWith( 'Some registries failed to register (Error when registering component hub ("login" must be a string))', ); @@ -122,23 +116,23 @@ test('registerTriggers should register all triggers', async () => { mock2: {}, }, }; - await registry.__get__('registerTriggers')(); - expect(Object.keys(registry.getState().trigger)).toEqual([ + await registry.registerTriggers(); + expect(Object.keys(getState().trigger)).toEqual([ 'mock.mock1', 'mock.mock2', ]); }); test('registerTriggers should warn when registration errors occur', async () => { - const spyLog = jest.spyOn(registry.__get__('log'), 'warn'); + const spyLog = jest.spyOn(registry.log, 'warn'); triggers = { trigger1: { fail: true, }, }; - await registry.__get__('registerTriggers')(); + await registry.registerTriggers(); expect(spyLog).toHaveBeenCalledWith( - "Some triggers failed to register (Error when registering component trigger1 (Cannot find module '../triggers/providers/trigger1/Trigger1' from 'registry/index.js'))", + "Some triggers failed to register (Error when registering component trigger1 (Cannot find module '../triggers/providers/trigger1/Trigger1' from 'registry/index.ts'))", ); }); @@ -151,26 +145,26 @@ test('registerWatchers should register all watchers', async () => { host: 'host2', }, }; - await registry.__get__('registerWatchers')(); - expect(Object.keys(registry.getState().watcher)).toEqual([ + await registry.registerWatchers(); + expect(Object.keys(getState().watcher)).toEqual([ 'docker.watcher1', 'docker.watcher2', ]); }); test('registerWatchers should register local docker watcher by default', async () => { - await registry.__get__('registerWatchers')(); - expect(Object.keys(registry.getState().watcher)).toEqual(['docker.local']); + await registry.registerWatchers(); + expect(Object.keys(getState().watcher)).toEqual(['docker.local']); }); test('registerWatchers should warn when registration errors occur', async () => { - const spyLog = jest.spyOn(registry.__get__('log'), 'warn'); + const spyLog = jest.spyOn(registry.log, 'warn'); watchers = { watcher1: { fail: true, }, }; - await registry.__get__('registerWatchers')(); + await registry.registerWatchers(); expect(spyLog).toHaveBeenCalledWith( 'Some watchers failed to register (Error when registering component docker ("fail" is not allowed))', ); @@ -189,15 +183,15 @@ test('registerAuthentications should register all auth strategies', async () => }, }, }; - await registry.__get__('registerAuthentications')(); - expect(Object.keys(registry.getState().authentication)).toEqual([ + await registry.registerAuthentications(); + expect(Object.keys(getState().authentication)).toEqual([ 'basic.john', 'basic.jane', ]); }); test('registerAuthentications should warn when registration errors occur', async () => { - const spyLog = jest.spyOn(registry.__get__('log'), 'warn'); + const spyLog = jest.spyOn(registry.log, 'warn'); authentications = { basic: { john: { @@ -205,15 +199,15 @@ test('registerAuthentications should warn when registration errors occur', async }, }, }; - await registry.__get__('registerAuthentications')(); + await registry.registerAuthentications(); expect(spyLog).toHaveBeenCalledWith( 'Some authentications failed to register (Error when registering component basic ("user" is required))', ); }); test('registerAuthentications should register anonymous auth by default', async () => { - await registry.__get__('registerAuthentications')(); - expect(Object.keys(registry.getState().authentication)).toEqual([ + await registry.registerAuthentications(); + expect(Object.keys(getState().authentication)).toEqual([ 'anonymous.anonymous', ]); }); @@ -261,22 +255,22 @@ test('init should register all components', async () => { }, }; await registry.init(); - expect(Object.keys(registry.getState().registry).sort()).toEqual([ + expect(Object.keys(getState().registry).sort()).toEqual([ 'ecr.private', 'gcr.public', 'ghcr.public', 'hub.private', 'quay.public', ]); - expect(Object.keys(registry.getState().trigger)).toEqual([ + expect(Object.keys(getState().trigger)).toEqual([ 'mock.mock1', 'mock.mock2', ]); - expect(Object.keys(registry.getState().watcher)).toEqual([ + expect(Object.keys(getState().watcher)).toEqual([ 'docker.watcher1', 'docker.watcher2', ]); - expect(Object.keys(registry.getState().authentication)).toEqual([ + expect(Object.keys(getState().authentication)).toEqual([ 'basic.john', 'basic.jane', ]); @@ -321,61 +315,61 @@ test('deregisterAll should deregister all components', async () => { }, }; await registry.init(); - await registry.__get__('deregisterAll')(); - expect(Object.keys(registry.getState().registry).length).toEqual(0); - expect(Object.keys(registry.getState().trigger).length).toEqual(0); - expect(Object.keys(registry.getState().watcher).length).toEqual(0); - expect(Object.keys(registry.getState().authentication).length).toEqual(0); + await registry.deregisterAll(); + expect(Object.keys(getState().registry).length).toEqual(0); + expect(Object.keys(getState().trigger).length).toEqual(0); + expect(Object.keys(getState().watcher).length).toEqual(0); + expect(Object.keys(getState().authentication).length).toEqual(0); }); test('deregisterAll should throw an error when any component fails to deregister', () => { - const component = new Component(); + const component = new Component() as Trigger; component.deregister = () => { throw new Error('Fail!!!'); }; - registry.getState().trigger = { - trigger1: component, + getState().trigger = { + 'trigger1': component, }; - expect(registry.__get__('deregisterAll')()).rejects.toThrowError( + expect(registry.deregisterAll()).rejects.toThrowError( 'Error when deregistering component undefined.undefined', ); }); test('deregisterRegistries should throw when errors occurred', async () => { - const component = new Component(); + const component = new Component() as Registry; component.deregister = () => { throw new Error('Fail!!!'); }; - registry.getState().registry = { + getState().registry = { registry1: component, }; - expect(registry.__get__('deregisterRegistries')()).rejects.toThrowError( + expect(registry.deregisterRegistries()).rejects.toThrowError( 'Error when deregistering component undefined.undefined', ); }); test('deregisterTriggers should throw when errors occurred', async () => { - const component = new Component(); + const component = new Component() as Trigger; component.deregister = () => { throw new Error('Fail!!!'); }; - registry.getState().trigger = { + getState().trigger = { trigger1: component, }; - expect(registry.__get__('deregisterTriggers')()).rejects.toThrowError( + expect(registry.deregisterTriggers()).rejects.toThrowError( 'Error when deregistering component undefined.undefined', ); }); test('deregisterWatchers should throw when errors occurred', async () => { - const component = new Component(); + const component = new Component() as Watcher; component.deregister = () => { throw new Error('Fail!!!'); }; - registry.getState().watcher = { + getState().watcher = { watcher1: component, }; - expect(registry.__get__('deregisterWatchers')()).rejects.toThrowError( + expect(registry.deregisterWatchers()).rejects.toThrowError( 'Error when deregistering component undefined.undefined', ); }); diff --git a/app/registry/index.js b/app/registry/index.ts similarity index 75% rename from app/registry/index.js rename to app/registry/index.ts index 1d5bede6..84dd8e46 100644 --- a/app/registry/index.js +++ b/app/registry/index.ts @@ -1,54 +1,36 @@ /** * Registry handling all components (registries, triggers, watchers). */ -const capitalize = require('capitalize'); -const log = require('../log').child({ component: 'registry' }); -const { - getWatcherConfigurations, - getTriggerConfigurations, - getRegistryConfigurations, - getAuthenticationConfigurations, -} = require('../configuration'); +import logger from '../log'; +import { Component, ComponentKind } from './Component'; +import { getWatcherConfigurations, getTriggerConfigurations, getRegistryConfigurations, getAuthenticationConfigurations, BaseConfiguration } from '../configuration'; +import { Trigger } from '../triggers/providers/Trigger'; +import { states, getState } from './states'; +import capitalize from 'capitalize'; -/** - * Registry state. - */ -const state = { - trigger: {}, - watcher: {}, - registry: {}, - authentication: {}, -}; +export const log = logger.child({ component: 'registry' }); -function getState() { - return state; -} /** * Register a component. - * - * @param {*} kind - * @param {*} provider - * @param {*} name - * @param {*} configuration - * @param {*} path */ -async function registerComponent(kind, provider, name, configuration, path) { +export async function registerComponent(kind: ComponentKind, provider: string, name: string, configuration: any, path: string): Promise { const providerLowercase = provider.toLowerCase(); const nameLowercase = name.toLowerCase(); const componentFile = `${path}/${providerLowercase.toLowerCase()}/${capitalize(provider)}`; try { - const Component = require(componentFile); - const component = new Component(); + const componentType = await import(componentFile); + const component = new componentType[capitalize(provider)](); const componentRegistered = await component.register( kind, providerLowercase, nameLowercase, configuration, ); - state[kind][component.getId()] = component; + states[kind][component.getId()] = component as any; return componentRegistered; - } catch (e) { + } catch (e: any) { + console.log(e); throw new Error( `Error when registering component ${providerLowercase} (${e.message})`, ); @@ -57,12 +39,8 @@ async function registerComponent(kind, provider, name, configuration, path) { /** * Register all found components. - * @param kind - * @param configurations - * @param path - * @returns {*[]} */ -async function registerComponents(kind, configurations, path) { +export async function registerComponents(kind: ComponentKind, configurations: BaseConfiguration, path: string) { if (configurations) { const providers = Object.keys(configurations); const providerPromises = providers @@ -71,6 +49,9 @@ async function registerComponents(kind, configurations, path) { `Register all components of kind ${kind} for provider ${provider}`, ); const providerConfigurations = configurations[provider]; + if (!providerConfigurations) { + throw new Error(`No configurations found for provider ${provider}`); + } return Object.keys(providerConfigurations).map( (configurationName) => registerComponent( @@ -92,9 +73,9 @@ async function registerComponents(kind, configurations, path) { * Register watchers. * @returns {Promise} */ -async function registerWatchers() { +export async function registerWatchers() { const configurations = getWatcherConfigurations(); - let watchersToRegister = []; + let watchersToRegister: Promise[] = []; try { if (Object.keys(configurations).length === 0) { log.info( @@ -124,7 +105,7 @@ async function registerWatchers() { ); } await Promise.all(watchersToRegister); - } catch (e) { + } catch (e: any) { log.warn(`Some watchers failed to register (${e.message})`); log.debug(e); } @@ -133,15 +114,15 @@ async function registerWatchers() { /** * Register triggers. */ -async function registerTriggers() { +export async function registerTriggers() { const configurations = getTriggerConfigurations(); try { await registerComponents( 'trigger', configurations, - '../triggers/providers', + '../triggers/providers' ); - } catch (e) { + } catch (e: any) { log.warn(`Some triggers failed to register (${e.message})`); log.debug(e); } @@ -151,7 +132,7 @@ async function registerTriggers() { * Register registries. * @returns {Promise} */ -async function registerRegistries() { +export async function registerRegistries() { const defaultRegistries = { ecr: { public: '' }, gcr: { public: '' }, @@ -170,7 +151,7 @@ async function registerRegistries() { registriesToRegister, '../registries/providers', ); - } catch (e) { + } catch (e: any) { log.warn(`Some registries failed to register (${e.message})`); log.debug(e); } @@ -179,7 +160,7 @@ async function registerRegistries() { /** * Register authentications. */ -async function registerAuthentications() { +export async function registerAuthentications() { const configurations = getAuthenticationConfigurations(); try { if (Object.keys(configurations).length === 0) { @@ -197,7 +178,7 @@ async function registerAuthentications() { configurations, '../authentications/providers', ); - } catch (e) { + } catch (e: any) { log.warn(`Some authentications failed to register (${e.message})`); log.debug(e); } @@ -207,17 +188,16 @@ async function registerAuthentications() { * Deregister a component. * @param component * @param kind - * @returns {Promise} */ -async function deregisterComponent(component, kind) { +export async function deregisterComponent(component: Component, kind: ComponentKind) { try { await component.deregister(); - } catch (e) { + } catch (e: any) { throw new Error( `Error when deregistering component ${component.getId()} (${e.message})`, ); } finally { - const components = getState()[kind]; + const components = states[kind]; if (components) { delete components[component.getId()]; } @@ -228,9 +208,8 @@ async function deregisterComponent(component, kind) { * Deregister all components of kind. * @param components * @param kind - * @returns {Promise} */ -async function deregisterComponents(components, kind) { +export async function deregisterComponents>(components: T[], kind: ComponentKind) { const deregisterPromises = components.map(async (component) => deregisterComponent(component, kind), ); @@ -239,33 +218,29 @@ async function deregisterComponents(components, kind) { /** * Deregister all watchers. - * @returns {Promise} */ -async function deregisterWatchers() { +export async function deregisterWatchers() { return deregisterComponents(Object.values(getState().watcher), 'watcher'); } /** * Deregister all triggers. - * @returns {Promise} */ -async function deregisterTriggers() { - return deregisterComponents(Object.values(getState().trigger), 'trigger'); +export async function deregisterTriggers() { + return deregisterComponents(Object.values(getState().trigger), 'trigger'); } /** * Deregister all registries. - * @returns {Promise} */ -async function deregisterRegistries() { +export async function deregisterRegistries() { return deregisterComponents(Object.values(getState().registry), 'registry'); } /** * Deregister all authentications. - * @returns {Promise} */ -async function deregisterAuthentications() { +export async function deregisterAuthentications() { return deregisterComponents( Object.values(getState().authentication), 'authentication', @@ -274,20 +249,19 @@ async function deregisterAuthentications() { /** * Deregister all components. - * @returns {Promise} */ -async function deregisterAll() { +export async function deregisterAll() { try { await deregisterWatchers(); await deregisterTriggers(); await deregisterRegistries(); await deregisterAuthentications(); - } catch (e) { + } catch (e: any) { throw new Error(`Error when trying to deregister ${e.message}`); } } -async function init() { +export async function init() { // Register triggers await registerTriggers(); @@ -304,8 +278,3 @@ async function init() { process.on('SIGINT', deregisterAll); process.on('SIGTERM', deregisterAll); } - -module.exports = { - init, - getState, -}; diff --git a/app/registry/states.ts b/app/registry/states.ts new file mode 100644 index 00000000..767e2534 --- /dev/null +++ b/app/registry/states.ts @@ -0,0 +1,23 @@ +import { Authentication } from '../authentications/providers/Authentication'; +import { Registry } from '../registries/Registry'; +import { Trigger } from '../triggers/providers/Trigger'; +import { Watcher } from '../watchers/Watcher'; + +/** + * Registry state. + */ +export const states: { + trigger: { [key: string]: Trigger; }; + watcher: { [key: string]: Watcher; }; + registry: { [key: string]: Registry; }; + authentication: { [key: string]: Authentication; }; +} = { + trigger: {}, + watcher: {}, + registry: {}, + authentication: {}, +}; + +export function getState() { + return states; +} diff --git a/app/store/app.test.js b/app/store/app.test.ts similarity index 78% rename from app/store/app.test.js rename to app/store/app.test.ts index d9bae091..458fc59a 100644 --- a/app/store/app.test.js +++ b/app/store/app.test.ts @@ -1,5 +1,5 @@ -const app = require('./app'); -const migrate = require('./migrate'); +import * as app from './app'; +import * as migrate from './migrate'; jest.mock('../configuration', () => ({ getVersion: () => '2.0.0', @@ -15,10 +15,10 @@ test('createCollections should create collection app when not exist', () => { const db = { getCollection: () => null, addCollection: () => ({ - findOne: () => {}, - insert: () => {}, + findOne: () => { }, + insert: () => { }, }), - }; + } as unknown as Loki; const spy = jest.spyOn(db, 'addCollection'); app.createCollections(db); expect(spy).toHaveBeenCalledWith('app'); @@ -27,11 +27,11 @@ test('createCollections should create collection app when not exist', () => { test('createCollections should not create collection app when already exist', () => { const db = { getCollection: () => ({ - findOne: () => {}, - insert: () => {}, + findOne: () => { }, + insert: () => { }, }), addCollection: () => null, - }; + } as unknown as Loki; const spy = jest.spyOn(db, 'addCollection'); app.createCollections(db); expect(spy).not.toHaveBeenCalled(); @@ -44,11 +44,11 @@ test('createCollections should call migrate when versions are different', () => name: 'wud', version: '1.0.0', }), - insert: () => {}, - remove: () => {}, + insert: () => { }, + remove: () => { }, }), addCollection: () => null, - }; + } as unknown as Loki; const spy = jest.spyOn(migrate, 'migrate'); app.createCollections(db); expect(spy).toHaveBeenCalledWith('1.0.0', '2.0.0'); @@ -61,11 +61,11 @@ test('createCollections should not call migrate when versions are identical', () name: 'wud', version: '2.0.0', }), - insert: () => {}, - remove: () => {}, + insert: () => { }, + remove: () => { }, }), addCollection: () => null, - }; + } as unknown as Loki; const spy = jest.spyOn(migrate, 'migrate'); app.createCollections(db); expect(spy).not.toHaveBeenCalledWith(); @@ -78,13 +78,13 @@ test('getAppInfos should return collection content', () => { name: 'wud', version: '1.0.0', }), - insert: () => {}, - remove: () => {}, + insert: () => { }, + remove: () => { }, }), addCollection: () => null, - }; + } as unknown as Loki; app.createCollections(db); - expect(app.getAppInfos(db)).toStrictEqual({ + expect(app.getAppInfos()).toStrictEqual({ name: 'wud', version: '1.0.0', }); diff --git a/app/store/app.js b/app/store/app.ts similarity index 68% rename from app/store/app.js rename to app/store/app.ts index 0bafaa66..7d9efecc 100644 --- a/app/store/app.js +++ b/app/store/app.ts @@ -1,11 +1,14 @@ /** * App store. */ -const log = require('../log').child({ component: 'store' }); -const { migrate } = require('./migrate'); -const { getVersion } = require('../configuration'); +import logger from '../log'; +import { migrate } from './migrate'; +import { getVersion } from '../configuration'; + +const log = logger.child({ component: 'store' }); + +let app: Collection; -let app; function saveAppInfosAndMigrate() { const appInfosCurrent = { @@ -24,7 +27,7 @@ function saveAppInfosAndMigrate() { app.insert(appInfosCurrent); } -function createCollections(db) { +export function createCollections(db: Loki) { app = db.getCollection('app'); if (app === null) { log.info('Create Collection app'); @@ -33,11 +36,11 @@ function createCollections(db) { saveAppInfosAndMigrate(); } -function getAppInfos() { +export function getAppInfos() { return app.findOne({}); } -module.exports = { - createCollections, - getAppInfos, -}; +export interface AppInfo { + name: string; + version: string; +} diff --git a/app/store/container.test.js b/app/store/container.test.ts similarity index 91% rename from app/store/container.test.js rename to app/store/container.test.ts index d23b755f..3929523a 100644 --- a/app/store/container.test.js +++ b/app/store/container.test.ts @@ -1,5 +1,5 @@ -const container = require('./container'); -const event = require('../event'); +import * as container from './container'; +import * as event from '../event'; jest.mock('./migrate'); jest.mock('../event'); @@ -12,10 +12,10 @@ test('createCollections should create collection containers when not exist', () const db = { getCollection: () => null, addCollection: () => ({ - findOne: () => {}, - insert: () => {}, + findOne: () => { }, + insert: () => { }, }), - }; + } as unknown as Loki; const spy = jest.spyOn(db, 'addCollection'); container.createCollections(db); expect(spy).toHaveBeenCalledWith('containers'); @@ -24,11 +24,11 @@ test('createCollections should create collection containers when not exist', () test('createCollections should not create collection containers when already exist', () => { const db = { getCollection: () => ({ - findOne: () => {}, - insert: () => {}, + findOne: () => { }, + insert: () => { }, }), addCollection: () => null, - }; + } as unknown as Loki; const spy = jest.spyOn(db, 'addCollection'); container.createCollections(db); expect(spy).not.toHaveBeenCalled(); @@ -36,13 +36,13 @@ test('createCollections should not create collection containers when already exi test('insertContainer should insert doc and emit an event', () => { const collection = { - findOne: () => {}, - insert: () => {}, + findOne: () => { }, + insert: () => { }, }; const db = { getCollection: () => collection, addCollection: () => null, - }; + } as unknown as Loki; const containerToSave = { id: 'container-123456789', name: 'test', @@ -80,7 +80,7 @@ test('insertContainer should insert doc and emit an event', () => { test('updateContainer should update doc and emit an event', () => { const collection = { - insert: () => {}, + insert: () => { }, chain: () => ({ find: () => ({ remove: () => ({}), @@ -90,7 +90,7 @@ test('updateContainer should update doc and emit an event', () => { const db = { getCollection: () => collection, addCollection: () => null, - }; + } as unknown as Loki; const containerToSave = { id: 'container-123456789', name: 'test', @@ -159,19 +159,19 @@ test('getContainers should return all containers sorted by name', () => { data: { ...containerExample, name: 'container3', - }, + } }, { data: { ...containerExample, name: 'container2', - }, + } }, { data: { ...containerExample, name: 'container1', - }, + } }, ]; const collection = { @@ -180,10 +180,10 @@ test('getContainers should return all containers sorted by name', () => { const db = { getCollection: () => collection, addCollection: () => ({ - findOne: () => {}, - insert: () => {}, + findOne: () => { }, + insert: () => { }, }), - }; + } as unknown as Loki; container.createCollections(db); const results = container.getContainers(); expect(results[0].name).toEqual('container1'); @@ -219,17 +219,17 @@ test('getContainer should return 1 container by id', () => { result: { tag: 'version', }, - }, + } }; const collection = { findOne: () => containerExample, }; const db = { getCollection: () => collection, - }; + } as unknown as Loki; container.createCollections(db); const result = container.getContainer('132456789'); - expect(result.name).toEqual(containerExample.data.name); + expect(result!.name).toEqual(containerExample.data.name); }); test('getContainer should return undefined when not found', () => { @@ -238,7 +238,7 @@ test('getContainer should return undefined when not found', () => { }; const db = { getCollection: () => collection, - }; + } as unknown as Loki; container.createCollections(db); const result = container.getContainer('123456789'); expect(result).toEqual(undefined); @@ -272,7 +272,7 @@ test('deleteContainer should delete doc and emit an event', () => { result: { tag: 'version', }, - }, + } }; const collection = { findOne: () => containerExample, @@ -285,9 +285,9 @@ test('deleteContainer should delete doc and emit an event', () => { const db = { getCollection: () => collection, addCollection: () => null, - }; + } as unknown as Loki; const spyEvent = jest.spyOn(event, 'emitContainerRemoved'); container.createCollections(db); - container.deleteContainer(containerExample); + container.deleteContainer(containerExample.data.id); expect(spyEvent).toHaveBeenCalled(); }); diff --git a/app/store/container.js b/app/store/container.ts similarity index 60% rename from app/store/container.js rename to app/store/container.ts index 37cb539d..ebf1ecf4 100644 --- a/app/store/container.js +++ b/app/store/container.ts @@ -1,26 +1,23 @@ /** * Container store. */ -const { byString, byValues } = require('sort-es'); -const log = require('../log').child({ component: 'store' }); -const { validate: validateContainer } = require('../model/container'); -const { - emitContainerAdded, - emitContainerUpdated, - emitContainerRemoved, -} = require('../event'); +import { byString, byValues } from 'sort-es'; +import logger from '../log'; +import { Container, validate as validateContainer } from '../model/container'; +import * as events from '../event'; -let containers; +const log = logger.child({ component: 'store' }); +let containers: Collection>; /** * Create container collections. * @param db */ -function createCollections(db) { +export function createCollections(db: Loki) { containers = db.getCollection('containers'); if (containers === null) { log.info('Create Collection containers'); - containers = db.addCollection('containers'); + containers = db.addCollection>('containers'); } } @@ -28,12 +25,12 @@ function createCollections(db) { * Insert new Container. * @param container */ -function insertContainer(container) { +export function insertContainer(container: Container) { const containerToSave = validateContainer(container); containers.insert({ - data: containerToSave, + data: containerToSave }); - emitContainerAdded(containerToSave); + events.emitContainerAdded(containerToSave); return containerToSave; } @@ -41,7 +38,7 @@ function insertContainer(container) { * Update existing container. * @param container */ -function updateContainer(container) { +export function updateContainer(container: Container) { const containerToReturn = validateContainer(container); // Remove existing container @@ -49,24 +46,21 @@ function updateContainer(container) { .chain() .find({ 'data.id': container.id, - }) + } as any) .remove(); // Insert new one - containers.insert({ - data: containerToReturn, - }); - emitContainerUpdated(containerToReturn); + containers.insert({ data: containerToReturn }); + events.emitContainerUpdated(containerToReturn); return containerToReturn; } /** * Get all (filtered) containers. * @param query - * @returns {*} */ -function getContainers(query = {}) { - const filter = {}; +export function getContainers(query: Query = {}) { + const filter: Query = {}; Object.keys(query).forEach((key) => { filter[`data.${key}`] = query[key]; }); @@ -88,12 +82,11 @@ function getContainers(query = {}) { /** * Get container by id. * @param id - * @returns {null|Image} */ -function getContainer(id) { +export function getContainer(id: string) { const container = containers.findOne({ 'data.id': id, - }); + } as any); if (container !== null) { return validateContainer(container.data); @@ -105,24 +98,23 @@ function getContainer(id) { * Delete container by id. * @param id */ -function deleteContainer(id) { +export function deleteContainer(id: string) { const container = getContainer(id); if (container) { containers .chain() .find({ 'data.id': id, - }) + } as any) .remove(); - emitContainerRemoved(container); + events.emitContainerRemoved(container); } } -module.exports = { - createCollections, - insertContainer, - updateContainer, - getContainers, - getContainer, - deleteContainer, -}; +export interface Query { + [key: string]: undefined | string | Query | (string | Query)[]; +} + +export interface LokiData { + data: T; +} \ No newline at end of file diff --git a/app/store/index.js b/app/store/index.ts similarity index 68% rename from app/store/index.js rename to app/store/index.ts index 96079b40..4fb75b4d 100644 --- a/app/store/index.js +++ b/app/store/index.ts @@ -1,11 +1,12 @@ -const joi = require('joi'); -const Loki = require('lokijs'); -const fs = require('fs'); -const log = require('../log').child({ component: 'store' }); -const { getStoreConfiguration } = require('../configuration'); +import joi from 'joi'; +import Loki from 'lokijs'; +import fs from 'fs'; +import logger from '../log'; +const log = logger.child({ component: 'store' }); +import { getStoreConfiguration } from '../configuration'; -const app = require('./app'); -const container = require('./container'); +import * as app from './app'; +import * as container from './container'; // Store Configuration Schema const configurationSchema = joi.object().keys({ @@ -25,6 +26,7 @@ const configuration = configurationToValidate.value; // Loki DB const db = new Loki(`${configuration.path}/${configuration.file}`, { autosave: true, + serializationMethod: 'pretty' }); function createCollections() { @@ -37,9 +39,8 @@ function createCollections() { * @param err * @param resolve * @param reject - * @returns {Promise} */ -async function loadDb(err, resolve, reject) { +async function loadDb(err: any, resolve: (value: void) => void, reject: (reason?: any) => void) { if (err) { reject(err); } else { @@ -51,28 +52,22 @@ async function loadDb(err, resolve, reject) { /** * Init DB. - * @returns {Promise} */ -async function init() { +export async function init() { log.info(`Load store from (${configuration.path}/${configuration.file})`); if (!fs.existsSync(configuration.path)) { log.info(`Create folder ${configuration.path}`); fs.mkdirSync(configuration.path); } - return new Promise((resolve, reject) => { + + return new Promise((resolve, reject) => { db.loadDatabase({}, (err) => loadDb(err, resolve, reject)); }); } /** * Get configuration. - * @returns {*} */ -function getConfiguration() { +export function getConfiguration() { return configuration; -} - -module.exports = { - init, - getConfiguration, -}; +} \ No newline at end of file diff --git a/app/store/migrate.test.js b/app/store/migrate.test.ts similarity index 50% rename from app/store/migrate.test.js rename to app/store/migrate.test.ts index 6bdedae3..2133fec1 100644 --- a/app/store/migrate.test.js +++ b/app/store/migrate.test.ts @@ -1,23 +1,24 @@ -const container = require('./container'); -jest.mock('./container'); +jest.mock('./container', () => ({ + ...jest.requireActual('./container'), + getContainers: jest.fn((args?: container.Query) => [ + { + name: 'container1', + }, + { + name: 'container2', + }, + ]), + deleteContainer: jest.fn(), +})); +import * as container from './container'; +import * as migrate from './migrate'; -container.getContainers = () => [ - { - name: 'container1', - }, - { - name: 'container2', - }, -]; - -const migrate = require('./migrate'); - -beforeEach(() => { - jest.resetAllMocks(); +afterEach(() => { + jest.clearAllMocks(); }); -test('migrate should delete all containers when from is lower than 8 and to is grater than 8', () => { +test('migrate should delete all containers when from is lower than 8 and to is greater than 8', () => { const spy = jest.spyOn(container, 'deleteContainer'); migrate.migrate('7.0.0', '8.0.0'); expect(spy).toHaveBeenCalledTimes(2); diff --git a/app/store/migrate.js b/app/store/migrate.ts similarity index 70% rename from app/store/migrate.js rename to app/store/migrate.ts index 8e485546..65cf38b8 100644 --- a/app/store/migrate.js +++ b/app/store/migrate.ts @@ -1,6 +1,7 @@ -const log = require('../log').child({ component: 'store' }); -const { getContainers, deleteContainer } = require('./container'); +import logger from '../log'; +import { getContainers, deleteContainer } from './container'; +const log = logger.child({ component: 'store' }); /** * Delete all containers from state. */ @@ -14,13 +15,11 @@ function deleteAllContainersFromState() { * @param from version * @param to version */ -function migrate(from, to) { +export function migrate(from: string | undefined, to: string) { log.info(`Migrate data from version ${from} to version ${to}`); if (from && !from.startsWith('8') && to && to.startsWith('8')) { deleteAllContainersFromState(); } } -module.exports = { - migrate, -}; + diff --git a/app/tag/index.test.js b/app/tag/index.test.ts similarity index 100% rename from app/tag/index.test.js rename to app/tag/index.test.ts diff --git a/app/tag/index.js b/app/tag/index.ts similarity index 86% rename from app/tag/index.js rename to app/tag/index.ts index 2b7add8f..9d8b0d66 100644 --- a/app/tag/index.js +++ b/app/tag/index.ts @@ -1,15 +1,15 @@ /** * Semver utils. */ -const semver = require('semver'); -const log = require('../log'); +import semver, { SemVer } from 'semver'; +import log from '../log'; /** * Parse a string to a semver (return null is it cannot be parsed as a valid semver). * @param rawVersion * @returns {*|SemVer} */ -function parse(rawVersion) { +export function parse(rawVersion: string): SemVer | null { const rawVersionCleaned = semver.clean(rawVersion, { loose: true }); const rawVersionSemver = semver.parse( rawVersionCleaned !== null ? rawVersionCleaned : rawVersion, @@ -28,7 +28,7 @@ function parse(rawVersion) { * @param version1 * @param version2 */ -function isGreater(version1, version2) { +export function isGreater(version1: string, version2: string) { const version1Semver = parse(version1); const version2Semver = parse(version2); @@ -45,7 +45,7 @@ function isGreater(version1, version2) { * @param version2 * @returns {*|string|null} */ -function diff(version1, version2) { +export function diff(version1: string, version2: string): semver.ReleaseType | null { const version1Semver = parse(version1); const version2Semver = parse(version2); @@ -62,7 +62,7 @@ function diff(version1, version2) { * @param originalTag * @return {*} */ -function transform(transformFormula, originalTag) { +export function transform(transformFormula: string | undefined, originalTag: string): string { // No formula ? return original tag value if (!transformFormula || transformFormula === '') { return originalTag; @@ -70,8 +70,8 @@ function transform(transformFormula, originalTag) { try { const transformFormulaSplit = transformFormula.split(/\s*=>\s*/); const transformRegex = new RegExp(transformFormulaSplit[0]); - const placeholders = transformFormulaSplit[1].match(/\$\d+/g); - const originalTagMatches = originalTag.match(transformRegex); + const placeholders = transformFormulaSplit[1].match(/\$\d+/g)!; + const originalTagMatches = originalTag.match(transformRegex)!; let transformedTag = transformFormulaSplit[1]; placeholders.forEach((placeholder) => { @@ -93,11 +93,4 @@ function transform(transformFormula, originalTag) { log.debug(e); return originalTag; } -} - -module.exports = { - parse, - isGreater, - diff, - transform, -}; +} \ No newline at end of file diff --git a/app/triggers/providers/Trigger.test.js b/app/triggers/providers/Trigger.test.ts similarity index 76% rename from app/triggers/providers/Trigger.test.js rename to app/triggers/providers/Trigger.test.ts index 9d763b68..33747157 100644 --- a/app/triggers/providers/Trigger.test.js +++ b/app/triggers/providers/Trigger.test.ts @@ -1,19 +1,22 @@ -const { ValidationError } = require('joi'); -const event = require('../../event'); -const log = require('../../log'); -const Trigger = require('./Trigger'); +import { ValidationError } from 'joi'; +import { Trigger, TriggerConfiguration } from './Trigger'; jest.mock('../../log'); jest.mock('../../event'); + +import log from '../../log'; +import * as event from '../../event'; +import { Container } from '../../model/container'; + jest.mock('../../prometheus/trigger', () => ({ getTriggerCounter: () => ({ inc: () => ({}), }), })); -let trigger; +let trigger: Trigger; -const configurationValid = { +const configurationValid: TriggerConfiguration = { threshold: 'all', once: true, mode: 'simple', @@ -43,7 +46,7 @@ test('validateConfiguration should return validated configuration when valid', ( test('validateConfiguration should throw error when invalid', () => { const configuration = { url: 'git://xxx.com', - }; + } as unknown as TriggerConfiguration; expect(() => { trigger.validateConfiguration(configuration); }).toThrowError(ValidationError); @@ -62,7 +65,14 @@ test('init should register to container reports when batch mode enabled', async expect(spy).toHaveBeenCalled(); }); -const handleContainerReportTestCases = [ +type ReportTestCase = { + shouldTrigger: boolean, + threshold: 'all' | 'minor' | 'patch', once: boolean, + changed: boolean, updateAvailable: boolean, + semverDiff: 'major' | 'minor' | 'patch', +} + +const handleContainerReportTestCases: ReportTestCase[] = [ { shouldTrigger: true, threshold: 'all', @@ -112,7 +122,7 @@ test.each(handleContainerReportTestCases)( threshold: item.threshold, once: item.once, mode: 'simple', - }; + } as TriggerConfiguration; await trigger.init(); const spy = jest.spyOn(trigger, 'trigger'); @@ -125,7 +135,7 @@ test.each(handleContainerReportTestCases)( kind: 'tag', semverDiff: item.semverDiff, }, - }, + } as Container, }); if (item.shouldTrigger) { expect(spy).toHaveBeenCalledWith({ @@ -146,7 +156,7 @@ test('handleContainerReport should warn when trigger method of the trigger fails trigger.configuration = { threshold: 'all', mode: 'simple', - }; + } as TriggerConfiguration; trigger.trigger = () => { throw new Error('Fail!!!'); }; @@ -157,12 +167,12 @@ test('handleContainerReport should warn when trigger method of the trigger fails container: { name: 'container1', updateAvailable: true, - }, + } as Container, }); expect(spyLog).toHaveBeenCalledWith('Error (Fail!!!)'); }); -const handleContainerReportsTestCases = [ +const handleContainerReportsTestCases: ReportTestCase[] = [ { shouldTrigger: true, threshold: 'all', @@ -212,7 +222,7 @@ test.each(handleContainerReportsTestCases)( threshold: item.threshold, once: item.once, mode: 'simple', - }; + } as TriggerConfiguration; await trigger.init(); const spy = jest.spyOn(trigger, 'triggerBatch'); @@ -226,7 +236,7 @@ test.each(handleContainerReportsTestCases)( kind: 'tag', semverDiff: item.semverDiff, }, - }, + } as Container, }, ]); if (item.shouldTrigger) { @@ -246,99 +256,104 @@ test.each(handleContainerReportsTestCases)( }, ); -const isThresholdReachedTestCases = [ - { - result: true, - threshold: 'all', - change: undefined, - kind: 'tag', - }, - { - result: true, - threshold: 'major', - change: 'major', - kind: 'tag', - }, - { - result: true, - threshold: 'major', - change: 'minor', - kind: 'tag', - }, - { - result: true, - threshold: 'major', - change: 'patch', - kind: 'tag', - }, - { - result: false, - threshold: 'minor', - change: 'major', - kind: 'tag', - }, - { - result: true, - threshold: 'minor', - change: 'minor', - kind: 'tag', - }, - { - result: true, - threshold: 'minor', - change: 'patch', - kind: 'tag', - }, - { - result: false, - threshold: 'patch', - change: 'major', - kind: 'tag', - }, - { - result: false, - threshold: 'patch', - change: 'minor', - kind: 'tag', - }, - { - result: true, - threshold: 'patch', - change: 'patch', - kind: 'tag', - }, - { - result: true, - threshold: 'all', - change: 'unknown', - kind: 'digest', - }, - { - result: true, - threshold: 'major', - change: 'unknown', - kind: 'digest', - }, - { - result: true, - threshold: 'minor', - change: 'unknown', - kind: 'digest', - }, - { - result: true, - threshold: 'patch', - change: 'unknown', - kind: 'digest', - }, -]; +const isThresholdReachedTestCases: { + result: boolean; + threshold: 'all' | 'major' | 'minor' | 'patch'; + change: 'major' | 'minor' | 'patch' | 'unknown' | undefined; + kind: 'tag' | 'digest'; +}[] = [ + { + result: true, + threshold: 'all', + change: undefined, + kind: 'tag', + }, + { + result: true, + threshold: 'major', + change: 'major', + kind: 'tag', + }, + { + result: true, + threshold: 'major', + change: 'minor', + kind: 'tag', + }, + { + result: true, + threshold: 'major', + change: 'patch', + kind: 'tag', + }, + { + result: false, + threshold: 'minor', + change: 'major', + kind: 'tag', + }, + { + result: true, + threshold: 'minor', + change: 'minor', + kind: 'tag', + }, + { + result: true, + threshold: 'minor', + change: 'patch', + kind: 'tag', + }, + { + result: false, + threshold: 'patch', + change: 'major', + kind: 'tag', + }, + { + result: false, + threshold: 'patch', + change: 'minor', + kind: 'tag', + }, + { + result: true, + threshold: 'patch', + change: 'patch', + kind: 'tag', + }, + { + result: true, + threshold: 'all', + change: 'unknown', + kind: 'digest', + }, + { + result: true, + threshold: 'major', + change: 'unknown', + kind: 'digest', + }, + { + result: true, + threshold: 'minor', + change: 'unknown', + kind: 'digest', + }, + { + result: true, + threshold: 'patch', + change: 'unknown', + kind: 'digest', + }, + ]; test.each(isThresholdReachedTestCases)( 'isThresholdReached should return $result when threshold is $threshold and change is $change', (item) => { trigger.configuration = { threshold: item.threshold, - }; + } as TriggerConfiguration; expect( Trigger.isThresholdReached( { @@ -346,7 +361,7 @@ test.each(isThresholdReachedTestCases)( kind: item.kind, semverDiff: item.change, }, - }, + } as Container, trigger.configuration.threshold, ), ).toEqual(item.result); @@ -356,12 +371,12 @@ test.each(isThresholdReachedTestCases)( test('isThresholdReached should return true when there is no semverDiff regardless of the threshold', async () => { trigger.configuration = { threshold: 'all', - }; + } as TriggerConfiguration; expect( Trigger.isThresholdReached( { updateKind: { kind: 'digest' }, - }, + } as Container, trigger.configuration.threshold, ), ).toBeTruthy(); @@ -374,7 +389,7 @@ test('renderSimpleTitle should replace placeholders when called', async () => { updateKind: { kind: 'tag', }, - }), + } as Container), ).toEqual('New tag found for container container-name'); }); @@ -390,7 +405,7 @@ test('renderSimpleBody should replace placeholders when called', async () => { result: { link: 'http://test', }, - }), + } as Container), ).toEqual( 'Container container-name running with tag 1.0.0 can be updated to tag 2.0.0\nhttp://test', ); @@ -403,7 +418,7 @@ test('renderSimpleBody should replace placeholders when template is a customized trigger.renderSimpleBody({ name: 'container-name', watcher: 'DUMMY', - }), + } as Container), ).toEqual( 'Watcher DUMMY reports container container-name available update', ); @@ -422,7 +437,7 @@ test('renderSimpleBody should evaluate js functions when template is a customize remoteValue: 'sha256:6cdd479147e4d2f1f853c7205ead7e2a0b0ccbad6e3ff0986e01936cbd179c17', }, - }), + } as Container), ).toEqual( 'Container container-name update from sha256:9a82d577 to sha256:6cdd4791', ); @@ -437,7 +452,7 @@ test('renderBatchTitle should replace placeholders when called', async () => { kind: 'tag', }, }, - ]), + ] as Container[]), ).toEqual('1 updates available'); }); @@ -455,7 +470,7 @@ test('renderBatchBody should replace placeholders when called', async () => { link: 'http://test', }, }, - ]), + ] as Container[]), ).toEqual( '- Container container-name running with tag 1.0.0 can be updated to tag 2.0.0\nhttp://test\n', ); diff --git a/app/triggers/providers/Trigger.js b/app/triggers/providers/Trigger.ts similarity index 84% rename from app/triggers/providers/Trigger.js rename to app/triggers/providers/Trigger.ts index ccef97dc..7dbcb95e 100644 --- a/app/triggers/providers/Trigger.js +++ b/app/triggers/providers/Trigger.ts @@ -1,7 +1,8 @@ -const Component = require('../../registry/Component'); -const event = require('../../event'); -const { getTriggerCounter } = require('../../prometheus/trigger'); -const { fullName } = require('../../model/container'); +import { BaseConfig, Component } from '../../registry/Component'; +import * as event from '../../event'; +import { getTriggerCounter } from '../../prometheus/trigger'; +import { Container, fullName } from '../../model/container'; +import { ObjectSchema } from 'joi'; /** * Render body or title simple template. @@ -9,7 +10,7 @@ const { fullName } = require('../../model/container'); * @param container * @returns {*} */ -function renderSimple(template, container) { +function renderSimple(template: string, container: Container): string { // Set deprecated vars for backward compatibility const id = container.id; const name = container.name; @@ -35,23 +36,32 @@ function renderSimple(template, container) { return eval('`' + template + '`'); } -function renderBatch(template, containers) { +function renderBatch(template: string, containers: Container[]) { // Set deprecated vars for backward compatibility const count = containers ? containers.length : 0; return eval('`' + template + '`'); } +export interface TriggerConfiguration extends BaseConfig { + auto: boolean; + threshold: 'all' | 'major' | 'minor' | 'patch'; + mode: 'simple' | 'batch'; + once: boolean; + simpletitle: string; + simplebody: string; + batchtitle: string; +} + /** * Trigger base component. */ -class Trigger extends Component { +export class Trigger extends Component { /** * Return true if update reaches trigger threshold. * @param containerResult * @param threshold - * @returns {boolean} */ - static isThresholdReached(containerResult, threshold) { + static isThresholdReached(containerResult: Container, threshold: string) { let thresholdPassing = true; if ( threshold.toLowerCase() !== 'all' && @@ -80,12 +90,14 @@ class Trigger extends Component { /** * Parse $name:$threshold string. * @param {*} includeOrExcludeTriggerString - * @returns */ - static parseIncludeOrIncludeTriggerString(includeOrExcludeTriggerString) { + static parseIncludeOrIncludeTriggerString(includeOrExcludeTriggerString: string) { const includeOrExcludeTriggerSplit = includeOrExcludeTriggerString.split(/\s*:\s*/); - const includeOrExcludeTrigger = { + const includeOrExcludeTrigger: { + id: string; + threshold: 'all' | 'major' | 'minor' | 'patch'; + } = { id: includeOrExcludeTriggerSplit[0], threshold: 'all', }; @@ -110,9 +122,8 @@ class Trigger extends Component { /** * Handle container report (simple mode). * @param containerReport - * @returns {Promise} */ - async handleContainerReport(containerReport) { + async handleContainerReport(containerReport: event.ContainerReport) { // Filter on changed containers with update available and passing trigger threshold if ( (containerReport.changed || !this.configuration.once) && @@ -138,7 +149,7 @@ class Trigger extends Component { await this.trigger(containerReport.container); } status = 'success'; - } catch (e) { + } catch (e: any) { logContainer.warn(`Error (${e.message})`); logContainer.debug(e); } finally { @@ -154,9 +165,8 @@ class Trigger extends Component { /** * Handle container reports (batch mode). * @param containerReports - * @returns {Promise} */ - async handleContainerReports(containerReports) { + async handleContainerReports(containerReports: event.ContainerReport[]) { // Filter on containers with update available and passing trigger threshold try { const containerReportsFiltered = containerReports @@ -184,13 +194,13 @@ class Trigger extends Component { this.log.debug('Run batch'); await this.triggerBatch(containersFiltered); } - } catch (e) { + } catch (e: any) { this.log.warn(`Error (${e.message})`); this.log.debug(e); } } - isTriggerIncludedOrExcluded(containerResult, trigger) { + isTriggerIncludedOrExcluded(containerResult: Container, trigger: string) { const triggers = trigger .split(/\s*,\s*/) .map((triggerToMatch) => @@ -209,7 +219,7 @@ class Trigger extends Component { ); } - isTriggerIncluded(containerResult, triggerInclude) { + isTriggerIncluded(containerResult: Container, triggerInclude: string | undefined) { if (!triggerInclude) { return true; } @@ -219,7 +229,7 @@ class Trigger extends Component { ); } - isTriggerExcluded(containerResult, triggerExclude) { + isTriggerExcluded(containerResult: Container, triggerExclude: string | undefined) { if (!triggerExclude) { return false; } @@ -232,9 +242,8 @@ class Trigger extends Component { /** * Return true if must trigger on this container. * @param containerResult - * @returns {boolean} */ - mustTrigger(containerResult) { + mustTrigger(containerResult: Container) { const { triggerInclude, triggerExclude } = containerResult; return ( this.isTriggerIncluded(containerResult, triggerInclude) && @@ -267,11 +276,10 @@ class Trigger extends Component { /** * Override method to merge with common Trigger options (threshold...). * @param configuration - * @returns {*} */ - validateConfiguration(configuration) { + validateConfiguration(configuration: TC): TC { const schema = this.getConfigurationSchema(); - const schemaWithDefaultOptions = schema.append({ + const schemaWithDefaultOptions = (schema as ObjectSchema).append({ auto: this.joi.bool().default(true), threshold: this.joi .string() @@ -303,78 +311,69 @@ class Trigger extends Component { if (schemaValidated.error) { throw schemaValidated.error; } - return schemaValidated.value ? schemaValidated.value : {}; + return (schemaValidated.value ? schemaValidated.value : {}) as TC; } /** * Init Trigger. Can be overridden in trigger implementation class. */ - initTrigger() { + async initTrigger() { // do nothing by default } /** * Trigger method. Must be overridden in trigger implementation class. */ - trigger(containerWithResult) { + async trigger(_containerWithResult: Container): Promise { // do nothing by default this.log.warn( 'Cannot trigger container result; this trigger doe not implement "simple" mode', ); - return containerWithResult; } /** * Trigger batch method. Must be overridden in trigger implementation class. * @param containersWithResult - * @returns {*} */ - triggerBatch(containersWithResult) { + async triggerBatch(_containersWithResult: Container[]): Promise { // do nothing by default this.log.warn( 'Cannot trigger container results; this trigger doe not implement "batch" mode', ); - return containersWithResult; } /** * Render trigger title simple. * @param container - * @returns {*} */ - renderSimpleTitle(container) { + renderSimpleTitle(container: Container) { return renderSimple(this.configuration.simpletitle, container); } /** * Render trigger body simple. * @param container - * @returns {*} */ - renderSimpleBody(container) { + renderSimpleBody(container: Container) { return renderSimple(this.configuration.simplebody, container); } /** * Render trigger title batch. * @param containers - * @returns {*} */ - renderBatchTitle(containers) { + renderBatchTitle(containers: Container[]) { return renderBatch(this.configuration.batchtitle, containers); } /** * Render trigger body batch. * @param containers - * @returns {*} */ - renderBatchBody(containers) { + renderBatchBody(containers: Container[]) { return containers .map((container) => `- ${this.renderSimpleBody(container)}\n`) .join('\n'); } } - -module.exports = Trigger; diff --git a/app/triggers/providers/apprise/Apprise.test.js b/app/triggers/providers/apprise/Apprise.test.ts similarity index 90% rename from app/triggers/providers/apprise/Apprise.test.js rename to app/triggers/providers/apprise/Apprise.test.ts index 07bea537..8fdfab54 100644 --- a/app/triggers/providers/apprise/Apprise.test.js +++ b/app/triggers/providers/apprise/Apprise.test.ts @@ -1,12 +1,13 @@ -const { ValidationError } = require('joi'); -const rp = require('request-promise-native'); +import { ValidationError } from 'joi'; +import rp from 'request-promise-native'; jest.mock('request-promise-native'); -const Apprise = require('./Apprise'); +import { Apprise, AppriseConfig } from './Apprise'; +import { Container } from '../../../model/container'; const apprise = new Apprise(); -const configurationValid = { +const configurationValid: AppriseConfig = { url: 'http://xxx.com', urls: 'maito://user:pass@gmail.com', threshold: 'all', @@ -36,7 +37,7 @@ test('validateConfiguration should return validated configuration when valid', ( test('validateConfiguration should throw error when invalid', () => { const configuration = { url: 'git://xxx.com', - }; + } as AppriseConfig; expect(() => { apprise.validateConfiguration(configuration); }).toThrowError(ValidationError); @@ -71,7 +72,7 @@ test('trigger should send POST http request to notify endpoint', async () => { remoteValue: '2.0.0', semverDiff: 'major', }, - }; + } as Container; await apprise.trigger(container); expect(rp).toHaveBeenCalledWith({ body: { diff --git a/app/triggers/providers/apprise/Apprise.js b/app/triggers/providers/apprise/Apprise.ts similarity index 76% rename from app/triggers/providers/apprise/Apprise.js rename to app/triggers/providers/apprise/Apprise.ts index 4870c020..300c489f 100644 --- a/app/triggers/providers/apprise/Apprise.js +++ b/app/triggers/providers/apprise/Apprise.ts @@ -1,14 +1,20 @@ -const rp = require('request-promise-native'); +import rp from 'request-promise-native'; +import { Trigger, TriggerConfiguration } from '../Trigger'; +import { Container } from '../../../model/container'; -const Trigger = require('../Trigger'); +export interface AppriseConfig extends TriggerConfiguration { + url: string; + urls?: string; + config?: string; + tag?: string; +} /** * Apprise Trigger implementation */ -class Apprise extends Trigger { +export class Apprise extends Trigger { /** * Get the Trigger configuration schema. - * @returns {*} */ getConfigurationSchema() { return this.joi @@ -26,7 +32,6 @@ class Apprise extends Trigger { /** * Sanitize sensitive data - * @returns {*} */ maskConfiguration() { return { @@ -39,11 +44,17 @@ class Apprise extends Trigger { /** * Send an HTTP Request to Apprise. * @param container the container - * @returns {Promise} */ - async trigger(container) { + async trigger(container: Container) { let uri = `${this.configuration.url}/notify`; - const body = { + const body: { + title: string; + body: string; + format: string; + type: string; + urls?: string; + tag?: string; + } = { title: this.renderSimpleTitle(container), body: this.renderSimpleBody(container), format: 'text', @@ -73,9 +84,8 @@ class Apprise extends Trigger { /** * Send an HTTP Request to Apprise. * @param containers - * @returns {Promise<*>} */ - async triggerBatch(containers) { + async triggerBatch(containers: Container[]) { const options = { method: 'POST', uri: `${this.configuration.url}/notify`, @@ -92,4 +102,3 @@ class Apprise extends Trigger { } } -module.exports = Apprise; diff --git a/app/triggers/providers/command/Command.test.js b/app/triggers/providers/command/Command.test.ts similarity index 88% rename from app/triggers/providers/command/Command.test.js rename to app/triggers/providers/command/Command.test.ts index 4f1741f8..b67da5f0 100644 --- a/app/triggers/providers/command/Command.test.js +++ b/app/triggers/providers/command/Command.test.ts @@ -1,10 +1,10 @@ -const { ValidationError } = require('joi'); +import { ValidationError } from 'joi'; -const Command = require('./Command'); +import { Command, CommandConfig } from './Command'; const command = new Command(); -const configurationValid = { +const configurationValid: CommandConfig = { cmd: 'echo "hello"', timeout: 60000, shell: '/bin/sh', @@ -32,14 +32,14 @@ test('validateConfiguration should return validated configuration when valid', ( test('validateConfiguration should apply_default_configuration', () => { const validatedConfiguration = command.validateConfiguration({ cmd: configurationValid.cmd, - }); + } as CommandConfig); expect(validatedConfiguration).toStrictEqual(configurationValid); }); test('validateConfiguration should throw error when invalid', () => { const configuration = { command: 123456789, - }; + } as unknown as CommandConfig; expect(() => { command.validateConfiguration(configuration); }).toThrowError(ValidationError); diff --git a/app/triggers/providers/command/Command.js b/app/triggers/providers/command/Command.ts similarity index 68% rename from app/triggers/providers/command/Command.js rename to app/triggers/providers/command/Command.ts index bc54a82b..84ad819c 100644 --- a/app/triggers/providers/command/Command.js +++ b/app/triggers/providers/command/Command.ts @@ -1,15 +1,22 @@ -const util = require('node:util'); -const exec = util.promisify(require('node:child_process').exec); -const Trigger = require('../Trigger'); +import util from 'node:util'; +import { exec as _exec } from 'child_process'; +const exec = util.promisify(_exec); +import { Trigger, TriggerConfiguration } from '../Trigger'; +import { Container, flatten } from '../../../model/container'; +import { ExecOptions } from 'node:child_process'; + +export interface CommandConfig extends TriggerConfiguration { + cmd: string; // Command to execute + shell: string; // Shell to use for command execution + timeout: number; // Timeout for command execution +} -const { flatten } = require('../../../model/container'); /** * Command Trigger implementation */ -class Command extends Trigger { +export class Command extends Trigger { /** * Get the Trigger configuration schema. - * @returns {*} */ getConfigurationSchema() { return this.joi.object().keys({ @@ -23,9 +30,8 @@ class Command extends Trigger { * Run the command with new image version details. * * @param container the container - * @returns {Promise} */ - async trigger(container) { + async trigger(container: Container) { return this.runCommand({ container_json: JSON.stringify(container), ...flatten(container), @@ -35,9 +41,8 @@ class Command extends Trigger { /** * Run the command with new image version details. * @param containers - * @returns {Promise<*>} */ - async triggerBatch(containers) { + async triggerBatch(containers: Container[]) { return this.runCommand({ containers_json: JSON.stringify(containers), }); @@ -47,8 +52,8 @@ class Command extends Trigger { * Run the command. * @param {*} extraEnvVars */ - async runCommand(extraEnvVars) { - const commandOptions = { + async runCommand(extraEnvVars: Record) { + const commandOptions: ExecOptions = { env: { ...process.env, ...extraEnvVars, @@ -71,12 +76,10 @@ class Command extends Trigger { `Command ${this.configuration.cmd} \nstderr ${stderr}`, ); } - } catch (err) { + } catch (err: any) { this.log.warn( `Command ${this.configuration.cmd} \nexecution error (${err.message})`, ); } } } - -module.exports = Command; diff --git a/app/triggers/providers/discord/Discord.test.js b/app/triggers/providers/discord/Discord.test.ts similarity index 91% rename from app/triggers/providers/discord/Discord.test.js rename to app/triggers/providers/discord/Discord.test.ts index 91bfad0c..51e1f6c0 100644 --- a/app/triggers/providers/discord/Discord.test.js +++ b/app/triggers/providers/discord/Discord.test.ts @@ -1,12 +1,13 @@ -const { ValidationError } = require('joi'); -const rp = require('request-promise-native'); +import { ValidationError } from 'joi'; +import rp from 'request-promise-native'; jest.mock('request-promise-native'); -const Discord = require('./Discord'); +import { Discord, DiscordConfiguration } from './Discord'; +import { Container } from '../../../model/container'; const discord = new Discord(); -const configurationValid = { +const configurationValid: DiscordConfiguration = { url: 'https://discord.com/api/webhooks/1', botusername: 'Bot Name', cardcolor: 65280, @@ -15,7 +16,6 @@ const configurationValid = { threshold: 'all', mode: 'simple', once: true, - auto: true, simpletitle: 'New ${container.updateKind.kind} found for container ${container.name}', @@ -36,7 +36,7 @@ test('validateConfiguration should return validated configuration when valid', ( }); test('validateConfiguration should throw error when invalid', () => { - const configuration = {}; + const configuration = {} as DiscordConfiguration; expect(() => { discord.validateConfiguration(configuration); }).toThrowError(ValidationError); @@ -81,7 +81,7 @@ test('trigger should send POST http request to webhook endpoint', async () => { remoteValue: '2.0.0', semverDiff: 'major', }, - }; + } as Container; await discord.trigger(container); expect(rp).toHaveBeenCalledWith({ body: { diff --git a/app/triggers/providers/discord/Discord.js b/app/triggers/providers/discord/Discord.ts similarity index 79% rename from app/triggers/providers/discord/Discord.js rename to app/triggers/providers/discord/Discord.ts index 17c03046..cad6777b 100644 --- a/app/triggers/providers/discord/Discord.js +++ b/app/triggers/providers/discord/Discord.ts @@ -1,11 +1,18 @@ -const rp = require('request-promise-native'); +import rp from 'request-promise-native'; +import { Trigger, TriggerConfiguration } from '../Trigger'; +import { Container } from '../../../model/container'; -const Trigger = require('../Trigger'); +export interface DiscordConfiguration extends TriggerConfiguration { + url: string; + botusername: string; + cardcolor: number; + cardlabel: string; +} /** * Discord Trigger implementation */ -class Discord extends Trigger { +export class Discord extends Trigger { /** * Get the Trigger configuration schema. * @returns {*} @@ -26,7 +33,6 @@ class Discord extends Trigger { /** * Sanitize sensitive data - * @returns {*} */ maskConfiguration() { return { @@ -38,9 +44,8 @@ class Discord extends Trigger { /** * Send an HTTP Request to Discord. * @param container the container - * @returns {Promise} */ - async trigger(container) { + async trigger(container: Container) { return this.sendMessage( this.renderSimpleTitle(container), this.renderSimpleBody(container), @@ -50,9 +55,8 @@ class Discord extends Trigger { /** * Send an HTTP Request to Discord. * @param containers the list of the containers - * @returns {Promise} */ - async triggerBatch(containers) { + async triggerBatch(containers: Container[]) { return this.sendMessage( this.renderBatchTitle(containers), this.renderBatchBody(containers), @@ -63,9 +67,8 @@ class Discord extends Trigger { * Post a message to discord webhook. * @param title the message title * @param bodyText the text to post - * @returns {Promise<>} */ - async sendMessage(title, bodyText) { + async sendMessage(title: string, bodyText: string) { const uri = this.configuration.url; const body = { username: this.configuration.botusername, @@ -92,5 +95,3 @@ class Discord extends Trigger { return rp(options); } } - -module.exports = Discord; diff --git a/app/triggers/providers/docker/Docker.test.js b/app/triggers/providers/docker/Docker.test.ts similarity index 83% rename from app/triggers/providers/docker/Docker.test.js rename to app/triggers/providers/docker/Docker.test.ts index 80d712bc..1d5f25e4 100644 --- a/app/triggers/providers/docker/Docker.test.js +++ b/app/triggers/providers/docker/Docker.test.ts @@ -1,8 +1,11 @@ -const { ValidationError } = require('joi'); -const Docker = require('./Docker'); -const log = require('../../../log'); +import { ValidationError } from 'joi'; +import { Docker, DockerTriggerConfiguration } from './Docker'; +import log from '../../../log'; +import { Container } from '../../../model/container'; +import { Docker as DockerWatcher } from '../../../watchers/providers/docker/Docker'; +import Dockerode from 'dockerode'; -const configurationValid = { +const configurationValid: DockerTriggerConfiguration = { prune: false, dryrun: false, threshold: 'all', @@ -20,7 +23,7 @@ const docker = new Docker(); docker.configuration = configurationValid; docker.log = log; -jest.mock('../../../registry', () => ({ +jest.mock('../../../registry/states', () => ({ getState() { return { watcher: { @@ -28,7 +31,7 @@ jest.mock('../../../registry', () => ({ getId: () => 'docker.test', watch: () => Promise.resolve(), dockerApi: { - getContainer: (id) => { + getContainer: (id: string) => { if (id === '123456789') { return Promise.resolve({ inspect: () => @@ -58,7 +61,7 @@ jest.mock('../../../registry', () => ({ new Error('Error when getting container'), ); }, - createContainer: (container) => { + createContainer: (container: Container) => { if (container.name === 'container-name') { return Promise.resolve({ start: () => Promise.resolve(), @@ -68,7 +71,7 @@ jest.mock('../../../registry', () => ({ new Error('Error when creating container'), ); }, - pull: (image) => { + pull: (image: string) => { if ( image === 'test/test:1.2.3' || image === 'my-registry/test/test:4.5.6' @@ -79,7 +82,7 @@ jest.mock('../../../registry', () => ({ new Error('Error when pulling image'), ); }, - getImage: (image) => + getImage: (image: string) => Promise.resolve({ remove: () => { if (image === 'test/test:1.2.3') { @@ -91,7 +94,7 @@ jest.mock('../../../registry', () => ({ }, }), modem: { - followProgress: (pullStream, res) => res(), + followProgress: (pullStream: any, res: () => {}) => res(), }, }, }, @@ -99,7 +102,7 @@ jest.mock('../../../registry', () => ({ registry: { hub: { getAuthPull: () => undefined, - getImageFullName: (image, tagOrDigest) => + getImageFullName: (image: any, tagOrDigest: string) => `${image.registry.url}/${image.name}:${tagOrDigest}`, }, }, @@ -120,7 +123,7 @@ test('validateConfiguration should return validated configuration when valid', ( test('validateConfiguration should throw error when invalid', () => { const configuration = { url: 'git://xxx.com', - }; + } as unknown as DockerTriggerConfiguration; expect(() => { docker.validateConfiguration(configuration); }).toThrowError(ValidationError); @@ -131,7 +134,7 @@ test('getWatcher should return watcher responsible for a container', () => { docker .getWatcher({ watcher: 'test', - }) + } as Container) .getId(), ).toEqual('docker.test'); }); @@ -139,10 +142,10 @@ test('getWatcher should return watcher responsible for a container', () => { test('getCurrentContainer should return container from dockerApi', async () => { await expect( docker.getCurrentContainer( - docker.getWatcher({ watcher: 'test' }).dockerApi, + (docker.getWatcher({ watcher: 'test' } as Container) as DockerWatcher).dockerApi, { id: '123456789', - }, + } as Container, ), ).resolves.not.toBeUndefined(); }); @@ -150,10 +153,10 @@ test('getCurrentContainer should return container from dockerApi', async () => { test('getCurrentContainer should throw error when error occurs', async () => { await expect( docker.getCurrentContainer( - docker.getWatcher({ watcher: 'test' }).dockerApi, + (docker.getWatcher({ watcher: 'test' } as Container) as DockerWatcher).dockerApi, { id: 'unknown', - }, + } as Container, ), ).rejects.toThrowError('Error when getting container'); }); @@ -163,7 +166,7 @@ test('inspectContainer should return container details from dockerApi', async () docker.inspectContainer( { inspect: () => Promise.resolve({}), - }, + } as Dockerode.Container, log, ), ).resolves.toEqual({}); @@ -174,7 +177,7 @@ test('inspectContainer should throw error when error occurs', async () => { docker.inspectContainer( { inspect: () => Promise.reject(new Error('No container')), - }, + } as Dockerode.Container, log, ), ).rejects.toThrowError('No container'); @@ -185,7 +188,7 @@ test('stopContainer should stop container from dockerApi', async () => { docker.stopContainer( { stop: () => Promise.resolve(), - }, + } as Dockerode.Container, 'name', 'id', log, @@ -198,7 +201,7 @@ test('stopContainer should throw error when error occurs', async () => { docker.stopContainer( { stop: () => Promise.reject(new Error('No container')), - }, + } as Dockerode.Container, 'name', 'id', log, @@ -211,7 +214,7 @@ test('removeContainer should stop container from dockerApi', async () => { docker.removeContainer( { remove: () => Promise.resolve(), - }, + } as Dockerode.Container, 'name', 'id', log, @@ -224,7 +227,7 @@ test('removeContainer should throw error when error occurs', async () => { docker.removeContainer( { remove: () => Promise.reject(new Error('No container')), - }, + } as Dockerode.Container, 'name', 'id', log, @@ -237,7 +240,7 @@ test('startContainer should stop container from dockerApi', async () => { docker.startContainer( { start: () => Promise.resolve(), - }, + } as Dockerode.Container, 'name', log, ), @@ -249,7 +252,7 @@ test('startContainer should throw error when error occurs', async () => { docker.startContainer( { start: () => Promise.reject(new Error('No container')), - }, + } as Dockerode.Container, 'name', log, ), @@ -259,10 +262,10 @@ test('startContainer should throw error when error occurs', async () => { test('createContainer should stop container from dockerApi', async () => { await expect( docker.createContainer( - docker.getWatcher({ watcher: 'test' }).dockerApi, + (docker.getWatcher({ watcher: 'test' } as Container) as DockerWatcher).dockerApi, { name: 'container-name', - }, + } as Dockerode.ContainerCreateOptions, 'name', log, ), @@ -272,7 +275,7 @@ test('createContainer should stop container from dockerApi', async () => { test('createContainer should throw error when error occurs', async () => { await expect( docker.createContainer( - docker.getWatcher({ watcher: 'test' }).dockerApi, + (docker.getWatcher({ watcher: 'test' } as Container) as DockerWatcher).dockerApi, { name: 'ko', }, @@ -285,7 +288,7 @@ test('createContainer should throw error when error occurs', async () => { test('pull should pull image from dockerApi', async () => { await expect( docker.pullImage( - docker.getWatcher({ watcher: 'test' }).dockerApi, + (docker.getWatcher({ watcher: 'test' } as Container) as DockerWatcher).dockerApi, undefined, 'test/test:1.2.3', log, @@ -296,7 +299,7 @@ test('pull should pull image from dockerApi', async () => { test('pull should throw error when error occurs', async () => { await expect( docker.pullImage( - docker.getWatcher({ watcher: 'test' }).dockerApi, + (docker.getWatcher({ watcher: 'test' } as Container) as DockerWatcher).dockerApi, undefined, 'test/test:unknown', log, @@ -307,7 +310,7 @@ test('pull should throw error when error occurs', async () => { test('removeImage should pull image from dockerApi', async () => { await expect( docker.removeImage( - docker.getWatcher({ watcher: 'test' }).dockerApi, + (docker.getWatcher({ watcher: 'test' } as Container) as DockerWatcher).dockerApi, 'test/test:1.2.3', log, ), @@ -317,7 +320,7 @@ test('removeImage should pull image from dockerApi', async () => { test('removeImage should throw error when error occurs', async () => { await expect( docker.removeImage( - docker.getWatcher({ watcher: 'test' }).dockerApi, + (docker.getWatcher({ watcher: 'test' } as Container) as DockerWatcher).dockerApi, 'test/test:unknown', log, ), @@ -344,7 +347,7 @@ test('clone should clone an existing container spec', () => { }, }, }, - }, + } as unknown as Dockerode.ContainerInspectInfo, 'test/test:2.0.0', ); expect(clone).toEqual({ @@ -371,7 +374,7 @@ test('trigger should not throw when all is ok', async () => { docker.trigger({ watcher: 'test', id: '123456789', - Name: '/container-name', + name: '/container-name', image: { name: 'test/test', registry: { @@ -382,6 +385,6 @@ test('trigger should not throw when all is ok', async () => { updateKind: { remoteValue: '4.5.6', }, - }), + } as Container), ).resolves.toBeUndefined(); }); diff --git a/app/triggers/providers/docker/Docker.js b/app/triggers/providers/docker/Docker.ts similarity index 81% rename from app/triggers/providers/docker/Docker.js rename to app/triggers/providers/docker/Docker.ts index a8c82075..cdcbe78b 100644 --- a/app/triggers/providers/docker/Docker.js +++ b/app/triggers/providers/docker/Docker.ts @@ -1,12 +1,21 @@ -const parse = require('parse-docker-image-name'); -const Trigger = require('../Trigger'); -const { getState } = require('../../../registry'); -const { fullName } = require('../../../model/container'); +import parse from 'parse-docker-image-name'; +import { Docker as DockerWatcher } from '../../../watchers/providers/docker/Docker'; +import { Trigger, TriggerConfiguration } from '../Trigger'; +import { getState } from '../../../registry/states'; +import { Container, ContainerImage, fullName } from '../../../model/container'; +import Dockerode, { ContainerCreateOptions, ContainerInspectInfo } from 'dockerode'; +import { Auth, Registry } from '../../../registries/Registry'; +import Logger from 'bunyan'; + +export interface DockerTriggerConfiguration extends TriggerConfiguration { + prune: boolean; + dryrun: boolean; +} /** * Replace a Docker container with an updated one. */ -class Docker extends Trigger { +export class Docker extends Trigger { /** * Get the Trigger configuration schema. * @returns {*} @@ -21,10 +30,8 @@ class Docker extends Trigger { /** * Get watcher responsible for the container. * @param container - * @returns {*} */ - - getWatcher(container) { + getWatcher(container: Container) { return getState().watcher[`docker.${container.watcher}`]; } @@ -32,9 +39,8 @@ class Docker extends Trigger { * Get current container. * @param dockerApi * @param container - * @returns {Promise<*>} */ - async getCurrentContainer(dockerApi, container) { + async getCurrentContainer(dockerApi: Dockerode, container: Container) { this.log.debug(`Get container ${container.id}`); try { return await dockerApi.getContainer(container.id); @@ -47,9 +53,8 @@ class Docker extends Trigger { /** * Inspect container. * @param container - * @returns {Promise<*>} */ - async inspectContainer(container, logContainer) { + async inspectContainer(container: Dockerode.Container, logContainer: Logger) { this.log.debug(`Inspect container ${container.id}`); try { return await container.inspect(); @@ -67,9 +72,8 @@ class Docker extends Trigger { * @param registry * @param container * @param logContainer - * @returns {Promise} */ - async pruneImages(dockerApi, registry, container, logContainer) { + async pruneImages(dockerApi: Dockerode, registry: Registry, container: Container, logContainer: Logger) { logContainer.info('Pruning previous tags'); try { // Get all pulled images @@ -88,10 +92,10 @@ class Docker extends Trigger { url: imageParsed.domain ? imageParsed.domain : '', }, tag: { - value: imageParsed.tag, + value: imageParsed.tag!, }, - name: imageParsed.path, - }); + name: imageParsed.path!, + } as ContainerImage); // Exclude different registries if ( @@ -109,7 +113,7 @@ class Docker extends Trigger { // Exclude current container image if ( imageNormalized.tag.value === - container.updateKind.localValue + container.updateKind!.localValue ) { return false; } @@ -117,7 +121,7 @@ class Docker extends Trigger { // Exclude candidate image if ( imageNormalized.tag.value === - container.updateKind.remoteValue + container.updateKind!.remoteValue ) { return false; } @@ -126,11 +130,11 @@ class Docker extends Trigger { .map((imageToRemove) => dockerApi.getImage(imageToRemove.Id)); await Promise.all( imagesToRemove.map((imageToRemove) => { - logContainer.info(`Prune image ${imageToRemove.name}`); + logContainer.info(`Prune image ${imageToRemove.id}`); return imageToRemove.remove(); }), ); - } catch (e) { + } catch (e: any) { logContainer.warn( `Some errors occurred when trying to prune previous tags (${e.message})`, ); @@ -143,10 +147,8 @@ class Docker extends Trigger { * @param auth * @param newImage * @param logContainer - * @returns {Promise} */ - - async pullImage(dockerApi, auth, newImage, logContainer) { + async pullImage(dockerApi: Dockerode, auth: Auth | undefined, newImage: string, logContainer: Logger) { logContainer.info(`Pull image ${newImage}`); try { const pullStream = await dockerApi.pull(newImage, { @@ -157,7 +159,7 @@ class Docker extends Trigger { dockerApi.modem.followProgress(pullStream, res), ); logContainer.info(`Image ${newImage} pulled with success`); - } catch (e) { + } catch (e: any) { logContainer.warn( `Error when pulling image ${newImage} (${e.message})`, ); @@ -167,14 +169,8 @@ class Docker extends Trigger { /** * Stop a container. - * @param container - * @param containerName - * @param containerId - * @param logContainer - * @returns {Promise} */ - - async stopContainer(container, containerName, containerId, logContainer) { + async stopContainer(container: Dockerode.Container, containerName: string, containerId: string, logContainer: Logger) { logContainer.info( `Stop container ${containerName} with id ${containerId}`, ); @@ -199,7 +195,7 @@ class Docker extends Trigger { * @param logContainer * @returns {Promise} */ - async removeContainer(container, containerName, containerId, logContainer) { + async removeContainer(container: Dockerode.Container, containerName: string, containerId: string, logContainer: Logger) { logContainer.info( `Remove container ${containerName} with id ${containerId}`, ); @@ -218,17 +214,12 @@ class Docker extends Trigger { /** * Create a new container. - * @param dockerApi - * @param containerToCreate - * @param containerName - * @param logContainer - * @returns {Promise<*>} */ async createContainer( - dockerApi, - containerToCreate, - containerName, - logContainer, + dockerApi: Dockerode, + containerToCreate: ContainerCreateOptions, + containerName: string, + logContainer: Logger, ) { logContainer.info(`Create container ${containerName}`); try { @@ -238,7 +229,7 @@ class Docker extends Trigger { `Container ${containerName} recreated on new image with success`, ); return newContainer; - } catch (e) { + } catch (e: any) { logContainer.warn( `Error when creating container ${containerName} (${e.message})`, ); @@ -251,9 +242,8 @@ class Docker extends Trigger { * @param container * @param containerName * @param logContainer - * @returns {Promise} */ - async startContainer(container, containerName, logContainer) { + async startContainer(container: Dockerode.Container, containerName: string, logContainer: Logger) { logContainer.info(`Start container ${containerName}`); try { await container.start(); @@ -268,12 +258,8 @@ class Docker extends Trigger { /** * Remove an image. - * @param dockerApi - * @param imageToRemove - * @param logContainer - * @returns {Promise} */ - async removeImage(dockerApi, imageToRemove, logContainer) { + async removeImage(dockerApi: Dockerode, imageToRemove: string, logContainer: Logger) { logContainer.info(`Remove image ${imageToRemove}`); try { const image = await dockerApi.getImage(imageToRemove); @@ -287,13 +273,10 @@ class Docker extends Trigger { /** * Clone container specs. - * @param currentContainer - * @param newImage - * @returns {*} */ - cloneContainer(currentContainer, newImage) { + cloneContainer(currentContainer: ContainerInspectInfo, newImage: string) { const containerName = currentContainer.Name.replace('/', ''); - const containerClone = { + const containerClone: ContainerCreateOptions = { ...currentContainer.Config, name: containerName, Image: newImage, @@ -303,7 +286,7 @@ class Docker extends Trigger { }, }; - if (containerClone.NetworkingConfig.EndpointsConfig) { + if (containerClone.NetworkingConfig?.EndpointsConfig) { Object.values( containerClone.NetworkingConfig.EndpointsConfig, ).forEach((endpointConfig) => { @@ -312,7 +295,7 @@ class Docker extends Trigger { endpointConfig.Aliases.length > 0 ) { endpointConfig.Aliases = endpointConfig.Aliases.filter( - (alias) => !currentContainer.Id.startsWith(alias), + (alias: any) => !currentContainer.Id.startsWith(alias), ); } }); @@ -335,14 +318,14 @@ class Docker extends Trigger { * @param registry the registry * @param container the container */ - getNewImageFullName(registry, container) { + getNewImageFullName(registry: Registry, container: Container) { // Tag to pull/run is // either the same (when updateKind is digest) // or the new one (when updateKind is tag) const tagOrDigest = - container.updateKind.kind === 'digest' + container.updateKind!.kind === 'digest' ? container.image.tag.value - : container.updateKind.remoteValue; + : container.updateKind!.remoteValue!; // Rebuild image definition string return registry.getImageFullName(container.image, tagOrDigest); @@ -350,10 +333,8 @@ class Docker extends Trigger { /** * Update the container. - * @param container the container - * @returns {Promise} */ - async trigger(container) { + async trigger(container: Container) { // Child logger for the container to process const logContainer = this.log.child({ container: fullName(container) }); @@ -361,13 +342,13 @@ class Docker extends Trigger { const watcher = this.getWatcher(container); // Get dockerApi from watcher - const { dockerApi } = watcher; + const { dockerApi } = watcher as DockerWatcher; // Get registry configuration logContainer.debug( `Get ${container.image.registry.name} registry manager`, ); - const registry = getState().registry[container.image.registry.name]; + const registry = getState().registry[container.image.registry.name!]; logContainer.debug( `Get ${container.image.registry.name} registry credentials`, @@ -413,7 +394,6 @@ class Docker extends Trigger { const containerToCreateInspect = this.cloneContainer( currentContainerSpec, newImage, - logContainer, ); // Stop current container @@ -454,9 +434,9 @@ class Docker extends Trigger { // Remove previous image (only when updateKind is tag) if (this.configuration.prune) { const tagOrDigestToRemove = - container.updateKind.kind === 'tag' + container.updateKind!.kind === 'tag' ? container.image.tag.value - : container.image.digest.repo; + : container.image.digest.repo!; // Rebuild image definition string const oldImage = registry.getImageFullName( @@ -475,14 +455,10 @@ class Docker extends Trigger { /** * Update the containers. - * @param containers - * @returns {Promise} */ - async triggerBatch(containers) { + async triggerBatch(containers: Container[]): Promise { return Promise.all( containers.map((container) => this.trigger(container)), ); } } - -module.exports = Docker; diff --git a/app/triggers/providers/dockercompose/Dockercompose.js b/app/triggers/providers/dockercompose/Dockercompose.ts similarity index 82% rename from app/triggers/providers/dockercompose/Dockercompose.js rename to app/triggers/providers/dockercompose/Dockercompose.ts index ea7f371f..1473768f 100644 --- a/app/triggers/providers/dockercompose/Dockercompose.js +++ b/app/triggers/providers/dockercompose/Dockercompose.ts @@ -1,17 +1,17 @@ -const fs = require('fs/promises'); -const yaml = require('yaml'); -const Docker = require('../docker/Docker'); -const { getState } = require('../../../registry'); +import fs from 'fs/promises'; +import yaml from 'yaml'; +import { ConstructorOptions } from 'docker-modem' +import { Docker as DockerTrigger, DockerTriggerConfiguration } from '../docker/Docker'; +import { getState } from '../../../registry/states'; +import { Container } from '../../../model/container'; +import { Docker as DockerWatcher } from '../../../watchers/providers/docker/Docker'; /** * Return true if the container belongs to the compose file. - * @param compose - * @param container - * @returns true/false */ -function doesContainerBelongToCompose(compose, container) { +function doesContainerBelongToCompose(compose: any, container: Container) { // Get registry configuration - const registry = getState().registry[container.image.registry.name]; + const registry = getState().registry[container.image.registry.name!]; // Rebuild image definition string const currentImage = registry.getImageFullName( @@ -24,10 +24,16 @@ function doesContainerBelongToCompose(compose, container) { }); } +export interface DockerComposeConfiguration extends DockerTriggerConfiguration { + file: string; + backup: boolean; +} + + /** * Update a Docker compose stack with an updated one. */ -class Dockercompose extends Docker { +export class Dockercompose extends DockerTrigger { /** * Get the Trigger configuration schema. * @returns {*} @@ -58,25 +64,22 @@ class Dockercompose extends Docker { /** * Update the container. * @param container the container - * @returns {Promise} */ - async trigger(container) { + async trigger(container: Container) { return this.triggerBatch([container]); } /** * Update the docker-compose stack. - * @param containers the containers - * @returns {Promise} */ - async triggerBatch(containers) { + async triggerBatch(containers: Container[]) { const compose = await this.getComposeFileAsObject(); const containersFiltered = containers // Filter on containers running on local host .filter((container) => { - const watcher = this.getWatcher(container); - if (watcher.dockerApi.modem.socketPath !== '') { + const watcher = this.getWatcher(container) as DockerWatcher; + if ((watcher.dockerApi.modem as ConstructorOptions).socketPath !== '') { return true; } this.log.warn( @@ -134,15 +137,12 @@ class Dockercompose extends Docker { /** * Backup a file. - * @param file - * @param backupFile - * @returns {Promise} */ - async backup(file, backupFile) { + async backup(file: string, backupFile: string) { try { this.log.debug(`Backup ${file} as ${backupFile}`); await fs.copyFile(file, backupFile); - } catch (e) { + } catch (e: any) { this.log.warn( `Error when trying to backup file ${file} to ${backupFile} (${e.message})`, ); @@ -153,14 +153,11 @@ class Dockercompose extends Docker { * Return a map containing the image declaration * with the current version * and the image declaration with the update version. - * @param compose - * @param container - * @returns {{current, update}|undefined} */ - mapCurrentVersionToUpdateVersion(compose, container) { + mapCurrentVersionToUpdateVersion(compose: any, container: Container) { // Get registry configuration this.log.debug(`Get ${container.image.registry.name} registry manager`); - const registry = getState().registry[container.image.registry.name]; + const registry = getState().registry[container.image.registry.name!]; // Rebuild image definition string const currentImage = registry.getImageFullName( @@ -173,7 +170,7 @@ class Dockercompose extends Docker { const service = compose.services[serviceKey]; return service.image.includes(currentImage); }, - ); + )!; // Rebuild image definition string return { @@ -184,14 +181,11 @@ class Dockercompose extends Docker { /** * Write docker-compose file. - * @param file - * @param data - * @returns {Promise} */ - async writeComposeFile(file, data) { + async writeComposeFile(file: string, data: string) { try { await fs.writeFile(file, data); - } catch (e) { + } catch (e: any) { this.log.error(`Error when writing ${file} (${e.message})`); this.log.debug(e); } @@ -199,12 +193,11 @@ class Dockercompose extends Docker { /** * Read docker-compose file as a buffer. - * @returns {Promise} */ getComposeFile() { try { return fs.readFile(this.configuration.file); - } catch (e) { + } catch (e: any) { this.log.error( `Error when reading the docker-compose yaml file (${e.message})`, ); @@ -214,18 +207,15 @@ class Dockercompose extends Docker { /** * Read docker-compose file as an object. - * @returns {Promise} */ async getComposeFileAsObject() { try { return yaml.parse((await this.getComposeFile()).toString()); - } catch (e) { + } catch (e: any) { this.log.error( `Error when parsing the docker-compose yaml file (${e.message})`, ); throw e; } } -} - -module.exports = Dockercompose; +} \ No newline at end of file diff --git a/app/triggers/providers/gotify/Gotify.test.js b/app/triggers/providers/gotify/Gotify.test.ts similarity index 86% rename from app/triggers/providers/gotify/Gotify.test.js rename to app/triggers/providers/gotify/Gotify.test.ts index 27406096..2eed1259 100644 --- a/app/triggers/providers/gotify/Gotify.test.js +++ b/app/triggers/providers/gotify/Gotify.test.ts @@ -1,10 +1,10 @@ -const { ValidationError } = require('joi'); +import { ValidationError } from 'joi'; -const Gotify = require('./Gotify'); +import { Gotify, GotifyConfiguration } from './Gotify'; const gotify = new Gotify(); -const configurationValid = { +const configurationValid: GotifyConfiguration = { url: 'http://xxx.com', token: 'xxx', priority: 2, @@ -29,7 +29,7 @@ test('validateConfiguration should return validated configuration when valid', ( test('validateConfiguration should throw error when invalid', () => { const configuration = { url: 'git://xxx.com', - }; + } as GotifyConfiguration; expect(() => { gotify.validateConfiguration(configuration); }).toThrowError(ValidationError); diff --git a/app/triggers/providers/gotify/Gotify.js b/app/triggers/providers/gotify/Gotify.ts similarity index 73% rename from app/triggers/providers/gotify/Gotify.js rename to app/triggers/providers/gotify/Gotify.ts index 212a2a1a..9a3420ca 100644 --- a/app/triggers/providers/gotify/Gotify.js +++ b/app/triggers/providers/gotify/Gotify.ts @@ -1,10 +1,19 @@ -const { GotifyClient } = require('gotify-client'); -const Trigger = require('../Trigger'); +import { GotifyClient } from 'gotify-client'; +import { Trigger, TriggerConfiguration } from '../Trigger'; +import { Container } from '../../../model/container'; + +export interface GotifyConfiguration extends TriggerConfiguration { + url: string; + token: string; + priority: number; +} /** * Gotify Trigger implementation */ -class Gotify extends Trigger { +export class Gotify extends Trigger { + private client!: GotifyClient; + /** * Get the Trigger configuration schema. * @returns {*} @@ -21,7 +30,6 @@ class Gotify extends Trigger { /** * Sanitize sensitive data - * @returns {*} */ maskConfiguration() { return { @@ -34,7 +42,7 @@ class Gotify extends Trigger { /** * Init trigger. */ - initTrigger() { + async initTrigger() { this.client = new GotifyClient(this.configuration.url, { app: this.configuration.token, }); @@ -42,10 +50,8 @@ class Gotify extends Trigger { /** * Send an HTTP Request to Gotify. - * @param container the container - * @returns {Promise} */ - async trigger(container) { + async trigger(container: Container) { return this.client.message.createMessage({ title: this.renderSimpleTitle(container), message: this.renderSimpleBody(container), @@ -55,10 +61,8 @@ class Gotify extends Trigger { /** * Send an HTTP Request to Gotify. - * @param containers - * @returns {Promise<*>} */ - async triggerBatch(containers) { + async triggerBatch(containers: Container[]) { return this.client.message.createMessage({ title: this.renderBatchTitle(containers), message: this.renderBatchBody(containers), @@ -67,4 +71,3 @@ class Gotify extends Trigger { } } -module.exports = Gotify; diff --git a/app/triggers/providers/http/Http.test.js b/app/triggers/providers/http/Http.test.ts similarity index 88% rename from app/triggers/providers/http/Http.test.js rename to app/triggers/providers/http/Http.test.ts index 19e31394..1baf435a 100644 --- a/app/triggers/providers/http/Http.test.js +++ b/app/triggers/providers/http/Http.test.ts @@ -1,12 +1,13 @@ -const { ValidationError } = require('joi'); -const rp = require('request-promise-native'); +import { ValidationError } from 'joi'; +import rp from 'request-promise-native'; jest.mock('request-promise-native'); -const Http = require('./Http'); +import { Http, HttpConfiguration } from './Http'; +import { Container } from '../../../model/container'; const http = new Http(); -const configurationValid = { +const configurationValid: HttpConfiguration = { url: 'http://xxx.com', method: 'POST', threshold: 'all', @@ -35,14 +36,14 @@ test('validateConfiguration should return validated configuration when valid', ( test('validateConfiguration should apply_default_configuration', () => { const validatedConfiguration = http.validateConfiguration({ url: configurationValid.url, - }); + } as HttpConfiguration); expect(validatedConfiguration).toStrictEqual(configurationValid); }); test('validateConfiguration should throw error when invalid', () => { const configuration = { url: 'git://xxx.com', - }; + } as HttpConfiguration; expect(() => { http.validateConfiguration(configuration); }).toThrowError(ValidationError); @@ -52,10 +53,10 @@ test('trigger should send GET http request when configured like that', async () http.configuration = { method: 'GET', url: 'https:///test', - }; + } as HttpConfiguration; const container = { name: 'container1', - }; + } as Container; await http.trigger(container); expect(rp).toHaveBeenCalledWith({ qs: { @@ -72,10 +73,10 @@ test('trigger should send POST http request when configured like that', async () method: 'POST', url: 'https:///test', auth: undefined, - }; + } as HttpConfiguration; const container = { name: 'container1', - }; + } as Container; await http.trigger(container); expect(rp).toHaveBeenCalledWith({ body: { @@ -92,10 +93,10 @@ test('trigger should use basic auth when configured like that', async () => { url: 'https:///test', method: 'POST', auth: { type: 'BASIC', user: 'user', password: 'pass' }, - }; + } as HttpConfiguration; const container = { name: 'container1', - }; + } as Container; await http.trigger(container); expect(rp).toHaveBeenCalledWith({ body: { @@ -113,10 +114,10 @@ test('trigger should use bearer auth when configured like that', async () => { url: 'https:///test', method: 'POST', auth: { type: 'BEARER', bearer: 'bearer' }, - }; + } as HttpConfiguration; const container = { name: 'container1', - }; + } as Container; await http.trigger(container); expect(rp).toHaveBeenCalledWith({ body: { @@ -134,10 +135,10 @@ test('trigger should use proxy when configured like that', async () => { url: 'https:///test', method: 'POST', proxy: 'http://proxy:3128', - }; + } as HttpConfiguration; const container = { name: 'container1', - }; + } as Container; await http.trigger(container); expect(rp).toHaveBeenCalledWith({ body: { diff --git a/app/triggers/providers/http/Http.js b/app/triggers/providers/http/Http.ts similarity index 70% rename from app/triggers/providers/http/Http.js rename to app/triggers/providers/http/Http.ts index fa462296..6f953189 100644 --- a/app/triggers/providers/http/Http.js +++ b/app/triggers/providers/http/Http.ts @@ -1,14 +1,26 @@ -const rp = require('request-promise-native'); +import rp, { OptionsWithUrl, RequestPromiseOptions } from 'request-promise-native'; +import { Trigger, TriggerConfiguration } from '../Trigger'; +import { Container } from '../../../model/container'; +import { UriOptions } from 'request'; -const Trigger = require('../Trigger'); +export interface HttpConfiguration extends TriggerConfiguration { + url: string; + method: 'GET' | 'POST'; + auth?: { + type: 'BASIC' | 'BEARER'; + user?: string; + password?: string; + bearer?: string; + }; + proxy?: string; +} /** * HTTP Trigger implementation */ -class Http extends Trigger { +export class Http extends Trigger { /** * Get the Trigger configuration schema. - * @returns {*} */ getConfigurationSchema() { return this.joi.object().keys({ @@ -36,27 +48,23 @@ class Http extends Trigger { /** * Send an HTTP Request with new image version details. - * - * @param container the container - * @returns {Promise} */ - async trigger(container) { + async trigger(container: Container) { return this.sendHttpRequest(container); } /** * Send an HTTP Request with new image versions details. - * @param containers - * @returns {Promise<*>} */ - async triggerBatch(containers) { + async triggerBatch(containers: Container[]) { return this.sendHttpRequest(containers); } - async sendHttpRequest(body) { - const options = { + async sendHttpRequest(body: Container | Container[]) { + const url = this.configuration.url; + const options: RequestPromiseOptions & UriOptions = { method: this.configuration.method, - uri: this.configuration.url, + uri: url, }; if (this.configuration.method === 'POST') { options.body = body; @@ -82,5 +90,3 @@ class Http extends Trigger { return rp(options); } } - -module.exports = Http; diff --git a/app/triggers/providers/ifttt/Ifttt.test.js b/app/triggers/providers/ifttt/Ifttt.test.ts similarity index 85% rename from app/triggers/providers/ifttt/Ifttt.test.js rename to app/triggers/providers/ifttt/Ifttt.test.ts index e5f42b2f..8572dada 100644 --- a/app/triggers/providers/ifttt/Ifttt.test.js +++ b/app/triggers/providers/ifttt/Ifttt.test.ts @@ -1,13 +1,14 @@ -const { ValidationError } = require('joi'); -const rp = require('request-promise-native'); +import { ValidationError } from 'joi'; +import rp from 'request-promise-native'; jest.mock('request-promise-native'); -const Ifttt = require('./Ifttt'); +import { Ifttt, IftttConfiguration } from './Ifttt'; +import { Container } from '../../../model/container'; const ifttt = new Ifttt(); -const configurationValid = { +const configurationValid: IftttConfiguration = { key: 'secret', event: 'wud-image', threshold: 'all', @@ -36,12 +37,12 @@ test('validateConfiguration should return validated configuration when valid', ( test('validateConfiguration should apply_default_configuration', () => { const validatedConfiguration = ifttt.validateConfiguration({ key: configurationValid.key, - }); + } as IftttConfiguration); expect(validatedConfiguration).toStrictEqual(configurationValid); }); test('validateConfiguration should throw error when invalid', () => { - const configuration = {}; + const configuration = {} as IftttConfiguration; expect(() => { ifttt.validateConfiguration(configuration); }).toThrowError(ValidationError); @@ -51,7 +52,7 @@ test('maskConfiguration should mask sensitive data', () => { ifttt.configuration = { key: 'key', event: 'event', - }; + } as IftttConfiguration; expect(ifttt.maskConfiguration()).toEqual({ key: 'k*y', event: 'event', @@ -62,13 +63,13 @@ test('trigger should send http request to IFTTT', async () => { ifttt.configuration = { key: 'key', event: 'event', - }; + } as IftttConfiguration; const container = { name: 'container1', result: { tag: '2.0.0', }, - }; + } as Container; await ifttt.trigger(container); expect(rp).toHaveBeenCalledWith({ body: { diff --git a/app/triggers/providers/ifttt/Ifttt.js b/app/triggers/providers/ifttt/Ifttt.ts similarity index 73% rename from app/triggers/providers/ifttt/Ifttt.js rename to app/triggers/providers/ifttt/Ifttt.ts index b26f281b..bcbb860e 100644 --- a/app/triggers/providers/ifttt/Ifttt.js +++ b/app/triggers/providers/ifttt/Ifttt.ts @@ -1,14 +1,18 @@ -const rp = require('request-promise-native'); +import rp from 'request-promise-native'; +import { Trigger, TriggerConfiguration } from '../Trigger'; +import { Container } from '../../../model/container'; -const Trigger = require('../Trigger'); +export interface IftttConfiguration extends TriggerConfiguration { + key: string; + event: string; +} /** * Ifttt Trigger implementation */ -class Ifttt extends Trigger { +export class Ifttt extends Trigger { /** * Get the Trigger configuration schema. - * @returns {*} */ getConfigurationSchema() { return this.joi.object().keys({ @@ -19,7 +23,6 @@ class Ifttt extends Trigger { /** * Sanitize sensitive data - * @returns {*} */ maskConfiguration() { return { @@ -31,24 +34,19 @@ class Ifttt extends Trigger { /** * Send an HTTP Request to Ifttt Webhook with new image version details. - * - * @param container the container - * @returns {Promise} */ - async trigger(container) { + async trigger(container: Container) { return this.sendHttpRequest({ value1: container.name, - value2: container.result.tag, + value2: container.result?.tag, value3: JSON.stringify(container), }); } /** * end an HTTP Request to Ifttt Webhook with new image versions details. - * @param containers - * @returns {Promise<*>} */ - async triggerBatch(containers) { + async triggerBatch(containers: Container[]) { return this.sendHttpRequest({ value1: JSON.stringify(containers), }); @@ -56,10 +54,8 @@ class Ifttt extends Trigger { /** * Send http request to ifttt. - * @param body - * @returns {Promise<*>} */ - async sendHttpRequest(body) { + async sendHttpRequest(body: any) { const options = { method: 'POST', uri: `https://maker.ifttt.com/trigger/${this.configuration.event}/with/key/${this.configuration.key}`, @@ -72,5 +68,3 @@ class Ifttt extends Trigger { return rp(options); } } - -module.exports = Ifttt; diff --git a/app/triggers/providers/kafka/Kafka.test.js b/app/triggers/providers/kafka/Kafka.test.ts similarity index 87% rename from app/triggers/providers/kafka/Kafka.test.js rename to app/triggers/providers/kafka/Kafka.test.ts index bc471eb2..81840838 100644 --- a/app/triggers/providers/kafka/Kafka.test.js +++ b/app/triggers/providers/kafka/Kafka.test.ts @@ -1,13 +1,14 @@ -const { ValidationError } = require('joi'); -const { Kafka: KafkaClient } = require('kafkajs'); +import { ValidationError } from 'joi'; +import { Kafka as KafkaClient, Producer, ProducerRecord } from 'kafkajs'; jest.mock('kafkajs'); -const Kafka = require('./Kafka'); +import { Kafka, KafkaConfiguration } from './Kafka'; +import { Container } from '../../../model/container'; const kafka = new Kafka(); -const configurationValid = { +const configurationValid: KafkaConfiguration = { brokers: 'broker1:9000, broker2:9000', topic: 'wud-container', clientId: 'wud', @@ -38,7 +39,7 @@ test('validateConfiguration should return validated configuration when valid', ( test('validateConfiguration should apply_default_configuration', () => { const validatedConfiguration = kafka.validateConfiguration({ brokers: 'broker1:9000, broker2:9000', - }); + } as KafkaConfiguration); expect(validatedConfiguration).toStrictEqual(configurationValid); }); @@ -49,7 +50,7 @@ test('validateConfiguration should validate_optional_authentication', () => { user: 'user', password: 'password', }, - }); + } as KafkaConfiguration); expect(validatedConfiguration).toStrictEqual({ ...configurationValid, authentication: { @@ -63,7 +64,7 @@ test('validateConfiguration should validate_optional_authentication', () => { test('validateConfiguration should throw error when invalid', () => { const configuration = { ssl: 'whynot', - }; + } as unknown as KafkaConfiguration; expect(() => { kafka.validateConfiguration(configuration); }).toThrowError(ValidationError); @@ -80,7 +81,7 @@ test('maskConfiguration should mask sensitive data', () => { user: 'user', password: 'password', }, - }; + } as KafkaConfiguration; expect(kafka.maskConfiguration()).toEqual({ brokers: 'broker1:9000, broker2:9000', topic: 'wud-image', @@ -100,7 +101,7 @@ test('maskConfiguration should not fail if no auth provided', () => { topic: 'wud-image', clientId: 'wud', ssl: false, - }; + } as KafkaConfiguration; expect(kafka.maskConfiguration()).toEqual({ brokers: 'broker1:9000, broker2:9000', topic: 'wud-image', @@ -115,7 +116,7 @@ test('initTrigger should init kafka client', async () => { topic: 'wud-image', clientId: 'wud', ssl: false, - }; + } as KafkaConfiguration; await kafka.initTrigger(); expect(KafkaClient).toHaveBeenCalledWith({ brokers: ['broker1:9000', 'broker2:9000'], @@ -135,14 +136,14 @@ test('initTrigger should init kafka client with auth when configured', async () user: 'user', password: 'password', }, - }; + } as KafkaConfiguration; await kafka.initTrigger(); expect(KafkaClient).toHaveBeenCalledWith({ brokers: ['broker1:9000', 'broker2:9000'], clientId: 'wud', ssl: false, sasl: { - mechanism: 'PLAIN', + mechanism: 'plain', password: 'password', username: 'user', }, @@ -152,17 +153,17 @@ test('initTrigger should init kafka client with auth when configured', async () test('trigger should post message to kafka', async () => { const producer = () => ({ connect: () => ({}), - send: (params) => params, - }); + send: (params: ProducerRecord) => params, + } as unknown as Producer); kafka.kafka = { producer, - }; + } as KafkaClient; kafka.configuration = { topic: 'topic', - }; + } as KafkaConfiguration; const container = { name: 'container1', - }; + } as Container; const result = await kafka.trigger(container); expect(result).toStrictEqual({ messages: [{ value: '{"name":"container1"}' }], diff --git a/app/triggers/providers/kafka/Kafka.js b/app/triggers/providers/kafka/Kafka.ts similarity index 71% rename from app/triggers/providers/kafka/Kafka.js rename to app/triggers/providers/kafka/Kafka.ts index 155830ad..ab3f348d 100644 --- a/app/triggers/providers/kafka/Kafka.js +++ b/app/triggers/providers/kafka/Kafka.ts @@ -1,13 +1,25 @@ -const { Kafka: KafkaClient } = require('kafkajs'); -const Trigger = require('../Trigger'); +import { Kafka as KafkaClient, KafkaConfig, SASLOptions } from 'kafkajs'; +import { Trigger, TriggerConfiguration } from '../Trigger'; +import { Container } from '../../../model/container'; + +export interface KafkaConfiguration extends TriggerConfiguration { + brokers: string; + topic: string; + clientId: string; + ssl: boolean; + authentication?: { + type: 'PLAIN' | 'SCRAM-SHA-256' | 'SCRAM-SHA-512'; + user: string; + password: string; + }; +} /** * Kafka Trigger implementation */ -class Kafka extends Trigger { +export class Kafka extends Trigger { /** * Get the Trigger configuration schema. - * @returns {*} */ getConfigurationSchema() { return this.joi.object().keys({ @@ -30,7 +42,6 @@ class Kafka extends Trigger { /** * Sanitize sensitive data - * @returns {*} */ maskConfiguration() { return { @@ -41,45 +52,43 @@ class Kafka extends Trigger { ssl: this.configuration.ssl, authentication: this.configuration.authentication ? { - type: this.configuration.authentication.type, - user: this.configuration.authentication.user, - password: Kafka.mask( - this.configuration.authentication.password, - ), - } + type: this.configuration.authentication.type, + user: this.configuration.authentication.user, + password: Kafka.mask( + this.configuration.authentication.password, + ), + } : undefined, }; } + public kafka!: KafkaClient; /** * Init trigger. */ - initTrigger() { + async initTrigger() { const brokers = this.configuration.brokers .split(/\s*,\s*/) .map((broker) => broker.trim()); - const clientConfiguration = { + const clientConfiguration: KafkaConfig = { clientId: this.configuration.clientId, brokers, ssl: this.configuration.ssl, }; if (this.configuration.authentication) { clientConfiguration.sasl = { - mechanism: this.configuration.authentication.type, + mechanism: this.configuration.authentication.type.toLocaleLowerCase(), username: this.configuration.authentication.user, password: this.configuration.authentication.password, - }; + } as SASLOptions; } this.kafka = new KafkaClient(clientConfiguration); } /** * Send a record to a Kafka topic with new container version details. - * - * @param container the container - * @returns {Promise} */ - async trigger(container) { + async trigger(container: Container) { const producer = this.kafka.producer(); await producer.connect(); return producer.send({ @@ -90,10 +99,8 @@ class Kafka extends Trigger { /** * Send a record to a Kafka topic with new container versions details. - * @param containers - * @returns {Promise} */ - async triggerBatch(containers) { + async triggerBatch(containers: Container[]) { const producer = this.kafka.producer(); await producer.connect(); return producer.send({ @@ -104,5 +111,3 @@ class Kafka extends Trigger { }); } } - -module.exports = Kafka; diff --git a/app/triggers/providers/mock/Mock.js b/app/triggers/providers/mock/Mock.ts similarity index 56% rename from app/triggers/providers/mock/Mock.js rename to app/triggers/providers/mock/Mock.ts index 025ad979..63bdad61 100644 --- a/app/triggers/providers/mock/Mock.js +++ b/app/triggers/providers/mock/Mock.ts @@ -1,25 +1,14 @@ -const Trigger = require('../Trigger'); +import { Container } from '../../../model/container'; +import { Trigger } from '../Trigger'; /** * Mock Trigger implementation (for tests) */ -class Mock extends Trigger { - /** - * Get the Trigger configuration schema. - * @returns {*} - */ - getConfigurationSchema() { - return this.joi.object().keys({ - mock: this.joi.string().default('mock'), - }); - } - +export class Mock extends Trigger { /** * Mock trigger only logs a dummy line... - * @param container - * @returns {Promise} */ - async trigger(container) { + async trigger(container: Container) { this.log.info( `MOCK triggered title = \n${this.renderSimpleTitle(container)}`, ); @@ -30,10 +19,8 @@ class Mock extends Trigger { /** * Mock trigger only logs a dummy line... - * @param containers - * @returns {Promise} */ - async triggerBatch(containers) { + async triggerBatch(containers: Container[]) { this.log.info( `MOCK triggered title = \n${this.renderBatchTitle(containers)}`, ); @@ -42,5 +29,3 @@ class Mock extends Trigger { ); } } - -module.exports = Mock; diff --git a/app/triggers/providers/mqtt/Hass.test.js b/app/triggers/providers/mqtt/Hass.test.ts similarity index 95% rename from app/triggers/providers/mqtt/Hass.test.js rename to app/triggers/providers/mqtt/Hass.test.ts index cc438c5c..c0d3f185 100644 --- a/app/triggers/providers/mqtt/Hass.test.js +++ b/app/triggers/providers/mqtt/Hass.test.ts @@ -1,23 +1,28 @@ -const log = require('../../../log'); -const Hass = require('./Hass'); +import { MqttClient } from 'mqtt/*'; +import log from '../../../log'; +import { Hass } from './Hass'; +import { MqttConfiguration } from './MqttConfiguration'; +import { Container } from '../../../model/container'; +import { Watcher } from '../../../watchers/Watcher'; -let hass; -let mqttClientMock; +let hass: Hass; +let mqttClientMock: jest.Mocked; beforeEach(() => { jest.resetAllMocks(); mqttClientMock = { - publish: jest.fn(() => {}), - }; + publish: jest.fn(() => { }), + } as unknown as jest.Mocked; hass = new Hass({ client: mqttClientMock, configuration: { topic: 'topic', hass: { + enabled: true, discovery: true, prefix: 'homeassistant', }, - }, + } as MqttConfiguration, log, }); }); @@ -59,7 +64,7 @@ test('addContainerSensor must publish sensor discovery message expected by HA', name: 'container-name', watcher: 'watcher-name', displayIcon: 'mdi:docker', - }); + } as Container); expect(mqttClientMock.publish).toHaveBeenCalledWith( 'homeassistant/update/topic_watcher-name_container-name/config', JSON.stringify({ @@ -93,7 +98,7 @@ test('removeContainerSensor must publish sensor discovery message expected by HA name: 'container-name', watcher: 'watcher-name', displayIcon: 'mdi:docker', - }); + } as Container); expect(mqttClientMock.publish).toHaveBeenCalledWith( 'homeassistant/update/topic_watcher-name_container-name/config', JSON.stringify({}), @@ -106,7 +111,7 @@ test('updateContainerSensors must publish all sensors expected by HA', async () name: 'container-name', watcher: 'watcher-name', displayIcon: 'mdi:docker', - }); + } as Container); expect(mqttClientMock.publish).toHaveBeenCalledTimes(15); expect(mqttClientMock.publish).toHaveBeenNthCalledWith( @@ -306,7 +311,7 @@ test('removeContainerSensor must publish all sensor removal messages expected by name: 'container-name', watcher: 'watcher-name', displayIcon: 'mdi:docker', - }); + } as Container); expect(mqttClientMock.publish).toHaveBeenCalledWith( 'homeassistant/update/topic_watcher-name_container-name/config', JSON.stringify({}), @@ -318,7 +323,7 @@ test('updateWatcherSensors must publish all watcher sensor messages expected by await hass.updateWatcherSensors({ watcher: { name: 'watcher-name', - }, + } as Watcher, isRunning: true, }); expect(mqttClientMock.publish).toHaveBeenCalledWith( diff --git a/app/triggers/providers/mqtt/Hass.js b/app/triggers/providers/mqtt/Hass.ts similarity index 86% rename from app/triggers/providers/mqtt/Hass.js rename to app/triggers/providers/mqtt/Hass.ts index f5e79789..1bef18e4 100644 --- a/app/triggers/providers/mqtt/Hass.js +++ b/app/triggers/providers/mqtt/Hass.ts @@ -1,12 +1,11 @@ -const { getVersion } = require('../../../configuration'); -const { - registerContainerAdded, - registerContainerUpdated, - registerContainerRemoved, - registerWatcherStart, - registerWatcherStop, -} = require('../../../event'); -const containerStore = require('../../../store/container'); +import { MqttClient } from 'mqtt/*'; +import { getVersion } from '../../../configuration'; +import { registerContainerAdded, registerContainerUpdated, registerContainerRemoved, registerWatcherStart, registerWatcherStop } from '../../../event'; +import * as containerStore from '../../../store/container'; +import { MqttConfiguration } from './MqttConfiguration'; +import Logger from 'bunyan'; +import { Container } from '../../../model/container'; +import { Watcher } from '../../../watchers/Watcher'; const HASS_DEVICE_ID = 'wud'; const HASS_DEVICE_NAME = 'wud'; @@ -17,16 +16,13 @@ const HASS_LATEST_VERSION_TEMPLATE = /** * Get hass entity unique id. - * @param topic - * @return {*} */ -function getHassEntityId(topic) { +function getHassEntityId(topic: string) { return topic.replace(/\//g, '_'); } /** * Get HA wud device info. - * @returns {*} */ function getHaDevice() { return { @@ -40,10 +36,8 @@ function getHaDevice() { /** * Sanitize icon to meet hass requirements. - * @param icon - * @return {*} */ -function sanitizeIcon(icon) { +function sanitizeIcon(icon: string) { return icon .replace('mdi-', 'mdi:') .replace('fa-', 'fa:') @@ -53,8 +47,16 @@ function sanitizeIcon(icon) { .replace('si-', 'si:'); } -class Hass { - constructor({ client, configuration, log }) { +export class Hass { + client: MqttClient; + configuration: MqttConfiguration; + log: Logger; + + constructor({ client, configuration, log }: { + client: MqttClient; + configuration: MqttConfiguration; + log: Logger; + }) { this.client = client; this.configuration = configuration; this.log = log; @@ -81,10 +83,8 @@ class Hass { /** * Add container sensor. - * @param container - * @returns {Promise} */ - async addContainerSensor(container) { + async addContainerSensor(container: Container) { const containerStateTopic = this.getContainerStateTopic({ container }); this.log.info( `Add hass container update sensor [${containerStateTopic}]`, @@ -97,7 +97,7 @@ class Hass { }), stateTopic: containerStateTopic, name: container.displayName, - icon: sanitizeIcon(container.displayIcon), + icon: sanitizeIcon(container.displayIcon!), options: { force_update: true, value_template: HASS_ENTITY_VALUE_TEMPLATE, @@ -115,10 +115,8 @@ class Hass { /** * Remove container sensor. - * @param container - * @returns {Promise} */ - async removeContainerSensor(container) { + async removeContainerSensor(container: Container) { const containerStateTopic = this.getContainerStateTopic({ container }); this.log.info( `Remove hass container update sensor [${containerStateTopic}]`, @@ -134,7 +132,7 @@ class Hass { await this.updateContainerSensors(container); } - async updateContainerSensors(container) { + async updateContainerSensors(container: Container) { // Sensor topics const totalCountTopic = `${this.configuration.topic}/total_count`; const totalUpdateCountTopic = `${this.configuration.topic}/update_count`; @@ -214,7 +212,7 @@ class Hass { // Count all containers const totalCount = containerStore.getContainers().length; const updateCount = containerStore.getContainers({ - updateAvailable: true, + updateAvailable: 'true', }).length; // Count all containers belonging to the current watcher @@ -223,7 +221,7 @@ class Hass { }).length; const watcherUpdateCount = containerStore.getContainers({ watcher: container.watcher, - updateAvailable: true, + updateAvailable: 'true', }).length; // Publish sensors @@ -266,7 +264,10 @@ class Hass { } } - async updateWatcherSensors({ watcher, isRunning }) { + async updateWatcherSensors({ watcher, isRunning }: { + watcher: Watcher; + isRunning: boolean; + }) { const watcherStatusTopic = `${this.configuration.topic}/${watcher.name}/running`; const watcherStatusDiscoveryTopic = this.getDiscoveryTopic({ kind: 'binary_sensor', @@ -295,12 +296,6 @@ class Hass { /** * Publish a discovery message. - * @param discoveryTopic - * @param stateTopic - * @param name - * @param icon - * @param options - * @returns {Promise<*>} */ async publishDiscoveryMessage({ discoveryTopic, @@ -308,6 +303,12 @@ class Hass { name, icon, options = {}, + }: { + discoveryTopic: string; + stateTopic: string; + name?: string; + icon?: string; + options?: any; }) { const entityId = getHassEntityId(stateTopic); return this.client.publish( @@ -334,7 +335,7 @@ class Hass { * @param discoveryTopic * @returns {Promise<*>} */ - async removeSensor({ discoveryTopic }) { + async removeSensor({ discoveryTopic }: { discoveryTopic: string }) { return this.client.publish(discoveryTopic, JSON.stringify({}), { retain: true, }); @@ -342,32 +343,26 @@ class Hass { /** * Publish a sensor message. - * @param topic - * @param value - * @returns {Promise<*>} */ - async updateSensor({ topic, value }) { + async updateSensor({ topic, value }: { topic: string; value: any }) { return this.client.publish(topic, value.toString(), { retain: true }); } /** * Get container state topic. - * @param container - * @return {string} */ - getContainerStateTopic({ container }) { + getContainerStateTopic({ container }: { container: Container }) { return `${this.configuration.topic}/${container.watcher}/${container.name}`; } /** * Get discovery topic for an entity topic. - * @param kind - * @param topic - * @returns {string} */ - getDiscoveryTopic({ kind, topic }) { + getDiscoveryTopic({ kind, topic }: { + kind: 'sensor' | 'binary_sensor' | 'update'; + topic: string; + }) { return `${this.configuration.hass.prefix}/${kind}/${getHassEntityId(topic)}/config`; } } -module.exports = Hass; diff --git a/app/triggers/providers/mqtt/Mqtt.test.js b/app/triggers/providers/mqtt/Mqtt.test.ts similarity index 90% rename from app/triggers/providers/mqtt/Mqtt.test.js rename to app/triggers/providers/mqtt/Mqtt.test.ts index 9429476f..ed0db358 100644 --- a/app/triggers/providers/mqtt/Mqtt.test.js +++ b/app/triggers/providers/mqtt/Mqtt.test.ts @@ -1,14 +1,15 @@ -const { ValidationError } = require('joi'); -const mqttClient = require('mqtt'); -const log = require('../../../log'); +import { ValidationError } from 'joi'; +import mqttClient, { MqttClient } from 'mqtt'; +import log from '../../../log'; jest.mock('mqtt'); -const Mqtt = require('./Mqtt'); +import { Mqtt } from './Mqtt'; +import { MqttConfiguration } from './MqttConfiguration'; const mqtt = new Mqtt(); mqtt.log = log; -const configurationValid = { +const configurationValid: MqttConfiguration = { url: 'mqtt://host:1883', topic: 'wud/container', clientid: 'wud', @@ -50,14 +51,14 @@ test('validateConfiguration should apply_default_configuration', () => { const validatedConfiguration = mqtt.validateConfiguration({ url: configurationValid.url, clientid: 'wud', - }); + } as MqttConfiguration); expect(validatedConfiguration).toStrictEqual(configurationValid); }); test('validateConfiguration should throw error when invalid', () => { const configuration = { url: 'http://invalid', - }; + } as MqttConfiguration; expect(() => { mqtt.validateConfiguration(configuration); }).toThrowError(ValidationError); @@ -73,7 +74,7 @@ test('maskConfiguration should mask sensitive data', () => { enabled: false, prefix: 'homeassistant', }, - }; + } as MqttConfiguration; expect(mqtt.maskConfiguration()).toEqual({ hass: { discovery: false, @@ -111,14 +112,14 @@ test('initTrigger should init Mqtt client', async () => { test('trigger should format json message payload as expected', async () => { mqtt.configuration = { topic: 'wud/container', - }; + } as MqttConfiguration; mqtt.client = { - publish: (topic, message) => ({ + publish: (topic: any, message: any) => ({ topic, message, }), - }; - const response = await mqtt.trigger({ + } as unknown as MqttClient; + const response: any = await mqtt.trigger({ id: '31a61a8305ef1fc9a71fa4f20a68d7ec88b28e32303bbc4a5f192e851165b816', name: 'homeassistant', watcher: 'local', diff --git a/app/triggers/providers/mqtt/Mqtt.js b/app/triggers/providers/mqtt/Mqtt.ts similarity index 84% rename from app/triggers/providers/mqtt/Mqtt.js rename to app/triggers/providers/mqtt/Mqtt.ts index 17f8e763..3d2c2474 100644 --- a/app/triggers/providers/mqtt/Mqtt.js +++ b/app/triggers/providers/mqtt/Mqtt.ts @@ -1,12 +1,10 @@ -const fs = require('fs').promises; -const mqtt = require('mqtt'); -const Trigger = require('../Trigger'); -const Hass = require('./Hass'); -const { - registerContainerAdded, - registerContainerUpdated, -} = require('../../../event'); -const { flatten } = require('../../../model/container'); +import { promises as fs } from 'fs'; +import mqtt, { IClientOptions, MqttClient } from 'mqtt'; +import { Trigger } from '../Trigger'; +import { Hass } from './Hass'; +import { registerContainerAdded, registerContainerUpdated } from '../../../event'; +import { Container, flatten } from '../../../model/container'; +import { MqttConfiguration } from './MqttConfiguration'; const containerDefaultTopic = 'wud/container'; const hassDefaultPrefix = 'homeassistant'; @@ -17,14 +15,17 @@ const hassDefaultPrefix = 'homeassistant'; * @param container * @return {string} */ -function getContainerTopic({ baseTopic, container }) { +function getContainerTopic({ baseTopic, container }: { + baseTopic: string; + container: Container; +}) { return `${baseTopic}/${container.watcher}/${container.name}`; } /** * MQTT Trigger implementation */ -class Mqtt extends Trigger { +export class Mqtt extends Trigger { /** * Get the Trigger configuration schema. * @returns {*} @@ -75,7 +76,6 @@ class Mqtt extends Trigger { /** * Sanitize sensitive data - * @returns {*} */ maskConfiguration() { return { @@ -88,11 +88,16 @@ class Mqtt extends Trigger { }; } + public client!: MqttClient; + // Hass instance for Home Assistant integration + // We have to clean this up, but we don't have to worry about it for now + private hass?: Hass; + async initTrigger() { // Enforce simple mode this.configuration.mode = 'simple'; - const options = { + const options: IClientOptions = { clientId: this.configuration.clientid, }; if (this.configuration.user) { @@ -127,11 +132,8 @@ class Mqtt extends Trigger { /** * Send an MQTT message with new image version details. - * - * @param container the container - * @returns {Promise} */ - async trigger(container) { + async trigger(container: Container) { const containerTopic = getContainerTopic({ baseTopic: this.configuration.topic, container, @@ -149,12 +151,9 @@ class Mqtt extends Trigger { /** * Mqtt trigger does not support batch mode. - * @returns {Promise} */ - async triggerBatch() { throw new Error('This trigger does not support "batch" mode'); } } -module.exports = Mqtt; diff --git a/app/triggers/providers/mqtt/MqttConfiguration.ts b/app/triggers/providers/mqtt/MqttConfiguration.ts new file mode 100644 index 00000000..1f25aea6 --- /dev/null +++ b/app/triggers/providers/mqtt/MqttConfiguration.ts @@ -0,0 +1,21 @@ +import { TriggerConfiguration } from '../Trigger'; + + +export interface MqttConfiguration extends TriggerConfiguration { + url: string; + topic: string; + clientid: string; + user?: string; + password?: string; + hass: { + enabled: boolean; + prefix: string; + discovery: boolean; + }; + tls: { + clientkey?: string; + clientcert?: string; + cachain?: string; + rejectunauthorized: boolean; + }; +} diff --git a/app/triggers/providers/ntfy/Ntfy.test.js b/app/triggers/providers/ntfy/Ntfy.test.ts similarity index 92% rename from app/triggers/providers/ntfy/Ntfy.test.js rename to app/triggers/providers/ntfy/Ntfy.test.ts index 85bd1804..c8d8b6fe 100644 --- a/app/triggers/providers/ntfy/Ntfy.test.js +++ b/app/triggers/providers/ntfy/Ntfy.test.ts @@ -1,12 +1,13 @@ -const { ValidationError } = require('joi'); -const rp = require('request-promise-native'); -const Ntfy = require('./Ntfy'); +import { ValidationError } from 'joi'; +import rp from 'request-promise-native'; +import { Ntfy, NtfyConfiguration } from './Ntfy'; +import { Container } from '../../../model/container'; jest.mock('request-promise-native'); const ntfy = new Ntfy(); -const configurationValid = { +const configurationValid: NtfyConfiguration = { url: 'http://xxx.com', topic: 'xxx', priority: 2, @@ -36,7 +37,7 @@ test('validateConfiguration should return validated configuration when valid', ( test('validateConfiguration should throw error when invalid', () => { const configuration = { url: 'git://xxx.com', - }; + } as NtfyConfiguration; expect(() => { ntfy.validateConfiguration(configuration); }).toThrowError(ValidationError); @@ -51,7 +52,7 @@ test('trigger should call http client', async () => { localValue: '1.0.0', remoteValue: '2.0.0', }, - }; + } as Container; await ntfy.trigger(container); expect(rp).toHaveBeenCalledWith({ body: { @@ -82,7 +83,7 @@ test('trigger should use basic auth when configured like that', async () => { localValue: '1.0.0', remoteValue: '2.0.0', }, - }; + } as Container; await ntfy.trigger(container); expect(rp).toHaveBeenCalledWith({ body: { @@ -114,7 +115,7 @@ test('trigger should use bearer auth when configured like that', async () => { localValue: '1.0.0', remoteValue: '2.0.0', }, - }; + } as Container; await ntfy.trigger(container); expect(rp).toHaveBeenCalledWith({ body: { diff --git a/app/triggers/providers/ntfy/Ntfy.js b/app/triggers/providers/ntfy/Ntfy.ts similarity index 71% rename from app/triggers/providers/ntfy/Ntfy.js rename to app/triggers/providers/ntfy/Ntfy.ts index 00ab876f..f5d767bb 100644 --- a/app/triggers/providers/ntfy/Ntfy.js +++ b/app/triggers/providers/ntfy/Ntfy.ts @@ -1,10 +1,23 @@ -const rp = require('request-promise-native'); -const Trigger = require('../Trigger'); +import rp, { RequestPromiseOptions } from 'request-promise-native'; +import { Trigger, TriggerConfiguration } from '../Trigger'; +import { Container } from '../../../model/container'; +import { RequiredUriUrl } from 'request'; + +export interface NtfyConfiguration extends TriggerConfiguration { + url: string; + topic: string; + priority: 0 | 1 | 2 | 3 | 4 | 5; + auth?: { + user?: string; + password?: string; + token?: string; + } | undefined; +} /** * Ntfy Trigger implementation */ -class Ntfy extends Trigger { +export class Ntfy extends Trigger { /** * Get the Trigger configuration schema. * @returns {*} @@ -29,27 +42,24 @@ class Ntfy extends Trigger { /** * Sanitize sensitive data - * @returns {*} */ maskConfiguration() { return { ...this.configuration, auth: this.configuration.auth ? { - user: Ntfy.mask(this.configuration.user), - password: Ntfy.mask(this.configuration.password), - token: Ntfy.mask(this.configuration.token), - } + user: Ntfy.mask(this.configuration.auth.user), + password: Ntfy.mask(this.configuration.auth.password), + token: Ntfy.mask(this.configuration.auth.token), + } : undefined, }; } /** * Send an HTTP Request to Ntfy. - * @param container the container - * @returns {Promise} */ - async trigger(container) { + async trigger(container: Container) { return this.sendHttpRequest({ topic: this.configuration.topic, title: this.renderSimpleTitle(container), @@ -60,10 +70,8 @@ class Ntfy extends Trigger { /** * Send an HTTP Request to Ntfy. - * @param containers - * @returns {Promise<*>} */ - async triggerBatch(containers) { + async triggerBatch(containers: Container[]) { return this.sendHttpRequest({ topic: this.configuration.topic, title: this.renderBatchTitle(containers), @@ -77,8 +85,8 @@ class Ntfy extends Trigger { * @param body * @returns {Promise<*>} */ - async sendHttpRequest(body) { - const options = { + async sendHttpRequest(body: any) { + const options: RequestPromiseOptions & RequiredUriUrl = { method: 'POST', uri: this.configuration.url, headers: { @@ -105,5 +113,3 @@ class Ntfy extends Trigger { return rp(options); } } - -module.exports = Ntfy; diff --git a/app/triggers/providers/pushover/Pushover.test.js b/app/triggers/providers/pushover/Pushover.test.ts similarity index 88% rename from app/triggers/providers/pushover/Pushover.test.js rename to app/triggers/providers/pushover/Pushover.test.ts index bc0d3d9a..aa31137c 100644 --- a/app/triggers/providers/pushover/Pushover.test.js +++ b/app/triggers/providers/pushover/Pushover.test.ts @@ -1,20 +1,23 @@ -const { ValidationError } = require('joi'); +import { ValidationError } from 'joi'; jest.mock( 'pushover-notifications', () => class Push { - send(message, cb) { - cb(undefined, message); + send(message: SendMessageOptions, cb: (error: Error | null, data?: any, response?: IncomingMessage) => void) { + cb(null, message); } }, ); -const Pushover = require('./Pushover'); +import { Pushover, PushoverConfiguration } from './Pushover'; +import { SendMessageOptions } from 'pushover-notifications'; +import { IncomingMessage } from 'http'; +import { Container } from '../../../model/container'; const pushover = new Pushover(); -const configurationValid = { +const configurationValid: PushoverConfiguration = { user: 'user', token: 'token', priority: 0, @@ -86,12 +89,12 @@ test('validateConfiguration should apply_default_configuration', () => { const validatedConfiguration = pushover.validateConfiguration({ user: configurationValid.user, token: configurationValid.token, - }); + } as PushoverConfiguration); expect(validatedConfiguration).toStrictEqual(configurationValid); }); test('validateConfiguration should throw error when invalid', () => { - const configuration = {}; + const configuration = {} as PushoverConfiguration; expect(() => { pushover.validateConfiguration(configuration); }).toThrowError(ValidationError); @@ -144,7 +147,7 @@ test('trigger should send message to pushover', async () => { remoteValue: '2.0.0', semverDiff: 'major', }, - }; + } as Container; const result = await pushover.trigger(container); expect(result).toStrictEqual({ device: undefined, diff --git a/app/triggers/providers/pushover/Pushover.js b/app/triggers/providers/pushover/Pushover.ts similarity index 82% rename from app/triggers/providers/pushover/Pushover.js rename to app/triggers/providers/pushover/Pushover.ts index 61d53040..a2f78daf 100644 --- a/app/triggers/providers/pushover/Pushover.js +++ b/app/triggers/providers/pushover/Pushover.ts @@ -1,13 +1,26 @@ -const Push = require('pushover-notifications'); -const Trigger = require('../Trigger'); +import Push, { SendMessageOptions } from 'pushover-notifications'; +import { Trigger, TriggerConfiguration } from '../Trigger'; +import e from 'express'; +import { Container } from '../../../model/container'; + +export interface PushoverConfiguration extends TriggerConfiguration { + user: string; + token: string; + device?: string; + html?: 0 | 1; + sound?: string; + priority?: number; + retry?: number; + expire?: number; + ttl?: number; +} /** * Ifttt Trigger implementation */ -class Pushover extends Trigger { +export class Pushover extends Trigger { /** * Get the Trigger configuration schema. - * @returns {*} */ getConfigurationSchema() { return this.joi.object().keys({ @@ -77,11 +90,8 @@ class Pushover extends Trigger { /** * Send a Pushover notification with new container version details. - * - * @param container the container - * @returns {Promise} */ - async trigger(container) { + async trigger(container: Container) { return this.sendMessage({ title: this.renderSimpleTitle(container), message: this.renderSimpleBody(container), @@ -90,25 +100,23 @@ class Pushover extends Trigger { /** * Send a Pushover notification with new container versions details. - * @param containers - * @returns {Promise} */ - async triggerBatch(containers) { + async triggerBatch(containers: Container[]) { return this.sendMessage({ title: this.renderBatchTitle(containers), message: this.renderBatchBody(containers), }); } - async sendMessage(message) { - const messageToSend = { + async sendMessage(message: { title: string; message: string }) { + const messageToSend: SendMessageOptions = { ...message, sound: this.configuration.sound, device: this.configuration.device, priority: this.configuration.priority, html: this.configuration.html, - }; + }; // Emergency priority needs retry/expire props if (this.configuration.priority === 2) { messageToSend.expire = this.configuration.expire; @@ -124,12 +132,12 @@ class Pushover extends Trigger { }); push.onerror = (err) => { - reject(new Error(err)); + reject(err); }; push.send(messageToSend, (err, res) => { if (err) { - reject(new Error(err)); + reject(err); } else { resolve(res); } @@ -139,14 +147,10 @@ class Pushover extends Trigger { /** * Render trigger body batch (override) to remove empty lines between containers. - * @param containers - * @returns {*} */ - renderBatchBody(containers) { + renderBatchBody(containers: Container[]) { return containers .map((container) => `- ${this.renderSimpleBody(container)}`) .join('\n'); } } - -module.exports = Pushover; diff --git a/app/triggers/providers/slack/Slack.test.js b/app/triggers/providers/slack/Slack.test.ts similarity index 87% rename from app/triggers/providers/slack/Slack.test.js rename to app/triggers/providers/slack/Slack.test.ts index 2efc0556..f86439b6 100644 --- a/app/triggers/providers/slack/Slack.test.js +++ b/app/triggers/providers/slack/Slack.test.ts @@ -1,12 +1,12 @@ -const { ValidationError } = require('joi'); -const { WebClient } = require('@slack/web-api'); +import { ValidationError } from 'joi'; +import { WebClient } from '@slack/web-api'; jest.mock('@slack/web-api'); -const Slack = require('./Slack'); +import { Slack, SlackConfiguration } from './Slack'; const slack = new Slack(); -const configurationValid = { +const configurationValid: SlackConfiguration = { token: 'token', channel: 'channel', threshold: 'all', @@ -30,7 +30,7 @@ test('validateConfiguration should return validated configuration when valid', ( test('validateConfiguration should throw error when invalid', () => { expect(() => { - slack.validateConfiguration({}); + slack.validateConfiguration({} as SlackConfiguration); }).toThrowError(ValidationError); }); @@ -38,7 +38,7 @@ test('maskConfiguration should mask sensitive data', () => { slack.configuration = { token: 'token', channel: 'channel', - }; + } as SlackConfiguration; expect(slack.maskConfiguration()).toEqual({ token: 't***n', channel: 'channel', @@ -55,10 +55,10 @@ test('trigger should format text as expected', async () => { slack.configuration = configurationValid; slack.client = { chat: { - postMessage: (conf) => conf, + postMessage: (conf: any) => conf, }, - }; - const response = await slack.trigger({ + } as unknown as WebClient; + const response: any = await slack.trigger({ id: '31a61a8305ef1fc9a71fa4f20a68d7ec88b28e32303bbc4a5f192e851165b816', name: 'homeassistant', watcher: 'local', diff --git a/app/triggers/providers/slack/Slack.js b/app/triggers/providers/slack/Slack.ts similarity index 66% rename from app/triggers/providers/slack/Slack.js rename to app/triggers/providers/slack/Slack.ts index 98364431..8b3d56ee 100644 --- a/app/triggers/providers/slack/Slack.js +++ b/app/triggers/providers/slack/Slack.ts @@ -1,10 +1,16 @@ -const { WebClient } = require('@slack/web-api'); -const Trigger = require('../Trigger'); +import { WebClient } from '@slack/web-api'; +import { Trigger, TriggerConfiguration } from '../Trigger'; +import { Container } from '../../../model/container'; + +export interface SlackConfiguration extends TriggerConfiguration { + token: string; + channel: string; +} /* * Slack Trigger implementation */ -class Slack extends Trigger { +export class Slack extends Trigger { /* * Get the Trigger configuration schema. * @returns {*} @@ -18,7 +24,6 @@ class Slack extends Trigger { /** * Sanitize sensitive data - * @returns {*} */ maskConfiguration() { return { @@ -28,38 +33,32 @@ class Slack extends Trigger { }; } + client!: WebClient; /* - * Init trigger. - */ - initTrigger() { + * Init trigger. + */ + async initTrigger() { this.client = new WebClient(this.configuration.token); } /* * Post a message with new image version details. - * - * @param image the image - * @returns {Promise} */ - async trigger(container) { + async trigger(container: Container) { return this.postMessage(this.renderSimpleBody(container)); } - async triggerBatch(containers) { + async triggerBatch(containers: Container[]) { return this.postMessage(this.renderBatchBody(containers)); } /** * Post a message to a Slack channel. - * @param text the text to post - * @returns {Promise} */ - async postMessage(text) { + async postMessage(text: string) { return this.client.chat.postMessage({ channel: this.configuration.channel, text, }); } } - -module.exports = Slack; diff --git a/app/triggers/providers/smtp/Smtp.test.js b/app/triggers/providers/smtp/Smtp.test.ts similarity index 93% rename from app/triggers/providers/smtp/Smtp.test.js rename to app/triggers/providers/smtp/Smtp.test.ts index bdf8047f..314360da 100644 --- a/app/triggers/providers/smtp/Smtp.test.js +++ b/app/triggers/providers/smtp/Smtp.test.ts @@ -1,12 +1,13 @@ -const { ValidationError } = require('joi'); -const Smtp = require('./Smtp'); -const log = require('../../../log'); +import { ValidationError } from 'joi'; +import { Smtp, SmtpConfiguration } from './Smtp'; +import log from '../../../log'; +import { Transporter } from 'nodemailer'; const smtp = new Smtp(); -const configurationValid = { +const configurationValid: SmtpConfiguration = { host: 'smtp.gmail.com', - port: '465', + port: 465, user: 'user', pass: 'pass', from: 'from@xx.com', @@ -43,7 +44,7 @@ test('validateConfiguration should throw error when invalid', () => { port: 'xyz', from: 'from@@xx.com', to: 'to@@xx.com', - }; + } as unknown as SmtpConfiguration; expect(() => { smtp.validateConfiguration(configuration); }).toThrowError(ValidationError); @@ -74,7 +75,7 @@ test('maskConfiguration should mask sensitive data', () => { port: configurationValid.port, user: configurationValid.user, pass: configurationValid.pass, - }; + } as SmtpConfiguration; expect(smtp.maskConfiguration()).toEqual({ host: configurationValid.host, port: configurationValid.port, @@ -86,8 +87,8 @@ test('maskConfiguration should mask sensitive data', () => { test('trigger should format mail as expected', async () => { smtp.configuration = configurationValid; smtp.transporter = { - sendMail: (conf) => conf, - }; + sendMail: (conf: any) => conf, + } as unknown as Transporter; const response = await smtp.trigger({ id: '31a61a8305ef1fc9a71fa4f20a68d7ec88b28e32303bbc4a5f192e851165b816', name: 'homeassistant', @@ -128,8 +129,8 @@ test('trigger should format mail as expected', async () => { test('triggerBatch should format mail as expected', async () => { smtp.configuration = configurationValid; smtp.transporter = { - sendMail: (conf) => conf, - }; + sendMail: (conf: any) => conf, + } as unknown as Transporter; const response = await smtp.triggerBatch([ { id: '31a61a8305ef1fc9a71fa4f20a68d7ec88b28e32303bbc4a5f192e851165b816', diff --git a/app/triggers/providers/smtp/Smtp.js b/app/triggers/providers/smtp/Smtp.ts similarity index 80% rename from app/triggers/providers/smtp/Smtp.js rename to app/triggers/providers/smtp/Smtp.ts index c32240ee..0e109272 100644 --- a/app/triggers/providers/smtp/Smtp.js +++ b/app/triggers/providers/smtp/Smtp.ts @@ -1,10 +1,25 @@ -const nodemailer = require('nodemailer'); -const Trigger = require('../Trigger'); +import nodemailer, { Transporter } from 'nodemailer'; +import { Trigger, TriggerConfiguration } from '../Trigger'; +import { Container } from '../../../model/container'; + +export interface SmtpConfiguration extends TriggerConfiguration { + host: string; + port: number; + user?: string; + pass?: string; + from: string; + to: string; + tls?: { + enabled: boolean; + verify: boolean; + }; +} /** * SMTP Trigger implementation */ -class Smtp extends Trigger { +export class Smtp extends Trigger { + transporter!: Transporter; /** * Get the Trigger configuration schema. * @returns {*} @@ -12,6 +27,7 @@ class Smtp extends Trigger { getConfigurationSchema() { return this.joi.object().keys({ host: [ + // allow IP address or hostname this.joi.string().hostname().required(), this.joi.string().ip().required(), ], @@ -34,7 +50,6 @@ class Smtp extends Trigger { /** * Sanitize sensitive data - * @returns {*} */ maskConfiguration() { return { @@ -52,7 +67,7 @@ class Smtp extends Trigger { /** * Init trigger. */ - initTrigger() { + async initTrigger() { let auth; if (this.configuration.user || this.configuration.pass) { auth = { @@ -75,11 +90,8 @@ class Smtp extends Trigger { /** * Send a mail with new container version details. - * - * @param container the container - * @returns {Promise} */ - async trigger(container) { + async trigger(container: Container) { return this.transporter.sendMail({ from: this.configuration.from, to: this.configuration.to, @@ -90,10 +102,8 @@ class Smtp extends Trigger { /** * Send a mail with new container versions details. - * @param containers - * @returns {Promise} */ - async triggerBatch(containers) { + async triggerBatch(containers: Container[]) { return this.transporter.sendMail({ from: this.configuration.from, to: this.configuration.to, @@ -102,5 +112,3 @@ class Smtp extends Trigger { }); } } - -module.exports = Smtp; diff --git a/app/triggers/providers/telegram/Telegram.test.js b/app/triggers/providers/telegram/Telegram.test.ts similarity index 90% rename from app/triggers/providers/telegram/Telegram.test.js rename to app/triggers/providers/telegram/Telegram.test.ts index aba69592..a96b2b0e 100644 --- a/app/triggers/providers/telegram/Telegram.test.js +++ b/app/triggers/providers/telegram/Telegram.test.ts @@ -1,9 +1,9 @@ -const { ValidationError } = require('joi'); -const Telegram = require('./Telegram'); +import { ValidationError } from 'joi'; +import { Telegram, TelegramConfiguration } from './Telegram'; const telegram = new Telegram(); -const configurationValid = { +const configurationValid: TelegramConfiguration = { bottoken: 'token', chatid: '123456789', threshold: 'all', @@ -30,7 +30,7 @@ test('validateConfiguration should return validated configuration when valid', ( }); test('validateConfiguration should throw error when invalid', () => { - const configuration = {}; + const configuration = {} as TelegramConfiguration; expect(() => { telegram.validateConfiguration(configuration); }).toThrowError(ValidationError); diff --git a/app/triggers/providers/telegram/Telegram.js b/app/triggers/providers/telegram/Telegram.ts similarity index 71% rename from app/triggers/providers/telegram/Telegram.js rename to app/triggers/providers/telegram/Telegram.ts index 3bab2e6e..cd3cccf6 100644 --- a/app/triggers/providers/telegram/Telegram.js +++ b/app/triggers/providers/telegram/Telegram.ts @@ -1,10 +1,18 @@ -const TelegramBot = require('node-telegram-bot-api'); -const Trigger = require('../Trigger'); +import TelegramBot from 'node-telegram-bot-api'; +import { Trigger, TriggerConfiguration } from '../Trigger'; +import { Container } from '../../../model/container'; + +export interface TelegramConfiguration extends TriggerConfiguration { + bottoken: string; + chatid: string; +} /** * Telegram Trigger implementation */ -class Telegram extends Trigger { +export class Telegram extends Trigger { + telegramBot!: TelegramBot; + /** * Get the Trigger configuration schema. * @returns {*} @@ -18,7 +26,6 @@ class Telegram extends Trigger { /** * Sanitize sensitive data - * @returns {*} */ maskConfiguration() { return { @@ -30,7 +37,6 @@ class Telegram extends Trigger { /** * Init trigger (create telegram client). - * @returns {Promise} */ async initTrigger() { this.telegramBot = new TelegramBot(this.configuration.bottoken); @@ -42,11 +48,11 @@ class Telegram extends Trigger { * @param image the image * @returns {Promise} */ - async trigger(container) { + async trigger(container: Container) { return this.sendMessage(this.renderSimpleBody(container)); } - async triggerBatch(containers) { + async triggerBatch(containers: Container[]) { return this.sendMessage(this.renderBatchBody(containers)); } @@ -55,9 +61,7 @@ class Telegram extends Trigger { * @param text the text to post * @returns {Promise<>} */ - async sendMessage(text) { + async sendMessage(text: string) { return this.telegramBot.sendMessage(this.configuration.chatid, text); } } - -module.exports = Telegram; diff --git a/app/tsconfig.json b/app/tsconfig.json new file mode 100644 index 00000000..61371c72 --- /dev/null +++ b/app/tsconfig.json @@ -0,0 +1,15 @@ +{ + // This is an alias to @tsconfig/node16: https://github.com/tsconfig/bases + "extends": "ts-node/node16/tsconfig.json", + "ts-node": { + "transpileOnly": true, + "files": true, + "compilerOptions": {} + }, + "compilerOptions": { + "module": "CommonJS", + "target": "ESNext", + "outDir": "dist", + "resolveJsonModule": true + } +} \ No newline at end of file diff --git a/app/types/aws-maintenance.d.ts b/app/types/aws-maintenance.d.ts new file mode 100644 index 00000000..b341c2a1 --- /dev/null +++ b/app/types/aws-maintenance.d.ts @@ -0,0 +1,7 @@ +module 'aws-sdk/lib/maintenance_mode_message' { + let maintenanceModeMessage: { + suppress: boolean; + }; + + export = maintenanceModeMessage; +} \ No newline at end of file diff --git a/app/types/capitalize.d.ts b/app/types/capitalize.d.ts new file mode 100644 index 00000000..971f8823 --- /dev/null +++ b/app/types/capitalize.d.ts @@ -0,0 +1,4 @@ +declare module 'capitalize' { + var capitalize: any; + export = capitalize; + } \ No newline at end of file diff --git a/app/types/connect-loki.d.ts b/app/types/connect-loki.d.ts new file mode 100644 index 00000000..c05496ac --- /dev/null +++ b/app/types/connect-loki.d.ts @@ -0,0 +1,83 @@ +declare module "connect-loki" { + import { Store, SessionData } from "express-session"; + + export interface ConnectLokiOptions { + /** + * When autosave is enabled the database file will be written automatically, disabling this saves a write per session change. + * Defaults to true. + */ + autosave?: boolean; + /** + * The file path to save the Loki database. Defaults to './session-store.db' + */ + path?: string; + /** + * The session time-to-live in seconds. Defaults to 1209600. A value of 0 implies no TTL. + */ + ttl?: number; + /** + * An optional error logging function or a boolean. + */ + logErrors?: ((err: any) => void) | boolean; + } + + export interface LokiStore extends Store { + /** + * Fetch the session by its sid. + * @param sid - Session ID. + * @param callback - Callback with error or session data. + */ + get(sid: string, callback: (err: any, session?: SessionData | null) => void): void; + + /** + * Commit the given session object to the store. + * @param sid - Session ID. + * @param session - Session data. + * @param callback - Optional callback on completion. + */ + set(sid: string, session: SessionData, callback?: (err?: any) => void): void; + + /** + * Destroy the session identified by sid. + * @param sid - Session ID. + * @param callback - Optional callback on completion. + */ + destroy(sid: string, callback?: (err?: any) => void): void; + + /** + * Clear all sessions from the store. + * @param callback - Optional callback on completion. + */ + clear(callback?: (err?: any) => void): void; + + /** + * Get the count of all sessions in the store. + * @param callback - Callback with error or count. + */ + length(callback: (err: any, length: number) => void): void; + + /** + * Refresh the time-to-live for the session identified by sid. + * @param sid - Session ID. + * @param session - Session data. + * @param callback - Optional callback on completion. + */ + touch(sid: string, session: SessionData, callback?: () => void): void; + } + + /** + * The exported factory function expects an instance of session (from express-session), + * and returns a LokiStore constructor that extends session.Store. + * + * Usage: + * import session from "express-session"; + * import connectLoki from "connect-loki"; + * const LokiStore = connectLoki(session); + * const store = new LokiStore(options); + */ + function connectLoki(session: any): { + new(options?: ConnectLokiOptions): LokiStore; + }; + + export = connectLoki; +} \ No newline at end of file diff --git a/app/types/dockerode.d.ts b/app/types/dockerode.d.ts new file mode 100644 index 00000000..67be4bd3 --- /dev/null +++ b/app/types/dockerode.d.ts @@ -0,0 +1,7 @@ +import 'dockerode'; + +declare module 'dockerode' { + interface ImageInspectInfo { + Variant: string; + } +} \ No newline at end of file diff --git a/app/types/express-healthcheck.d.ts b/app/types/express-healthcheck.d.ts new file mode 100644 index 00000000..29ba9310 --- /dev/null +++ b/app/types/express-healthcheck.d.ts @@ -0,0 +1,4 @@ +declare module 'express-healthcheck' { + var express_healthcheck: any; + export = express_healthcheck; + } \ No newline at end of file diff --git a/app/types/joi-cron-expression.d.ts b/app/types/joi-cron-expression.d.ts new file mode 100644 index 00000000..275f4cf3 --- /dev/null +++ b/app/types/joi-cron-expression.d.ts @@ -0,0 +1,14 @@ + +declare module 'joi-cron-expression' { + import Joi, { StringSchema } from 'joi'; + + export default function joiCronExpression(joi: Joi.Root): Root; + + interface StringWithCronSchema extends StringSchema { + cron(): this; + } + + interface Root extends Joi.Root { + string(): StringWithCronSchema; + } +} \ No newline at end of file diff --git a/app/types/parse-docker-image-name.d.ts b/app/types/parse-docker-image-name.d.ts new file mode 100644 index 00000000..a92311a3 --- /dev/null +++ b/app/types/parse-docker-image-name.d.ts @@ -0,0 +1,18 @@ +declare module 'parse-docker-image-name' { + interface ParsedDockerImage { + domain?: string; + path?: string; + tag?: string; + digest?: string; + } + + /** + * Parses a Docker image name and returns its components. + * @param image The Docker image name as a string or an array of strings. + * @returns An object (or an array of objects) containing the parsed components. + */ + function parse(image: string): ParsedDockerImage; + function parse(image: string[]): ParsedDockerImage[]; + + export = parse; +} \ No newline at end of file diff --git a/app/types/pass.d.ts b/app/types/pass.d.ts new file mode 100644 index 00000000..0f60d139 --- /dev/null +++ b/app/types/pass.d.ts @@ -0,0 +1,7 @@ +declare module 'pass' { + export function validate( + password: string, + hash: string, + callback: (err: Error | null, success: boolean) => void + ): void; +} \ No newline at end of file diff --git a/app/types/pushover-notifications.d.ts b/app/types/pushover-notifications.d.ts new file mode 100644 index 00000000..d9789bff --- /dev/null +++ b/app/types/pushover-notifications.d.ts @@ -0,0 +1,57 @@ +declare module "pushover-notifications" { + import { IncomingMessage } from "http"; + + export interface PushoverOptions { + token: string; + user: string; + httpOptions?: { + proxy?: string; + [key: string]: any; + }; + debug?: boolean; + onerror?: (error: Error | string, response?: IncomingMessage) => void; + update_sounds?: boolean; + } + + export interface SendMessageOptions { + token?: string; + user?: string; + message: string; + title?: string; + device?: string; + url?: string; + html?: 0 | 1; + expire?: number; + retry?: number; + ttl?: number; + url_title?: string; + priority?: number; + timestamp?: number; + sound?: string; + file?: string | { name: string; data: Buffer; type?: string }; + } + + interface SoundList { + [key: string]: string; + } + + type SendCallback = (error: Error | null, data?: string, response?: IncomingMessage) => void; + + class Pushover { + constructor(options: PushoverOptions); + + boundary: string; + token: string; + user: string; + httpOptions?: PushoverOptions["httpOptions"]; + sounds: SoundList; + debug?: boolean; + onerror?: PushoverOptions["onerror"]; + + send(message: SendMessageOptions, callback?: SendCallback): void; + updateSounds(): void; + errors(data: string | Error, response?: IncomingMessage): void; + } + + export = Pushover; +} \ No newline at end of file diff --git a/app/watchers/Watcher.ts b/app/watchers/Watcher.ts new file mode 100644 index 00000000..39425365 --- /dev/null +++ b/app/watchers/Watcher.ts @@ -0,0 +1,19 @@ +import { Container } from "../model/container"; +import { BaseConfig, Component } from "../registry/Component"; + +export class Watcher extends Component { + async watch(): Promise { + throw new Error("Method not implemented."); + } + + async getContainers(): Promise { + throw new Error("Method not implemented."); + } + + async watchContainer(container: Container): Promise<{ + container: Container; + changed?: boolean; + }> { + throw new Error("Method not implemented."); + } +} \ No newline at end of file diff --git a/app/watchers/providers/docker/Docker.test.js b/app/watchers/providers/docker/Docker.test.ts similarity index 82% rename from app/watchers/providers/docker/Docker.test.js rename to app/watchers/providers/docker/Docker.test.ts index e8bb31b1..48562e38 100644 --- a/app/watchers/providers/docker/Docker.test.js +++ b/app/watchers/providers/docker/Docker.test.ts @@ -1,22 +1,23 @@ -const { ValidationError } = require('joi'); -const log = require('../../../log'); -const prometheusWatcher = require('../../../prometheus/watcher'); +import { ValidationError } from 'joi'; +import * as prometheusWatcher from '../../../prometheus/watcher'; jest.mock('../../../event'); jest.mock('../../../log'); -const storeContainer = require('../../../store/container'); +import log from '../../../log'; -const Docker = require('./Docker'); -const Hub = require('../../../registries/providers/hub/Hub'); -const Ecr = require('../../../registries/providers/ecr/Ecr'); -const Gcr = require('../../../registries/providers/gcr/Gcr'); -const Acr = require('../../../registries/providers/acr/Acr'); +import * as storeContainer from '../../../store/container'; -const sampleSemver = require('../../samples/semver.json'); -const sampleCoercedSemver = require('../../samples/coercedSemver.json'); +import { Hub } from '../../../registries/providers/hub/Hub'; +import { Ecr } from '../../../registries/providers/ecr/Ecr'; +import { Gcr } from '../../../registries/providers/gcr/Gcr'; +import { Acr } from '../../../registries/providers/acr/Acr'; + +import sampleSemver from '../../samples/semver.json'; +import sampleCoercedSemver from '../../samples/coercedSemver.json'; +import { Container } from '../../../model/container'; +import Dockerode from 'dockerode'; -let docker; const hub = new Hub(); hub.kind = 'registry'; hub.type = 'hub'; @@ -37,6 +38,28 @@ acr.kind = 'registry'; acr.type = 'acr'; acr.name = 'private'; +jest.mock('../../../registry/states', () => { + return { + states: { + trigger: {}, + watcher: {}, + registry: { + acr: acr, + ecr: ecr, + gcr: gcr, + hub: hub, + }, + authentication: {}, + }, + getState: () => { return states; }, + }; +}); + +import { Docker, DockerConfiguration, normalizeContainer, getTagCandidates, getOldContainers, getRegistries, isContainerToWatch, isDigestToWatch, getRegistry } from './Docker'; +import { states } from '../../../registry/states'; + +let docker: Docker; + const configurationValid = { socket: '/var/run/docker.sock', port: 2375, @@ -65,16 +88,9 @@ afterEach(() => { docker.deregister(); }); -Docker.__set__('getRegistries', () => ({ - acr, - ecr, - gcr, - hub, -})); - -Docker.__set__('getWatchContainerGauge', () => ({ - set: () => {}, -})); +// Docker.__set__('getWatchContainerGauge', () => ({ +// set: () => { }, +// })); test('validatedConfiguration should initialize when configuration is valid', () => { const validatedConfiguration = @@ -83,20 +99,20 @@ test('validatedConfiguration should initialize when configuration is valid', () }); test('validatedConfiguration should initialize with default values when not provided', () => { - const validatedConfiguration = docker.validateConfiguration({}); + const validatedConfiguration = docker.validateConfiguration({} as DockerConfiguration); expect(validatedConfiguration).toStrictEqual(configurationValid); }); test('validatedConfiguration should failed when configuration is invalid', () => { expect(() => { - docker.validateConfiguration({ watchbydefault: 'xxx' }); + docker.validateConfiguration({ watchbydefault: 'xxx' } as unknown as DockerConfiguration); }).toThrowError(ValidationError); }); test('initWatcher should create a configured DockerApi instance', () => { docker.configuration = docker.validateConfiguration(configurationValid); docker.initWatcher(); - expect(docker.dockerApi.modem.socketPath).toBe(configurationValid.socket); + expect((docker.dockerApi.modem as any).socketPath).toBe(configurationValid.socket); }); const getTagCandidatesTestCases = [ @@ -175,8 +191,8 @@ test.each(getTagCandidatesTestCases)( 'getTagCandidates should behave as expected', (item) => { expect( - Docker.__get__('getTagCandidates')( - item.source, + getTagCandidates( + item.source as Container, item.items, docker.log, ), @@ -186,7 +202,7 @@ test.each(getTagCandidatesTestCases)( test('normalizeContainer should return ecr when applicable', () => { expect( - Docker.__get__('normalizeContainer')({ + normalizeContainer({ id: '31a61a8305ef1fc9a71fa4f20a68d7ec88b28e32303bbc4a5f192e851165b816', name: 'homeassistant', watcher: 'local', @@ -236,7 +252,7 @@ test('normalizeContainer should return ecr when applicable', () => { test('normalizeContainer should return gcr when applicable', () => { expect( - Docker.__get__('normalizeContainer')({ + normalizeContainer({ id: '31a61a8305ef1fc9a71fa4f20a68d7ec88b28e32303bbc4a5f192e851165b816', name: 'homeassistant', watcher: 'local', @@ -286,7 +302,7 @@ test('normalizeContainer should return gcr when applicable', () => { test('normalizeContainer should return acr when applicable', () => { expect( - Docker.__get__('normalizeContainer')({ + normalizeContainer({ id: '31a61a8305ef1fc9a71fa4f20a68d7ec88b28e32303bbc4a5f192e851165b816', name: 'homeassistant', watcher: 'local', @@ -336,7 +352,7 @@ test('normalizeContainer should return acr when applicable', () => { test('normalizeContainer should return original container when no matching provider found', () => { expect( - Docker.__get__('normalizeContainer')({ + normalizeContainer({ id: '31a61a8305ef1fc9a71fa4f20a68d7ec88b28e32303bbc4a5f192e851165b816', name: 'homeassistant', watcher: 'local', @@ -385,8 +401,8 @@ test('normalizeContainer should return original container when no matching provi }); test('findNewVersion should return new image version when found', async () => { - hub.getTags = () => ['7.8.9']; - hub.getImageManifestDigest = () => ({ + hub.getTags = () => Promise.resolve(['7.8.9']); + hub.getImageManifestDigest = () => Promise.resolve({ digest: 'sha256:abcdef', version: 2, }); @@ -399,8 +415,8 @@ test('findNewVersion should return new image version when found', async () => { }); test('findNewVersion should return same result as current when no image version found', async () => { - hub.getTags = () => []; - hub.getImageManifestDigest = () => ({ + hub.getTags = () => Promise.resolve([]); + hub.getImageManifestDigest = () => Promise.resolve({ digest: 'sha256:abcdef', version: 2, }); @@ -413,7 +429,7 @@ test('findNewVersion should return same result as current when no image version }); test('addImageDetailsToContainer should add an image definition to the container', async () => { - storeContainer.getContainer = () => undefined; + jest.spyOn(storeContainer, 'getContainer').mockReturnValue(undefined); docker.dockerApi = { getImage: () => ({ inspect: () => ({ @@ -431,7 +447,7 @@ test('addImageDetailsToContainer should add an image definition to the container }, }), }), - }; + } as unknown as Dockerode; const container = { Id: 'container-123456789', Image: 'organization/image:version', @@ -440,7 +456,7 @@ test('addImageDetailsToContainer should add an image definition to the container }; const containerWithImage = - await docker.addImageDetailsToContainer(container); + await docker.addImageDetailsToContainer(container as Dockerode.ContainerInfo); expect(containerWithImage).toMatchObject({ id: 'container-123456789', name: 'test', @@ -476,7 +492,7 @@ test('addImageDetailsToContainer should support transforms', async () => { Os: 'os', }), }), - }; + } as unknown as Dockerode; const container = { Id: 'container-123456789', Image: 'organization/image:version', @@ -486,7 +502,7 @@ test('addImageDetailsToContainer should support transforms', async () => { const tagTransform = '^(version)$ => $1-1.0.0'; const containerWithImage = await docker.addImageDetailsToContainer( - container, + container as Dockerode.ContainerInfo, undefined, // tagInclude undefined, // tagExclude tagTransform, @@ -505,12 +521,12 @@ test('addImageDetailsToContainer should support transforms', async () => { }); test('watchContainer should return container report when found', async () => { - storeContainer.getContainer = () => undefined; - storeContainer.insertContainer = (container) => container; - docker.findNewVersion = () => ({ + jest.spyOn(storeContainer, 'getContainer').mockReturnValue(undefined); + jest.spyOn(storeContainer, 'insertContainer').mockImplementation((container) => container); + docker.findNewVersion = () => Promise.resolve({ tag: '7.8.9', }); - hub.getTags = () => ['7.8.9']; + hub.getTags = () => Promise.resolve(['7.8.9']); await expect(docker.watchContainer(sampleSemver)).resolves.toMatchObject({ changed: true, container: { @@ -522,10 +538,10 @@ test('watchContainer should return container report when found', async () => { }); test('watchContainer should return container report when no image version found', async () => { - storeContainer.getContainer = () => undefined; - storeContainer.insertContainer = (container) => container; - docker.findNewVersion = () => undefined; - hub.getTags = () => []; + jest.spyOn(storeContainer, 'getContainer').mockReturnValue(undefined); + jest.spyOn(storeContainer, 'insertContainer').mockImplementation((container) => container); + docker.findNewVersion = async () => undefined; + hub.getTags = () => Promise.resolve([]); await expect(docker.watchContainer(sampleSemver)).resolves.toMatchObject({ changed: true, container: { @@ -535,8 +551,8 @@ test('watchContainer should return container report when no image version found' }); test('watchContainer should return container report with error when something bad happens', async () => { - storeContainer.getContainer = () => undefined; - storeContainer.insertContainer = (container) => container; + jest.spyOn(storeContainer, 'getContainer').mockReturnValue(undefined); + jest.spyOn(storeContainer, 'insertContainer').mockImplementation((container) => container); docker.findNewVersion = () => { throw new Error('Failure!!!'); }; @@ -546,9 +562,8 @@ test('watchContainer should return container report with error when something ba }); test('watch should return a list of containers with changed', async () => { - storeContainer.getContainer = () => undefined; - storeContainer.insertContainer = (containerWithResult) => - containerWithResult; + jest.spyOn(storeContainer, 'getContainer').mockReturnValue(undefined); + jest.spyOn(storeContainer, 'insertContainer').mockImplementation((container) => container); const container1 = { Id: 'container-123456789', @@ -576,7 +591,7 @@ test('watch should return a list of containers with changed', async () => { Id: 'image-123456789', }), }), - }; + } as unknown as Dockerode; await expect(docker.watch()).resolves.toMatchObject([ { changed: true, @@ -588,9 +603,8 @@ test('watch should return a list of containers with changed', async () => { }); test('watch should log error when watching a container fails', async () => { - storeContainer.getContainer = () => undefined; - storeContainer.insertContainer = (containerWithResult) => - containerWithResult; + jest.spyOn(storeContainer, 'getContainer').mockReturnValue(undefined); + jest.spyOn(storeContainer, 'insertContainer').mockImplementation((container) => container); const container1 = { Id: 'container-123456789', Image: 'organization/image:version', @@ -617,12 +631,12 @@ test('watch should log error when watching a container fails', async () => { Id: 'image-123456789', }), }), - }; + } as unknown as Dockerode; // Fake conf docker.configuration = { watchbydefault: true, - }; + } as DockerConfiguration; const spylog = jest.spyOn(docker.log, 'warn'); docker.watchContainer = () => { throw new Error('Failure!!!'); @@ -645,22 +659,22 @@ test('watch should log error when an error occurs when listing containers fails' }); test('pruneOldContainers should prune old containers', () => { - const oldContainers = [{ id: 1 }, { id: 2 }]; - const newContainers = [{ id: 1 }]; + const oldContainers = [{ id: 1 }, { id: 2 }] as unknown as Container[]; + const newContainers = [{ id: 1 }] as unknown as Container[]; expect( - Docker.__get__('getOldContainers')(newContainers, oldContainers), + getOldContainers(newContainers, oldContainers), ).toEqual([{ id: 2 }]); }); test('pruneOldContainers should operate when lists are empty or undefined', () => { - expect(Docker.__get__('getOldContainers')([], [])).toEqual([]); - expect(Docker.__get__('getOldContainers')(undefined, undefined)).toEqual( + expect(getOldContainers([], [])).toEqual([]); + expect(getOldContainers(undefined, undefined)).toEqual( [], ); }); test('getRegistries should return all registered registries when called', () => { - expect(Object.keys(Docker.__get__('getRegistries')())).toEqual([ + expect(Object.keys(getRegistries())).toEqual([ 'acr', 'ecr', 'gcr', @@ -669,11 +683,11 @@ test('getRegistries should return all registered registries when called', () => }); test('getRegistry should return all registered registries when called', () => { - expect(Docker.__get__('getRegistry')('acr')).toBeDefined(); + expect(getRegistry('acr')).toBeDefined(); }); test('getRegistry should return all registered registries when called', () => { - expect(() => Docker.__get__('getRegistry')('registry_fail')).toThrowError( + expect(() => getRegistry('registry_fail')).toThrowError( 'Unsupported Registry registry_fail', ); }); @@ -682,9 +696,9 @@ test('mapContainerToContainerReport should not emit event when no update availab const containerWithResult = { id: 'container-123456789', updateAvailable: false, - }; - storeContainer.getContainer = () => undefined; - storeContainer.insertContainer = () => containerWithResult; + } as Container; + jest.spyOn(storeContainer, 'getContainer').mockReturnValue(undefined); + jest.spyOn(storeContainer, 'insertContainer').mockImplementation((container) => container); expect(docker.mapContainerToContainerReport(containerWithResult)).toEqual({ changed: true, container: { @@ -740,8 +754,7 @@ const containerToWatchTestCases = [ test.each(containerToWatchTestCases)( 'isContainerToWatch should return $result when wud.watch label = $label and watchbydefault = $default ', (item) => { - const isContainerToWatch = Docker.__get__('isContainerToWatch'); - expect(isContainerToWatch(item.label, item.default)).toEqual( + expect(isContainerToWatch(item.label!, item.default)).toEqual( item.result, ); }, @@ -793,7 +806,6 @@ const digestToWatchTestCases = [ test.each(digestToWatchTestCases)( 'isDigestToWatch should return $result when wud.watch label = $label and semver = semver ', (item) => { - const isDigestToWatch = Docker.__get__('isDigestToWatch'); - expect(isDigestToWatch(item.label, item.semver)).toEqual(item.result); + expect(isDigestToWatch(item.label!, item.semver)).toEqual(item.result); }, ); diff --git a/app/watchers/providers/docker/Docker.js b/app/watchers/providers/docker/Docker.ts similarity index 80% rename from app/watchers/providers/docker/Docker.js rename to app/watchers/providers/docker/Docker.ts index ae4ecb71..52e4b181 100644 --- a/app/watchers/providers/docker/Docker.js +++ b/app/watchers/providers/docker/Docker.ts @@ -1,36 +1,21 @@ -const fs = require('fs'); -const Dockerode = require('dockerode'); -const joi = require('joi-cron-expression')(require('joi')); -const cron = require('node-cron'); -const parse = require('parse-docker-image-name'); -const debounce = require('just-debounce'); -const { - parse: parseSemver, - isGreater: isGreaterSemver, - transform: transformTag, -} = require('../../../tag'); -const event = require('../../../event'); -const { - wudWatch, - wudTagInclude, - wudTagExclude, - wudTagTransform, - wudWatchDigest, - wudLinkTemplate, - wudDisplayName, - wudDisplayIcon, - wudTriggerInclude, - wudTriggerExclude, -} = require('./label'); -const storeContainer = require('../../../store/container'); -const log = require('../../../log'); -const Component = require('../../../registry/Component'); -const { - validate: validateContainer, - fullName, -} = require('../../../model/container'); -const registry = require('../../../registry'); -const { getWatchContainerGauge } = require('../../../prometheus/watcher'); +import fs from 'fs'; +import Dockerode from 'dockerode'; +import joiBase from 'joi'; +import joiCronExpression from 'joi-cron-expression'; +const joi = joiCronExpression(joiBase); +import cron, { ScheduledTask } from 'node-cron'; +import parse from 'parse-docker-image-name'; +import debounce from 'just-debounce'; +import { parse as parseSemver, isGreater as isGreaterSemver, transform as transformTag } from '../../../tag'; +import * as event from '../../../event'; +import { WUD_LABELS } from './label'; +import { deleteContainer, getContainer, getContainers, insertContainer, updateContainer } from '../../../store/container'; +import log from '../../../log'; +import { Watcher } from '../../Watcher'; +import { validate as validateContainer, fullName, Container } from '../../../model/container'; +import { getState } from '../../../registry/states'; +import { getWatchContainerGauge } from '../../../prometheus/watcher'; +import Logger from 'bunyan'; // The delay before starting the watcher when the app is started const START_WATCHER_DELAY_MS = 1000; @@ -40,10 +25,9 @@ const DEBOUNCED_WATCH_CRON_MS = 5000; /** * Return all supported registries - * @returns {*} */ -function getRegistries() { - return registry.getState().registry; +export function getRegistries() { + return getState().registry; } /** @@ -52,7 +36,7 @@ function getRegistries() { * @param tags * @returns {*} */ -function getTagCandidates(container, tags, logContainer) { +export function getTagCandidates(container: Container, tags: string[], logContainer: Logger) { let filteredTags = tags; // Match include tag regex @@ -110,7 +94,7 @@ function getTagCandidates(container, tags, logContainer) { return filteredTags; } -function normalizeContainer(container) { +export function normalizeContainer(container: Container) { const containerWithNormalizedImage = container; const registryProvider = Object.values(getRegistries()).find((provider) => provider.match(container.image), @@ -132,7 +116,7 @@ function normalizeContainer(container) { * Get the Docker Registry by name. * @param registryName */ -function getRegistry(registryName) { +export function getRegistry(registryName: string) { const registryToReturn = getRegistries()[registryName]; if (!registryToReturn) { throw new Error(`Unsupported Registry ${registryName}`); @@ -144,9 +128,8 @@ function getRegistry(registryName) { * Get old containers to prune. * @param newContainers * @param containersFromTheStore - * @returns {*[]|*} */ -function getOldContainers(newContainers, containersFromTheStore) { +export function getOldContainers(newContainers: Container[] | undefined, containersFromTheStore: Container[] | undefined) { if (!containersFromTheStore || !newContainers) { return []; } @@ -163,18 +146,18 @@ function getOldContainers(newContainers, containersFromTheStore) { * @param newContainers * @param containersFromTheStore */ -function pruneOldContainers(newContainers, containersFromTheStore) { +function pruneOldContainers(newContainers: Container[], containersFromTheStore: Container[]) { const containersToRemove = getOldContainers( newContainers, containersFromTheStore, ); containersToRemove.forEach((containerToRemove) => { - storeContainer.deleteContainer(containerToRemove.id); + deleteContainer(containerToRemove.id); }); } -function getContainerName(container) { - let containerName; +function getContainerName(container: Dockerode.ContainerInfo) { + let containerName: string = ''; const names = container.Names; if (names && names.length > 0) { [containerName] = names; @@ -187,9 +170,8 @@ function getContainerName(container) { /** * Get image repo digest. * @param containerImage - * @returns {*} digest */ -function getRepoDigest(containerImage) { +function getRepoDigest(containerImage: Dockerode.ImageInspectInfo) { if ( !containerImage.RepoDigests || containerImage.RepoDigests.length === 0 @@ -205,9 +187,8 @@ function getRepoDigest(containerImage) { * Return true if container must be watched. * @param wudWatchLabelValue the value of the wud.watch label * @param watchByDefault true if containers must be watched by default - * @returns {boolean} */ -function isContainerToWatch(wudWatchLabelValue, watchByDefault) { +export function isContainerToWatch(wudWatchLabelValue: string, watchByDefault: boolean) { return wudWatchLabelValue !== undefined && wudWatchLabelValue !== '' ? wudWatchLabelValue.toLowerCase() === 'true' : watchByDefault; @@ -217,9 +198,8 @@ function isContainerToWatch(wudWatchLabelValue, watchByDefault) { * Return true if container digest must be watched. * @param wudWatchDigestLabelValue the value of wud.watch.digest label * @param isSemver if image is semver - * @returns {boolean|*} */ -function isDigestToWatch(wudWatchDigestLabelValue, isSemver) { +export function isDigestToWatch(wudWatchDigestLabelValue: string, isSemver: boolean) { let result = false; if (isSemver) { if ( @@ -240,12 +220,33 @@ function isDigestToWatch(wudWatchDigestLabelValue, isSemver) { return result; } +export interface DockerConfiguration { + socket: string; + host?: string; + port: number; + cafile?: string; + certfile?: string; + keyfile?: string; + cron: string; + watchbydefault: boolean; + watchall: boolean; + watchdigest?: any; + watchevents: boolean; + watchatstart: boolean; +} + /** * Docker Watcher Component. */ -class Docker extends Component { +export class Docker extends Watcher { + public dockerApi!: Dockerode; + private watchCron?: ScheduledTask; + private watchCronTimeout?: NodeJS.Timeout; + private watchCronDebounced?: () => void; + private listenDockerEventsTimeout?: NodeJS.Timeout; + getConfigurationSchema() { - return joi.object().keys({ + return joi.object().keys({ socket: this.joi.string().default('/var/run/docker.sock'), host: this.joi.string(), port: this.joi.number().port().default(2375), @@ -272,13 +273,13 @@ class Docker extends Component { ); } this.log.info(`Cron scheduled (${this.configuration.cron})`); - this.watchCron = cron.schedule(this.configuration.cron, () => + this.watchCron = cron.schedule(this.configuration.cron!, () => this.watchFromCron(), ); // Force watchatstart value based on the state store (empty or not) this.configuration.watchatstart = - storeContainer.getContainers().length === 0; + getContainers().length === 0; // watch at startup if enabled (after all components have been registered) if (this.configuration.watchatstart) { @@ -301,7 +302,7 @@ class Docker extends Component { } initWatcher() { - const options = {}; + const options: Dockerode.DockerOptions = {}; if (this.configuration.host) { options.host = this.configuration.host; options.port = this.configuration.port; @@ -322,7 +323,6 @@ class Docker extends Component { /** * Deregister the component. - * @returns {Promise} */ async deregisterComponent() { if (this.watchCron) { @@ -340,11 +340,10 @@ class Docker extends Component { /** * Listen and react to docker events. - * @return {Promise} */ async listenDockerEvents() { this.log.info('Listening to docker events'); - const options = { + const options: Dockerode.GetEventsOptions = { filters: { type: ['container'], event: [ @@ -366,32 +365,29 @@ class Docker extends Component { ); this.log.debug(err); } else { - stream.on('data', (chunk) => this.onDockerEvent(chunk)); + stream!.on('data', (chunk) => this.onDockerEvent(chunk)); } }); } /** * Process a docker event. - * @param dockerEventChunk - * @return {Promise} */ - async onDockerEvent(dockerEventChunk) { + async onDockerEvent(dockerEventChunk: string) { const dockerEvent = JSON.parse(dockerEventChunk.toString()); const action = dockerEvent.Action; const containerId = dockerEvent.id; // If the container was created or destroyed => perform a watch if (action === 'destroy' || action === 'create') { - await this.watchCronDebounced(); + this.watchCronDebounced!(); } else { // Update container state in db if so try { - const container = - await this.dockerApi.getContainer(containerId); + const container = await this.dockerApi.getContainer(containerId); const containerInspect = await container.inspect(); const newStatus = containerInspect.State.Status; - const containerFound = storeContainer.getContainer(containerId); + const containerFound = getContainer(containerId); if (containerFound) { // Child logger for the container to process const logContainer = this.log.child({ @@ -400,13 +396,13 @@ class Docker extends Component { const oldStatus = containerFound.status; containerFound.status = newStatus; if (oldStatus !== newStatus) { - storeContainer.updateContainer(containerFound); + updateContainer(containerFound); logContainer.info( `Status changed from ${oldStatus} to ${newStatus}`, ); } } - } catch (e) { + } catch (e: any) { this.log.debug( `Unable to get container details for container id=[${containerId}] (${e.message})`, ); @@ -416,7 +412,6 @@ class Docker extends Component { /** * Watch containers (called by cron scheduled tasks). - * @returns {Promise<*[]>} */ async watchFromCron() { this.log.info(`Cron started (${this.configuration.cron})`); @@ -444,10 +439,9 @@ class Docker extends Component { /** * Watch main method. - * @returns {Promise<*[]>} */ async watch() { - let containers = []; + let containers: Container[] = []; // Dispatch event to notify start watching event.emitWatcherStart(this); @@ -455,7 +449,7 @@ class Docker extends Component { // List images to watch try { containers = await this.getContainers(); - } catch (e) { + } catch (e: any) { this.log.warn( `Error when trying to get the list of the containers to watch (${e.message})`, ); @@ -466,7 +460,7 @@ class Docker extends Component { ); event.emitContainerReports(containerReports); return containerReports; - } catch (e) { + } catch (e: any) { this.log.warn( `Error when processing some containers (${e.message})`, ); @@ -480,9 +474,8 @@ class Docker extends Component { /** * Watch a Container. * @param container - * @returns {Promise<*>} */ - async watchContainer(container) { + async watchContainer(container: Container) { // Child logger for the container to process const logContainer = this.log.child({ container: fullName(container) }); const containerWithResult = container; @@ -497,7 +490,7 @@ class Docker extends Component { container, logContainer, ); - } catch (e) { + } catch (e: any) { logContainer.warn(`Error when processing (${e.message})`); logContainer.debug(e); containerWithResult.error = { @@ -513,10 +506,9 @@ class Docker extends Component { /** * Get all containers to watch. - * @returns {Promise} */ async getContainers() { - const listContainersOptions = {}; + const listContainersOptions: Dockerode.ContainerListOptions = {}; if (this.configuration.watchall) { listContainersOptions.all = true; } @@ -527,21 +519,21 @@ class Docker extends Component { // Filter on containers to watch const filteredContainers = containers.filter((container) => isContainerToWatch( - container.Labels[wudWatch], + container.Labels[WUD_LABELS.wudWatch], this.configuration.watchbydefault, ), ); const containerPromises = filteredContainers.map((container) => this.addImageDetailsToContainer( container, - container.Labels[wudTagInclude], - container.Labels[wudTagExclude], - container.Labels[wudTagTransform], - container.Labels[wudLinkTemplate], - container.Labels[wudDisplayName], - container.Labels[wudDisplayIcon], - container.Labels[wudTriggerInclude], - container.Labels[wudTriggerExclude], + container.Labels[WUD_LABELS.wudTagInclude], + container.Labels[WUD_LABELS.wudTagExclude], + container.Labels[WUD_LABELS.wudTagTransform], + container.Labels[WUD_LABELS.wudLinkTemplate], + container.Labels[WUD_LABELS.wudDisplayName], + container.Labels[WUD_LABELS.wudDisplayIcon], + container.Labels[WUD_LABELS.wudTriggerInclude], + container.Labels[WUD_LABELS.wudTriggerExclude], ), ); const containersWithImage = await Promise.all(containerPromises); @@ -553,16 +545,16 @@ class Docker extends Component { // Prune old containers from the store try { - const containersFromTheStore = storeContainer.getContainers({ + const containersFromTheStore = getContainers({ watcher: this.name, }); pruneOldContainers(containersToReturn, containersFromTheStore); - } catch (e) { + } catch (e: any) { this.log.warn( `Error when trying to prune the old containers (${e.message})`, ); } - getWatchContainerGauge().set( + getWatchContainerGauge()!.set( { type: this.type, name: this.name, @@ -577,9 +569,12 @@ class Docker extends Component { * Find new version for a Container. */ - async findNewVersion(container, logContainer) { - const registryProvider = getRegistry(container.image.registry.name); - const result = { tag: container.image.tag.value }; + async findNewVersion(container: Container, logContainer: Logger) { + const registryProvider = getRegistry(container.image.registry.name!); + const result: { + tag: string; + digest?: string; + } = { tag: container.image.tag.value }; if (!registryProvider) { logContainer.error( `Unsupported registry (${container.image.registry.name})`, @@ -614,7 +609,6 @@ class Docker extends Component { ); result.digest = remoteDigest.digest; - result.created = remoteDigest.created; if (remoteDigest.version === 2) { // Regular v2 manifest => Get manifest digest @@ -655,23 +649,22 @@ class Docker extends Component { * @param linkTemplate * @param displayName * @param displayIcon - * @returns {Promise} */ async addImageDetailsToContainer( - container, - includeTags, - excludeTags, - transformTags, - linkTemplate, - displayName, - displayIcon, - triggerInclude, - triggerExclude, + container: Dockerode.ContainerInfo, + includeTags?: string, + excludeTags?: string, + transformTags?: string, + linkTemplate?: string, + displayName?: string, + displayIcon?: string, + triggerInclude?: string, + triggerExclude?: string, ) { const containerId = container.Id; // Is container already in store? just return it :) - const containerInStore = storeContainer.getContainer(containerId); + const containerInStore = getContainer(containerId); if ( containerInStore !== undefined && containerInStore.error === undefined @@ -681,7 +674,8 @@ class Docker extends Component { } // Get container image details - const image = await this.dockerApi.getImage(container.Image).inspect(); + const dockerImage = this.dockerApi.getImage(container.Image); + const image = await dockerImage.inspect(); // Get useful properties const containerName = getContainerName(container); @@ -710,7 +704,7 @@ class Docker extends Component { const parsedTag = parseSemver(transformTag(transformTags, tagName)); const isSemver = parsedTag !== null && parsedTag !== undefined; const watchDigest = isDigestToWatch( - container.Labels[wudWatchDigest], + container.Labels[WUD_LABELS.wudWatchDigest], isSemver, ); if (!isSemver && !watchDigest) { @@ -734,9 +728,9 @@ class Docker extends Component { image: { id: imageId, registry: { - url: parsedImage.domain, + url: parsedImage.domain!, }, - name: parsedImage.path, + name: parsedImage.path!, tag: { value: tagName, semver: isSemver, @@ -760,19 +754,18 @@ class Docker extends Component { /** * Process a Container with result and map to a containerReport. * @param containerWithResult - * @return {*} */ - mapContainerToContainerReport(containerWithResult) { + mapContainerToContainerReport(containerWithResult: Container) { const logContainer = this.log.child({ container: fullName(containerWithResult), }); - const containerReport = { + const containerReport: { container: Container, changed?: boolean } = { container: containerWithResult, changed: false, }; // Find container in db & compare - const containerInDb = storeContainer.getContainer( + const containerInDb = getContainer( containerWithResult.id, ); @@ -780,19 +773,15 @@ class Docker extends Component { if (!containerInDb) { logContainer.debug('Container watched for the first time'); containerReport.container = - storeContainer.insertContainer(containerWithResult); + insertContainer(containerWithResult); containerReport.changed = true; // Found in DB? => update it } else { - containerReport.container = - storeContainer.updateContainer(containerWithResult); - containerReport.changed = - containerInDb.resultChanged(containerReport.container) && + containerReport.container = updateContainer(containerWithResult); + containerReport.changed = containerInDb.resultChanged!(containerReport.container) && containerWithResult.updateAvailable; } return containerReport; } -} - -module.exports = Docker; +} \ No newline at end of file diff --git a/app/watchers/providers/docker/label.js b/app/watchers/providers/docker/label.ts similarity index 97% rename from app/watchers/providers/docker/label.js rename to app/watchers/providers/docker/label.ts index 77745450..978347d7 100644 --- a/app/watchers/providers/docker/label.js +++ b/app/watchers/providers/docker/label.ts @@ -1,7 +1,7 @@ /** * WUD supported Docker labels. */ -module.exports = { +export const WUD_LABELS = { /** * Should the container be tracked? (true | false). */ diff --git a/e2e/features/api-container.feature b/e2e/features/api-container.feature index c87b9077..d0363714 100644 --- a/e2e/features/api-container.feature +++ b/e2e/features/api-container.feature @@ -21,11 +21,11 @@ Feature: WUD Container API Exposure | 0 | ecr.private | ecr_sub_sub_test | https://229211676173.dkr.ecr.eu-west-1.amazonaws.com/v2 | sub/sub/test | 1.0.0 | 2.0.0 | true | | 1 | ecr.private | ecr_sub_test | https://229211676173.dkr.ecr.eu-west-1.amazonaws.com/v2 | sub/test | 1.0.0 | 2.0.0 | true | | 2 | ecr.private | ecr_test | https://229211676173.dkr.ecr.eu-west-1.amazonaws.com/v2 | test | 1.0.0 | 2.0.0 | true | - | 3 | ghcr.private | ghcr_radarr | https://ghcr.io/v2 | linuxserver/radarr | 5.14.0.9383-ls245 | 5.21.1.9799-ls270 | true | + | 3 | ghcr.private | ghcr_radarr | https://ghcr.io/v2 | linuxserver/radarr | 5.14.0.9383-ls245 | 5.22.4.9896-ls272 | true | | 4 | gitlab.private | gitlab_test | https://registry.gitlab.com/v2 | manfred-martin/docker-registry-test | 1.0.0 | 2.0.0 | true | - | 5 | hub.public | hub_homeassistant_202161 | https://registry-1.docker.io/v2 | homeassistant/home-assistant | 2021.6.1 |2025.4.3 | true | + | 5 | hub.public | hub_homeassistant_202161 | https://registry-1.docker.io/v2 | homeassistant/home-assistant | 2021.6.1 |2025.4.4 | true | | 6 | hub.public | hub_homeassistant_latest | https://registry-1.docker.io/v2 | homeassistant/home-assistant | latest | latest | false | - | 7 | hub.public | hub_nginx_120 | https://registry-1.docker.io/v2 | library/nginx | 1.20-alpine | 1.27-alpine | true | + | 7 | hub.public | hub_nginx_120 | https://registry-1.docker.io/v2 | library/nginx | 1.20-alpine | 1.28-alpine | true | | 8 | hub.public | hub_nginx_latest | https://registry-1.docker.io/v2 | library/nginx | latest | latest | true | | 9 | hub.public | hub_omnidb_latest | https://registry-1.docker.io/v2 | omnidbteam/omnidb | latest | latest | true | | 10 | hub.public | hub_pihole_57 | https://registry-1.docker.io/v2 | pihole/pihole | v5.7 | v5.8.1 | true | @@ -36,7 +36,7 @@ Feature: WUD Container API Exposure | 15 | hub.public | hub_vaultwarden_1222 | https://registry-1.docker.io/v2 | vaultwarden/server | 1.33.2-alpine | 1.33.2-alpine | false | | 16 | hub.public | hub_vaultwarden_latest | https://registry-1.docker.io/v2 | vaultwarden/server | latest | latest | false | | 17 | hub.public | hub_youtubedb_latest | https://registry-1.docker.io/v2 | jeeaaasustest/youtube-dl | latest | latest | false | - | 18 | lscr.private | lscr_radarr | https://lscr.io/v2 | linuxserver/radarr | 5.14.0.9383-ls245 | 5.21.1.9799-ls270 | true | + | 18 | lscr.private | lscr_radarr | https://lscr.io/v2 | linuxserver/radarr | 5.14.0.9383-ls245 | 5.22.4.9896-ls272 | true | | 19 | quay.public | quay_prometheus | https://quay.io/v2 | prometheus/prometheus | v2.52.0 | v3.3.0 | true | Scenario: WUD must allow to get a container with semver @@ -85,7 +85,7 @@ Feature: WUD Container API Exposure Then response code should be 200 And response body should be valid json And response body path $.link should be https://github.com/home-assistant/core/releases/tag/2021.6.1 - And response body path $.result.link should be https://github.com/home-assistant/core/releases/tag/2025.4.3 + And response body path $.result.link should be https://github.com/home-assistant/core/releases/tag/2025.4.4 Scenario: WUD must allow to trigger a watch on a container Given I GET /api/containers diff --git a/e2e/features/prometheus.feature b/e2e/features/prometheus.feature index 5db09ca5..ed27834c 100644 --- a/e2e/features/prometheus.feature +++ b/e2e/features/prometheus.feature @@ -25,11 +25,11 @@ Feature: Prometheus exposure | ecr_sub_sub_test | ecr.private | https://229211676173.dkr.ecr.eu-west-1.amazonaws.com/v2 | sub/sub/test | 1.0.0 | 2.0.0 | true | | ecr_sub_test | ecr.private | https://229211676173.dkr.ecr.eu-west-1.amazonaws.com/v2 | sub/test | 1.0.0 | 2.0.0 | true | | ecr_test | ecr.private | https://229211676173.dkr.ecr.eu-west-1.amazonaws.com/v2 | test | 1.0.0 | 2.0.0 | true | - | ghcr_radarr | ghcr.private | https://ghcr.io/v2 | linuxserver/radarr | 5.14.0.9383-ls245 |5.21.1.9799-ls270 | true | + | ghcr_radarr | ghcr.private | https://ghcr.io/v2 | linuxserver/radarr | 5.14.0.9383-ls245 |5.22.4.9896-ls272 | true | | gitlab_test | gitlab.private | https://registry.gitlab.com/v2 | manfred-martin/docker-registry-test | 1.0.0 | 2.0.0 | true | - | hub_homeassistant_202161 | hub.public | https://registry-1.docker.io/v2 | homeassistant/home-assistant | 2021.6.1 |2025.4.3 | true | + | hub_homeassistant_202161 | hub.public | https://registry-1.docker.io/v2 | homeassistant/home-assistant | 2021.6.1 |2025.4.4 | true | | hub_homeassistant_latest | hub.public | https://registry-1.docker.io/v2 | homeassistant/home-assistant | latest | latest | false | - | hub_nginx_120 | hub.public | https://registry-1.docker.io/v2 | library/nginx | 1.20-alpine | 1.27-alpine | true | + | hub_nginx_120 | hub.public | https://registry-1.docker.io/v2 | library/nginx | 1.20-alpine | 1.28-alpine | true | | hub_nginx_latest | hub.public | https://registry-1.docker.io/v2 | library/nginx | latest | latest | true | | hub_omnidb_latest | hub.public | https://registry-1.docker.io/v2 | omnidbteam/omnidb | latest | latest | false | | hub_pihole_57 | hub.public | https://registry-1.docker.io/v2 | pihole/pihole | v5.7 | v5.8.1 | true | @@ -40,5 +40,5 @@ Feature: Prometheus exposure | hub_vaultwarden_1222 | hub.public | https://registry-1.docker.io/v2 | vaultwarden/server | 1.33.2-alpine | 1.33.2-alpine | false | | hub_vaultwarden_latest | hub.public | https://registry-1.docker.io/v2 | vaultwarden/server | latest | latest | false | | hub_youtubedb_latest | hub.public | https://registry-1.docker.io/v2 | jeeaaasustest/youtube-dl | latest | latest | false | - | lscr_radarr | lscr.private | https://lscr.io/v2 | linuxserver/radarr | 5.14.0.9383-ls245 |5.21.1.9799-ls270 | true | + | lscr_radarr | lscr.private | https://lscr.io/v2 | linuxserver/radarr | 5.14.0.9383-ls245 |5.22.4.9896-ls272 | true | | quay_prometheus | quay.public | https://quay.io/v2 | prometheus/prometheus | v2.52.0 |v3.3.0 | true |