diff --git a/.gitignore b/.gitignore index abb1d573786..a9d5eac339a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ **/certs/ **/node_modules/ **/coverage/ -**/lib/ **/dist/ **/build/ **/.eslintcache diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000000..d1cdf2f06be --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict = true \ No newline at end of file diff --git a/@open-cluster-management/resources/package.json b/@open-cluster-management/resources/package.json deleted file mode 100644 index 1509767e8eb..00000000000 --- a/@open-cluster-management/resources/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "@open-cluster-management/resources", - "version": "1.0.0", - "main": "lib/index.js", - "types": "lib/index.d.ts", - "private": "true", - "files": [ - "lib", - "src" - ], - "scripts": { - "build": "tsc", - "watch": "tsc --watch --preserveWatchOutput", - "test": "tsc --noEmit", - "lint": "echo no lint", - "check": "prettier --check src", - "check:fix": "prettier --write src" - }, - "devDependencies": { - "@kubernetes/client-node": "^0.15.0", - "@types/node": "^16.4.5", - "prettier": "^2.3.2", - "typescript": "4.3.5" - }, - "dependencies": { - "openshift-assisted-ui-lib": "1.5.32" - }, - "prettier": { - "printWidth": 120, - "tabWidth": 4, - "semi": false, - "singleQuote": true - } -} diff --git a/@open-cluster-management/resources/tsconfig.json b/@open-cluster-management/resources/tsconfig.json deleted file mode 100644 index 5b2918cde15..00000000000 --- a/@open-cluster-management/resources/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "lib", - "rootDir": "src" - }, - "include": ["src/**/*"] -} diff --git a/Dockerfile.prow b/Dockerfile.prow index aeecbbb3ebe..e8aef7dadbf 100644 --- a/Dockerfile.prow +++ b/Dockerfile.prow @@ -1,19 +1,33 @@ # Copyright Contributors to the Open Cluster Management project - -FROM registry.ci.openshift.org/open-cluster-management/builder:nodejs14-linux as builder +FROM registry.ci.openshift.org/open-cluster-management/builder:nodejs14-linux as packages WORKDIR /app -COPY . . +COPY package.json yarn.lock ./ +COPY ./backend/package.json /app/backend/package.json +COPY ./frontend/package.json /app/frontend/package.json + +FROM packages as builder RUN yarn install --frozen-lockfile --ignore-optional + +FROM packages as production +RUN yarn install --production --frozen-lockfile --ignore-optional + +FROM builder as backend +COPY ./backend ./backend +WORKDIR /app/backend +RUN yarn run build + +FROM builder as frontend +COPY ./frontend ./frontend +WORKDIR /app/frontend RUN yarn run build -RUN rm -rf node_modules && yarn install --frozen-lockfile --production --ignore-optional FROM registry.access.redhat.com/ubi8/ubi-minimal COPY --from=builder /usr/bin/node /usr/bin/node WORKDIR /app ENV NODE_ENV production -COPY --from=builder /app/node_modules ./node_modules -COPY --from=builder /app/backend/node_modules ./backend/node_modules -COPY --from=builder /app/backend/build ./backend -COPY --from=builder /app/frontend/build ./public +COPY --from=production /app/node_modules ./node_modules +COPY --from=production /app/backend/node_modules ./backend/node_modules +COPY --from=backend /app/backend/build ./backend +COPY --from=frontend /app/frontend/build ./public USER 1001 CMD ["node", "backend/lib/main.js"] diff --git a/backend/package.json b/backend/package.json index 1a7aa717605..243330cb9d9 100644 --- a/backend/package.json +++ b/backend/package.json @@ -2,8 +2,13 @@ "name": "@open-cluster-management/console-backend", "version": "0.0.1", "private": true, + "engines": { + "npm": "please-use-yarn", + "yarn": ">= 1.17.3", + "node": ">= 14" + }, "scripts": { - "watch": "yarn run start", + "watch": "echo", "postinstall": "[ ! -d ./certs ] && yarn run generate-certs || true", "build": "tsc --sourceMap false --declaration false", "clean": "rm -rf coverage build", @@ -28,21 +33,21 @@ "devDependencies": { "@types/dotenv": "^8.2.0", "@types/eslint": "^7.28.0", - "@types/jest": "^26.0.24", - "@types/node": "^16.4.3", + "@types/jest": "^27.0.0", + "@types/node": "^16.6.0", "@types/node-fetch": "^2.5.12", - "@types/pino": "^6.3.10", + "@types/pino": "^6.3.11", "@types/prettier": "^2.3.2", "@types/raw-body": "^2.3.0", - "@typescript-eslint/eslint-plugin": "^4.28.5", - "@typescript-eslint/parser": "^4.28.5", - "eslint": "^7.31.0", - "jest": "^26.x.x", + "@typescript-eslint/eslint-plugin": "^4.29.1", + "@typescript-eslint/parser": "^4.29.1", + "eslint": "^7.32.0", + "jest": "^27.x.x", "nock": "^13.1.1", "pino-zen": "^1.0.20", "prettier": "^2.3.2", - "ts-jest": "^26.x.x", - "ts-node": "^10.1.0", + "ts-jest": "^27.x.x", + "ts-node": "^10.2.0", "ts-node-dev": "^1.1.8", "typescript": "^4.3.5" }, diff --git a/backend/src/app.ts b/backend/src/app.ts index e60de878b05..f95f49d6a9f 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -38,12 +38,12 @@ router.get(`/logout/`, logout) router.get(`/events`, events) router.post(`/proxy/search`, search) router.get(`/authenticated`, authenticated) -router.get(`/*`, serve) router.post(`/ansibletower`, ansibleTower) +router.get(`/*`, serve) export async function requestHandler(req: Http2ServerRequest, res: Http2ServerResponse): Promise { if (process.env.NODE_ENV !== 'production') { - cors(req, res) + if (cors(req, res)) return await delay(req, res) } @@ -76,6 +76,7 @@ export function start(): Promise { export async function stop(): Promise { if (process.env.NODE_ENV === 'development') { setTimeout(() => { + logger.warn('process stop timeout. exiting...') process.exit(1) }, 0.5 * 1000).unref() } diff --git a/backend/src/lib/cors.ts b/backend/src/lib/cors.ts index 987c6b0a368..26736450b57 100644 --- a/backend/src/lib/cors.ts +++ b/backend/src/lib/cors.ts @@ -3,7 +3,7 @@ import { Http2ServerRequest, Http2ServerResponse } from 'http2' -export function cors(req: Http2ServerRequest, res: Http2ServerResponse): void { +export function cors(req: Http2ServerRequest, res: Http2ServerResponse): boolean { if (process.env.NODE_ENV !== 'production') { if (req.headers['origin']) { res.setHeader('Access-Control-Allow-Origin', req.headers['origin']) @@ -18,7 +18,9 @@ export function cors(req: Http2ServerRequest, res: Http2ServerResponse): void { if (req.headers['access-control-request-headers']) { res.setHeader('Access-Control-Allow-Headers', req.headers['access-control-request-headers']) } - return res.writeHead(200).end() + res.writeHead(200).end() + return true } } + return false } diff --git a/backend/src/lib/main.ts b/backend/src/lib/main.ts index 6b292589d73..9e6eba475d4 100644 --- a/backend/src/lib/main.ts +++ b/backend/src/lib/main.ts @@ -40,8 +40,9 @@ process.on('SIGTERM', () => { }) process.on('uncaughtException', (err) => { - logger.error({ msg: `process uncaughtException`, error: err.message }) - console.log(err.stack) + // console.error(err) + // logger.error({ msg: `process uncaughtException`, error: err.message }) + // console.log(err.stack) }) process.on('multipleResolves', (type, _promise, reason) => { diff --git a/backend/src/lib/request-retry.ts b/backend/src/lib/request-retry.ts index 799789e28db..4571fd382fa 100644 --- a/backend/src/lib/request-retry.ts +++ b/backend/src/lib/request-retry.ts @@ -107,6 +107,12 @@ export function requestRetry(options: { options.onResponse(response) } }) + .on('error', (err) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any + if ((err as any).code !== 'ABORT_ERR') { + throw err + } + }) .on('timeout', () => { // Emitted when the underlying socket times out from inactivity. // This only notifies that the socket has been idle. diff --git a/backend/src/routes/serve.ts b/backend/src/routes/serve.ts index 46d42bd935a..02bc3844d3d 100644 --- a/backend/src/routes/serve.ts +++ b/backend/src/routes/serve.ts @@ -1,15 +1,14 @@ /* Copyright Contributors to the Open Cluster Management project */ -import { createReadStream } from 'fs' +import { createReadStream, Stats } from 'fs' +import { stat } from 'fs/promises' import { constants, Http2ServerRequest, Http2ServerResponse } from 'http2' import { extname } from 'path' import { pipeline } from 'stream' -import { parseCookies } from '../lib/cookies' import { logger } from '../lib/logger' -import { redirect } from '../lib/respond' const cacheControl = process.env.NODE_ENV === 'production' ? 'public, max-age=604800' : 'no-store' -export function serve(req: Http2ServerRequest, res: Http2ServerResponse): void { +export async function serve(req: Http2ServerRequest, res: Http2ServerResponse): Promise { try { let url = req.url @@ -41,15 +40,50 @@ export function serve(req: Http2ServerRequest, res: Http2ServerResponse): void { logger.debug('unknown content type', `ext=${ext}`) return res.writeHead(404).end() } + + const filePath = './public' + url + let stats: Stats + try { + stats = await stat(filePath) + } catch { + return res.writeHead(404).end() + } + + if (/\bbr\b/.test(acceptEncoding)) { + try { + const brStats = await stat(filePath + '.br') + const readStream = createReadStream('./public' + url + '.br', { autoClose: true }) + readStream + .on('open', () => { + res.writeHead(200, { + [constants.HTTP2_HEADER_CONTENT_ENCODING]: 'br', + [constants.HTTP2_HEADER_CONTENT_TYPE]: contentType, + [constants.HTTP2_HEADER_CONTENT_LENGTH]: brStats.size.toString(), + }) + }) + .on('error', (err) => { + // logger.error(err) + res.writeHead(404).end() + }) + pipeline(readStream, res as unknown as NodeJS.WritableStream, (err) => { + // if (err) logger.error(err) + }) + return + } catch { + // Do nothing + } + } + if (/\bgzip\b/.test(acceptEncoding)) { try { + const gzStats = await stat(filePath + '.gz') const readStream = createReadStream('./public' + url + '.gz', { autoClose: true }) readStream .on('open', () => { res.writeHead(200, { [constants.HTTP2_HEADER_CONTENT_ENCODING]: 'gzip', [constants.HTTP2_HEADER_CONTENT_TYPE]: contentType, - // [constants.HTTP2_HEADER_CONTENT_LENGTH]: stats.size.toString(), + [constants.HTTP2_HEADER_CONTENT_LENGTH]: gzStats.size.toString(), }) }) .on('error', (err) => { @@ -59,27 +93,27 @@ export function serve(req: Http2ServerRequest, res: Http2ServerResponse): void { pipeline(readStream, res as unknown as NodeJS.WritableStream, (err) => { // if (err) logger.error(err) }) - } catch (err) { - logger.error(err) - return res.writeHead(404).end() + return + } catch { + // Do nothing } - } else { - const readStream = createReadStream('./public' + url, { autoClose: true }) - readStream - .on('open', () => { - res.writeHead(200, { - [constants.HTTP2_HEADER_CONTENT_TYPE]: contentType, - }) - }) - .on('error', (err) => { - // logger.error(err) - res.writeHead(404).end() + } + + const readStream = createReadStream('./public' + url, { autoClose: true }) + readStream + .on('open', () => { + res.writeHead(200, { + [constants.HTTP2_HEADER_CONTENT_TYPE]: contentType, + [constants.HTTP2_HEADER_CONTENT_LENGTH]: stats.size.toString(), }) - pipeline(readStream, res as unknown as NodeJS.WritableStream, (err) => { - // if (err) logger.error(err) }) - } - return + .on('error', (err) => { + // logger.error(err) + res.writeHead(404).end() + }) + pipeline(readStream, res as unknown as NodeJS.WritableStream, (err) => { + // if (err) logger.error(err) + }) } catch (err) { logger.error(err) return res.writeHead(404).end() diff --git a/frontend/babel.config.json b/frontend/babel.config.json new file mode 100644 index 00000000000..b08e4535bf6 --- /dev/null +++ b/frontend/babel.config.json @@ -0,0 +1,4 @@ +// https://babeljs.io/docs/en/configuration +{ + "presets": ["@babel/env", "@babel/react", "@babel/preset-typescript"] +} diff --git a/frontend/jest.config.js b/frontend/jest.config.js index 240a346a125..c392d8bb327 100644 --- a/frontend/jest.config.js +++ b/frontend/jest.config.js @@ -3,13 +3,21 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'jsdom', + automock: false, rootDir: './src', testResultsProcessor: 'jest-sonar-reporter', setupFilesAfterEnv: ['/setupTests.ts'], moduleNameMapper: { - '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '/css.mock.js', - '\\.(css|less)$': '/css.mock.js', + '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '/file.mock.js', + '\\.(css|less)$': 'identity-obj-proxy', + 'monaco-editor': '/../../node_modules/react-monaco-editor', }, watchPathIgnorePatterns: ['/../node_modules', '/../.eslintcache', '/../coverage'], moduleFileExtensions: ['js', 'json', 'jsx', 'node', 'ts', 'tsx'], + transform: { + '^.+\\.jsx?$': 'babel-jest', + '^.+\\.hbs$': 'jest-raw-loader', + '\\.(css|less)$': 'jest-raw-loader', + }, + coverageReporters: ['text', 'text-summary', 'html', 'lcov'], } diff --git a/frontend/package.json b/frontend/package.json index cab9d5178ed..89f3c22766d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -2,85 +2,135 @@ "name": "@open-cluster-management/console-frontend", "version": "0.0.1", "private": true, - "homepage": "/multicloud/", - "proxy": "https://localhost:4000", + "engines": { + "npm": "please-use-yarn", + "yarn": ">= 1.17.3", + "node": ">= 14" + }, + "scripts": { + "start": "TS_NODE_PROJECT=webpack.tsconfig.json webpack serve --mode development --stats-children", + "build": "TS_NODE_PROJECT=webpack.tsconfig.json webpack --mode production", + "watch": "echo", + "test": "jest --runInBand", + "lint": "eslint src --ext .ts,.tsx --max-warnings=0", + "lint:fix": "eslint src --ext .ts,.tsx --fix", + "check": "prettier --check src", + "check:fix": "prettier --write src", + "update": "rm -rf package-lock.json node_modules && npx npm-check-updates -u -t minor && yarn install && yarn audit fix && npm dedup && yarn test && yarn run lint:fix && yarn run lint && yarn run check:fix", + "clean": "rm -rf coverage build" + }, "devDependencies": { - "@kubernetes/client-node": "^0.15.0", - "@open-cluster-management/resources": "^1.0.0", - "@open-cluster-management/ui-components": "^1.3.0", + "@babel/core": "^7.15.0", + "@babel/preset-env": "^7.15.0", + "@babel/preset-react": "^7.14.5", + "@babel/preset-typescript": "^7.15.0", + "@kubernetes/client-node": "^0.15.1", + "@open-cluster-management/ui-components": "^1.4.0", "@patternfly/react-code-editor": "^4.1.25", "@patternfly/react-core": "^4.135.5", - "@patternfly/react-table": "^4.29.5", "@patternfly/react-icons": "^4.10.11", + "@patternfly/react-table": "^4.29.5", + "@pmmmwh/react-refresh-webpack-plugin": "^0.5.0-rc.3", "@redhat-cloud-services/rule-components": "^3.2.1", "@reduxjs/toolkit": "1.5.x", "@sentry/browser": "5.19.1", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^11.2.7", "@testing-library/user-event": "^12.8.3", - "@types/get-value": "^3.0.1", - "@types/jest": "^26.0.23", - "@types/lodash": "^4.14.170", - "@types/node": "^14.17.4", - "@types/react": "^17.0.13", - "@types/react-dom": "^17.0.8", - "@types/react-router-dom": "^5.1.7", + "@types/compression-webpack-plugin": "^6.0.6", + "@types/copy-webpack-plugin": "^8.0.1", + "@types/css-minimizer-webpack-plugin": "^3.0.2", + "@types/get-value": "^3.0.2", + "@types/jest": "^27.0.0", + "@types/lodash": "^4.14.172", + "@types/mini-css-extract-plugin": "^2.2.0", + "@types/node": "^16.6.0", + "@types/react": "^17.0.17", + "@types/react-dom": "^17.0.9", + "@types/react-router-dom": "^5.1.8", + "@types/testing-library__jest-dom": "^5.14.1", + "@types/validator": "^13.6.3", + "@types/webpack-dev-server": "^4.0.0", "axios": "^0.19.2", - "babel-jest": "^26.6.3", + "babel-jest": "^27.0.6", + "babel-loader": "^8.2.2", + "browserify-fs": "^1.0.0", + "buffer": "^6.0.3", + "bundle-loader": "^0.5.6", + "compression-webpack-plugin": "^8.0.1", + "copy-webpack-plugin": "^9.0.1", + "crypto-browserify": "^3.12.0", + "css-loader": "^6.2.0", + "css-minimizer-webpack-plugin": "^3.0.2", + "eslint-plugin-jest": "^24.4.0", "eslint-plugin-react": "^7.24.0", + "eslint-plugin-react-hooks": "^4.2.0", + "file-loader": "^6.2.0", "handlebars": "^4.7.7", + "handlebars-loader": "^1.7.1", + "html-webpack-plugin": "^5.3.2", "i18next": "^20.3.2", "i18next-browser-languagedetector": "^6.1.2", "i18next-http-backend": "^1.2.6", - "jest-environment-jsdom-sixteen": "^1.0.3", + "identity-obj-proxy": "^3.0.0", + "jest": "^27.0.6", + "jest-fetch-mock": "^3.0.3", "jest-raw-loader": "1.0.1", "jest-sonar-reporter": "^2.0.0", "jest-watch-typeahead": "^0.6.4", + "json-loader": "^0.5.7", "lodash": "^4.17.21", + "mini-css-extract-plugin": "^2.2.1", "moment": "^2.29.1", "monaco-editor": "^0.25.2", "monaco-editor-webpack-plugin": "^4.0.0", "nock": "^13.1.1", + "node-util": "^0.0.1", "openshift-assisted-ui-lib": "1.5.32", + "path-browserify": "^1.0.1", "prettier": "^2.3.2", + "process": "^0.11.10", "raw-loader": "^4.0.2", "react": "^17.0.2", - "react-app-rewired": "^2.1.8", "react-dom": "^17.0.2", + "react-hot-loader": "^4.13.0", "react-i18next": "^11.11.1", "react-monaco-editor": "0.36.0", "react-redux": "^7.2.0", + "react-refresh": "^0.10.0", + "react-refresh-typescript": "^2.0.2", "react-router-dom": "^5.2.0", - "react-scripts": "4.0.3", "react-tagsinput": "3.19.x", "recoil": "^0.3.1", "redux": "^4.0.5", "redux-thunk": "^2.3.0", "reselect": "^4.0.0", + "stream-browserify": "^3.0.0", + "style-loader": "^3.2.1", "swr": "^0.5.6", "temptifly": "^0.4.8", - "ts-jest": "^26.5.6", - "typescript": "^4.2.4", + "ts-import-plugin": "^1.6.7", + "ts-jest": "^27.0.4", + "ts-loader": "^9.2.5", + "tsconfig-paths": "^3.10.1", + "type-fest": "^2.1.0", + "typescript": "^4.3.5", "uuid": "8.1.0", + "v8-compile-cache": "^2.3.0", + "validator": "^13.6.0", + "webpack": "^5.51.1", + "webpack-cli": "^4.8.0", + "webpack-dev-server": "^4.0.0", + "whatwg-fetch": "^3.6.2", "yaml": "^1.10.2", "yup": "^0.28.3" }, - "scripts": { - "watch": "yarn run start", - "start": "react-app-rewired start", - "build": "react-app-rewired --max_old_space_size=8192 build && cd build && gzip * -k -r --best", - "test": "react-app-rewired test --all --watchAll=false --ci --runInBand --coverage --reporters=default --coverageReporters=lcov --coverageReporters=html --coverageReporters=text --coverageReporters=text-summary --collectCoverageFrom=!**/*.d.ts --env=jest-environment-jsdom-sixteen --testResultsProcessor jest-sonar-reporter", - "test:watch": "react-app-rewired test", - "lint": "eslint src --ext .ts,.tsx --max-warnings=0", - "lint:fix": "eslint src --ext .ts,.tsx --fix", - "check": "prettier --check src", - "check:fix": "prettier --write src", - "update": "rm -rf package-lock.json node_modules && npx npm-check-updates -u -t minor && yarn install && yarn audit fix && npm dedup && yarn test && yarn run lint:fix && yarn run lint && yarn run check:fix", - "clean": "rm -rf coverage build" - }, "eslintConfig": { + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": "./tsconfig.json" + }, "extends": [ - "react-app", "eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:jest/recommended" @@ -88,12 +138,9 @@ "plugins": [ "react", "@typescript-eslint", - "jest" + "jest", + "react-hooks" ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "project": "./tsconfig.json" - }, "rules": { "@typescript-eslint/no-non-null-assertion": "off", "react/react-in-jsx-scope": "off", @@ -136,34 +183,6 @@ "last 1 safari version" ] }, - "jest": { - "collectCoverageFrom": [ - "src/**/*.{ts,tsx}", - "!src/**/*.d.ts", - "!/node_modules/" - ], - "coverageThreshold": { - "global": { - "branches": 0, - "functions": 0, - "lines": 0, - "statements": 0 - } - }, - "coverageReporters": [ - "text", - "text-summary", - "html", - "lcov" - ], - "moduleNameMapper": { - "monaco-editor": "/../node_modules/react-monaco-editor" - }, - "watchPathIgnorePatterns": [ - "node_modules", - "coverage" - ] - }, "prettier": { "printWidth": 120, "tabWidth": 4, @@ -171,7 +190,6 @@ "singleQuote": true }, "dependencies": { - "@types/validator": "^13.6.3", - "validator": "^13.6.0" + "@types/js-yaml": "^4.0.3" } } diff --git a/frontend/public/index.html b/frontend/public/index.html index e4f9cb693e5..0a64617c72e 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -4,7 +4,7 @@ - + Red Hat Advanced Cluster Management for Kubernetes