diff --git a/.gitignore b/.gitignore index 72ad00e6c7..a4a7a2879f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # IntelliJ project files -.idea +.idea/** +!.idea/php.xml +!.idea/.name # Logs logs diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000000..100e370cac --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +VOL Monorepository \ No newline at end of file diff --git a/.idea/php.xml b/.idea/php.xml new file mode 100644 index 0000000000..bfa7ae1eff --- /dev/null +++ b/.idea/php.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.lintstagedrc.mjs b/.lintstagedrc.mjs index 0e97d9a419..2494ce2e36 100644 --- a/.lintstagedrc.mjs +++ b/.lintstagedrc.mjs @@ -1,6 +1,6 @@ import chalk from "chalk"; -import path from "path"; -import { execSync } from "child_process"; +import path from "node:path"; +import { execSync } from "node:child_process"; const generateTerraformDocs = (filenames) => { try { diff --git a/app/api/config/autoload/config.global.php b/app/api/config/autoload/config.global.php index 1105136891..6318d9bbfc 100644 --- a/app/api/config/autoload/config.global.php +++ b/app/api/config/autoload/config.global.php @@ -54,13 +54,8 @@ ], 'export' => [ 'driverClass' => \Doctrine\DBAL\Driver\PDO\MySQL\Driver::class, - 'params' => $doctrine_connection_params + - [ - 'driverOptions' => [ - PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false, - PDO::CURSOR_FWDONLY => true, - ], - ], + // Database connection details + 'params' => $doctrine_connection_params, ], ], 'driver' => [ diff --git a/app/api/config/autoload/local.php.dist b/app/api/config/autoload/local.php.dist index bdbd217e20..5710271e5b 100644 --- a/app/api/config/autoload/local.php.dist +++ b/app/api/config/autoload/local.php.dist @@ -346,12 +346,14 @@ return [ 'client' => [ // Guzzle client options; see https://docs.guzzlephp.org/en/stable/quickstart.html 'base_uri' => '', //param 'headers' => [], // additional or override default client headers + 'proxy' => new \Laminas\Stdlib\ArrayUtils\MergeRemoveKey(), ], 'oauth2' => [ // if client['headers']['Authorization'] is not set, then this will be used to get token 'client_id' => '', //param 'client_secret' => '', // secret 'token_url' => '', //param 'scope' => '', //param + 'proxy' => new \Laminas\Stdlib\ArrayUtils\MergeRemoveKey(), ], ], ]; diff --git a/app/internal/config/application.config.php b/app/internal/config/application.config.php index be7488a7c8..71e27a0088 100644 --- a/app/internal/config/application.config.php +++ b/app/internal/config/application.config.php @@ -86,12 +86,4 @@ // 'service_manager' => array(), ]; -if (file_exists(__DIR__ . '/../vendor/laminas/laminas-developer-tools/src/Module.php')) { - $config['modules'][] = 'Laminas\DeveloperTools'; - - if (file_exists(__DIR__ . '/../vendor/san/san-session-toolbar/src/Module.php')) { - $config['modules'][] = 'SanSessionToolbar'; - } -} - return $config; diff --git a/app/internal/config/autoload/local.php.dist b/app/internal/config/autoload/local.php.dist index 537e6bca75..8afe63d72c 100644 --- a/app/internal/config/autoload/local.php.dist +++ b/app/internal/config/autoload/local.php.dist @@ -18,7 +18,7 @@ return [ 'backend' => [ 'options' => [ // Backend service URI *Environment specific* - 'route' => 'backend-nginx', + 'route' => 'api.local.olcs.dev-dvsacloud.uk', ] ] ] @@ -31,11 +31,11 @@ return [ 'endpoints' => [ // Backend service URI *Environment specific* 'backend' => [ - 'url' => 'backend-nginx', + 'url' => 'api.local.olcs.dev-dvsacloud.uk', ], // Postcode/Address service URI *Environment specific* 'postcode' => [ - 'url' => 'backend-nginx', + 'url' => 'https://int.nonprod.address.dvsa.api.gov.uk/', ], ] ], @@ -65,7 +65,7 @@ return [ ], // Asset path, URI to olcs-static (CSS, JS, etc] *Environment specific* - 'asset_path' => 'http://localhost:7001', + 'asset_path' => 'http://cdn.local.olcs.dev-dvsacloud.uk/', 'openam' => [ 'url' => 'http://olcs-auth.olcs.gov.uk:8081/secure/', diff --git a/app/internal/config/development.config.php.dist b/app/internal/config/development.config.php.dist index 1b69fc4bd3..b1bb4350a4 100644 --- a/app/internal/config/development.config.php.dist +++ b/app/internal/config/development.config.php.dist @@ -3,6 +3,8 @@ return [ // Additional modules to include when in development mode 'modules' => [ + 'Laminas\DeveloperTools', + 'SanSessionToolbar', ], // Configuration overrides during development mode 'module_listener_options' => [ diff --git a/app/selfserve/config/application.config.php b/app/selfserve/config/application.config.php index d9f298179f..7298f6747e 100644 --- a/app/selfserve/config/application.config.php +++ b/app/selfserve/config/application.config.php @@ -69,12 +69,4 @@ ], ]; -if (file_exists(__DIR__ . '/../vendor/laminas/laminas-developer-tools/src/Module.php')) { - $config['modules'][] = 'Laminas\DeveloperTools'; - - if (file_exists(__DIR__ . '/../vendor/san/san-session-toolbar/src/Module.php')) { - $config['modules'][] = 'SanSessionToolbar'; - } -} - return $config; diff --git a/app/selfserve/config/autoload/local.php.dist b/app/selfserve/config/autoload/local.php.dist index a492760ebe..3852900cf3 100644 --- a/app/selfserve/config/autoload/local.php.dist +++ b/app/selfserve/config/autoload/local.php.dist @@ -18,7 +18,7 @@ return [ 'backend' => [ 'options' => [ // Backend service URI *Environment specific* - 'route' => 'backend-nginx', + 'route' => 'api.local.olcs.dev-dvsacloud.uk', ] ] ] @@ -31,17 +31,17 @@ return [ 'endpoints' => [ // Backend service URI *Environment specific* 'backend' => [ - 'url' => 'backend-nginx', + 'url' => 'api.local.olcs.dev-dvsacloud.uk', ], // Postcode/Address service URI *Environment specific* 'postcode' => [ - 'url' => 'http://address.reg.olcs.dev-dvsacloud.uk/', + 'url' => 'https://int.nonprod.address.dvsa.api.gov.uk/', ], ] ], // Asset path, URI to olcs-static (CSS, JS, etc) *Environment specific for local use http://127.0.0.1:7001* - 'asset_path' => 'http://localhost:7001', + 'asset_path' => 'http://cdn.local.olcs.dev-dvsacloud.uk/', 'openam' => new \Laminas\Stdlib\ArrayUtils\MergeRemoveKey(), diff --git a/app/selfserve/config/development.config.php.dist b/app/selfserve/config/development.config.php.dist index 1b69fc4bd3..b1bb4350a4 100644 --- a/app/selfserve/config/development.config.php.dist +++ b/app/selfserve/config/development.config.php.dist @@ -3,6 +3,8 @@ return [ // Additional modules to include when in development mode 'modules' => [ + 'Laminas\DeveloperTools', + 'SanSessionToolbar', ], // Configuration overrides during development mode 'module_listener_options' => [ diff --git a/infra/docker/cli/Dockerfile b/infra/docker/cli/Dockerfile index d0de99bd0b..72c8d3247c 100644 --- a/infra/docker/cli/Dockerfile +++ b/infra/docker/cli/Dockerfile @@ -47,6 +47,11 @@ RUN apk add --no-cache linux-headers $PHPIZE_DEPS \ && docker-php-ext-enable xdebug \ && apk del linux-headers $PHPIZE_DEPS +RUN apk update && \ + apk add --no-cache openldap-dev \ + && docker-php-ext-install ldap \ + && docker-php-ext-enable ldap + RUN \ # Disable OPCache in development. echo "opcache.enable=0" >> ${PHP_INI_DIR}/conf.d/1000-php.ini \ diff --git a/package-lock.json b/package-lock.json index 861d222d8f..ecbb95f79d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -431,6 +431,15 @@ "integrity": "sha512-xiNMgCuoy4mCL4JTywk9XFs5xpRUcKxtWEcMR6FNMtsgewYTIgIR+nvlP4A4iRCAzRsHMnPhvTRrzp4AGcRTEA==", "dev": true }, + "node_modules/@types/cli-progress": { + "version": "3.11.6", + "resolved": "https://registry.npmjs.org/@types/cli-progress/-/cli-progress-3.11.6.tgz", + "integrity": "sha512-cE3+jb9WRlu+uOSAugewNpITJDt1VF8dHOopPO4IABFc3SXYL5WE/+PTz/FCdZRRfIujiWW3n3aMbv1eIGVRWA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/conventional-commits-parser": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", @@ -478,12 +487,12 @@ "dev": true }, "node_modules/@types/node": { - "version": "22.3.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.3.0.tgz", - "integrity": "sha512-nrWpWVaDZuaVc5X84xJ0vNrLvomM205oQyLsRt7OHNZbSHslcWsvgFR7O7hire2ZonjLrWBbedmotmIlJDVd6g==", + "version": "22.4.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.4.1.tgz", + "integrity": "sha512-1tbpb9325+gPnKK0dMm+/LMriX0vKxf6RnB0SZUqfyVkQ4fMgUSySqhxE/y8Jvs4NyF1yHzTfG9KlnkIODxPKg==", "dev": true, "dependencies": { - "undici-types": "~6.18.2" + "undici-types": "~6.19.2" } }, "node_modules/@types/prompts": { @@ -671,6 +680,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-progress": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", + "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", + "dev": true, + "dependencies": { + "string-width": "^4.2.3" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/cli-truncate": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", @@ -687,6 +708,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-truncate/node_modules/emoji-regex": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", + "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "dev": true + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -725,35 +769,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/cliui/node_modules/emoji-regex": { - "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 - }, - "node_modules/cliui/node_modules/is-fullwidth-code-point": { - "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, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/string-width": { - "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, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/cliui/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -1002,9 +1017,9 @@ } }, "node_modules/emoji-regex": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", - "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "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 }, "node_modules/env-paths": { @@ -2196,20 +2211,47 @@ } }, "node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "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, "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=18" + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/is-fullwidth-code-point": { + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=8" } }, "node_modules/strip-ansi": { @@ -2350,9 +2392,9 @@ } }, "node_modules/undici-types": { - "version": "6.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.18.2.tgz", - "integrity": "sha512-5ruQbENj95yDYJNS3TvcaxPMshV7aizdv/hWYjGIKoANWKjhWNBsr2YEuYZKodQulB1b8l7ILOuDQep3afowQQ==", + "version": "6.19.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.6.tgz", + "integrity": "sha512-e/vggGopEfTKSvj4ihnOLTsqhrKRN3LeO6qSN/GxohhuRv8qH9bNQ4B8W7e/vFL+0XTnmHPB4/kegunZGA4Org==", "dev": true }, "node_modules/unicorn-magic": { @@ -2405,6 +2447,29 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", + "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "dev": true + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -2459,56 +2524,6 @@ "node": ">=12" } }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "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 - }, - "node_modules/yargs/node_modules/is-fullwidth-code-point": { - "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, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/string-width": { - "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, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", @@ -2531,14 +2546,17 @@ } }, "packages/local-refresh": { + "name": "@vol-app/local-refresh", "version": "1.0.0", "license": "MIT", "devDependencies": { "@tsconfig/recommended": "^1.0.7", + "@types/cli-progress": "^3.11.6", "@types/flat-cache": "^2.0.2", "@types/prompts": "^2.4.9", "@types/shelljs": "^0.8.15", "chalk": "^4.1.2", + "cli-progress": "^3.12.0", "commander": "^12.1.0", "dedent": "^1.5.3", "flat-cache": "^5.0.0", diff --git a/package.json b/package.json index c4439cd240..226c3b1703 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,6 @@ ], "scripts": { "prepare": "husky", - "refresh": "ts-node packages/local-refresh/index.ts" + "refresh": "npm run start --workspace @vol-app/local-refresh" } } diff --git a/packages/local-refresh/actions/ComposerInstall.ts b/packages/local-refresh/actions/ComposerInstall.ts deleted file mode 100644 index d56f31d2c1..0000000000 --- a/packages/local-refresh/actions/ComposerInstall.ts +++ /dev/null @@ -1,49 +0,0 @@ -import prompts from "prompts"; -import shell from "shelljs"; -import path from "path"; -import chalk from "chalk"; -import ActionInterface from "./ActionInterface"; -import createDebug from "debug"; - -const debug = createDebug("refresh:actions:ComposerInstall"); - -const phpAppDirectoryNames = ["api", "selfserve", "internal"]; -const phpAppDirectories = phpAppDirectoryNames.map((dir) => path.resolve(__dirname, `../../../app/${dir}`)); - -export default class ComposerInstall implements ActionInterface { - async prompt(): Promise { - const isComposerInstalled = shell.exec("composer --version", { silent: !debug.enabled }).code === 0; - - if (!isComposerInstalled) { - console.error(chalk.red("Error: Composer is not installed. Skipping Composer install...")); - return false; - } - - const response = await prompts({ - type: "confirm", - name: "composer-install", - message: "Install Composer dependencies?", - }); - - return response["composer-install"]; - } - - async execute(): Promise { - phpAppDirectories.forEach((dir) => { - debug(chalk.blue(`Running composer install in ${dir}...`)); - - if ( - shell.exec("composer install --no-interaction --no-progress", { - cwd: dir, - silent: !debug.enabled, - env: { - ...process.env, - FORCE_COLOR: "1", - }, - }).code !== 0 - ) { - console.error(chalk.red(`Error: Composer install failed in ${dir}`)); - } - }); - } -} diff --git a/packages/local-refresh/actions/CopyAppDistFiles.ts b/packages/local-refresh/actions/CopyAppDistFiles.ts deleted file mode 100644 index 64d58ef662..0000000000 --- a/packages/local-refresh/actions/CopyAppDistFiles.ts +++ /dev/null @@ -1,65 +0,0 @@ -import prompts from "prompts"; -import fs from "node:fs"; -import path from "path"; -import chalk from "chalk"; -import ActionInterface from "./ActionInterface"; -import createDebug from "debug"; - -const debug = createDebug("refresh:actions:CopyAppDistFiles"); - -const phpAppDirectoryNames = ["api", "selfserve", "internal"]; -const phpAppDirectories = phpAppDirectoryNames.map((dir) => path.resolve(__dirname, `../../../app/${dir}`)); - -export default class ResetDatabase implements ActionInterface { - filesToCopy: string[] = []; - - async prompt(): Promise { - const response = await prompts({ - type: "confirm", - name: "should-copy", - message: "Copy the Laminas configuration dist files?", - warn: "This will overwrite existing configuration files.", - }); - - if (!response["should-copy"]) { - return false; - } - - const appConfigDistFiles = phpAppDirectories - .map((dir) => { - const configDir = path.join(dir, "config"); - - return fs - .readdirSync(configDir, { recursive: true }) - .filter((file) => typeof file === "string") - .map((fileName) => { - return path.join(configDir, fileName); - }) - .filter((fileName) => fs.lstatSync(fileName).isFile()) - .filter((fileName) => fileName.endsWith(".dist")); - }) - .flat(); - - this.filesToCopy = ( - await prompts({ - type: "multiselect", - name: "files", - message: "Which config files do you want to copy?", - choices: appConfigDistFiles.map((file) => ({ title: file, value: file })), - hint: "- Space to select. Return to submit", - }) - ).files; - - return this.filesToCopy.length > 0; - } - - async execute(): Promise { - this.filesToCopy.forEach((file) => { - const destination = file.replace(".dist", ""); - - debug(chalk.greenBright(`Copying ${file} to ${destination}...`)); - - fs.copyFileSync(file, destination); - }); - } -} diff --git a/packages/local-refresh/actions/FlushRedis.ts b/packages/local-refresh/actions/FlushRedis.ts deleted file mode 100644 index b94fe79428..0000000000 --- a/packages/local-refresh/actions/FlushRedis.ts +++ /dev/null @@ -1,24 +0,0 @@ -import prompts from "prompts"; -import chalk from "chalk"; -import ActionInterface from "./ActionInterface"; -import shell from "shelljs"; - -export default class FlushRedis implements ActionInterface { - async prompt(): Promise { - const response = await prompts({ - type: "confirm", - name: "should-flush", - message: "Flush the Redis cache?", - warn: "This will remove all cached data.", - }); - - return response["should-flush"]; - } - - async execute(): Promise { - if (shell.exec(`docker compose exec redis redis-cli -c "FLUSHALL"`, { silent: true }).code !== 0) { - console.error(chalk.red(`Error: Failed to flush Redis cache`)); - return; - } - } -} diff --git a/packages/local-refresh/actions/ResetDatabase.ts b/packages/local-refresh/actions/ResetDatabase.ts deleted file mode 100644 index 98c4b7709b..0000000000 --- a/packages/local-refresh/actions/ResetDatabase.ts +++ /dev/null @@ -1,180 +0,0 @@ -import prompts from "prompts"; -import fs from "node:fs"; -import flatCache from "flat-cache"; -import shell from "shelljs"; -import chalk from "chalk"; -import ActionInterface from "./ActionInterface"; -import dedent from "dedent"; -import createDebug from "debug"; - -const debug = createDebug("refresh:actions:ResetDatabase"); - -const cache = flatCache.load("reset-database"); - -enum DatabaseRefreshEnum { - FULL, - MIGRATIONS, - NONE, -} - -export default class ResetDatabase implements ActionInterface { - bucketName = "devapp-olcs-pri-olcs-deploy-s3"; - - etlDirectory: string | undefined; - refreshType: DatabaseRefreshEnum = DatabaseRefreshEnum.NONE; - - async prompt(): Promise { - const response = await prompts([ - { - type: "confirm", - name: "database-refresh", - message: "Reset the database?", - }, - { - type: (prev) => (prev === true ? "select" : null), - name: "refresh-type", - message: "Choose the type of database reset?", - choices: [ - { title: "Full refresh", value: DatabaseRefreshEnum.FULL }, - { title: "Just migrations", value: DatabaseRefreshEnum.MIGRATIONS }, - ], - }, - ]); - - if (response["refresh-type"] === undefined) { - return false; - } - - this.refreshType = response["refresh-type"]; - - const etlDirectoryPrompt = await prompts({ - type: "text", - name: "directory", - message: "Enter the path to the ETL directory", - initial: cache.getKey("etlDirectory") || "../olcs-etl", - validate: (value) => (fs.existsSync(value) ? true : "Path does not exist"), - }); - - if (etlDirectoryPrompt.directory === undefined) { - return false; - } - - cache.setKey("etlDirectory", etlDirectoryPrompt.directory); - cache.save(); - - this.etlDirectory = etlDirectoryPrompt.directory; - - return this.refreshType !== DatabaseRefreshEnum.NONE; - } - - async execute(): Promise { - // Full reset requires AWS credentials to pull down the anonymised dataset from S3. - if (this.refreshType === DatabaseRefreshEnum.FULL) { - if (shell.exec("aws sts get-caller-identity").code !== 0) { - console.error( - chalk.red( - "Error: Valid AWS credentials are required for a full database reset. Authenticate with VOL `nonprod` and retry.", - ), - ); - - return; - } - } - - const myCnf = dedent` - [client] - user=root - password=olcs - host=host.docker.internal - port=3306`; - - if (this.refreshType !== DatabaseRefreshEnum.MIGRATIONS) { - if ( - shell.exec( - dedent`docker compose exec db /bin/bash -c "\ - echo '${myCnf}' > ~/.my.cnf; \ - cd /var/lib/etl \ - && ./create-base.sh olcs_be - " - `, - { - env: { - ...process.env, - FORCE_COLOR: "1", - }, - }, - ).code !== 0 - ) { - console.error(chalk.red(`Error: \`create-base.sh\` failed`)); - return; - } - } - - if ( - shell.exec( - `docker run \ - --rm \ - -e INSTALL_MYSQL=true \ - -v "$PWD/${this.etlDirectory}/":/liquibase/changelog \ - -w /liquibase/changelog \ - liquibase/liquibase \ - --defaultsFile=liquibase.properties \ - update \ - -Ddataset=testdata`, - { - env: { - ...process.env, - FORCE_COLOR: "1", - }, - }, - ).code !== 0 - ) { - console.error(chalk.red(`Error: \`liquibase\` failed`)); - return; - } - - // Fetch file from S3. - const latestAnonDatasetCmd = shell.exec( - `aws s3 ls s3://${this.bucketName}/anondata/olcs-db-localdev-anon-prod --recursive 2>/dev/null | sort | tail -n 1 | awk '{print $4}'`, - { - silent: !debug.enabled, - }, - ); - - if (latestAnonDatasetCmd.code !== 0) { - console.error(chalk.red("Error: Could not find the latest anonymised dataset on S3")); - return; - } - - const latestAnonDataset = latestAnonDatasetCmd.stdout.trim(); - - debug(chalk.greenBright(`Fetching the latest anonymised dataset from S3: ${latestAnonDataset}`)); - - if ( - shell.exec( - `aws s3 cp s3://${this.bucketName}/${latestAnonDataset} ${this.etlDirectory}/olcs-db-localdev-anon-prod.sql.gz`, - ).code !== 0 - ) { - console.error(chalk.red("Error: Could not fetch the latest anonymised dataset from S3")); - - shell.exec(`rm ${this.etlDirectory}/olcs-db-localdev-anon-prod.sql.gz`); - return; - } - - if ( - shell.exec( - `docker compose exec -T db /bin/bash -c 'zcat /var/lib/etl/olcs-db-localdev-anon-prod.sql.gz | mysql -u mysql -polcs olcs_be'`, - { - silent: !debug.enabled, - }, - ).code !== 0 - ) { - console.error(chalk.red("Error: Could not import the anonymised dataset into the database")); - - shell.exec(`rm ${this.etlDirectory}/olcs-db-localdev-anon-prod.sql.gz`); - return; - } - - shell.exec(`rm ${this.etlDirectory}/olcs-db-localdev-anon-prod.sql.gz`); - } -} diff --git a/packages/local-refresh/package.json b/packages/local-refresh/package.json index d095390a37..78e2f70c57 100644 --- a/packages/local-refresh/package.json +++ b/packages/local-refresh/package.json @@ -5,10 +5,12 @@ "license": "MIT", "devDependencies": { "@tsconfig/recommended": "^1.0.7", + "@types/cli-progress": "^3.11.6", "@types/flat-cache": "^2.0.2", "@types/prompts": "^2.4.9", "@types/shelljs": "^0.8.15", "chalk": "^4.1.2", + "cli-progress": "^3.12.0", "commander": "^12.1.0", "dedent": "^1.5.3", "flat-cache": "^5.0.0", @@ -18,6 +20,7 @@ "typescript": "~5.5.2" }, "scripts": { - "prepare": "husky" + "prepare": "husky", + "start": "ts-node src/index.ts" } } diff --git a/packages/local-refresh/actions/ActionInterface.ts b/packages/local-refresh/src/actions/ActionInterface.ts similarity index 71% rename from packages/local-refresh/actions/ActionInterface.ts rename to packages/local-refresh/src/actions/ActionInterface.ts index ba6e1c5ef3..11d7f8f267 100644 --- a/packages/local-refresh/actions/ActionInterface.ts +++ b/packages/local-refresh/src/actions/ActionInterface.ts @@ -1,3 +1,5 @@ +import { GenericBar } from "cli-progress"; + export default interface ActionInterface { /** * Prompt the user for input. @@ -9,5 +11,5 @@ export default interface ActionInterface { /** * Execute the action. */ - execute(): Promise; + execute(progressBar: GenericBar): Promise; } diff --git a/packages/local-refresh/src/actions/ComposerInstall.ts b/packages/local-refresh/src/actions/ComposerInstall.ts new file mode 100644 index 0000000000..6245aa7c7b --- /dev/null +++ b/packages/local-refresh/src/actions/ComposerInstall.ts @@ -0,0 +1,46 @@ +import prompts from "prompts"; +import exec from "../exec"; +import path from "node:path"; +import chalk from "chalk"; +import ActionInterface from "./ActionInterface"; +import createDebug from "debug"; +import { GenericBar } from "cli-progress"; + +const debug = createDebug("refresh:actions:ComposerInstall"); + +const phpAppDirectoryNames = ["api", "selfserve", "internal"]; +const phpAppDirectories = phpAppDirectoryNames.map((dir) => path.resolve(__dirname, `../../../../app/${dir}`)); + +export default class ComposerInstall implements ActionInterface { + async prompt(): Promise { + const { shouldInstall } = await prompts({ + type: "confirm", + name: "shouldInstall", + message: "Install Composer dependencies?", + }); + + return shouldInstall; + } + + async execute(progress: GenericBar): Promise { + progress.start(phpAppDirectories.length, 0); + + try { + exec("composer --version", debug); + } catch (e: unknown) { + throw new Error("Composer is not installed. Please install Composer before running this action."); + } + + phpAppDirectories.forEach((dir) => { + debug(chalk.blue(`Running composer install in ${dir}...`)); + + exec("composer install --no-interaction --no-progress", debug, { + cwd: dir, + }); + + progress.increment(); + }); + + progress.stop(); + } +} diff --git a/packages/local-refresh/src/actions/CopyAppDistFiles.ts b/packages/local-refresh/src/actions/CopyAppDistFiles.ts new file mode 100644 index 0000000000..2196298d05 --- /dev/null +++ b/packages/local-refresh/src/actions/CopyAppDistFiles.ts @@ -0,0 +1,89 @@ +import prompts from "prompts"; +import fs from "node:fs"; +import path from "node:path"; +import chalk from "chalk"; +import ActionInterface from "./ActionInterface"; +import createDebug from "debug"; +import { GenericBar } from "cli-progress"; + +const debug = createDebug("refresh:actions:CopyAppDistFiles"); + +const phpAppDirectoryNames = ["api", "selfserve", "internal"]; +const phpAppDirectories = phpAppDirectoryNames.map((dir) => path.resolve(__dirname, `../../../../app/${dir}`)); + +export default class ResetDatabase implements ActionInterface { + filesToCopy: string[] = []; + + async prompt(): Promise { + const { shouldCopy } = await prompts({ + type: "confirm", + name: "shouldCopy", + message: "Copy the Laminas configuration dist files?", + warn: "This will overwrite existing configuration files.", + }); + + if (!shouldCopy) { + return false; + } + + let appConfigDistFiles: Map = new Map(); + + for (const dir of phpAppDirectories) { + const configDir = path.join(dir, "config"); + + if (!fs.existsSync(configDir)) { + continue; + } + + const files = fs + .readdirSync(configDir, { recursive: true }) + .filter((file) => typeof file === "string") + .map((fileName) => { + return path.join(configDir, fileName); + }) + .filter((fileName) => fs.lstatSync(fileName).isFile()) + .filter((fileName) => fileName.endsWith(".dist")); + + files.forEach((file) => { + const truncatedPath = file.replace(path.dirname(dir), ""); + + appConfigDistFiles.set(file, truncatedPath); + }); + } + + const { files } = await prompts({ + type: "multiselect", + name: "files", + message: "Which config files do you want to copy?", + choices: Array.from(appConfigDistFiles.keys()).map((file) => ({ + title: appConfigDistFiles.get(file) || file, + value: file, + })), + hint: "- Space to select. Return to submit", + }); + + if (!files) { + return false; + } + + this.filesToCopy = files; + + return this.filesToCopy.length > 0; + } + + async execute(progress: GenericBar): Promise { + progress.start(this.filesToCopy.length, 0); + + this.filesToCopy.forEach((file) => { + const destination = file.replace(".dist", ""); + + debug(chalk.greenBright(`Copying ${file} to ${destination}...`)); + + fs.copyFileSync(file, destination); + + progress.increment(); + }); + + progress.stop(); + } +} diff --git a/packages/local-refresh/src/actions/FlushRedis.ts b/packages/local-refresh/src/actions/FlushRedis.ts new file mode 100644 index 0000000000..74c6516c65 --- /dev/null +++ b/packages/local-refresh/src/actions/FlushRedis.ts @@ -0,0 +1,28 @@ +import prompts from "prompts"; +import ActionInterface from "./ActionInterface"; +import exec from "../exec"; +import createDebug from "debug"; +import { GenericBar } from "cli-progress"; + +const debug = createDebug("refresh:actions:FlushRedis"); + +export default class FlushRedis implements ActionInterface { + async prompt(): Promise { + const { shouldFlush } = await prompts({ + type: "confirm", + name: "shouldFlush", + message: "Flush the Redis cache?", + warn: "This will remove all cached data.", + }); + + return shouldFlush; + } + + async execute(progress: GenericBar): Promise { + progress.start(1, 0); + + exec(`docker compose exec redis redis-cli -c "FLUSHALL"`, debug); + + progress.stop(); + } +} diff --git a/packages/local-refresh/src/actions/ResetDatabase.ts b/packages/local-refresh/src/actions/ResetDatabase.ts new file mode 100644 index 0000000000..0c7dfca145 --- /dev/null +++ b/packages/local-refresh/src/actions/ResetDatabase.ts @@ -0,0 +1,226 @@ +import prompts from "prompts"; +import fs from "node:fs"; +import flatCache from "flat-cache"; +import path from "node:path"; +import exec from "../exec"; +import chalk from "chalk"; +import ActionInterface from "./ActionInterface"; +import dedent from "dedent"; +import createDebug from "debug"; +import { GenericBar } from "cli-progress"; + +const debug = createDebug("refresh:actions:ResetDatabase"); + +const cache = flatCache.load("reset-database"); + +enum DatabaseRefreshEnum { + NONE, + FULL, + MIGRATIONS, +} + +const liquibasePropertiesTemplate = dedent` + url: jdbc:mysql://host.docker.internal/olcs_be?useSSL=false&allowPublicKeyRetrieval=true&characterEncoding=utf8&connectionCollation=utf8_general_ci + username: root + password: olcs + changeLogFile: changesets/OLCS.xml + logLevel: error +`; + +export default class ResetDatabase implements ActionInterface { + bucketName = "devapp-olcs-pri-olcs-deploy-s3"; + + etlDirectory = "../olcs-etl"; + refreshType: DatabaseRefreshEnum = DatabaseRefreshEnum.NONE; + + liquibasePropertiesFileName = `vol-app.liquibase.properties`; + createLiquibaseProperties = false; + + async prompt(): Promise { + const { shouldResetDatabase, refreshType } = await prompts([ + { + type: "confirm", + name: "shouldResetDatabase", + message: "Reset the database?", + }, + { + type: (prev) => (prev === true ? "select" : null), + name: "refreshType", + message: "Choose the type of database reset?", + choices: [ + { title: "Full refresh", value: DatabaseRefreshEnum.FULL }, + { title: "Just migrations", value: DatabaseRefreshEnum.MIGRATIONS }, + ], + }, + ]); + + if (shouldResetDatabase === undefined || refreshType === undefined) { + return false; + } + + this.refreshType = refreshType; + + const { directory } = await prompts({ + type: "text", + name: "directory", + message: "Enter the path to the ETL directory", + initial: cache.getKey("etlDirectory") || this.etlDirectory, + validate: (value) => + fs.existsSync(path.isAbsolute(value) ? value : path.resolve(__dirname, "../../../../", value)) + ? true + : "Path does not exist", + }); + + if (typeof directory !== "string") { + return false; + } + + this.etlDirectory = path.isAbsolute(directory) ? directory : path.resolve(__dirname, "../../../../", directory); + + cache.setKey("etlDirectory", this.etlDirectory); + cache.save(); + + debug( + `Checking for liquibase properties file at: ${path.join(this.etlDirectory, this.liquibasePropertiesFileName)}`, + ); + + const liquibasePropertiesExists = fs.existsSync(path.join(this.etlDirectory, this.liquibasePropertiesFileName)); + + const liquibasePropertiesIsDifferent = + liquibasePropertiesExists && + fs.readFileSync(path.join(this.etlDirectory, this.liquibasePropertiesFileName), "utf8") !== + liquibasePropertiesTemplate; + + if (!liquibasePropertiesExists || liquibasePropertiesIsDifferent) { + const { createLiquibaseProperties } = await prompts({ + type: "confirm", + name: "createLiquibaseProperties", + message: + liquibasePropertiesExists && liquibasePropertiesIsDifferent + ? "Liquibase properties file is out-of-date. Overwrite?" + : "Create liquibase properties file?", + }); + + if (!createLiquibaseProperties) { + return false; + } + + this.createLiquibaseProperties = createLiquibaseProperties; + } else { + debug("Liquibase properties file already exists and is up-to-date. Skipping step."); + } + + return this.refreshType !== DatabaseRefreshEnum.NONE; + } + + async execute(progress: GenericBar): Promise { + const isFullRefresh = this.refreshType === DatabaseRefreshEnum.FULL; + + progress.start(10, 0); + + if (isFullRefresh) { + await this.#createBaseDatabase(); + } + + progress.increment(4); + + if (this.createLiquibaseProperties) { + this.#createLiquidbasePropertiesFile(); + } + + progress.increment(1); + + await this.#runLiquibaseUpdate(); + + progress.increment(5); + + if (isFullRefresh) { + await this.#fetchAnonymisedDataset(); + } + + progress.stop(); + } + + async #createBaseDatabase(): Promise { + const myCnf = dedent` + [client] + user=root + password=olcs + host=host.docker.internal + port=3306`; + + exec( + dedent`docker compose exec db /bin/bash -c "\ + echo '${myCnf}' > ~/.my.cnf; \ + cd /var/lib/etl \ + && ./create-base.sh olcs_be + "`, + debug, + ); + } + + #createLiquidbasePropertiesFile(): void { + debug(`Creating liquibase properties file at: ${path.join(this.etlDirectory, this.liquibasePropertiesFileName)}`); + + fs.writeFileSync(path.join(this.etlDirectory, this.liquibasePropertiesFileName), liquibasePropertiesTemplate); + } + + async #runLiquibaseUpdate(): Promise { + exec( + `docker run \ + --rm \ + -e INSTALL_MYSQL=true \ + -v "${this.etlDirectory}":/liquibase/changelog \ + -w /liquibase/changelog \ + liquibase/liquibase \ + --defaultsFile=${this.liquibasePropertiesFileName} \ + update \ + -Ddataset=testdata + `, + debug, + ); + } + + async #fetchAnonymisedDataset(): Promise { + // Full reset requires AWS credentials to pull down the anonymised dataset from S3. + try { + exec("aws sts get-caller-identity", debug); + } catch (e: unknown) { + throw new Error( + "Valid AWS credentials are required for a full database reset. Authenticate with VOL `nonprod` and retry.", + ); + } + + const latestAnonDatasetCmd = exec( + `aws s3 ls s3://${this.bucketName}/anondata/olcs-db-localdev-anon-prod --recursive 2>/dev/null | sort | tail -n 1 | awk '{print $4}'`, + debug, + ); + + if (latestAnonDatasetCmd.code !== 0) { + throw new Error("Could not find the latest anonymised dataset on S3"); + } + + const latestAnonDataset = latestAnonDatasetCmd.stdout.trim(); + + const cleanUp = () => { + debug("Removing the anonymised dataset from the ETL directory"); + exec(`rm ${path.join(this.etlDirectory, "/olcs-db-localdev-anon-prod.sql.gz")}`, debug); + }; + + debug(chalk.greenBright(`Fetching the latest anonymised dataset from S3: ${latestAnonDataset}`)); + + try { + exec( + `aws s3 cp s3://${this.bucketName}/${latestAnonDataset} ${path.join(this.etlDirectory, "/olcs-db-localdev-anon-prod.sql.gz")}`, + debug, + ); + + exec( + `docker compose exec -T db /bin/bash -c 'zcat /var/lib/etl/olcs-db-localdev-anon-prod.sql.gz | mysql -u mysql -polcs olcs_be'`, + debug, + ); + } finally { + cleanUp(); + } + } +} diff --git a/packages/local-refresh/actions/ResetLdap.ts b/packages/local-refresh/src/actions/ResetLdap.ts similarity index 63% rename from packages/local-refresh/actions/ResetLdap.ts rename to packages/local-refresh/src/actions/ResetLdap.ts index 42beb28fe5..7b19f1934a 100644 --- a/packages/local-refresh/actions/ResetLdap.ts +++ b/packages/local-refresh/src/actions/ResetLdap.ts @@ -1,34 +1,37 @@ import prompts from "prompts"; -import shell from "shelljs"; +import exec from "../exec"; import chalk from "chalk"; import dedent from "dedent"; import ActionInterface from "./ActionInterface"; import createDebug from "debug"; +import { GenericBar } from "cli-progress"; const debug = createDebug("refresh:actions:ResetLdap"); export default class ResetLdap implements ActionInterface { async prompt(): Promise { - const response = await prompts({ + const { shouldRefresh } = await prompts({ type: "confirm", - name: "ldap-refresh", + name: "shouldRefresh", message: "Do you want to reset the LDAP userpool?", }); - return response["ldap-refresh"]; + return shouldRefresh; } - async execute(): Promise { - debug(chalk.greenBright(`Deleting existing users`)); + async execute(progress: GenericBar): Promise { + progress.start(3, 0); + + debug(chalk.greenBright(`Deleting existing users from LDAP`)); const searchLdap = `ldapsearch -D "cn=admin,dc=vol,dc=dvsa" -H ldap://localhost:1389 -w admin -LLL -s one -b "ou=users,dc=vol,dc=dvsa" "(cn=*)" dn`; - const search = shell.exec( + const search = exec( `docker compose exec -T openldap /bin/bash -c '${searchLdap}' | awk '/^dn: / {print $2}'`, - { - silent: !debug.enabled, - }, + debug, ); + progress.increment(); + const allExistingUsers = search.stdout.split("\n").filter(Boolean); const deleteLdif = allExistingUsers.map((dn) => `dn: ${dn}\nchangetype: delete`).join("\n\n"); @@ -37,23 +40,25 @@ export default class ResetLdap implements ActionInterface { ${deleteLdif} !`; - const deleteUsers = shell.exec(`docker compose exec openldap /bin/bash -c "${ldifDeletions}"`); + const deleteUsers = exec(`docker compose exec openldap /bin/bash -c "${ldifDeletions}"`, debug); if (deleteUsers.code !== 0) { - console.error(chalk.red(`Error while deleting users from LDAP`)); - return; + throw new Error("Delete users from LDAP failed"); } - const selectAllUsersCmd = shell.exec( + progress.increment(); + + const selectAllUsersCmd = exec( `docker compose exec db /bin/bash -c 'mysql -u mysql -polcs -N -e "SELECT login_id FROM olcs_be.user WHERE login_id IS NOT NULL"'`, - { silent: !debug.enabled, env: { ...process.env, FORCE_COLOR: "1" } }, + debug, ); if (selectAllUsersCmd.code !== 0) { - console.error(chalk.red(`Error while selecting users from database`)); - return; + throw new Error("Select users from database failed"); } + progress.increment(); + const allUsers = selectAllUsersCmd.stdout.split("\n").filter(Boolean); const ldif = allUsers @@ -67,22 +72,13 @@ export default class ResetLdap implements ActionInterface { ) .join("\n\n"); - debug(chalk.greenBright(`Adding new users`)); + debug(`Adding new users into LDAP`); const ldifModify = dedent`ldapmodify -D "cn=admin,dc=vol,dc=dvsa" -H ldap://localhost:1389 -w admin -c < { + const optionsWithDefaults = { + silent: true, + env: { + ...process.env, + FORCE_COLOR: "1", + }, + ...options, + }; + + const result = shell.exec(command, optionsWithDefaults); + + if (result.stdout && debug.enabled) { + debug(result.stdout); + } + + if (result.stderr) { + debug(result.stderr); + } + + if (result.code !== 0) { + throw new Error(`Command: ${command} failed. Stderr: ${result.stderr}`); + } + + return result; +}; + +export default exec; diff --git a/packages/local-refresh/index.ts b/packages/local-refresh/src/index.ts similarity index 68% rename from packages/local-refresh/index.ts rename to packages/local-refresh/src/index.ts index ff504897fb..e40bc29d50 100644 --- a/packages/local-refresh/index.ts +++ b/packages/local-refresh/src/index.ts @@ -2,11 +2,21 @@ import { program } from "commander"; import fs from "node:fs"; -import path from "path"; +import path from "node:path"; import chalk from "chalk"; +import cliProgress from "cli-progress"; import ActionInterface from "./actions/ActionInterface"; -program.description("Reset the VOL local environment.").action(async () => { +const progressBarFactory = () => { + return new cliProgress.Bar( + { + clearOnComplete: true, + }, + cliProgress.Presets.shades_classic, + ); +}; + +program.description("Script to refresh the local VOL application").action(async () => { const actions = await Promise.all( fs .readdirSync(path.resolve(__dirname, "actions")) @@ -29,29 +39,34 @@ program.description("Reset the VOL local environment.").action(async () => { const shouldRun = await instance.prompt(); if (shouldRun) { - console.info(`Running action: ${instance.constructor.name}`); - await instance.execute(); + try { + await instance.execute(progressBarFactory()); + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`\n\n${chalk.red(e.message)}\n`); + } + } } } const hostsFile = fs.readFileSync("/etc/hosts", "utf8"); if (!hostsFile.includes("local.olcs.dev-dvsacloud.uk")) { - console.warn(chalk.yellow(`/etc/hosts has not been updated with local domains. Please run:`)); + console.warn(chalk.yellow(`/etc/hosts has not been updated with the local domains. Please run:`)); console.warn( chalk.bgYellow( `sudo echo "127.0.0.1 iuweb.local.olcs.dev-dvsacloud.uk ssweb.local.olcs.dev-dvsacloud.uk api.local.olcs.dev-dvsacloud.uk cdn.local.olcs.dev-dvsacloud.uk" >> /etc/hosts`, ), ); - - return; } - console.info(chalk.greenBright("Local environment reset complete.")); + process.exit(0); }); program.parse(process.argv); process.on("unhandledRejection", (err) => { + console.error(`\n\nUncaught Error: ${chalk.red(err)}\n\n`); + process.exit(1); });