From 4fdb1def391fb1c3471145da01dd1262d685d821 Mon Sep 17 00:00:00 2001
From: blam
Date: Sun, 7 Jun 2020 03:07:46 +0200
Subject: [PATCH 01/63] chore(react-router-v6): starting to move things to
react-router-v6
---
packages/app/package.json | 3 +-
.../default-app/packages/app/package.json.hbs | 102 ++++++++--------
.../plugins/welcome/package.json.hbs | 74 ++++++------
packages/core-api/package.json | 2 +-
packages/core-api/src/app/App.tsx | 20 ++--
packages/core/package.json | 5 +-
.../core/src/layout/ErrorPage/ErrorPage.tsx | 4 +-
packages/dev-utils/package.json | 4 +-
packages/test-utils/package.json | 4 +-
plugins/catalog/package.json | 4 +-
plugins/circleci/package.json | 4 +-
plugins/circleci/src/components/App.tsx | 23 ++--
plugins/graphiql/package.json | 2 +-
plugins/home-page/package.json | 2 +-
plugins/lighthouse/package.json | 2 +-
.../src/components/AuditList/index.test.tsx | 8 +-
.../src/components/AuditList/index.tsx | 4 +-
.../src/components/CreateAudit/index.test.tsx | 8 +-
.../src/components/CreateAudit/index.tsx | 12 +-
plugins/register-component/package.json | 4 +-
plugins/scaffolder/package.json | 2 +-
plugins/welcome/package.json | 2 +-
yarn.lock | 109 ++++--------------
23 files changed, 167 insertions(+), 237 deletions(-)
diff --git a/packages/app/package.json b/packages/app/package.json
index 15681d0548..6b5c5b4e78 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -22,7 +22,7 @@
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-hot-loader": "^4.12.21",
- "react-router-dom": "^5.2.0",
+ "react-router-dom": "6.0.0-alpha.5",
"react-use": "^14.2.0",
"zen-observable": "^0.8.15"
},
@@ -34,7 +34,6 @@
"@types/jest": "^25.2.2",
"@types/jquery": "^3.3.34",
"@types/node": "^12.0.0",
- "@types/react-router-dom": "^5.1.3",
"@types/zen-observable": "^0.8.0",
"cross-env": "^7.0.0",
"cypress": "^4.2.0",
diff --git a/packages/cli/templates/default-app/packages/app/package.json.hbs b/packages/cli/templates/default-app/packages/app/package.json.hbs
index 2c35dee754..46e27c5b91 100644
--- a/packages/cli/templates/default-app/packages/app/package.json.hbs
+++ b/packages/cli/templates/default-app/packages/app/package.json.hbs
@@ -1,53 +1,53 @@
{
- "name": "app",
- "version": "0.0.0",
- "private": true,
- "dependencies": {
- "@material-ui/core": "^4.9.1",
- "@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.45",
- "@backstage/cli": "^{{version}}",
- "@backstage/core": "^{{version}}",
- "@backstage/theme": "^{{version}}",
- "plugin-welcome": "0.0.0",
- "react": "^16.13.1",
- "react-dom": "^16.13.1",
- "react-router-dom": "^5.2.0",
- "react-use": "^14.2.0"
- },
- "devDependencies": {
- "@testing-library/jest-dom": "^5.7.0",
- "@testing-library/react": "^9.3.2",
- "@testing-library/user-event": "^10.2.4",
- "@types/jest": "^25.2.2",
- "@types/node": "^12.0.0",
- "@types/react-router-dom": "^5.1.3",
- "@types/testing-library__jest-dom": "^5.0.4",
- "cross-env": "^7.0.0",
- "cypress": "^4.2.0",
- "eslint-plugin-cypress": "^2.10.3",
- "start-server-and-test": "^1.10.11"
- },
- "scripts": {
- "start": "backstage-cli app:serve",
- "bundle": "backstage-cli app:build",
- "test": "backstage-cli test",
- "lint": "backstage-cli lint",
- "test:e2e": "cross-env PORT=3001 start-server-and-test start http://localhost:3001 cy:dev",
- "test:e2e:ci": "cross-env PORT=3001 start-server-and-test start http://localhost:3001 cy:run",
- "cy:dev": "cypress open",
- "cy:run": "cypress run"
- },
- "browserslist": {
- "production": [
- ">0.2%",
- "not dead",
- "not op_mini all"
- ],
- "development": [
- "last 1 chrome version",
- "last 1 firefox version",
- "last 1 safari version"
- ]
- }
+"name": "app",
+"version": "0.0.0",
+"private": true,
+"dependencies": {
+"@material-ui/core": "^4.9.1",
+"@material-ui/icons": "^4.9.1",
+"@material-ui/lab": "4.0.0-alpha.45",
+"@backstage/cli": "^{{version}}",
+"@backstage/core": "^{{version}}",
+"@backstage/theme": "^{{version}}",
+"plugin-welcome": "0.0.0",
+"react": "^16.13.1",
+"react-dom": "^16.13.1",
+"react-router-dom": "6.0.0-alpha.5",
+"react-use": "^14.2.0"
+},
+"devDependencies": {
+"@testing-library/jest-dom": "^5.7.0",
+"@testing-library/react": "^9.3.2",
+"@testing-library/user-event": "^10.2.4",
+"@types/jest": "^25.2.2",
+"@types/node": "^12.0.0",
+"@types/react-router-dom": "6.0.0-alpha.5",
+"@types/testing-library__jest-dom": "^5.0.4",
+"cross-env": "^7.0.0",
+"cypress": "^4.2.0",
+"eslint-plugin-cypress": "^2.10.3",
+"start-server-and-test": "^1.10.11"
+},
+"scripts": {
+"start": "backstage-cli app:serve",
+"bundle": "backstage-cli app:build",
+"test": "backstage-cli test",
+"lint": "backstage-cli lint",
+"test:e2e": "cross-env PORT=3001 start-server-and-test start http://localhost:3001 cy:dev",
+"test:e2e:ci": "cross-env PORT=3001 start-server-and-test start http://localhost:3001 cy:run",
+"cy:dev": "cypress open",
+"cy:run": "cypress run"
+},
+"browserslist": {
+"production": [
+">0.2%",
+"not dead",
+"not op_mini all"
+],
+"development": [
+"last 1 chrome version",
+"last 1 firefox version",
+"last 1 safari version"
+]
+}
}
diff --git a/packages/cli/templates/default-app/plugins/welcome/package.json.hbs b/packages/cli/templates/default-app/plugins/welcome/package.json.hbs
index b18839bab6..42676f56ee 100644
--- a/packages/cli/templates/default-app/plugins/welcome/package.json.hbs
+++ b/packages/cli/templates/default-app/plugins/welcome/package.json.hbs
@@ -1,39 +1,39 @@
{
- "name": "plugin-welcome",
- "version": "0.0.0",
- "main": "dist/index.esm.js",
- "main:src": "src/index.ts",
- "types": "src/index.ts",
- "private": true,
- "scripts": {
- "build": "backstage-cli plugin:build",
- "start": "backstage-cli plugin:serve",
- "lint": "backstage-cli lint",
- "test": "backstage-cli test",
- "diff": "backstage-cli plugin:diff",
- "prepack": "backstage-cli prepack",
- "postpack": "backstage-cli postpack",
- "clean": "backstage-cli clean"
- },
- "dependencies": {
- "@backstage/core": "^{{version}}",
- "@backstage/theme": "^{{version}}",
- "@material-ui/core": "^4.9.1",
- "@material-ui/icons": "^4.9.1",
- "react": "^16.13.1",
- "react-dom": "^16.13.1",
- "react-use": "^14.2.0",
- "react-router-dom": "^5.2.0"
- },
- "devDependencies": {
- "@backstage/cli": "^{{version}}",
- "@backstage/dev-utils": "^{{version}}",
- "@testing-library/jest-dom": "^5.7.0",
- "@testing-library/react": "^9.3.2",
- "@types/testing-library__jest-dom": "^5.0.4",
- "jest-fetch-mock": "^3.0.3"
- },
- "files": [
- "dist/**/*.{js,d.ts}"
- ]
+"name": "plugin-welcome",
+"version": "0.0.0",
+"main": "dist/index.esm.js",
+"main:src": "src/index.ts",
+"types": "src/index.ts",
+"private": true,
+"scripts": {
+"build": "backstage-cli plugin:build",
+"start": "backstage-cli plugin:serve",
+"lint": "backstage-cli lint",
+"test": "backstage-cli test",
+"diff": "backstage-cli plugin:diff",
+"prepack": "backstage-cli prepack",
+"postpack": "backstage-cli postpack",
+"clean": "backstage-cli clean"
+},
+"dependencies": {
+"@backstage/core": "^{{version}}",
+"@backstage/theme": "^{{version}}",
+"@material-ui/core": "^4.9.1",
+"@material-ui/icons": "^4.9.1",
+"react": "^16.13.1",
+"react-dom": "^16.13.1",
+"react-use": "^14.2.0",
+"react-router-dom": "6.0.0-alpha.5"
+},
+"devDependencies": {
+"@backstage/cli": "^{{version}}",
+"@backstage/dev-utils": "^{{version}}",
+"@testing-library/jest-dom": "^5.7.0",
+"@testing-library/react": "^9.3.2",
+"@types/testing-library__jest-dom": "^5.0.4",
+"jest-fetch-mock": "^3.0.3"
+},
+"files": [
+"dist/**/*.{js,d.ts}"
+]
}
diff --git a/packages/core-api/package.json b/packages/core-api/package.json
index 8139f2c54a..aa88fb1aeb 100644
--- a/packages/core-api/package.json
+++ b/packages/core-api/package.json
@@ -34,7 +34,7 @@
"@types/react": "^16.9",
"prop-types": "^15.7.2",
"react": "^16.12.0",
- "react-router-dom": "^5.2.0",
+ "react-router-dom": "6.0.0-alpha.5",
"react-use": "^14.2.0",
"zen-observable": "^0.8.15"
},
diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx
index 575dcfc59d..e47aa545ab 100644
--- a/packages/core-api/src/app/App.tsx
+++ b/packages/core-api/src/app/App.tsx
@@ -15,7 +15,7 @@
*/
import React, { ComponentType, FC } from 'react';
-import { Route, Switch, Redirect } from 'react-router-dom';
+import { Route, Redirect, Routes, Navigate } from 'react-router-dom';
import { AppContextProvider } from './AppContext';
import { BackstageApp, AppComponents, AppConfigLoader } from './types';
import { BackstagePlugin } from '../plugin';
@@ -85,26 +85,26 @@ export class PrivateAppImpl implements BackstageApp {
for (const output of plugin.output()) {
switch (output.type) {
case 'legacy-route': {
- const { path, component, options = {} } = output;
+ const { path, component: Component, options = {} } = output;
const { exact = true } = options;
routes.push(
}
exact={exact}
/>,
);
break;
}
case 'route': {
- const { target, component, options = {} } = output;
+ const { target, component: Component, options = {} } = output;
const { exact = true } = options;
routes.push(
}
exact={exact}
/>,
);
@@ -114,7 +114,7 @@ export class PrivateAppImpl implements BackstageApp {
const { path, target, options = {} } = output;
const { exact = true } = options;
routes.push(
- ,
+ ,
);
break;
}
@@ -122,7 +122,7 @@ export class PrivateAppImpl implements BackstageApp {
const { from, to, options = {} } = output;
const { exact = true } = options;
routes.push(
-
+
{routes}
-
-
+ } />
+
);
return () => rendered;
diff --git a/packages/core/package.json b/packages/core/package.json
index 818634238b..6c54b2c59e 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -34,7 +34,6 @@
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
"@types/react": "^16.9",
- "@types/react-router-dom": "^5.1.5",
"@types/react-sparklines": "^1.7.0",
"classnames": "^2.2.6",
"clsx": "^1.1.0",
@@ -45,8 +44,8 @@
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-helmet": "6.0.0",
- "react-router": "^5.2.0",
- "react-router-dom": "^5.2.0",
+ "react-router": "6.0.0-alpha.5",
+ "react-router-dom": "6.0.0-alpha.5",
"react-sparklines": "^1.7.0",
"react-syntax-highlighter": "^12.2.1",
"react-use": "^14.2.0"
diff --git a/packages/core/src/layout/ErrorPage/ErrorPage.tsx b/packages/core/src/layout/ErrorPage/ErrorPage.tsx
index b94c7409fc..fa2315307f 100644
--- a/packages/core/src/layout/ErrorPage/ErrorPage.tsx
+++ b/packages/core/src/layout/ErrorPage/ErrorPage.tsx
@@ -19,7 +19,7 @@ import { Typography, Link, Grid } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import { BackstageTheme } from '@backstage/theme';
import { MicDrop } from './MicDrop';
-import { useHistory } from 'react-router';
+import { useNavigate } from 'react-router';
interface IErrorPageProps {
status: string;
@@ -40,7 +40,7 @@ const useStyles = makeStyles(theme => ({
export const ErrorPage = ({ status, statusMessage }: IErrorPageProps) => {
const classes = useStyles();
- const history = useHistory();
+ const history = useNavigate();
return (
diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json
index ab595ea9a7..b2c5e1fa59 100644
--- a/packages/dev-utils/package.json
+++ b/packages/dev-utils/package.json
@@ -41,8 +41,8 @@
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-hot-loader": "^4.12.21",
- "react-router": "^5.2.0",
- "react-router-dom": "^5.2.0"
+ "react-router": "^6.0.0-alpha.5",
+ "react-router-dom": "^6.0.0-alpha.5"
},
"devDependencies": {
"@types/jest": "^25.2.2",
diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json
index 136b6d6854..8f23e6b05e 100644
--- a/packages/test-utils/package.json
+++ b/packages/test-utils/package.json
@@ -39,8 +39,8 @@
"@types/react": "^16.9",
"react": "^16.12.0",
"react-dom": "^16.12.0",
- "react-router": "^5.2.0",
- "react-router-dom": "^5.2.0"
+ "react-router": "^6.0.0-alpha.5",
+ "react-router-dom": "^6.0.0-alpha.5"
},
"devDependencies": {
"@types/jest": "^25.2.2",
diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json
index 5445fffb32..44b9c63435 100644
--- a/plugins/catalog/package.json
+++ b/plugins/catalog/package.json
@@ -27,8 +27,8 @@
"@material-ui/lab": "4.0.0-alpha.45",
"react": "^16.13.1",
"react-dom": "^16.13.1",
- "react-router": "^5.2.0",
- "react-router-dom": "^5.2.0",
+ "react-router": "^6.0.0-alpha.5",
+ "react-router-dom": "^6.0.0-alpha.5",
"react-use": "^14.2.0"
},
"devDependencies": {
diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json
index bbcac8e9eb..3158acadfb 100644
--- a/plugins/circleci/package.json
+++ b/plugins/circleci/package.json
@@ -37,8 +37,8 @@
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-lazylog": "^4.5.2",
- "react-router": "^5.1.2",
- "react-router-dom": "^5.1.2",
+ "react-router": "^6.0.0-alpha.5",
+ "react-router-dom": "^6.0.0-alpha.5",
"react-use": "^14.2.0"
},
"devDependencies": {
diff --git a/plugins/circleci/src/components/App.tsx b/plugins/circleci/src/components/App.tsx
index 1649c01b70..5907146340 100644
--- a/plugins/circleci/src/components/App.tsx
+++ b/plugins/circleci/src/components/App.tsx
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import React from 'react';
-import { Switch, Route, MemoryRouter } from 'react-router';
+import { Route, MemoryRouter, Routes } from 'react-router';
import { BuildsPage, Builds } from '../pages/BuildsPage';
import { DetailedViewPage, BuildWithSteps } from '../pages/BuildWithStepsPage';
import { AppStateProvider } from '../state';
@@ -24,14 +24,13 @@ export const App = () => {
return (
<>
-
-
+
+ } />
}
/>
-
+
>
@@ -45,14 +44,10 @@ export const CircleCIWidget = () => (
<>
-
-
-
-
+
+ } />
+ } />
+
>
diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json
index a3fee6e217..3062971dec 100644
--- a/plugins/graphiql/package.json
+++ b/plugins/graphiql/package.json
@@ -53,7 +53,7 @@
"@types/node": "^12.0.0",
"@types/testing-library__jest-dom": "^5.0.4",
"jest-fetch-mock": "^3.0.3",
- "react-router-dom": "^5.2.0"
+ "react-router-dom": "6.0.0-alpha.5"
},
"files": [
"dist/**/*.{js,d.ts}"
diff --git a/plugins/home-page/package.json b/plugins/home-page/package.json
index fa1236861c..cfe196a9d3 100644
--- a/plugins/home-page/package.json
+++ b/plugins/home-page/package.json
@@ -36,7 +36,7 @@
"@types/node": "^12.0.0",
"@types/testing-library__jest-dom": "^5.0.4",
"jest-fetch-mock": "^3.0.3",
- "react-router-dom": "^5.2.0"
+ "react-router-dom": "6.0.0-alpha.5"
},
"files": [
"dist/**/*.{js,d.ts}"
diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json
index d8e2b53ace..aba8d964be 100644
--- a/plugins/lighthouse/package.json
+++ b/plugins/lighthouse/package.json
@@ -25,7 +25,7 @@
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-markdown": "^4.3.1",
- "react-router-dom": "^5.2.0",
+ "react-router-dom": "6.0.0-alpha.5",
"react-use": "^14.2.0"
},
"devDependencies": {
diff --git a/plugins/lighthouse/src/components/AuditList/index.test.tsx b/plugins/lighthouse/src/components/AuditList/index.test.tsx
index bcd486e876..612bed4635 100644
--- a/plugins/lighthouse/src/components/AuditList/index.test.tsx
+++ b/plugins/lighthouse/src/components/AuditList/index.test.tsx
@@ -22,7 +22,7 @@ jest.mock('react-router-dom', () => {
};
return {
...actual,
- useHistory: jest.fn(() => mocks),
+ useNavigate: jest.fn(() => mocks),
};
});
@@ -41,7 +41,7 @@ import AuditList from '.';
import * as data from '../../__fixtures__/website-list-response.json';
-const { useHistory } = jest.requireMock('react-router-dom');
+const { useNavigate } = jest.requireMock('react-router-dom');
const websiteListResponse = data as WebsiteListResponse;
describe('AuditList', () => {
@@ -145,7 +145,9 @@ describe('AuditList', () => {
);
const element = await rendered.findByLabelText(/Go to page 1/);
fireEvent.click(element);
- expect(useHistory().replace).toHaveBeenCalledWith(`/lighthouse?page=1`);
+ expect(useNavigate().replace).toHaveBeenCalledWith(
+ `/lighthouse?page=1`,
+ );
});
});
});
diff --git a/plugins/lighthouse/src/components/AuditList/index.tsx b/plugins/lighthouse/src/components/AuditList/index.tsx
index 4dbbb05295..28fbc06ce1 100644
--- a/plugins/lighthouse/src/components/AuditList/index.tsx
+++ b/plugins/lighthouse/src/components/AuditList/index.tsx
@@ -16,7 +16,7 @@
import React, { useState, useMemo, FC, ReactNode } from 'react';
import { useLocalStorage, useAsync } from 'react-use';
-import { useHistory } from 'react-router-dom';
+import { useNavigate } from 'react-router-dom';
import { Grid, Button } from '@material-ui/core';
import Alert from '@material-ui/lab/Alert';
import Pagination from '@material-ui/lab/Pagination';
@@ -65,7 +65,7 @@ const AuditList: FC<{}> = () => {
return 0;
}, [value?.total, value?.limit]);
- const history = useHistory();
+ const history = useNavigate();
let content: ReactNode = null;
if (value) {
diff --git a/plugins/lighthouse/src/components/CreateAudit/index.test.tsx b/plugins/lighthouse/src/components/CreateAudit/index.test.tsx
index 08e8005530..39c109788f 100644
--- a/plugins/lighthouse/src/components/CreateAudit/index.test.tsx
+++ b/plugins/lighthouse/src/components/CreateAudit/index.test.tsx
@@ -22,7 +22,7 @@ jest.mock('react-router-dom', () => {
};
return {
...actual,
- useHistory: jest.fn(() => mocks),
+ useNavigate: jest.fn(() => mocks),
};
});
@@ -41,7 +41,7 @@ import { lighthouseApiRef, LighthouseRestApi, Audit } from '../../api';
import CreateAudit from '.';
import * as data from '../../__fixtures__/create-audit-response.json';
-const { useHistory }: { useHistory: jest.Mock } = jest.requireMock(
+const { useNavigate }: { useNavigate: jest.Mock } = jest.requireMock(
'react-router-dom',
);
const createAuditResponse = data as Audit;
@@ -115,7 +115,7 @@ describe('CreateAudit', () => {
describe('when the audit is successfully created', () => {
it('triggers a location change to the table', async () => {
- useHistory().push.mockClear();
+ useNavigate().push.mockClear();
mockFetch.mockResponseOnce(JSON.stringify(createAuditResponse));
const rendered = render(
@@ -140,7 +140,7 @@ describe('CreateAudit', () => {
await wait(() => expect(rendered.getByLabelText(/URL/)).toBeEnabled());
- expect(useHistory().push).toHaveBeenCalledWith('/lighthouse');
+ expect(useNavigate().push).toHaveBeenCalledWith('/lighthouse');
});
});
diff --git a/plugins/lighthouse/src/components/CreateAudit/index.tsx b/plugins/lighthouse/src/components/CreateAudit/index.tsx
index 55ae99d1db..4c7570221c 100644
--- a/plugins/lighthouse/src/components/CreateAudit/index.tsx
+++ b/plugins/lighthouse/src/components/CreateAudit/index.tsx
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import React, { useState, useCallback, FC } from 'react';
-import { useHistory } from 'react-router-dom';
+import { useNavigate } from 'react-router-dom';
import {
makeStyles,
Grid,
@@ -40,7 +40,7 @@ import { lighthouseApiRef } from '../../api';
import { useQuery } from '../../utils';
import LighthouseSupportButton from '../SupportButton';
-const useStyles = makeStyles((theme) => ({
+const useStyles = makeStyles(theme => ({
input: {
minWidth: 300,
},
@@ -58,7 +58,7 @@ const CreateAudit: FC<{}> = () => {
const lighthouseApi = useApi(lighthouseApiRef);
const classes = useStyles();
const query = useQuery();
- const history = useHistory();
+ const history = useNavigate();
const [submitting, setSubmitting] = useState(false);
const [url, setUrl] = useState(query.get('url') || '');
const [emulatedFormFactor, setEmulatedFormFactor] = useState('mobile');
@@ -113,7 +113,7 @@ const CreateAudit: FC<{}> = () => {
} />
+ Route 2} />
>,
- { routeEntries: ['/route2'] },
+ { routeEntries: ['/'] },
),
);
- expect(rendered.getByText('Route 2')).toBeInTheDocument();
+ expect(rendered.getByText('Route 2')).toBeInTheDocument();
// Wait for async actions to trigger the act() warnings that we assert below
await Promise.resolve();
});
diff --git a/packages/test-utils/src/testUtils/appWrappers.tsx b/packages/test-utils/src/testUtils/appWrappers.tsx
index ab627f8712..c474678fd1 100644
--- a/packages/test-utils/src/testUtils/appWrappers.tsx
+++ b/packages/test-utils/src/testUtils/appWrappers.tsx
@@ -92,7 +92,9 @@ export function wrapInTestApp(
return (
- } />
+ {/* The path of * here is needed to be set as a catch all, so it will render the wrapper element
+ * and work with nested routes if they exist too */}
+ } />
);
}
diff --git a/plugins/gitops-profiles/src/components/ClusterPage/ClusterPage.tsx b/plugins/gitops-profiles/src/components/ClusterPage/ClusterPage.tsx
index 29559e4fec..d454278255 100644
--- a/plugins/gitops-profiles/src/components/ClusterPage/ClusterPage.tsx
+++ b/plugins/gitops-profiles/src/components/ClusterPage/ClusterPage.tsx
@@ -33,8 +33,7 @@ import { gitOpsApiRef, Status } from '../../api';
import { transformRunStatus } from '../ProfileCatalog';
const ClusterPage: FC<{}> = () => {
- const params = useParams<{ owner: string; repo: string }>();
-
+ const params = useParams() as { owner: string; repo: string };
const [loginInfo] = useLocalStorage<{
token: string;
username: string;
diff --git a/yarn.lock b/yarn.lock
index 2402b971ef..7b7490f5f7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9966,6 +9966,18 @@ history@5.0.0-beta.9:
dependencies:
"@babel/runtime" "^7.7.6"
+history@^4.9.0:
+ version "4.10.1"
+ resolved "https://registry.npmjs.org/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"
+ integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==
+ dependencies:
+ "@babel/runtime" "^7.1.2"
+ loose-envify "^1.2.0"
+ resolve-pathname "^3.0.0"
+ tiny-invariant "^1.0.2"
+ tiny-warning "^1.0.0"
+ value-equal "^1.0.1"
+
hmac-drbg@^1.0.0:
version "1.0.1"
resolved "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
@@ -9975,7 +9987,7 @@ hmac-drbg@^1.0.0:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
-hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
+hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@@ -11118,6 +11130,11 @@ is-yarn-global@^0.3.0:
resolved "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232"
integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==
+isarray@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
+ integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
+
isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
@@ -12430,7 +12447,7 @@ loglevel@^1.6.8:
resolved "https://registry.npmjs.org/loglevel/-/loglevel-1.6.8.tgz#8a25fb75d092230ecd4457270d80b54e28011171"
integrity sha512-bsU7+gc9AJ2SqpzxwU3+1fedl8zAntbtC5XYlt3s2j1hJcn2PsXSmgN8TaLG/J1/2mod4+cE/3vNL70/c1RNCA==
-loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.0, loose-envify@^1.4.0:
+loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.0, loose-envify@^1.3.1, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@@ -12880,6 +12897,14 @@ min-indent@^1.0.0:
resolved "https://registry.npmjs.org/min-indent/-/min-indent-1.0.0.tgz#cfc45c37e9ec0d8f0a0ec3dd4ef7f7c3abe39256"
integrity sha1-z8RcN+nsDY8KDsPdTvf3w6vjklY=
+mini-create-react-context@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.0.tgz#df60501c83151db69e28eac0ef08b4002efab040"
+ integrity sha512-b0TytUgFSbgFJGzJqXPKCFCBWigAjpjo+Fl7Vf7ZbKRDptszpppKxXH6DRXEABZ/gcEQczeb0iZ7JvL8e8jjCA==
+ dependencies:
+ "@babel/runtime" "^7.5.5"
+ tiny-warning "^1.0.3"
+
mini-css-extract-plugin@^0.7.0:
version "0.7.0"
resolved "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.7.0.tgz#5ba8290fbb4179a43dd27cca444ba150bee743a0"
@@ -14244,6 +14269,13 @@ path-to-regexp@0.1.7:
resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
+path-to-regexp@^1.7.0:
+ version "1.8.0"
+ resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a"
+ integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==
+ dependencies:
+ isarray "0.0.1"
+
path-type@^1.0.0:
version "1.1.0"
resolved "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
@@ -15466,7 +15498,7 @@ react-inspector@^4.0.0:
is-dom "^1.0.9"
prop-types "^15.6.1"
-react-is@^16.12.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6, react-is@^16.9.0:
+react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6, react-is@^16.9.0:
version "16.13.1"
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -15545,6 +15577,35 @@ react-router-dom@6.0.0-alpha.5, react-router-dom@^6.0.0-alpha.5:
history "5.0.0-beta.9"
prop-types "^15.7.2"
+react-router-dom@^5.2.0:
+ version "5.2.0"
+ resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.0.tgz#9e65a4d0c45e13289e66c7b17c7e175d0ea15662"
+ integrity sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==
+ dependencies:
+ "@babel/runtime" "^7.1.2"
+ history "^4.9.0"
+ loose-envify "^1.3.1"
+ prop-types "^15.6.2"
+ react-router "5.2.0"
+ tiny-invariant "^1.0.2"
+ tiny-warning "^1.0.0"
+
+react-router@5.2.0:
+ version "5.2.0"
+ resolved "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz#424e75641ca8747fbf76e5ecca69781aa37ea293"
+ integrity sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==
+ dependencies:
+ "@babel/runtime" "^7.1.2"
+ history "^4.9.0"
+ hoist-non-react-statics "^3.1.0"
+ loose-envify "^1.3.1"
+ mini-create-react-context "^0.4.0"
+ path-to-regexp "^1.7.0"
+ prop-types "^15.6.2"
+ react-is "^16.6.0"
+ tiny-invariant "^1.0.2"
+ tiny-warning "^1.0.0"
+
react-router@6.0.0-alpha.5, react-router@^6.0.0-alpha.5:
version "6.0.0-alpha.5"
resolved "https://registry.npmjs.org/react-router/-/react-router-6.0.0-alpha.5.tgz#c98805e50dc0e64787aa8aa4fa6753b435f2496b"
@@ -16168,6 +16229,11 @@ resolve-from@^4.0.0:
resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
+resolve-pathname@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd"
+ integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==
+
resolve-url@^0.2.1:
version "0.2.1"
resolved "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
@@ -17934,12 +18000,12 @@ tiny-emitter@^2.0.0:
resolved "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423"
integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==
-tiny-invariant@^1.0.4, tiny-invariant@^1.0.6:
+tiny-invariant@^1.0.2, tiny-invariant@^1.0.4, tiny-invariant@^1.0.6:
version "1.1.0"
resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
-tiny-warning@^1.0.2:
+tiny-warning@^1.0.0, tiny-warning@^1.0.2, tiny-warning@^1.0.3:
version "1.0.3"
resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
@@ -18731,6 +18797,11 @@ validate-npm-package-name@^3.0.0:
dependencies:
builtins "^1.0.3"
+value-equal@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c"
+ integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==
+
vary@^1, vary@~1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
From da493b1fc9f412b453dda0e330571be87ac79d3b Mon Sep 17 00:00:00 2001
From: blam
Date: Thu, 11 Jun 2020 19:12:58 +0200
Subject: [PATCH 08/63] chore(react-router-v6): revert paralell for tests, as
my computer stared over 9000 node processes making my machine grind to a
machine babbage would have disapointed with
---
package.json | 2 +-
packages/core-api/src/app/App.tsx | 4 +---
.../core/src/components/Button/Button.test.jsx | 15 +++++++--------
packages/core/src/components/Link/Link.test.jsx | 7 ++++---
.../lighthouse/src/components/AuditView/index.tsx | 2 +-
5 files changed, 14 insertions(+), 16 deletions(-)
diff --git a/package.json b/package.json
index 5c45c4e17e..437ce36c4b 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,7 @@
"tsc": "tsc",
"clean": "backstage-cli clean && lerna run clean",
"diff": "lerna run diff --",
- "test": "lerna run test --since origin/master --parallel -- --coverage",
+ "test": "lerna run test --since origin/master -- --coverage",
"test:all": "lerna run test -- --coverage",
"lint": "lerna run lint --since origin/master --",
"lint:all": "lerna run lint --",
diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx
index c408303205..3f77d826ae 100644
--- a/packages/core-api/src/app/App.tsx
+++ b/packages/core-api/src/app/App.tsx
@@ -191,9 +191,7 @@ export class PrivateAppImpl implements BackstageApp {
-
- {children}
-
+ {children}
diff --git a/packages/core/src/components/Button/Button.test.jsx b/packages/core/src/components/Button/Button.test.jsx
index 2563d367c2..bf057e4261 100644
--- a/packages/core/src/components/Button/Button.test.jsx
+++ b/packages/core/src/components/Button/Button.test.jsx
@@ -15,26 +15,25 @@
*/
import React from 'react';
-import { render, fireEvent } from '@testing-library/react';
+import { render, fireEvent, act } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import { Button } from './Button';
-import { MemoryRouter, Route } from 'react-router';
-import { act } from 'react-dom/test-utils';
+import { Route } from 'react-router';
describe('', () => {
it('navigates using react-router', async () => {
const testString = 'This is test string';
const buttonLabel = 'Navigate!';
- const { getByText } = render(
+ const { getByText, getByRole } = render(
wrapInTestApp(
-
+ <>
+ {testString}} />
- {testString}{' '}
- ,
+ >,
),
);
expect(() => getByText(testString)).toThrow();
- await act(async () => fireEvent.click(getByText(buttonLabel)));
+ await act(async () => fireEvent.click(getByRole('button')));
expect(getByText(testString)).toBeInTheDocument();
});
});
diff --git a/packages/core/src/components/Link/Link.test.jsx b/packages/core/src/components/Link/Link.test.jsx
index fcbc92a67a..c35cdfdbe6 100644
--- a/packages/core/src/components/Link/Link.test.jsx
+++ b/packages/core/src/components/Link/Link.test.jsx
@@ -18,7 +18,7 @@ import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import { Link } from './Link';
-import { MemoryRouter, Route } from 'react-router';
+import { Route } from 'react-router';
import { act } from 'react-dom/test-utils';
describe('', () => {
@@ -27,14 +27,15 @@ describe('', () => {
const linkText = 'Navigate!';
const { getByText } = render(
wrapInTestApp(
-
+ <>
{linkText}
{testString}
- ,
+ >,
),
);
expect(() => getByText(testString)).toThrow();
await act(async () => fireEvent.click(getByText(linkText)));
+
expect(getByText(testString)).toBeInTheDocument();
});
});
diff --git a/plugins/lighthouse/src/components/AuditView/index.tsx b/plugins/lighthouse/src/components/AuditView/index.tsx
index af7636cf59..06ab812cf4 100644
--- a/plugins/lighthouse/src/components/AuditView/index.tsx
+++ b/plugins/lighthouse/src/components/AuditView/index.tsx
@@ -18,7 +18,6 @@ import { Link, useParams } from 'react-router-dom';
import { useAsync } from 'react-use';
import {
makeStyles,
- Button,
Grid,
List,
ListItem,
@@ -33,6 +32,7 @@ import {
Header,
Page,
Content,
+ Button,
ContentHeader,
HeaderLabel,
Progress,
From 47766635b67c52be748d68aa1631780fa5388142 Mon Sep 17 00:00:00 2001
From: blam
Date: Thu, 11 Jun 2020 20:20:58 +0200
Subject: [PATCH 09/63] chore(react-router-v6): Fixing navigation in the
lighthouse plugin
---
.../src/components/AuditList/index.test.tsx | 12 ++++--------
.../lighthouse/src/components/AuditView/index.tsx | 2 +-
.../src/components/CreateAudit/index.test.tsx | 11 ++++-------
3 files changed, 9 insertions(+), 16 deletions(-)
diff --git a/plugins/lighthouse/src/components/AuditList/index.test.tsx b/plugins/lighthouse/src/components/AuditList/index.test.tsx
index 612bed4635..076bb5819a 100644
--- a/plugins/lighthouse/src/components/AuditList/index.test.tsx
+++ b/plugins/lighthouse/src/components/AuditList/index.test.tsx
@@ -16,13 +16,10 @@
jest.mock('react-router-dom', () => {
const actual = jest.requireActual('react-router-dom');
- const mocks = {
- replace: jest.fn(),
- push: jest.fn(),
- };
+ const mockNavigation = jest.fn();
return {
...actual,
- useNavigate: jest.fn(() => mocks),
+ useNavigate: jest.fn(() => mockNavigation),
};
});
@@ -145,9 +142,8 @@ describe('AuditList', () => {
);
const element = await rendered.findByLabelText(/Go to page 1/);
fireEvent.click(element);
- expect(useNavigate().replace).toHaveBeenCalledWith(
- `/lighthouse?page=1`,
- );
+
+ expect(useNavigate()).toHaveBeenCalledWith(`/lighthouse?page=1`);
});
});
});
diff --git a/plugins/lighthouse/src/components/AuditView/index.tsx b/plugins/lighthouse/src/components/AuditView/index.tsx
index 06ab812cf4..d323b1a005 100644
--- a/plugins/lighthouse/src/components/AuditView/index.tsx
+++ b/plugins/lighthouse/src/components/AuditView/index.tsx
@@ -21,6 +21,7 @@ import {
Grid,
List,
ListItem,
+ Button,
ListItemIcon,
ListItemText,
} from '@material-ui/core';
@@ -32,7 +33,6 @@ import {
Header,
Page,
Content,
- Button,
ContentHeader,
HeaderLabel,
Progress,
diff --git a/plugins/lighthouse/src/components/CreateAudit/index.test.tsx b/plugins/lighthouse/src/components/CreateAudit/index.test.tsx
index 39c109788f..ee0404fc22 100644
--- a/plugins/lighthouse/src/components/CreateAudit/index.test.tsx
+++ b/plugins/lighthouse/src/components/CreateAudit/index.test.tsx
@@ -16,13 +16,10 @@
jest.mock('react-router-dom', () => {
const actual = jest.requireActual('react-router-dom');
- const mocks = {
- replace: jest.fn(),
- push: jest.fn(),
- };
+ const mockNavigate = jest.fn();
return {
...actual,
- useNavigate: jest.fn(() => mocks),
+ useNavigate: jest.fn(() => mockNavigate),
};
});
@@ -115,7 +112,7 @@ describe('CreateAudit', () => {
describe('when the audit is successfully created', () => {
it('triggers a location change to the table', async () => {
- useNavigate().push.mockClear();
+ useNavigate.mockClear();
mockFetch.mockResponseOnce(JSON.stringify(createAuditResponse));
const rendered = render(
@@ -140,7 +137,7 @@ describe('CreateAudit', () => {
await wait(() => expect(rendered.getByLabelText(/URL/)).toBeEnabled());
- expect(useNavigate().push).toHaveBeenCalledWith('/lighthouse');
+ expect(useNavigate()).toHaveBeenCalledWith('/lighthouse');
});
});
From 273db0314a771df699b8e3d95ccd30ffc83ef8df Mon Sep 17 00:00:00 2001
From: blam
Date: Thu, 11 Jun 2020 20:35:48 +0200
Subject: [PATCH 10/63] chore(react-router-v6): Managed to get the Link +
Button components working as expected
---
packages/core/src/components/Button/Button.test.jsx | 9 +++++----
packages/core/src/components/Link/Link.test.jsx | 6 +++---
packages/core/src/components/Link/Link.tsx | 2 +-
3 files changed, 9 insertions(+), 8 deletions(-)
diff --git a/packages/core/src/components/Button/Button.test.jsx b/packages/core/src/components/Button/Button.test.jsx
index bf057e4261..5750f84101 100644
--- a/packages/core/src/components/Button/Button.test.jsx
+++ b/packages/core/src/components/Button/Button.test.jsx
@@ -18,20 +18,21 @@ import React from 'react';
import { render, fireEvent, act } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import { Button } from './Button';
-import { Route } from 'react-router';
+import { Route, Routes } from 'react-router';
describe('', () => {
it('navigates using react-router', async () => {
const testString = 'This is test string';
const buttonLabel = 'Navigate!';
- const { getByText, getByRole } = render(
+ const { getByText, getByRole, ...all } = render(
wrapInTestApp(
- <>
+
{testString}} />
- >,
+ ,
),
);
+
expect(() => getByText(testString)).toThrow();
await act(async () => fireEvent.click(getByRole('button')));
expect(getByText(testString)).toBeInTheDocument();
diff --git a/packages/core/src/components/Link/Link.test.jsx b/packages/core/src/components/Link/Link.test.jsx
index c35cdfdbe6..bc279d2fd6 100644
--- a/packages/core/src/components/Link/Link.test.jsx
+++ b/packages/core/src/components/Link/Link.test.jsx
@@ -18,7 +18,7 @@ import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import { Link } from './Link';
-import { Route } from 'react-router';
+import { Route, Routes } from 'react-router';
import { act } from 'react-dom/test-utils';
describe('', () => {
@@ -27,10 +27,10 @@ describe('', () => {
const linkText = 'Navigate!';
const { getByText } = render(
wrapInTestApp(
- <>
+
{linkText}
{testString}
- >,
+ ,
),
);
expect(() => getByText(testString)).toThrow();
diff --git a/packages/core/src/components/Link/Link.tsx b/packages/core/src/components/Link/Link.tsx
index 8d800aadcd..ebcc2819d9 100644
--- a/packages/core/src/components/Link/Link.tsx
+++ b/packages/core/src/components/Link/Link.tsx
@@ -26,5 +26,5 @@ type Props = ComponentProps &
* Makes the Link to utilise react-router
*/
export const Link = React.forwardRef((props, ref) => (
-
+
));
From 0cd9a8a7d4573e130b4f9a4f7fb3fce602323716 Mon Sep 17 00:00:00 2001
From: blam
Date: Thu, 11 Jun 2020 20:42:39 +0200
Subject: [PATCH 11/63] chore(react-router-v6): tidy up parts of the PR i
wasn't happy with
---
packages/core/src/components/Button/Button.test.jsx | 4 ++--
packages/test-utils/src/testUtils/appWrappers.test.tsx | 8 ++++----
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/packages/core/src/components/Button/Button.test.jsx b/packages/core/src/components/Button/Button.test.jsx
index 5750f84101..115835be3f 100644
--- a/packages/core/src/components/Button/Button.test.jsx
+++ b/packages/core/src/components/Button/Button.test.jsx
@@ -24,7 +24,7 @@ describe('', () => {
it('navigates using react-router', async () => {
const testString = 'This is test string';
const buttonLabel = 'Navigate!';
- const { getByText, getByRole, ...all } = render(
+ const { getByText } = render(
wrapInTestApp(
{testString}} />
@@ -34,7 +34,7 @@ describe('', () => {
);
expect(() => getByText(testString)).toThrow();
- await act(async () => fireEvent.click(getByRole('button')));
+ await act(async () => fireEvent.click(getByText(buttonLabel)));
expect(getByText(testString)).toBeInTheDocument();
});
});
diff --git a/packages/test-utils/src/testUtils/appWrappers.test.tsx b/packages/test-utils/src/testUtils/appWrappers.test.tsx
index 75fcbe4e04..19c02c2158 100644
--- a/packages/test-utils/src/testUtils/appWrappers.test.tsx
+++ b/packages/test-utils/src/testUtils/appWrappers.test.tsx
@@ -17,7 +17,7 @@
import React, { FC, useEffect } from 'react';
import { render } from '@testing-library/react';
import { wrapInTestApp, renderInTestApp } from './appWrappers';
-import { Route } from 'react-router';
+import { Route, Routes } from 'react-router';
import { withLogCollector } from '@backstage/test-utils-core';
import {
useApi,
@@ -32,11 +32,11 @@ describe('wrapInTestApp', () => {
const { error } = await withLogCollector(['error'], async () => {
const rendered = render(
wrapInTestApp(
- <>
+
Route 1} />
Route 2} />
- >,
- { routeEntries: ['/'] },
+ ,
+ { routeEntries: ['/route2'] },
),
);
From acfe3103dc031a4f8f763d080b0bd83849129c46 Mon Sep 17 00:00:00 2001
From: blam
Date: Thu, 11 Jun 2020 21:03:04 +0200
Subject: [PATCH 12/63] chore(react-router-v6): fixing broken test with the new
router pattern
---
packages/core/src/components/Link/Link.test.jsx | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/packages/core/src/components/Link/Link.test.jsx b/packages/core/src/components/Link/Link.test.jsx
index bc279d2fd6..4bb78ef6d4 100644
--- a/packages/core/src/components/Link/Link.test.jsx
+++ b/packages/core/src/components/Link/Link.test.jsx
@@ -29,13 +29,12 @@ describe('', () => {
wrapInTestApp(
{linkText}
- {testString}
+ {testString}} />
,
),
);
expect(() => getByText(testString)).toThrow();
await act(async () => fireEvent.click(getByText(linkText)));
-
expect(getByText(testString)).toBeInTheDocument();
});
});
From 6f80d1b9bb02cd6cf2fad8c87db1579b85e6d3b0 Mon Sep 17 00:00:00 2001
From: blam
Date: Thu, 11 Jun 2020 21:31:02 +0200
Subject: [PATCH 13/63] chore(react-router-v6): Fixing what I think is the last
of the routing for the application
---
.../components/EntityPage/EntityPage.test.tsx | 42 ++++++++++---------
.../src/components/EntityPage/EntityPage.tsx | 30 +++++--------
2 files changed, 34 insertions(+), 38 deletions(-)
diff --git a/plugins/catalog/src/components/EntityPage/EntityPage.test.tsx b/plugins/catalog/src/components/EntityPage/EntityPage.test.tsx
index 22a47c192e..aeb9fc543b 100644
--- a/plugins/catalog/src/components/EntityPage/EntityPage.test.tsx
+++ b/plugins/catalog/src/components/EntityPage/EntityPage.test.tsx
@@ -14,32 +14,38 @@
* limitations under the License.
*/
+jest.mock('react-router-dom', () => {
+ const actual = jest.requireActual('react-router-dom');
+ const mockNavigate = jest.fn();
+ return {
+ ...actual,
+ useNavigate: jest.fn(() => mockNavigate),
+ useParams: jest.fn(),
+ };
+});
+
import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core';
import { wrapInTestApp } from '@backstage/test-utils';
import { render, wait } from '@testing-library/react';
import * as React from 'react';
import { CatalogApi, catalogApiRef } from '../../api/types';
import { EntityPage } from './EntityPage';
-
-const getTestProps = (name: string) => {
- return {
- match: {
- params: {
- optionalNamespaceAndName: name,
- kind: 'Component',
- },
- },
- history: {
- push: jest.fn(),
- },
- };
-};
+const {
+ useParams,
+ useNavigate,
+}: { useParams: jest.Mock; useNavigate: () => jest.Mock } = jest.requireMock(
+ 'react-router-dom',
+);
const errorApi = { post: () => {} };
describe('EntityPage', () => {
it('should redirect to catalog page when name is not provided', async () => {
- const props = getTestProps('');
+ useParams.mockReturnValue({
+ kind: 'Component',
+ optionalNamespaceAndName: '',
+ });
+
render(
wrapInTestApp(
{
],
])}
>
-
+
,
),
);
- await wait(() =>
- expect(props.history.push).toHaveBeenCalledWith('/catalog'),
- );
+ await wait(() => expect(useNavigate()).toHaveBeenCalledWith('/catalog'));
});
});
diff --git a/plugins/catalog/src/components/EntityPage/EntityPage.tsx b/plugins/catalog/src/components/EntityPage/EntityPage.tsx
index 96132819e8..e74276697b 100644
--- a/plugins/catalog/src/components/EntityPage/EntityPage.tsx
+++ b/plugins/catalog/src/components/EntityPage/EntityPage.tsx
@@ -34,21 +34,9 @@ import { catalogApiRef } from '../..';
import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu';
import { EntityMetadataCard } from '../EntityMetadataCard/EntityMetadataCard';
import { UnregisterEntityDialog } from '../UnregisterEntityDialog/UnregisterEntityDialog';
+import { useParams, useNavigate } from 'react-router-dom';
const REDIRECT_DELAY = 1000;
-
-type Props = {
- match: {
- params: {
- optionalNamespaceAndName: string;
- kind: string;
- };
- };
- history: {
- push: (url: string) => void;
- };
-};
-
function headerProps(
kind: string,
namespace: string | undefined,
@@ -68,8 +56,12 @@ function headerProps(
};
}
-export const EntityPage: FC = ({ match, history }) => {
- const { optionalNamespaceAndName, kind } = match.params;
+export const EntityPage: FC<{}> = () => {
+ const { optionalNamespaceAndName, kind } = useParams() as {
+ optionalNamespaceAndName: string;
+ kind: string;
+ };
+ const navigate = useNavigate();
const [name, namespace] = optionalNamespaceAndName.split(':').reverse();
const errorApi = useApi(errorApiRef);
@@ -85,19 +77,19 @@ export const EntityPage: FC = ({ match, history }) => {
if (!error && !loading && !entity) {
errorApi.post(new Error('Entity not found!'));
setTimeout(() => {
- history.push('/');
+ navigate('/');
}, REDIRECT_DELAY);
}
- }, [errorApi, history, error, loading, entity]);
+ }, [errorApi, navigate, error, loading, entity]);
if (!name) {
- history.push('/catalog');
+ navigate('/catalog');
return null;
}
const cleanUpAfterRemoval = async () => {
setConfirmationDialogOpen(false);
- history.push('/');
+ navigate('/');
};
const showRemovalDialog = () => setConfirmationDialogOpen(true);
From 22e411df3b167ab2ec3cb7b0f5de469b9e708870 Mon Sep 17 00:00:00 2001
From: blam
Date: Thu, 11 Jun 2020 21:48:07 +0200
Subject: [PATCH 14/63] chore(react-router-v6): fixing the comment
---
packages/core/src/components/Button/Button.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/core/src/components/Button/Button.tsx b/packages/core/src/components/Button/Button.tsx
index 39ecc3401e..ca45b3da7f 100644
--- a/packages/core/src/components/Button/Button.tsx
+++ b/packages/core/src/components/Button/Button.tsx
@@ -22,8 +22,8 @@ type Props = ComponentProps &
ComponentProps;
/**
- * Thin wrapper on top of material-ui's Link component
- * Makes the Link to utilise react-router
+ * Thin wrapper on top of material-ui's Button component
+ * Makes the Button to utilise react-router
*/
export const Button = React.forwardRef((props, ref) => (
From 2face4e519660b825a2db43964813c4a79cf78f5 Mon Sep 17 00:00:00 2001
From: blam
Date: Fri, 12 Jun 2020 00:29:22 +0200
Subject: [PATCH 15/63] chore(swr): Updating the yarn.lock
---
yarn.lock | 19 +++++++------------
1 file changed, 7 insertions(+), 12 deletions(-)
diff --git a/yarn.lock b/yarn.lock
index 73d9ccfad1..364f228c51 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6221,11 +6221,6 @@ clone-response@^1.0.2:
dependencies:
mimic-response "^1.0.0"
-clone@2.x:
- version "2.1.2"
- resolved "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
- integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=
-
clone@^1.0.2:
version "1.0.4"
resolved "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
@@ -13264,13 +13259,6 @@ nocache@2.1.0:
resolved "https://registry.npmjs.org/nocache/-/nocache-2.1.0.tgz#120c9ffec43b5729b1d5de88cd71aa75a0ba491f"
integrity sha512-0L9FvHG3nfnnmaEQPjT9xhfN4ISk0A8/2j4M37Np4mcDesJjHgEUfgPhdCyZuFI954tjokaIj/A3NdpFNdEh4Q==
-node-cache@^5.1.1:
- version "5.1.1"
- resolved "https://registry.npmjs.org/node-cache/-/node-cache-5.1.1.tgz#5fcc887176b23bdcd19cd1461b9544d2d501e786"
- integrity sha512-bJ9nH25Z51HG2QIu66K4dMVyMs6o8bNQpviDnXzG+O/gfNxPU9IpIig0j4pzlO707GcGZ6QA4rWhlRxjJsjnZw==
- dependencies:
- clone "2.x"
-
node-cleanup@^2.1.2:
version "2.1.2"
resolved "https://registry.npmjs.org/node-cleanup/-/node-cleanup-2.1.2.tgz#7ac19abd297e09a7f72a71545d951b517e4dde2c"
@@ -17700,6 +17688,13 @@ svgo@^1.0.0, svgo@^1.2.2:
unquote "~1.1.1"
util.promisify "~1.0.0"
+swr@^0.2.2:
+ version "0.2.2"
+ resolved "https://registry.npmjs.org/swr/-/swr-0.2.2.tgz#6e1b3e5c0e545c4fdb36ae3aa38cd94d0f9a88b7"
+ integrity sha512-D/z+PTUchZhoUA0tNC8TNJivf7Hc61WPxbUdXPi+VxRloddWYNP1ZicaEgyAph42ZnKl1L7twcZr4q6d0UMXcg==
+ dependencies:
+ fast-deep-equal "2.0.1"
+
symbol-observable@^1.1.0, symbol-observable@^1.2.0:
version "1.2.0"
resolved "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
From 1a581d4d260e6ddac9acaebd88a7cdfcfa9a2736 Mon Sep 17 00:00:00 2001
From: blam
Date: Fri, 12 Jun 2020 00:29:42 +0200
Subject: [PATCH 16/63] feat(catalog/swr): Added a nice stale-while-revalidate
pattern for fetching data
---
plugins/catalog/package.json | 4 +--
plugins/catalog/src/api/CatalogClient.ts | 11 -------
.../components/CatalogPage/CatalogPage.tsx | 31 +++++++++++++------
3 files changed, 24 insertions(+), 22 deletions(-)
diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json
index 106f6c713a..c6457aa8a8 100644
--- a/plugins/catalog/package.json
+++ b/plugins/catalog/package.json
@@ -30,12 +30,12 @@
"@material-ui/core": "^4.9.1",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
- "node-cache": "^5.1.1",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
- "react-use": "^14.2.0"
+ "react-use": "^14.2.0",
+ "swr": "^0.2.2"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.7",
diff --git a/plugins/catalog/src/api/CatalogClient.ts b/plugins/catalog/src/api/CatalogClient.ts
index d8add18f68..3804315ace 100644
--- a/plugins/catalog/src/api/CatalogClient.ts
+++ b/plugins/catalog/src/api/CatalogClient.ts
@@ -19,14 +19,9 @@ import {
Location,
LOCATION_ANNOTATION,
} from '@backstage/catalog-model';
-import Cache from 'node-cache';
import { CatalogApi, EntityCompoundName } from './types';
export class CatalogClient implements CatalogApi {
- // TODO(blam): This cache is just temporary until we have GraphQL.
- // And client side caching using things like React Apollo or Relay.
- // There's a lot of loading states that cause flickering around the app which aren't needed.
- private cache: Cache;
private apiOrigin: string;
private basePath: string;
@@ -39,7 +34,6 @@ export class CatalogClient implements CatalogApi {
}) {
this.apiOrigin = apiOrigin;
this.basePath = basePath;
- this.cache = new Cache({ stdTTL: 10 });
}
private async getRequired(path: string): Promise {
@@ -79,11 +73,6 @@ export class CatalogClient implements CatalogApi {
async getEntities(
filter?: Record,
): Promise {
- const cachedValue = this.cache.get(
- `get:${JSON.stringify(filter)}`,
- );
- if (cachedValue) return cachedValue;
-
let path = `/entities`;
if (filter) {
const params = new URLSearchParams();
diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx
index 570648f26b..50f8622aff 100644
--- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx
+++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx
@@ -33,9 +33,8 @@ import Edit from '@material-ui/icons/Edit';
import GitHub from '@material-ui/icons/GitHub';
import Star from '@material-ui/icons/Star';
import StarOutline from '@material-ui/icons/StarBorder';
-import React, { FC, useCallback, useState } from 'react';
+import React, { FC, useCallback, useState, useEffect } from 'react';
import { Link as RouterLink } from 'react-router-dom';
-import { useAsync } from 'react-use';
import { catalogApiRef } from '../..';
import { defaultFilter, entityFilters, filterGroups } from '../../data/filters';
import { findLocationForEntityMeta } from '../../data/utils';
@@ -45,6 +44,7 @@ import {
CatalogFilterItem,
} from '../CatalogFilter/CatalogFilter';
import { CatalogTable } from '../CatalogTable/CatalogTable';
+import useStaleWhileRevalidate from 'swr';
const useStyles = makeStyles(theme => ({
contentWrapper: {
@@ -70,11 +70,24 @@ export const CatalogPage: FC<{}> = () => {
defaultFilter,
);
- const { value, error, loading } = useAsync(async () => {
- const filter = entityFilters[selectedFilter.id];
- const all = await catalogApi.getEntities();
- return all.filter(e => filter(e, { isStarred: isStarredEntity(e) }));
- }, [selectedFilter.id, starredEntities.size]);
+ const { data: entities, error, revalidate } = useStaleWhileRevalidate(
+ ['catalog', entityFilters[selectedFilter.id]],
+ async (_, filter) => {
+ return await catalogApi.getEntities();
+ },
+ { compare: () => false },
+ );
+
+ const data =
+ entities?.filter(e =>
+ entityFilters[selectedFilter.id](e, { isStarred: isStarredEntity(e) }),
+ ) ?? [];
+
+ const revalidator = starredEntities.size || {};
+
+ useEffect(() => {
+ revalidate();
+ }, [revalidate, revalidator]);
const onFilterSelected = useCallback(
selected => setSelectedFilter(selected),
@@ -195,8 +208,8 @@ export const CatalogPage: FC<{}> = () => {
From 5ae13652fe70ce8403242dacd179c391acf44b63 Mon Sep 17 00:00:00 2001
From: blam
Date: Fri, 12 Jun 2020 00:34:11 +0200
Subject: [PATCH 17/63] feat(catalog/swr): simplify the code significantly. now
we are talking. this library is the nutz
---
.../components/CatalogPage/CatalogPage.tsx | 22 +++++--------------
1 file changed, 5 insertions(+), 17 deletions(-)
diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx
index 50f8622aff..2c929c6867 100644
--- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx
+++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx
@@ -61,21 +61,15 @@ const useStyles = makeStyles(theme => ({
export const CatalogPage: FC<{}> = () => {
const catalogApi = useApi(catalogApiRef);
- const {
- starredEntities,
- toggleStarredEntity,
- isStarredEntity,
- } = useStarredEntities();
+ const { toggleStarredEntity, isStarredEntity } = useStarredEntities();
+
const [selectedFilter, setSelectedFilter] = useState(
defaultFilter,
);
- const { data: entities, error, revalidate } = useStaleWhileRevalidate(
- ['catalog', entityFilters[selectedFilter.id]],
- async (_, filter) => {
- return await catalogApi.getEntities();
- },
- { compare: () => false },
+ const { data: entities, error } = useStaleWhileRevalidate(
+ ['catalog/all', entityFilters[selectedFilter.id]],
+ async () => catalogApi.getEntities(),
);
const data =
@@ -83,12 +77,6 @@ export const CatalogPage: FC<{}> = () => {
entityFilters[selectedFilter.id](e, { isStarred: isStarredEntity(e) }),
) ?? [];
- const revalidator = starredEntities.size || {};
-
- useEffect(() => {
- revalidate();
- }, [revalidate, revalidator]);
-
const onFilterSelected = useCallback(
selected => setSelectedFilter(selected),
[],
From 55cc1584e1b5edf615183953f1a4d6725e12928c Mon Sep 17 00:00:00 2001
From: blam
Date: Fri, 12 Jun 2020 00:53:05 +0200
Subject: [PATCH 18/63] chore(catalog/swr): Removing superfluous import for
useEffect
---
plugins/catalog/src/components/CatalogPage/CatalogPage.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx
index 2c929c6867..4d3321efc9 100644
--- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx
+++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx
@@ -33,7 +33,7 @@ import Edit from '@material-ui/icons/Edit';
import GitHub from '@material-ui/icons/GitHub';
import Star from '@material-ui/icons/Star';
import StarOutline from '@material-ui/icons/StarBorder';
-import React, { FC, useCallback, useState, useEffect } from 'react';
+import React, { FC, useCallback, useState } from 'react';
import { Link as RouterLink } from 'react-router-dom';
import { catalogApiRef } from '../..';
import { defaultFilter, entityFilters, filterGroups } from '../../data/filters';
From ed8436717a0eea5e699765745dc71f81fc9f95bd Mon Sep 17 00:00:00 2001
From: Patrik Oldsberg
Date: Fri, 12 Jun 2020 01:24:18 +0200
Subject: [PATCH 19/63] packages/config-loader: add support for transforming
config and identifying secrets
---
packages/config-loader/src/loader.ts | 75 ++++++++++++++++++++++++++--
1 file changed, 70 insertions(+), 5 deletions(-)
diff --git a/packages/config-loader/src/loader.ts b/packages/config-loader/src/loader.ts
index c8b5bb538d..6bdc954e04 100644
--- a/packages/config-loader/src/loader.ts
+++ b/packages/config-loader/src/loader.ts
@@ -17,7 +17,7 @@
import fs from 'fs-extra';
import yaml from 'yaml';
import { resolve as resolvePath } from 'path';
-import { AppConfig, JsonObject } from '@backstage/config';
+import { AppConfig, JsonObject, JsonValue } from '@backstage/config';
import { findRootPath } from './paths';
import { LoadConfigOptions } from './types';
@@ -77,7 +77,70 @@ export function readEnv(env: {
return config ? [config] : [];
}
-export async function readStaticConfig(
+type ReadFileFunc = (path: string) => Promise;
+
+function isObject(obj: JsonValue | undefined): obj is JsonObject {
+ if (typeof obj !== 'object') {
+ return false;
+ } else if (Array.isArray(obj)) {
+ return false;
+ }
+ return obj !== null;
+}
+
+export async function readConfigFile(filePath: string, readFile: ReadFileFunc) {
+ const configYaml = await readFile(filePath);
+ const config = yaml.parse(configYaml);
+
+ async function transform(
+ obj: JsonValue,
+ path: string,
+ ): Promise {
+ if (typeof obj !== 'object') {
+ return obj;
+ } else if (obj === null) {
+ return obj;
+ } else if (Array.isArray(obj)) {
+ const arr = new Array();
+
+ for (const [index, value] of obj.entries()) {
+ const out = await transform(value, `${path}[${index}]`);
+ if (out !== undefined) {
+ arr.push(out);
+ }
+ }
+
+ return arr;
+ }
+
+ if ('$secret' in obj) {
+ if (!isObject(obj.$secret)) {
+ throw TypeError(`Secret expected object at secret ${path}.$secret`);
+ }
+
+ return undefined;
+ }
+
+ const out: JsonObject = {};
+
+ for (const [key, value] of Object.entries(obj)) {
+ const result = await transform(value, `${path}.${key}`);
+ if (result !== undefined) {
+ out[key] = result;
+ }
+ }
+
+ return out;
+ }
+
+ const finalConfig = await transform(config, '');
+ if (!isObject(finalConfig)) {
+ throw new TypeError('Expected object at config root');
+ }
+ return finalConfig;
+}
+
+export async function loadStaticConfig(
options: LoadConfigOptions,
): Promise {
// TODO: We'll want this to be a bit more elaborate, probably adding configs for
@@ -91,8 +154,10 @@ export async function readStaticConfig(
}
try {
- const configYaml = await fs.readFile(configPath, 'utf8');
- const config = yaml.parse(configYaml);
+ const rootPath = configPath;
+ const config = await readConfigFile(configPath, (path: string) => {
+ return fs.readFile(resolvePath(rootPath, path), 'utf8');
+ });
return [config];
} catch (error) {
throw new Error(`Failed to read static configuration file, ${error}`);
@@ -105,7 +170,7 @@ export async function loadConfig(
const configs = [];
configs.push(...readEnv(process.env));
- configs.push(...(await readStaticConfig(options)));
+ configs.push(...(await loadStaticConfig(options)));
return configs;
}
From 2d543b9a3a9a98c72c87b897dacae0b54d6df355 Mon Sep 17 00:00:00 2001
From: blam
Date: Fri, 12 Jun 2020 02:24:53 +0200
Subject: [PATCH 20/63] chore(react-router-v6): reverting an old story
---
packages/core/src/components/Link/Link.stories.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/core/src/components/Link/Link.stories.tsx b/packages/core/src/components/Link/Link.stories.tsx
index ce09f5c9d0..2bd499fa33 100644
--- a/packages/core/src/components/Link/Link.stories.tsx
+++ b/packages/core/src/components/Link/Link.stories.tsx
@@ -73,7 +73,7 @@ export const PassProps = () => {
From 6a64ed933f17f7b392ff3ccc6e303976e50ea1bc Mon Sep 17 00:00:00 2001
From: blam
Date: Fri, 12 Jun 2020 02:44:10 +0200
Subject: [PATCH 21/63] chore(react-router-v6): quick fix for some type
problems
---
packages/core/src/components/Link/Link.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/core/src/components/Link/Link.tsx b/packages/core/src/components/Link/Link.tsx
index ebcc2819d9..8e7e3f179e 100644
--- a/packages/core/src/components/Link/Link.tsx
+++ b/packages/core/src/components/Link/Link.tsx
@@ -19,7 +19,7 @@ import { Link as MaterialLink } from '@material-ui/core';
import { Link as RouterLink } from 'react-router-dom';
type Props = ComponentProps &
- ComponentProps & { as?: React.FC };
+ ComponentProps & { component?: React.FC };
/**
* Thin wrapper on top of material-ui's Link component
From 86e399221c21e6e840f384cf684c67aa95a3c386 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Stefan=20=C3=85lund?=
Date: Fri, 12 Jun 2020 08:43:57 +0200
Subject: [PATCH 22/63] Link to SECURITY in CONTRIBUTING
---
CONTRIBUTING.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 43c1f0d7b8..dca9e678dc 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -68,4 +68,4 @@ This project adheres to the [Spotify FOSS Code of Conduct][code-of-conduct]. By
# Security Issues?
-Please report sensitive security issues via Spotify's [bug-bounty program](https://hackerone.com/spotify) rather than GitHub.
+See [SECURITY](SECURITY.md).
From 5e034d687bbea6d4a547d0c3407d6a0539a138ad Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Stefan=20=C3=85lund?=
Date: Fri, 12 Jun 2020 09:56:06 +0200
Subject: [PATCH 23/63] Repo hygiene: Add SECURITY.md (#1262)
* Repo hygiene: Add SECURITY.md
* Update SECURITY.md
---
SECURITY.md | 11 +++++++++++
1 file changed, 11 insertions(+)
create mode 100644 SECURITY.md
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000000..3ec51b4210
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,11 @@
+# Security Policy
+
+## Supported Versions
+
+| Version | Supported |
+| ------- | ------------------ |
+| 0.x | :white_check_mark: |
+
+## Reporting a Vulnerability
+
+Please report sensitive security issues via Spotify's [bug-bounty program](https://hackerone.com/spotify) rather than GitHub.
From 7de5e10408368b20da45f6b650e8420e342ba017 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?=
Date: Fri, 12 Jun 2020 11:37:26 +0200
Subject: [PATCH 24/63] feat(catalog-backend): reject almost-same-name entities
---
packages/backend/src/plugins/catalog.ts | 2 +-
.../catalog/DatabaseLocationsCatalog.test.ts | 19 +--
.../src/database/CommonDatabase.test.ts | 161 ++++++++----------
.../src/database/CommonDatabase.ts | 12 +-
.../src/database/DatabaseManager.ts | 38 ++++-
.../20200612102317_unique_normalized.ts | 51 ++++++
plugins/catalog-backend/src/database/types.ts | 3 +
.../src/service/standaloneServer.ts | 2 +-
8 files changed, 168 insertions(+), 120 deletions(-)
create mode 100644 plugins/catalog-backend/src/database/migrations/20200612102317_unique_normalized.ts
diff --git a/packages/backend/src/plugins/catalog.ts b/packages/backend/src/plugins/catalog.ts
index 8d8ac5b47b..c472623c3d 100644
--- a/packages/backend/src/plugins/catalog.ts
+++ b/packages/backend/src/plugins/catalog.ts
@@ -31,7 +31,7 @@ export default async function createPlugin({
}: PluginEnvironment) {
const locationReader = new LocationReaders(logger);
- const db = await DatabaseManager.createDatabase(database, logger);
+ const db = await DatabaseManager.createDatabase(database, { logger });
const entitiesCatalog = new DatabaseEntitiesCatalog(db);
const locationsCatalog = new DatabaseLocationsCatalog(db);
const higherOrderOperation = new HigherOrderOperations(
diff --git a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts
index 907bdea2d5..8c7897fa2c 100644
--- a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts
+++ b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts
@@ -14,29 +14,14 @@
* limitations under the License.
*/
-import { getVoidLogger } from '@backstage/backend-common';
-import Knex from 'knex';
-import path from 'path';
-import { CommonDatabase } from '../database';
+import { DatabaseManager } from '../database';
import { DatabaseLocationsCatalog } from './DatabaseLocationsCatalog';
describe('DatabaseLocationsCatalog', () => {
let catalog: DatabaseLocationsCatalog;
beforeEach(async () => {
- const knex = Knex({
- client: 'sqlite3',
- connection: ':memory:',
- useNullAsDefault: true,
- });
- knex.client.pool.on('createSuccess', (_eventId: any, resource: any) => {
- resource.run('PRAGMA foreign_keys = ON', () => {});
- });
- await knex.migrate.latest({
- directory: path.resolve(__dirname, '../database/migrations'),
- loadExtensions: ['.ts'],
- });
- const db = new CommonDatabase(knex, getVoidLogger());
+ const db = await DatabaseManager.createTestDatabase();
catalog = new DatabaseLocationsCatalog(db);
});
diff --git a/plugins/catalog-backend/src/database/CommonDatabase.test.ts b/plugins/catalog-backend/src/database/CommonDatabase.test.ts
index 1802ea5e7d..169880e3c9 100644
--- a/plugins/catalog-backend/src/database/CommonDatabase.test.ts
+++ b/plugins/catalog-backend/src/database/CommonDatabase.test.ts
@@ -14,16 +14,10 @@
* limitations under the License.
*/
-import {
- ConflictError,
- getVoidLogger,
- NotFoundError,
-} from '@backstage/backend-common';
+import { ConflictError, NotFoundError } from '@backstage/backend-common';
import type { Entity, Location } from '@backstage/catalog-model';
-import Knex from 'knex';
-import path from 'path';
-import { CommonDatabase } from './CommonDatabase';
-import { DatabaseLocationUpdateLogStatus } from './types';
+import { DatabaseManager } from './DatabaseManager';
+import { Database, DatabaseLocationUpdateLogStatus } from './types';
import type {
DbEntityRequest,
DbEntityResponse,
@@ -31,22 +25,12 @@ import type {
} from './types';
describe('CommonDatabase', () => {
- let knex: Knex;
+ let db: Database;
let entityRequest: DbEntityRequest;
let entityResponse: DbEntityResponse;
beforeEach(async () => {
- knex = Knex({
- client: 'sqlite3',
- connection: ':memory:',
- useNullAsDefault: true,
- });
-
- await knex.raw('PRAGMA foreign_keys = ON');
- await knex.migrate.latest({
- directory: path.resolve(__dirname, 'migrations'),
- loadExtensions: ['.ts'],
- });
+ db = await DatabaseManager.createTestDatabase();
entityRequest = {
entity: {
@@ -84,7 +68,6 @@ describe('CommonDatabase', () => {
});
it('manages locations', async () => {
- const db = new CommonDatabase(knex, getVoidLogger());
const input: Location = {
id: 'dd12620d-0436-422f-93bd-929aa0788123',
type: 'a',
@@ -115,55 +98,76 @@ describe('CommonDatabase', () => {
describe('addEntity', () => {
it('happy path: adds entity to empty database', async () => {
- const catalog = new CommonDatabase(knex, getVoidLogger());
- const added = await catalog.transaction(tx =>
- catalog.addEntity(tx, entityRequest),
- );
+ const added = await db.transaction(tx => db.addEntity(tx, entityRequest));
expect(added).toStrictEqual(entityResponse);
expect(added.entity.metadata.generation).toBe(1);
});
it('rejects adding the same-named entity twice', async () => {
- const catalog = new CommonDatabase(knex, getVoidLogger());
- await catalog.transaction(tx => catalog.addEntity(tx, entityRequest));
+ await db.transaction(tx => db.addEntity(tx, entityRequest));
await expect(
- catalog.transaction(tx => catalog.addEntity(tx, entityRequest)),
+ db.transaction(tx => db.addEntity(tx, entityRequest)),
+ ).rejects.toThrow(ConflictError);
+ });
+
+ it('rejects adding the almost-same-kind entity twice', async () => {
+ entityRequest.entity.kind = 'some-kind';
+ await db.transaction(tx => db.addEntity(tx, entityRequest));
+ entityRequest.entity.kind = 'SomeKind';
+ await expect(
+ db.transaction(tx => db.addEntity(tx, entityRequest)),
+ ).rejects.toThrow(ConflictError);
+ });
+
+ it('rejects adding the almost-same-named entity twice', async () => {
+ entityRequest.entity.metadata.name = 'some-name';
+ await db.transaction(tx => db.addEntity(tx, entityRequest));
+ entityRequest.entity.metadata.name = 'SomeName';
+ await expect(
+ db.transaction(tx => db.addEntity(tx, entityRequest)),
+ ).rejects.toThrow(ConflictError);
+ });
+
+ it('rejects adding the almost-same-namespace entity twice', async () => {
+ entityRequest.entity.metadata.namespace = undefined;
+ await db.transaction(tx => db.addEntity(tx, entityRequest));
+ entityRequest.entity.metadata.namespace = '';
+ await expect(
+ db.transaction(tx => db.addEntity(tx, entityRequest)),
).rejects.toThrow(ConflictError);
});
it('accepts adding the same-named entity twice if on different namespaces', async () => {
- const catalog = new CommonDatabase(knex, getVoidLogger());
entityRequest.entity.metadata.namespace = 'namespace1';
- await catalog.transaction(tx => catalog.addEntity(tx, entityRequest));
+ await db.transaction(tx => db.addEntity(tx, entityRequest));
entityRequest.entity.metadata.namespace = 'namespace2';
await expect(
- catalog.transaction(tx => catalog.addEntity(tx, entityRequest)),
+ db.transaction(tx => db.addEntity(tx, entityRequest)),
).resolves.toBeDefined();
});
});
describe('locationHistory', () => {
it('outputs the history correctly', async () => {
- const catalog = new CommonDatabase(knex, getVoidLogger());
const location: Location = {
id: 'dd12620d-0436-422f-93bd-929aa0788123',
type: 'a',
target: 'b',
};
- await catalog.addLocation(location);
+ await db.addLocation(location);
- await catalog.addLocationUpdateLogEvent(
+ await db.addLocationUpdateLogEvent(
'dd12620d-0436-422f-93bd-929aa0788123',
DatabaseLocationUpdateLogStatus.SUCCESS,
);
- await catalog.addLocationUpdateLogEvent(
+ await db.addLocationUpdateLogEvent(
'dd12620d-0436-422f-93bd-929aa0788123',
DatabaseLocationUpdateLogStatus.FAIL,
undefined,
'Something went wrong',
);
- const result = await catalog.locationHistory(
+ const result = await db.locationHistory(
'dd12620d-0436-422f-93bd-929aa0788123',
);
expect(result).toEqual([
@@ -189,12 +193,9 @@ describe('CommonDatabase', () => {
describe('updateEntity', () => {
it('can read and no-op-update an entity', async () => {
- const catalog = new CommonDatabase(knex, getVoidLogger());
- const added = await catalog.transaction(tx =>
- catalog.addEntity(tx, entityRequest),
- );
- const updated = await catalog.transaction(tx =>
- catalog.updateEntity(tx, { entity: added.entity }),
+ const added = await db.transaction(tx => db.addEntity(tx, entityRequest));
+ const updated = await db.transaction(tx =>
+ db.updateEntity(tx, { entity: added.entity }),
);
expect(updated.entity.apiVersion).toEqual(added.entity.apiVersion);
expect(updated.entity.kind).toEqual(added.entity.kind);
@@ -211,77 +212,55 @@ describe('CommonDatabase', () => {
});
it('can update name if uid matches', async () => {
- const catalog = new CommonDatabase(knex, getVoidLogger());
- const added = await catalog.transaction(tx =>
- catalog.addEntity(tx, entityRequest),
- );
+ const added = await db.transaction(tx => db.addEntity(tx, entityRequest));
added.entity.metadata.name! = 'new!';
- const updated = await catalog.transaction(tx =>
- catalog.updateEntity(tx, { entity: added.entity }),
+ const updated = await db.transaction(tx =>
+ db.updateEntity(tx, { entity: added.entity }),
);
expect(updated.entity.metadata.name).toEqual('new!');
});
it('can update fields if kind, name, and namespace match', async () => {
- const catalog = new CommonDatabase(knex, getVoidLogger());
- const added = await catalog.transaction(tx =>
- catalog.addEntity(tx, entityRequest),
- );
+ const added = await db.transaction(tx => db.addEntity(tx, entityRequest));
added.entity.apiVersion = 'something.new';
delete added.entity.metadata.uid;
delete added.entity.metadata.generation;
- const updated = await catalog.transaction(tx =>
- catalog.updateEntity(tx, { entity: added.entity }),
+ const updated = await db.transaction(tx =>
+ db.updateEntity(tx, { entity: added.entity }),
);
expect(updated.entity.apiVersion).toEqual('something.new');
});
it('rejects if kind, name, but not namespace match', async () => {
- const catalog = new CommonDatabase(knex, getVoidLogger());
- const added = await catalog.transaction(tx =>
- catalog.addEntity(tx, entityRequest),
- );
+ const added = await db.transaction(tx => db.addEntity(tx, entityRequest));
added.entity.apiVersion = 'something.new';
delete added.entity.metadata.uid;
delete added.entity.metadata.generation;
added.entity.metadata.namespace = 'something.wrong';
await expect(
- catalog.transaction(tx =>
- catalog.updateEntity(tx, { entity: added.entity }),
- ),
+ db.transaction(tx => db.updateEntity(tx, { entity: added.entity })),
).rejects.toThrow(NotFoundError);
});
it('fails to update an entity if etag does not match', async () => {
- const catalog = new CommonDatabase(knex, getVoidLogger());
- const added = await catalog.transaction(tx =>
- catalog.addEntity(tx, entityRequest),
- );
+ const added = await db.transaction(tx => db.addEntity(tx, entityRequest));
added.entity.metadata.etag = 'garbage';
await expect(
- catalog.transaction(tx =>
- catalog.updateEntity(tx, { entity: added.entity }),
- ),
+ db.transaction(tx => db.updateEntity(tx, { entity: added.entity })),
).rejects.toThrow(ConflictError);
});
it('fails to update an entity if generation does not match', async () => {
- const catalog = new CommonDatabase(knex, getVoidLogger());
- const added = await catalog.transaction(tx =>
- catalog.addEntity(tx, entityRequest),
- );
+ const added = await db.transaction(tx => db.addEntity(tx, entityRequest));
added.entity.metadata.generation! += 100;
await expect(
- catalog.transaction(tx =>
- catalog.updateEntity(tx, { entity: added.entity }),
- ),
+ db.transaction(tx => db.updateEntity(tx, { entity: added.entity })),
).rejects.toThrow(ConflictError);
});
});
describe('entities', () => {
it('can get all entities with empty filters list', async () => {
- const catalog = new CommonDatabase(knex, getVoidLogger());
const e1: Entity = {
apiVersion: 'a',
kind: 'k1',
@@ -293,13 +272,11 @@ describe('CommonDatabase', () => {
metadata: { name: 'n' },
spec: { c: null },
};
- await catalog.transaction(async tx => {
- await catalog.addEntity(tx, { entity: e1 });
- await catalog.addEntity(tx, { entity: e2 });
+ await db.transaction(async tx => {
+ await db.addEntity(tx, { entity: e1 });
+ await db.addEntity(tx, { entity: e2 });
});
- const result = await catalog.transaction(async tx =>
- catalog.entities(tx, []),
- );
+ const result = await db.transaction(async tx => db.entities(tx, []));
expect(result.length).toEqual(2);
expect(result).toEqual(
expect.arrayContaining([
@@ -316,7 +293,6 @@ describe('CommonDatabase', () => {
});
it('can get all specific entities for matching filters (naive case)', async () => {
- const catalog = new CommonDatabase(knex, getVoidLogger());
const entities: Entity[] = [
{ apiVersion: 'a', kind: 'k1', metadata: { name: 'n' } },
{
@@ -333,15 +309,15 @@ describe('CommonDatabase', () => {
},
];
- await catalog.transaction(async tx => {
+ await db.transaction(async tx => {
for (const entity of entities) {
- await catalog.addEntity(tx, { entity });
+ await db.addEntity(tx, { entity });
}
});
await expect(
- catalog.transaction(async tx =>
- catalog.entities(tx, [
+ db.transaction(async tx =>
+ db.entities(tx, [
{ key: 'kind', values: ['k2'] },
{ key: 'spec.c', values: ['some'] },
]),
@@ -355,7 +331,6 @@ describe('CommonDatabase', () => {
});
it('can get all specific entities for matching filters with nulls (both missing and literal null value)', async () => {
- const catalog = new CommonDatabase(knex, getVoidLogger());
const entities: Entity[] = [
{ apiVersion: 'a', kind: 'k1', metadata: { name: 'n' } },
{
@@ -372,14 +347,14 @@ describe('CommonDatabase', () => {
},
];
- await catalog.transaction(async tx => {
+ await db.transaction(async tx => {
for (const entity of entities) {
- await catalog.addEntity(tx, { entity });
+ await db.addEntity(tx, { entity });
}
});
- const rows = await catalog.transaction(async tx =>
- catalog.entities(tx, [
+ const rows = await db.transaction(async tx =>
+ db.entities(tx, [
{ key: 'apiVersion', values: ['a'] },
{ key: 'spec.c', values: [null, 'some'] },
]),
diff --git a/plugins/catalog-backend/src/database/CommonDatabase.ts b/plugins/catalog-backend/src/database/CommonDatabase.ts
index 2a1b33bbbe..67feb82fac 100644
--- a/plugins/catalog-backend/src/database/CommonDatabase.ts
+++ b/plugins/catalog-backend/src/database/CommonDatabase.ts
@@ -43,7 +43,6 @@ function getStrippedMetadata(metadata: EntityMeta): EntityMeta {
delete output.uid;
delete output.etag;
delete output.generation;
-
return output;
}
@@ -62,6 +61,7 @@ function serializeSpec(spec: Entity['spec']): DbEntitiesRow['spec'] {
function toEntityRow(
locationId: string | undefined,
entity: Entity,
+ normalize: (value: string) => string,
): DbEntitiesRow {
return {
id: entity.metadata.uid!,
@@ -70,8 +70,11 @@ function toEntityRow(
generation: entity.metadata.generation!,
api_version: entity.apiVersion,
kind: entity.kind,
- name: entity.metadata.name || null,
+ name: entity.metadata.name,
namespace: entity.metadata.namespace || null,
+ kind_normalized: normalize(entity.kind),
+ name_normalized: normalize(entity.metadata.name),
+ namespace_normalized: normalize(entity.metadata.namespace || ''),
metadata: serializeMetadata(entity.metadata),
spec: serializeSpec(entity.spec),
};
@@ -124,6 +127,7 @@ function generateEtag(): string {
export class CommonDatabase implements Database {
constructor(
private readonly database: Knex,
+ private readonly normalize: (value: string) => string,
private readonly logger: Logger,
) {}
@@ -166,7 +170,7 @@ export class CommonDatabase implements Database {
generation: 1,
};
- const newRow = toEntityRow(request.locationId, newEntity);
+ const newRow = toEntityRow(request.locationId, newEntity, this.normalize);
await tx('entities').insert(newRow);
await this.updateEntitiesSearch(tx, newRow.id, newEntity);
@@ -257,7 +261,7 @@ export class CommonDatabase implements Database {
// Store the updated entity; select on the old etag to ensure that we do
// not lose to another writer
- const newRow = toEntityRow(request.locationId, newEntity);
+ const newRow = toEntityRow(request.locationId, newEntity, this.normalize);
const updatedRows = await tx('entities')
.where({ id: oldRow.id, etag: oldRow.etag })
.update(newRow);
diff --git a/plugins/catalog-backend/src/database/DatabaseManager.ts b/plugins/catalog-backend/src/database/DatabaseManager.ts
index f0fbe92600..8710aede4c 100644
--- a/plugins/catalog-backend/src/database/DatabaseManager.ts
+++ b/plugins/catalog-backend/src/database/DatabaseManager.ts
@@ -14,26 +14,39 @@
* limitations under the License.
*/
+import { getVoidLogger } from '@backstage/backend-common';
+import { makeValidator } from '@backstage/catalog-model';
import Knex from 'knex';
import path from 'path';
import { Logger } from 'winston';
import { CommonDatabase } from './CommonDatabase';
import { Database } from './types';
+export type CreateDatabaseOptions = {
+ logger: Logger;
+ fieldNormalizer: (value: string) => string;
+};
+
+const defaultOptions: CreateDatabaseOptions = {
+ logger: getVoidLogger(),
+ fieldNormalizer: makeValidator().normalizeEntityName,
+};
+
export class DatabaseManager {
public static async createDatabase(
knex: Knex,
- logger: Logger,
+ options: Partial = {},
): Promise {
await knex.migrate.latest({
directory: path.resolve(__dirname, 'migrations'),
loadExtensions: ['.js'],
});
- return new CommonDatabase(knex, logger);
+ const { logger, fieldNormalizer } = { ...defaultOptions, ...options };
+ return new CommonDatabase(knex, fieldNormalizer, logger);
}
public static async createInMemoryDatabase(
- logger: Logger,
+ options: Partial = {},
): Promise {
const knex = Knex({
client: 'sqlite3',
@@ -43,6 +56,23 @@ export class DatabaseManager {
knex.client.pool.on('createSuccess', (_eventId: any, resource: any) => {
resource.run('PRAGMA foreign_keys = ON', () => {});
});
- return DatabaseManager.createDatabase(knex, logger);
+ return DatabaseManager.createDatabase(knex, options);
+ }
+
+ public static async createTestDatabase(): Promise {
+ const knex = Knex({
+ client: 'sqlite3',
+ connection: ':memory:',
+ useNullAsDefault: true,
+ });
+ knex.client.pool.on('createSuccess', (_eventId: any, resource: any) => {
+ resource.run('PRAGMA foreign_keys = ON', () => {});
+ });
+ await knex.migrate.latest({
+ directory: path.resolve(__dirname, 'migrations'),
+ loadExtensions: ['.ts'],
+ });
+ const { logger, fieldNormalizer } = defaultOptions;
+ return new CommonDatabase(knex, fieldNormalizer, logger);
}
}
diff --git a/plugins/catalog-backend/src/database/migrations/20200612102317_unique_normalized.ts b/plugins/catalog-backend/src/database/migrations/20200612102317_unique_normalized.ts
new file mode 100644
index 0000000000..38ed7f6047
--- /dev/null
+++ b/plugins/catalog-backend/src/database/migrations/20200612102317_unique_normalized.ts
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import * as Knex from 'knex';
+
+export async function up(knex: Knex): Promise {
+ return knex.schema.alterTable('entities', table => {
+ // these are added as nullable because sqlite does not support alter column
+ table
+ .string('kind_normalized')
+ .nullable()
+ .comment('The kind field of the entity, normalized');
+ table
+ .string('name_normalized')
+ .nullable()
+ .comment('The metadata.name field of the entity, normalized');
+ table
+ .string('namespace_normalized')
+ .nullable()
+ .comment('The metadata.namespace field of the entity, normalized');
+ table.dropUnique([], 'entities_unique_name');
+ table.unique(
+ ['kind_normalized', 'name_normalized', 'namespace_normalized'],
+ 'entities_unique_name_normalized',
+ );
+ });
+}
+
+export async function down(knex: Knex): Promise {
+ return knex.schema.alterTable('entities', table => {
+ table.dropUnique([], 'entities_unique_name_normalized');
+ table.unique(['kind', 'name', 'namespace'], 'entities_unique_name');
+ table.dropColumns(
+ 'kind_normalized',
+ 'name_normalized',
+ 'namespace_normalized',
+ );
+ });
+}
diff --git a/plugins/catalog-backend/src/database/types.ts b/plugins/catalog-backend/src/database/types.ts
index fc81e124ba..1cf949c4c2 100644
--- a/plugins/catalog-backend/src/database/types.ts
+++ b/plugins/catalog-backend/src/database/types.ts
@@ -23,6 +23,9 @@ export type DbEntitiesRow = {
kind: string;
name: string | null;
namespace: string | null;
+ kind_normalized: string;
+ name_normalized: string;
+ namespace_normalized: string;
etag: string;
generation: number;
metadata: string;
diff --git a/plugins/catalog-backend/src/service/standaloneServer.ts b/plugins/catalog-backend/src/service/standaloneServer.ts
index f19dfaf282..6a9755c0c4 100644
--- a/plugins/catalog-backend/src/service/standaloneServer.ts
+++ b/plugins/catalog-backend/src/service/standaloneServer.ts
@@ -36,7 +36,7 @@ export async function startStandaloneServer(
const logger = options.logger.child({ service: 'catalog-backend' });
logger.debug('Creating application...');
- const db = await DatabaseManager.createInMemoryDatabase(logger);
+ const db = await DatabaseManager.createInMemoryDatabase({ logger });
const entitiesCatalog = new DatabaseEntitiesCatalog(db);
const locationsCatalog = new DatabaseLocationsCatalog(db);
const locationReader = new LocationReaders();
From 5f13a53c6ac7c2693c0e46a8abfb80945d58377c Mon Sep 17 00:00:00 2001
From: Raghunandan
Date: Fri, 12 Jun 2020 12:06:46 +0200
Subject: [PATCH 25/63] Making auth backend work based on config. Config is yet
to be read from the YAML and passed in. Localhost development will fail
gracefully if AUTH config env vars are not set
---
.../implementations/auth/github/GithubAuth.ts | 2 +-
.../implementations/auth/google/GoogleAuth.ts | 2 +-
.../src/lib/EnvironmentHandler.ts | 62 ++++++++
.../{providers => lib}/OAuthProvider.test.ts | 113 +++++++------
.../src/{providers => lib}/OAuthProvider.ts | 148 +++++++++---------
.../PassportStrategyHelper.test.ts | 0
.../PassportStrategyHelper.ts | 6 +-
plugins/auth-backend/src/providers/config.ts | 43 -----
.../auth-backend/src/providers/factories.ts | 15 +-
.../src/providers/github/provider.ts | 60 +++++--
.../src/providers/google/provider.ts | 62 ++++++--
.../src/providers/saml/provider.ts | 58 +++++--
plugins/auth-backend/src/providers/types.ts | 35 ++++-
plugins/auth-backend/src/service/router.ts | 63 +++++++-
14 files changed, 441 insertions(+), 228 deletions(-)
create mode 100644 plugins/auth-backend/src/lib/EnvironmentHandler.ts
rename plugins/auth-backend/src/{providers => lib}/OAuthProvider.test.ts (78%)
rename plugins/auth-backend/src/{providers => lib}/OAuthProvider.ts (64%)
rename plugins/auth-backend/src/{providers => lib}/PassportStrategyHelper.test.ts (100%)
rename plugins/auth-backend/src/{providers => lib}/PassportStrategyHelper.ts (97%)
delete mode 100644 plugins/auth-backend/src/providers/config.ts
diff --git a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts
index ac05b718e5..f4a7092f79 100644
--- a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts
+++ b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts
@@ -57,7 +57,7 @@ class GithubAuth implements OAuthApi, SessionStateApi {
static create({
apiOrigin,
basePath,
- environment = 'dev',
+ environment = 'development',
provider = DEFAULT_PROVIDER,
oauthRequestApi,
}: CreateOptions) {
diff --git a/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts b/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts
index 1fc6f4f6b8..183ea592b4 100644
--- a/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts
+++ b/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts
@@ -66,7 +66,7 @@ class GoogleAuth
static create({
apiOrigin,
basePath,
- environment = 'dev',
+ environment = 'development',
provider = DEFAULT_PROVIDER,
oauthRequestApi,
}: CreateOptions) {
diff --git a/plugins/auth-backend/src/lib/EnvironmentHandler.ts b/plugins/auth-backend/src/lib/EnvironmentHandler.ts
new file mode 100644
index 0000000000..4fb80ca3e8
--- /dev/null
+++ b/plugins/auth-backend/src/lib/EnvironmentHandler.ts
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import express from 'express';
+import { AuthProviderRouteHandlers } from '../providers/types';
+import { NotFoundError } from '@backstage/backend-common';
+
+export type EnvironmentHandlers = {
+ [key: string]: AuthProviderRouteHandlers;
+};
+
+export class EnvironmentHandler implements AuthProviderRouteHandlers {
+ constructor(private readonly providers: EnvironmentHandlers) {}
+
+ private getProviderForEnv(req: express.Request): AuthProviderRouteHandlers {
+ const env = req.query.env?.toString();
+ if (!this.providers.hasOwnProperty(env)) {
+ throw new NotFoundError(
+ `No environment for ${env} found in this provider`,
+ );
+ }
+ return this.providers[env];
+ }
+
+ async start(req: express.Request, res: express.Response): Promise {
+ const provider = this.getProviderForEnv(req);
+ provider.start(req, res);
+ }
+
+ async frameHandler(
+ req: express.Request,
+ res: express.Response,
+ ): Promise {
+ const provider = this.getProviderForEnv(req);
+ provider.frameHandler(req, res);
+ }
+
+ async refresh(req: express.Request, res: express.Response): Promise {
+ const provider = this.getProviderForEnv(req);
+ if (provider.refresh) {
+ provider.refresh(req, res);
+ }
+ }
+
+ async logout(req: express.Request, res: express.Response): Promise {
+ const provider = this.getProviderForEnv(req);
+ provider.logout(req, res);
+ }
+}
diff --git a/plugins/auth-backend/src/providers/OAuthProvider.test.ts b/plugins/auth-backend/src/lib/OAuthProvider.test.ts
similarity index 78%
rename from plugins/auth-backend/src/providers/OAuthProvider.test.ts
rename to plugins/auth-backend/src/lib/OAuthProvider.test.ts
index 362653b5f7..3a211e7b95 100644
--- a/plugins/auth-backend/src/providers/OAuthProvider.test.ts
+++ b/plugins/auth-backend/src/lib/OAuthProvider.test.ts
@@ -18,15 +18,12 @@ import express from 'express';
import {
ensuresXRequestedWith,
postMessageResponse,
- removeRefreshTokenCookie,
- setRefreshTokenCookie,
THOUSAND_DAYS_MS,
- setNonceCookie,
TEN_MINUTES_MS,
verifyNonce,
OAuthProvider,
} from './OAuthProvider';
-import { AuthResponse, OAuthProviderHandlers } from './types';
+import { AuthResponse, OAuthProviderHandlers } from '../providers/types';
describe('OAuthProvider Utils', () => {
describe('verifyNonce', () => {
@@ -80,52 +77,8 @@ describe('OAuthProvider Utils', () => {
});
});
- describe('setNonceCookie', () => {
- it('should set nonce cookie', () => {
- const mockResponse = ({
- cookie: jest.fn().mockReturnThis(),
- } as unknown) as express.Response;
- setNonceCookie(mockResponse, 'providera');
- expect(mockResponse.cookie).toBeCalledTimes(1);
- expect(mockResponse.cookie).toBeCalledWith(
- 'providera-nonce',
- expect.any(String),
- expect.objectContaining({ maxAge: TEN_MINUTES_MS }),
- );
- });
- });
-
- describe('setRefreshTokenCookie', () => {
- it('should set refresh token cookie', () => {
- const mockResponse = ({
- cookie: jest.fn().mockReturnThis(),
- } as unknown) as express.Response;
- setRefreshTokenCookie(mockResponse, 'providera', 'REFRESH_TOKEN');
- expect(mockResponse.cookie).toBeCalledTimes(1);
- expect(mockResponse.cookie).toBeCalledWith(
- 'providera-refresh-token',
- 'REFRESH_TOKEN',
- expect.objectContaining({ maxAge: THOUSAND_DAYS_MS }),
- );
- });
- });
-
- describe('removeRefreshTokenCookie', () => {
- it('should remove refresh token cookie', () => {
- const mockResponse = ({
- cookie: jest.fn().mockReturnThis(),
- } as unknown) as express.Response;
- removeRefreshTokenCookie(mockResponse, 'providera');
- expect(mockResponse.cookie).toBeCalledTimes(1);
- expect(mockResponse.cookie).toBeCalledWith(
- 'providera-refresh-token',
- '',
- expect.objectContaining({ maxAge: 0 }),
- );
- });
- });
-
describe('postMessageResponse', () => {
+ const appOrigin = 'http://localhost:3000';
it('should post a message back with payload success', () => {
const mockResponse = ({
end: jest.fn().mockReturnThis(),
@@ -144,7 +97,7 @@ describe('OAuthProvider Utils', () => {
const jsonData = JSON.stringify(data);
const base64Data = Buffer.from(jsonData, 'utf8').toString('base64');
- postMessageResponse(mockResponse, data);
+ postMessageResponse(mockResponse, appOrigin, data);
expect(mockResponse.setHeader).toBeCalledTimes(2);
expect(mockResponse.end).toBeCalledTimes(1);
expect(mockResponse.end).toBeCalledWith(
@@ -165,7 +118,7 @@ describe('OAuthProvider Utils', () => {
const jsonData = JSON.stringify(data);
const base64Data = Buffer.from(jsonData, 'utf8').toString('base64');
- postMessageResponse(mockResponse, data);
+ postMessageResponse(mockResponse, appOrigin, data);
expect(mockResponse.setHeader).toBeCalledTimes(2);
expect(mockResponse.end).toBeCalledTimes(1);
expect(mockResponse.end).toBeCalledWith(
@@ -221,10 +174,19 @@ describe('OAuthProvider', () => {
}
}
const providerInstance = new MyAuthProvider();
- const providerId = 'test-provider';
+ const oAuthProviderOptions = {
+ providerId: 'test-provider',
+ secure: false,
+ disableRefresh: true,
+ baseUrl: 'http://localhost:7000/auth',
+ appOrigin: 'http://localhost:3000',
+ };
it('sets the correct headers in start', async () => {
- const oauthProvider = new OAuthProvider(providerInstance, providerId);
+ const oauthProvider = new OAuthProvider(
+ providerInstance,
+ oAuthProviderOptions,
+ );
const mockRequest = ({
query: {
scope: 'user',
@@ -239,6 +201,14 @@ describe('OAuthProvider', () => {
} as unknown) as express.Response;
await oauthProvider.start(mockRequest, mockResponse);
+ // nonce cookie checks
+ expect(mockResponse.cookie).toBeCalledTimes(1);
+ expect(mockResponse.cookie).toBeCalledWith(
+ `${oAuthProviderOptions.providerId}-nonce`,
+ expect.any(String),
+ expect.objectContaining({ maxAge: TEN_MINUTES_MS }),
+ );
+ // redirect checks
expect(mockResponse.setHeader).toHaveBeenCalledTimes(2);
expect(mockResponse.setHeader).toHaveBeenCalledWith('Location', '/url');
expect(mockResponse.setHeader).toHaveBeenCalledWith('Content-Length', '0');
@@ -247,7 +217,11 @@ describe('OAuthProvider', () => {
});
it('sets the refresh cookie if refresh is enabled', async () => {
- const oauthProvider = new OAuthProvider(providerInstance, providerId);
+ oAuthProviderOptions.disableRefresh = false;
+ const oauthProvider = new OAuthProvider(
+ providerInstance,
+ oAuthProviderOptions,
+ );
const mockRequest = ({
cookies: {
@@ -269,12 +243,19 @@ describe('OAuthProvider', () => {
expect(mockResponse.cookie).toHaveBeenCalledWith(
expect.stringContaining('test-provider-refresh-token'),
expect.stringContaining('token'),
- expect.objectContaining({ path: '/auth/test-provider' }),
+ expect.objectContaining({
+ path: '/auth/test-provider',
+ maxAge: THOUSAND_DAYS_MS,
+ }),
);
});
- it('does no set the refresh cookie if refresh is disabled', async () => {
- const oauthProvider = new OAuthProvider(providerInstance, providerId, true);
+ it('does not set the refresh cookie if refresh is disabled', async () => {
+ oAuthProviderOptions.disableRefresh = true;
+ const oauthProvider = new OAuthProvider(
+ providerInstance,
+ oAuthProviderOptions,
+ );
const mockRequest = ({
cookies: {
@@ -296,7 +277,11 @@ describe('OAuthProvider', () => {
});
it('removes refresh cookie when logging out', async () => {
- const oauthProvider = new OAuthProvider(providerInstance, providerId);
+ oAuthProviderOptions.disableRefresh = false;
+ const oauthProvider = new OAuthProvider(
+ providerInstance,
+ oAuthProviderOptions,
+ );
const mockRequest = ({
header: () => 'XMLHttpRequest',
@@ -317,7 +302,11 @@ describe('OAuthProvider', () => {
});
it('gets new access-token when refreshing', async () => {
- const oauthProvider = new OAuthProvider(providerInstance, providerId);
+ oAuthProviderOptions.disableRefresh = false;
+ const oauthProvider = new OAuthProvider(
+ providerInstance,
+ oAuthProviderOptions,
+ );
const mockRequest = ({
header: () => 'XMLHttpRequest',
@@ -341,7 +330,11 @@ describe('OAuthProvider', () => {
});
it('handles refresh without capabilities', async () => {
- const oauthProvider = new OAuthProvider(providerInstance, providerId, true);
+ oAuthProviderOptions.disableRefresh = true;
+ const oauthProvider = new OAuthProvider(
+ providerInstance,
+ oAuthProviderOptions,
+ );
const mockRequest = ({
header: () => 'XMLHttpRequest',
diff --git a/plugins/auth-backend/src/providers/OAuthProvider.ts b/plugins/auth-backend/src/lib/OAuthProvider.ts
similarity index 64%
rename from plugins/auth-backend/src/providers/OAuthProvider.ts
rename to plugins/auth-backend/src/lib/OAuthProvider.ts
index a2be783155..62b565b60d 100644
--- a/plugins/auth-backend/src/providers/OAuthProvider.ts
+++ b/plugins/auth-backend/src/lib/OAuthProvider.ts
@@ -14,20 +14,29 @@
* limitations under the License.
*/
-import express, { CookieOptions } from 'express';
+import express from 'express';
import crypto from 'crypto';
+import { URL } from 'url';
import {
AuthResponse,
AuthProviderRouteHandlers,
OAuthProviderHandlers,
-} from './types';
+} from '../providers/types';
import { InputError } from '@backstage/backend-common';
export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000;
export const TEN_MINUTES_MS = 600 * 1000;
-export const verifyNonce = (req: express.Request, provider: string) => {
- const cookieNonce = req.cookies[`${provider}-nonce`];
+export type Options = {
+ providerId: string;
+ secure: boolean;
+ disableRefresh?: boolean;
+ baseUrl: string;
+ appOrigin: string;
+};
+
+export const verifyNonce = (req: express.Request, providerId: string) => {
+ const cookieNonce = req.cookies[`${providerId}-nonce`];
const stateNonce = req.query.state;
if (!cookieNonce || !stateNonce) {
@@ -39,58 +48,9 @@ export const verifyNonce = (req: express.Request, provider: string) => {
}
};
-export const setNonceCookie = (res: express.Response, provider: string) => {
- const nonce = crypto.randomBytes(16).toString('base64');
-
- const options: CookieOptions = {
- maxAge: TEN_MINUTES_MS,
- secure: false,
- sameSite: 'none',
- domain: 'localhost',
- path: `/auth/${provider}/handler`,
- httpOnly: true,
- };
-
- res.cookie(`${provider}-nonce`, nonce, options);
-
- return nonce;
-};
-
-export const setRefreshTokenCookie = (
- res: express.Response,
- provider: string,
- refreshToken: string,
-) => {
- const options: CookieOptions = {
- maxAge: THOUSAND_DAYS_MS,
- secure: false,
- sameSite: 'none',
- domain: 'localhost',
- path: `/auth/${provider}`,
- httpOnly: true,
- };
-
- res.cookie(`${provider}-refresh-token`, refreshToken, options);
-};
-
-export const removeRefreshTokenCookie = (
- res: express.Response,
- provider: string,
-) => {
- const options: CookieOptions = {
- maxAge: 0,
- secure: false,
- sameSite: 'none',
- domain: 'localhost',
- path: `/auth/${provider}`,
- httpOnly: true,
- };
-
- res.cookie(`${provider}-refresh-token`, '', options);
-};
-
export const postMessageResponse = (
res: express.Response,
+ appOrigin: string,
data: AuthResponse,
) => {
const jsonData = JSON.stringify(data);
@@ -104,7 +64,7 @@ export const postMessageResponse = (
@@ -122,17 +82,16 @@ export const ensuresXRequestedWith = (req: express.Request) => {
};
export class OAuthProvider implements AuthProviderRouteHandlers {
- private readonly provider: string;
- private readonly providerHandlers: OAuthProviderHandlers;
- private readonly disableRefresh: boolean;
+ private readonly domain: string;
+ private readonly basePath: string;
+
constructor(
- providerHandlers: OAuthProviderHandlers,
- provider: string,
- disableRefresh?: boolean,
+ private readonly providerHandlers: OAuthProviderHandlers,
+ private readonly options: Options,
) {
- this.provider = provider;
- this.providerHandlers = providerHandlers;
- this.disableRefresh = disableRefresh ?? false;
+ const url = new URL(options.baseUrl);
+ this.domain = url.hostname;
+ this.basePath = url.pathname;
}
async start(req: express.Request, res: express.Response): Promise {
@@ -143,8 +102,9 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
throw new InputError('missing scope parameter');
}
+ const nonce = crypto.randomBytes(16).toString('base64');
// set a nonce cookie before redirecting to oauth provider
- const nonce = setNonceCookie(res, this.provider);
+ this.setNonceCookie(res, nonce);
const options = {
scope,
@@ -152,6 +112,7 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
prompt: 'consent',
state: nonce,
};
+
const { url, status } = await this.providerHandlers.start(req, options);
res.statusCode = status || 302;
@@ -166,11 +127,11 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
): Promise {
try {
// verify nonce cookie and state cookie on callback
- verifyNonce(req, this.provider);
+ verifyNonce(req, this.options.providerId);
const { user, info } = await this.providerHandlers.handler(req);
- if (!this.disableRefresh) {
+ if (!this.options.disableRefresh) {
// throw error if missing refresh token
const { refreshToken } = info;
if (!refreshToken) {
@@ -178,17 +139,17 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
}
// set new refresh token
- setRefreshTokenCookie(res, this.provider, refreshToken);
+ this.setRefreshTokenCookie(res, refreshToken);
}
// post message back to popup if successful
- return postMessageResponse(res, {
+ return postMessageResponse(res, this.options.appOrigin, {
type: 'auth-result',
payload: user,
});
} catch (error) {
// post error message back to popup if failure
- return postMessageResponse(res, {
+ return postMessageResponse(res, this.options.appOrigin, {
type: 'auth-result',
error: {
name: error.name,
@@ -203,9 +164,9 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
return res.status(401).send('Invalid X-Requested-With header');
}
- if (!this.disableRefresh) {
+ if (!this.options.disableRefresh) {
// remove refresh token cookie before logout
- removeRefreshTokenCookie(res, this.provider);
+ this.removeRefreshTokenCookie(res);
}
return res.send('logout!');
}
@@ -215,14 +176,15 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
return res.status(401).send('Invalid X-Requested-With header');
}
- if (!this.providerHandlers.refresh || this.disableRefresh) {
+ if (!this.providerHandlers.refresh || this.options.disableRefresh) {
return res.send(
- `Refresh token not supported for provider: ${this.provider}`,
+ `Refresh token not supported for provider: ${this.options.providerId}`,
);
}
try {
- const refreshToken = req.cookies[`${this.provider}-refresh-token`];
+ const refreshToken =
+ req.cookies[`${this.options.providerId}-refresh-token`];
// throw error if refresh token is missing in the request
if (!refreshToken) {
@@ -241,4 +203,40 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
return res.status(401).send(`${error.message}`);
}
}
+
+ private setNonceCookie = (res: express.Response, nonce: string) => {
+ res.cookie(`${this.options.providerId}-nonce`, nonce, {
+ maxAge: TEN_MINUTES_MS,
+ secure: this.options.secure,
+ sameSite: 'none',
+ domain: this.domain,
+ path: `${this.basePath}/${this.options.providerId}/handler`,
+ httpOnly: true,
+ });
+ };
+
+ private setRefreshTokenCookie = (
+ res: express.Response,
+ refreshToken: string,
+ ) => {
+ res.cookie(`${this.options.providerId}-refresh-token`, refreshToken, {
+ maxAge: THOUSAND_DAYS_MS,
+ secure: this.options.secure,
+ sameSite: 'none',
+ domain: this.domain,
+ path: `${this.basePath}/${this.options.providerId}`,
+ httpOnly: true,
+ });
+ };
+
+ private removeRefreshTokenCookie = (res: express.Response) => {
+ res.cookie(`${this.options.providerId}-refresh-token`, '', {
+ maxAge: 0,
+ secure: false,
+ sameSite: 'none',
+ domain: `${this.domain}`,
+ path: `${this.basePath}/${this.options.providerId}`,
+ httpOnly: true,
+ });
+ };
}
diff --git a/plugins/auth-backend/src/providers/PassportStrategyHelper.test.ts b/plugins/auth-backend/src/lib/PassportStrategyHelper.test.ts
similarity index 100%
rename from plugins/auth-backend/src/providers/PassportStrategyHelper.test.ts
rename to plugins/auth-backend/src/lib/PassportStrategyHelper.test.ts
diff --git a/plugins/auth-backend/src/providers/PassportStrategyHelper.ts b/plugins/auth-backend/src/lib/PassportStrategyHelper.ts
similarity index 97%
rename from plugins/auth-backend/src/providers/PassportStrategyHelper.ts
rename to plugins/auth-backend/src/lib/PassportStrategyHelper.ts
index 7b7e467281..f9e32a2b6f 100644
--- a/plugins/auth-backend/src/providers/PassportStrategyHelper.ts
+++ b/plugins/auth-backend/src/lib/PassportStrategyHelper.ts
@@ -17,7 +17,11 @@
import express from 'express';
import passport from 'passport';
import jwtDecoder from 'jwt-decode';
-import { RedirectInfo, RefreshTokenResponse, ProfileInfo } from './types';
+import {
+ RedirectInfo,
+ RefreshTokenResponse,
+ ProfileInfo,
+} from '../providers/types';
export const makeProfileInfo = (
profile: passport.Profile,
diff --git a/plugins/auth-backend/src/providers/config.ts b/plugins/auth-backend/src/providers/config.ts
deleted file mode 100644
index 5ec73b7827..0000000000
--- a/plugins/auth-backend/src/providers/config.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright 2020 Spotify AB
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-export const providers = [
- {
- provider: 'google',
- options: {
- clientID: process.env.AUTH_GOOGLE_CLIENT_ID!,
- clientSecret: process.env.AUTH_GOOGLE_CLIENT_SECRET!,
- callbackURL: 'http://localhost:7000/auth/google/handler/frame',
- },
- },
- {
- provider: 'github',
- options: {
- clientID: process.env.AUTH_GITHUB_CLIENT_ID!,
- clientSecret: process.env.AUTH_GITHUB_CLIENT_SECRET!,
- callbackURL: 'http://localhost:7000/auth/github/handler/frame',
- },
- disableRefresh: true,
- },
- {
- provider: 'saml',
- options: {
- path: '/auth/saml/handler/frame',
- entryPoint: 'http://localhost:7001/',
- issuer: 'passport-saml',
- },
- },
-];
diff --git a/plugins/auth-backend/src/providers/factories.ts b/plugins/auth-backend/src/providers/factories.ts
index 8576e36096..4cc9212620 100644
--- a/plugins/auth-backend/src/providers/factories.ts
+++ b/plugins/auth-backend/src/providers/factories.ts
@@ -19,6 +19,7 @@ import { createGithubProvider } from './github';
import { createGoogleProvider } from './google';
import { createSamlProvider } from './saml';
import { AuthProviderFactory, AuthProviderConfig } from './types';
+import { Logger } from 'winston';
const factories: { [providerId: string]: AuthProviderFactory } = {
google: createGoogleProvider,
@@ -26,17 +27,18 @@ const factories: { [providerId: string]: AuthProviderFactory } = {
saml: createSamlProvider,
};
-export function createAuthProvider(providerId: string, config: any) {
+export const createAuthProviderRouter = (
+ providerId: string,
+ globalConfig: AuthProviderConfig,
+ providerConfig: any, // TODO: make this a config reader object of sorts
+ logger: Logger,
+) => {
const factory = factories[providerId];
if (!factory) {
throw Error(`No auth provider available for '${providerId}'`);
}
- return factory(config);
-}
-export const createAuthProviderRouter = (config: AuthProviderConfig) => {
- const providerId = config.provider;
- const provider = createAuthProvider(providerId, config);
+ const provider = factory(globalConfig, providerConfig, logger);
const router = Router();
router.get('/start', provider.start.bind(provider));
@@ -46,5 +48,6 @@ export const createAuthProviderRouter = (config: AuthProviderConfig) => {
if (provider.refresh) {
router.get('/refresh', provider.refresh.bind(provider));
}
+
return router;
};
diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts
index 09622d9303..aa96b0f38b 100644
--- a/plugins/auth-backend/src/providers/github/provider.ts
+++ b/plugins/auth-backend/src/providers/github/provider.ts
@@ -19,24 +19,30 @@ import { Strategy as GithubStrategy } from 'passport-github2';
import {
executeFrameHandlerStrategy,
executeRedirectStrategy,
-} from '../PassportStrategyHelper';
+} from '../../lib/PassportStrategyHelper';
import {
OAuthProviderHandlers,
AuthProviderConfig,
RedirectInfo,
AuthInfoBase,
AuthInfoPrivate,
+ EnvironmentProviderConfig,
+ OAuthProviderOptions,
+ OAuthProviderConfig,
} from '../types';
-import { OAuthProvider } from '../OAuthProvider';
+import { OAuthProvider } from '../../lib/OAuthProvider';
+import {
+ EnvironmentHandlers,
+ EnvironmentHandler,
+} from '../../lib/EnvironmentHandler';
+import { Logger } from 'winston';
export class GithubAuthProvider implements OAuthProviderHandlers {
- private readonly providerConfig: AuthProviderConfig;
private readonly _strategy: GithubStrategy;
- constructor(providerConfig: AuthProviderConfig) {
- this.providerConfig = providerConfig;
+ constructor(options: OAuthProviderOptions) {
this._strategy = new GithubStrategy(
- { ...this.providerConfig.options },
+ { ...options },
(accessToken: any, _: any, params: any, profile: any, done: any) => {
done(undefined, {
profile,
@@ -59,8 +65,42 @@ export class GithubAuthProvider implements OAuthProviderHandlers {
}
}
-export function createGithubProvider(config: AuthProviderConfig) {
- const provider = new GithubAuthProvider(config);
- const oauthProvider = new OAuthProvider(provider, config.provider, true);
- return oauthProvider;
+export function createGithubProvider(
+ { baseUrl }: AuthProviderConfig,
+ providerConfig: EnvironmentProviderConfig,
+ logger: Logger,
+) {
+ const envProviders: EnvironmentHandlers = {};
+
+ for (const [env, envConfig] of Object.entries(providerConfig)) {
+ const config = (envConfig as unknown) as OAuthProviderConfig;
+ const { secure, appOrigin } = config;
+ const callbackURLParam = env === 'development' ? '?env=development' : '';
+ const opts = {
+ clientID: config.clientId,
+ clientSecret: config.clientSecret,
+ callbackURL: `${baseUrl}/github/handler/frame${callbackURLParam}`,
+ };
+
+ if (!opts.clientID || !opts.clientSecret) {
+ if (process.env.NODE_ENV !== 'development') {
+ throw new Error(
+ 'Failed to initialize Github auth provider, set AUTH_GITHUB_CLIENT_ID and AUTH_GITHUB_CLIENT_SECRET env vars',
+ );
+ }
+
+ logger.warn(
+ 'Github auth provider disabled, set AUTH_GITHUB_CLIENT_ID and AUTH_GITHUB_CLIENT_SECRET env vars to enable',
+ );
+ continue;
+ }
+
+ envProviders[env] = new OAuthProvider(new GithubAuthProvider(opts), {
+ providerId: 'github',
+ secure,
+ baseUrl,
+ appOrigin,
+ });
+ }
+ return new EnvironmentHandler(envProviders);
}
diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts
index 066e16e330..b1d82266fc 100644
--- a/plugins/auth-backend/src/providers/google/provider.ts
+++ b/plugins/auth-backend/src/providers/google/provider.ts
@@ -22,7 +22,7 @@ import {
executeRefreshTokenStrategy,
makeProfileInfo,
executeFetchUserProfileStrategy,
-} from '../PassportStrategyHelper';
+} from '../../lib/PassportStrategyHelper';
import {
OAuthProviderHandlers,
AuthInfoBase,
@@ -30,19 +30,27 @@ import {
RedirectInfo,
AuthProviderConfig,
AuthInfoWithProfile,
+ EnvironmentProviderConfig,
+ OAuthProviderOptions,
+ OAuthProviderConfig,
} from '../types';
-import { OAuthProvider } from '../OAuthProvider';
+import { OAuthProvider } from '../../lib/OAuthProvider';
import passport from 'passport';
+import {
+ EnvironmentHandler,
+ EnvironmentHandlers,
+} from '../../lib/EnvironmentHandler';
+import { Logger } from 'winston';
export class GoogleAuthProvider implements OAuthProviderHandlers {
- private readonly providerConfig: AuthProviderConfig;
private readonly _strategy: GoogleStrategy;
- constructor(providerConfig: AuthProviderConfig) {
- this.providerConfig = providerConfig;
+ constructor(options: OAuthProviderOptions) {
// TODO: throw error if env variables not set?
this._strategy = new GoogleStrategy(
- { ...this.providerConfig.options },
+ // We need passReqToCallback set to false to get params, but there's
+ // no matching type signature for that, so instead behold this beauty
+ { ...options, passReqToCallback: false as true },
(
accessToken: any,
refreshToken: any,
@@ -104,8 +112,42 @@ export class GoogleAuthProvider implements OAuthProviderHandlers {
}
}
-export function createGoogleProvider(config: AuthProviderConfig) {
- const provider = new GoogleAuthProvider(config);
- const oauthProvider = new OAuthProvider(provider, config.provider);
- return oauthProvider;
+export function createGoogleProvider(
+ { baseUrl }: AuthProviderConfig,
+ providerConfig: EnvironmentProviderConfig,
+ logger: Logger,
+) {
+ const envProviders: EnvironmentHandlers = {};
+
+ for (const [env, envConfig] of Object.entries(providerConfig)) {
+ const config = (envConfig as unknown) as OAuthProviderConfig;
+ const { secure, appOrigin } = config;
+ const callbackURLParam = env === 'development' ? '?env=development' : '';
+ const opts = {
+ clientID: config.clientId,
+ clientSecret: config.clientSecret,
+ callbackURL: `${baseUrl}/google/handler/frame${callbackURLParam}`,
+ };
+
+ if (!opts.clientID || !opts.clientSecret) {
+ if (process.env.NODE_ENV !== 'development') {
+ throw new Error(
+ 'Failed to initialize Google auth provider, set AUTH_GOOGLE_CLIENT_ID and AUTH_GOOGLE_CLIENT_SECRET env vars',
+ );
+ }
+
+ logger.warn(
+ 'Google auth provider disabled, set AUTH_GOOGLE_CLIENT_ID and AUTH_GOOGLE_CLIENT_SECRET env vars to enable',
+ );
+ continue;
+ }
+
+ envProviders[env] = new OAuthProvider(new GoogleAuthProvider(opts), {
+ providerId: 'google',
+ secure,
+ baseUrl,
+ appOrigin,
+ });
+ }
+ return new EnvironmentHandler(envProviders);
}
diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts
index 50bea3495e..621b75078c 100644
--- a/plugins/auth-backend/src/providers/saml/provider.ts
+++ b/plugins/auth-backend/src/providers/saml/provider.ts
@@ -19,16 +19,26 @@ import { Strategy as SamlStrategy } from 'passport-saml';
import {
executeFrameHandlerStrategy,
executeRedirectStrategy,
-} from '../PassportStrategyHelper';
-import { AuthProviderConfig, AuthProviderRouteHandlers } from '../types';
-import { postMessageResponse } from '../OAuthProvider';
+} from '../../lib/PassportStrategyHelper';
+import {
+ AuthProviderConfig,
+ AuthProviderRouteHandlers,
+ EnvironmentProviderConfig,
+ SAMLProviderConfig,
+} from '../types';
+import { postMessageResponse } from '../../lib/OAuthProvider';
+import {
+ EnvironmentHandlers,
+ EnvironmentHandler,
+} from '../../lib/EnvironmentHandler';
+import { Logger } from 'winston';
export class SamlAuthProvider implements AuthProviderRouteHandlers {
private readonly strategy: SamlStrategy;
- constructor(providerConfig: AuthProviderConfig) {
+ constructor(options: SAMLProviderOptions) {
this.strategy = new SamlStrategy(
- { ...providerConfig.options },
+ { ...options },
(profile: any, done: any) => {
// TODO: There's plenty more validation and profile handling to do here,
// this provider is currently only intended to validate the provider pattern
@@ -57,12 +67,12 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers {
try {
const { user } = await executeFrameHandlerStrategy(req, this.strategy);
- return postMessageResponse(res, {
+ return postMessageResponse(res, 'http://localhost:3000', {
type: 'auth-result',
payload: user,
});
} catch (error) {
- return postMessageResponse(res, {
+ return postMessageResponse(res, 'http://localhost:3000', {
type: 'auth-result',
error: {
name: error.name,
@@ -77,6 +87,36 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers {
}
}
-export function createSamlProvider(config: AuthProviderConfig) {
- return new SamlAuthProvider(config);
+type SAMLProviderOptions = {
+ entryPoint: string;
+ issuer: string;
+ path: string;
+};
+
+export function createSamlProvider(
+ _authProviderConfig: AuthProviderConfig,
+ providerConfig: EnvironmentProviderConfig,
+ logger: Logger,
+) {
+ const envProviders: EnvironmentHandlers = {};
+
+ for (const [env, envConfig] of Object.entries(providerConfig)) {
+ const config = (envConfig as unknown) as SAMLProviderConfig;
+ const opts = {
+ entryPoint: config.entryPoint,
+ issuer: config.issuer,
+ path: '/auth/saml/handler/frame',
+ };
+
+ if (!opts.entryPoint || !opts.issuer) {
+ logger.warn(
+ 'SAML auth provider disabled, set entryPoint and entryPoint in saml auth config to enable',
+ );
+ continue;
+ }
+
+ envProviders[env] = new SamlAuthProvider(opts);
+ }
+
+ return new EnvironmentHandler(envProviders);
}
diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts
index b8252ddc97..419a041637 100644
--- a/plugins/auth-backend/src/providers/types.ts
+++ b/plugins/auth-backend/src/providers/types.ts
@@ -15,11 +15,32 @@
*/
import express from 'express';
+import { Logger } from 'winston';
+
+export type OAuthProviderOptions = {
+ clientID: string;
+ clientSecret: string;
+ callbackURL: string;
+};
+
+export type SAMLProviderConfig = {
+ entryPoint: string;
+ issuer: string;
+};
+
+export type EnvironmentProviderConfig = {
+ [key: string]: OAuthProviderConfig | SAMLProviderConfig;
+};
export type AuthProviderConfig = {
- provider: string;
- options: any;
- disableRefresh?: boolean;
+ baseUrl: string;
+};
+
+export type OAuthProviderConfig = {
+ secure: boolean;
+ appOrigin: string; // http://localhost:3000
+ clientId: string;
+ clientSecret: string;
};
export interface OAuthProviderHandlers {
@@ -36,8 +57,14 @@ export interface AuthProviderRouteHandlers {
logout(req: express.Request, res: express.Response): Promise;
}
+export type SAMLEnvironmentProviderConfig = {
+ [key: string]: SAMLProviderConfig;
+};
+
export type AuthProviderFactory = (
- config: AuthProviderConfig,
+ globalConfig: AuthProviderConfig,
+ providerConfig: EnvironmentProviderConfig,
+ logger: Logger,
) => AuthProviderRouteHandlers;
export type AuthInfoBase = {
diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts
index 9de5fb80a5..20c35a4084 100644
--- a/plugins/auth-backend/src/service/router.ts
+++ b/plugins/auth-backend/src/service/router.ts
@@ -19,7 +19,6 @@ import Router from 'express-promise-router';
import cookieParser from 'cookie-parser';
import bodyParser from 'body-parser';
import { Logger } from 'winston';
-import { providers } from './../providers/config';
import { createAuthProviderRouter } from '../providers';
export interface RouterOptions {
@@ -36,13 +35,61 @@ export async function createRouter(
router.use(bodyParser.urlencoded({ extended: false }));
router.use(bodyParser.json());
- // configure all the providers
- for (const providerConfig of providers) {
- const { provider } = providerConfig;
- const providerRouter = createAuthProviderRouter(providerConfig);
- logger.info(`Configuring provider, ${provider}`);
- router.use(`/${provider}`, providerRouter);
- }
+ // TODO: read from app config
+ const config = {
+ backend: {
+ baseUrl: 'http://localhost:7000',
+ },
+ auth: {
+ providers: {
+ google: {
+ development: {
+ appOrigin: 'http://localhost:3000',
+ secure: false,
+ clientId: process.env.AUTH_GOOGLE_CLIENT_ID!,
+ clientSecret: process.env.AUTH_GOOGLE_CLIENT_SECRET!,
+ },
+ production: {
+ appOrigin: 'http://localhost:3000',
+ secure: false,
+ clientId: '',
+ clientSecret: '',
+ },
+ },
+ github: {
+ development: {
+ appOrigin: 'http://localhost:3000',
+ secure: false,
+ clientId: process.env.AUTH_GITHUB_CLIENT_ID!,
+ clientSecret: process.env.AUTH_GITHUB_CLIENT_SECRET!,
+ },
+ },
+ saml: {
+ development: {
+ entryPoint: 'http://localhost:7001/',
+ issuer: 'passport-saml',
+ },
+ },
+ },
+ },
+ };
+ const providerConfigs = config.auth.providers;
+
+ for (const [providerId, providerConfig] of Object.entries(providerConfigs)) {
+ const baseUrl = `${config.backend.baseUrl}/auth`;
+ logger.info(`Configuring provider, ${providerId}`);
+ try {
+ const providerRouter = createAuthProviderRouter(
+ providerId,
+ { baseUrl },
+ providerConfig,
+ logger,
+ );
+ router.use(`/${providerId}`, providerRouter);
+ } catch (e) {
+ logger.error(e.message);
+ }
+ }
return router;
}
From 66fef7f2e42dd61d484d27c87d59cf457176bfe5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?=
Date: Fri, 12 Jun 2020 13:41:05 +0200
Subject: [PATCH 26/63] chore(catalog-backend): use code instead of db to
implement similarity rule
---
.../src/database/CommonDatabase.ts | 54 ++++++++++++++++---
.../20200612102317_unique_normalized.ts | 51 ------------------
plugins/catalog-backend/src/database/types.ts | 3 --
3 files changed, 48 insertions(+), 60 deletions(-)
delete mode 100644 plugins/catalog-backend/src/database/migrations/20200612102317_unique_normalized.ts
diff --git a/plugins/catalog-backend/src/database/CommonDatabase.ts b/plugins/catalog-backend/src/database/CommonDatabase.ts
index 67feb82fac..43eb66e9d7 100644
--- a/plugins/catalog-backend/src/database/CommonDatabase.ts
+++ b/plugins/catalog-backend/src/database/CommonDatabase.ts
@@ -61,7 +61,6 @@ function serializeSpec(spec: Entity['spec']): DbEntitiesRow['spec'] {
function toEntityRow(
locationId: string | undefined,
entity: Entity,
- normalize: (value: string) => string,
): DbEntitiesRow {
return {
id: entity.metadata.uid!,
@@ -72,9 +71,6 @@ function toEntityRow(
kind: entity.kind,
name: entity.metadata.name,
namespace: entity.metadata.namespace || null,
- kind_normalized: normalize(entity.kind),
- name_normalized: normalize(entity.metadata.name),
- namespace_normalized: normalize(entity.metadata.namespace || ''),
metadata: serializeMetadata(entity.metadata),
spec: serializeSpec(entity.spec),
};
@@ -162,6 +158,8 @@ export class CommonDatabase implements Database {
throw new InputError('May not specify generation for new entities');
}
+ await this.ensureNoSimilarNames(tx, request.entity);
+
const newEntity = lodash.cloneDeep(request.entity);
newEntity.metadata = {
...newEntity.metadata,
@@ -170,7 +168,7 @@ export class CommonDatabase implements Database {
generation: 1,
};
- const newRow = toEntityRow(request.locationId, newEntity, this.normalize);
+ const newRow = toEntityRow(request.locationId, newEntity);
await tx('entities').insert(newRow);
await this.updateEntitiesSearch(tx, newRow.id, newEntity);
@@ -259,9 +257,11 @@ export class CommonDatabase implements Database {
}
}
+ await this.ensureNoSimilarNames(tx, newEntity);
+
// Store the updated entity; select on the old etag to ensure that we do
// not lose to another writer
- const newRow = toEntityRow(request.locationId, newEntity, this.normalize);
+ const newRow = toEntityRow(request.locationId, newEntity);
const updatedRows = await tx('entities')
.where({ id: oldRow.id, etag: oldRow.etag })
.update(newRow);
@@ -451,4 +451,46 @@ export class CommonDatabase implements Database {
// we got around to writing the entries
}
}
+
+ private async ensureNoSimilarNames(
+ tx: Knex.Transaction,
+ data: Entity,
+ ): Promise {
+ const newKind = data.kind;
+ const newName = data.metadata.name;
+ const newNamespace = data.metadata.namespace;
+ const newKindNorm = this.normalize(newKind);
+ const newNameNorm = this.normalize(newName);
+ const newNamespaceNorm = this.normalize(newNamespace || '');
+
+ for (const item of await this.entities(tx)) {
+ if (data.metadata.uid === item.entity.metadata.uid) {
+ continue;
+ }
+
+ const oldKind = item.entity.kind;
+ const oldName = item.entity.metadata.name;
+ const oldNamespace = item.entity.metadata.namespace;
+ const oldKindNorm = this.normalize(oldKind);
+ const oldNameNorm = this.normalize(oldName);
+ const oldNamespaceNorm = this.normalize(oldNamespace || '');
+
+ if (
+ oldKindNorm === newKindNorm &&
+ oldNameNorm === newNameNorm &&
+ oldNamespaceNorm === newNamespaceNorm
+ ) {
+ // Only throw if things were actually different - for completely equal
+ // things, we let the database handle the conflict
+ if (
+ oldKind !== newKind ||
+ oldName !== newName ||
+ oldNamespace !== newNamespace
+ ) {
+ const message = `Kind, namespace, name are too similar to an existing entity`;
+ throw new ConflictError(message);
+ }
+ }
+ }
+ }
}
diff --git a/plugins/catalog-backend/src/database/migrations/20200612102317_unique_normalized.ts b/plugins/catalog-backend/src/database/migrations/20200612102317_unique_normalized.ts
deleted file mode 100644
index 38ed7f6047..0000000000
--- a/plugins/catalog-backend/src/database/migrations/20200612102317_unique_normalized.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright 2020 Spotify AB
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import * as Knex from 'knex';
-
-export async function up(knex: Knex): Promise {
- return knex.schema.alterTable('entities', table => {
- // these are added as nullable because sqlite does not support alter column
- table
- .string('kind_normalized')
- .nullable()
- .comment('The kind field of the entity, normalized');
- table
- .string('name_normalized')
- .nullable()
- .comment('The metadata.name field of the entity, normalized');
- table
- .string('namespace_normalized')
- .nullable()
- .comment('The metadata.namespace field of the entity, normalized');
- table.dropUnique([], 'entities_unique_name');
- table.unique(
- ['kind_normalized', 'name_normalized', 'namespace_normalized'],
- 'entities_unique_name_normalized',
- );
- });
-}
-
-export async function down(knex: Knex): Promise {
- return knex.schema.alterTable('entities', table => {
- table.dropUnique([], 'entities_unique_name_normalized');
- table.unique(['kind', 'name', 'namespace'], 'entities_unique_name');
- table.dropColumns(
- 'kind_normalized',
- 'name_normalized',
- 'namespace_normalized',
- );
- });
-}
diff --git a/plugins/catalog-backend/src/database/types.ts b/plugins/catalog-backend/src/database/types.ts
index 1cf949c4c2..fc81e124ba 100644
--- a/plugins/catalog-backend/src/database/types.ts
+++ b/plugins/catalog-backend/src/database/types.ts
@@ -23,9 +23,6 @@ export type DbEntitiesRow = {
kind: string;
name: string | null;
namespace: string | null;
- kind_normalized: string;
- name_normalized: string;
- namespace_normalized: string;
etag: string;
generation: number;
metadata: string;
From 33a96003c2058626b8faed000f5bdfa218427c4d Mon Sep 17 00:00:00 2001
From: Raghunandan
Date: Fri, 12 Jun 2020 14:28:18 +0200
Subject: [PATCH 27/63] fail gracefully if sentry token is missing in localhost
---
plugins/sentry-backend/src/service/router.ts | 16 +++++++++++-----
1 file changed, 11 insertions(+), 5 deletions(-)
diff --git a/plugins/sentry-backend/src/service/router.ts b/plugins/sentry-backend/src/service/router.ts
index a949f3583f..e7978042d3 100644
--- a/plugins/sentry-backend/src/service/router.ts
+++ b/plugins/sentry-backend/src/service/router.ts
@@ -22,13 +22,19 @@ export async function createRouter(logger: Logger): Promise {
const router = Router();
const SENTRY_TOKEN = process.env.SENTRY_TOKEN;
if (!SENTRY_TOKEN) {
- throw new Error(
- 'Sentry token must be provided in SENTRY_TOKEN environment variable to start the API.',
+ if (process.env.NODE_ENV !== 'development') {
+ throw new Error(
+ 'Sentry token must be provided in SENTRY_TOKEN environment variable to start the API.',
+ );
+ }
+ logger.warn(
+ 'Failed to initialize Sentry backend, set SENTRY_TOKEN environment variable to start the API.',
);
- }
- const sentryForwarder = getSentryApiForwarder(SENTRY_TOKEN, logger);
+ } else {
+ const sentryForwarder = getSentryApiForwarder(SENTRY_TOKEN, logger);
- router.use(sentryForwarder);
+ router.use(sentryForwarder);
+ }
return router;
}
From 2c6b8659d36478a22dab134bc7ea401c7d460416 Mon Sep 17 00:00:00 2001
From: Raghunandan
Date: Fri, 12 Jun 2020 14:40:06 +0200
Subject: [PATCH 28/63] review fixes
---
.../src/lib/OAuthProvider.test.ts | 44 +++++++++----------
.../src/providers/github/provider.ts | 2 +-
.../src/providers/google/provider.ts | 2 +-
3 files changed, 22 insertions(+), 26 deletions(-)
diff --git a/plugins/auth-backend/src/lib/OAuthProvider.test.ts b/plugins/auth-backend/src/lib/OAuthProvider.test.ts
index 3a211e7b95..bb261d1380 100644
--- a/plugins/auth-backend/src/lib/OAuthProvider.test.ts
+++ b/plugins/auth-backend/src/lib/OAuthProvider.test.ts
@@ -217,11 +217,10 @@ describe('OAuthProvider', () => {
});
it('sets the refresh cookie if refresh is enabled', async () => {
- oAuthProviderOptions.disableRefresh = false;
- const oauthProvider = new OAuthProvider(
- providerInstance,
- oAuthProviderOptions,
- );
+ const oauthProvider = new OAuthProvider(providerInstance, {
+ ...oAuthProviderOptions,
+ disableRefresh: false,
+ });
const mockRequest = ({
cookies: {
@@ -251,11 +250,10 @@ describe('OAuthProvider', () => {
});
it('does not set the refresh cookie if refresh is disabled', async () => {
- oAuthProviderOptions.disableRefresh = true;
- const oauthProvider = new OAuthProvider(
- providerInstance,
- oAuthProviderOptions,
- );
+ const oauthProvider = new OAuthProvider(providerInstance, {
+ ...oAuthProviderOptions,
+ disableRefresh: true,
+ });
const mockRequest = ({
cookies: {
@@ -277,11 +275,10 @@ describe('OAuthProvider', () => {
});
it('removes refresh cookie when logging out', async () => {
- oAuthProviderOptions.disableRefresh = false;
- const oauthProvider = new OAuthProvider(
- providerInstance,
- oAuthProviderOptions,
- );
+ const oauthProvider = new OAuthProvider(providerInstance, {
+ ...oAuthProviderOptions,
+ disableRefresh: false,
+ });
const mockRequest = ({
header: () => 'XMLHttpRequest',
@@ -303,10 +300,10 @@ describe('OAuthProvider', () => {
it('gets new access-token when refreshing', async () => {
oAuthProviderOptions.disableRefresh = false;
- const oauthProvider = new OAuthProvider(
- providerInstance,
- oAuthProviderOptions,
- );
+ const oauthProvider = new OAuthProvider(providerInstance, {
+ ...oAuthProviderOptions,
+ disableRefresh: false,
+ });
const mockRequest = ({
header: () => 'XMLHttpRequest',
@@ -330,11 +327,10 @@ describe('OAuthProvider', () => {
});
it('handles refresh without capabilities', async () => {
- oAuthProviderOptions.disableRefresh = true;
- const oauthProvider = new OAuthProvider(
- providerInstance,
- oAuthProviderOptions,
- );
+ const oauthProvider = new OAuthProvider(providerInstance, {
+ ...oAuthProviderOptions,
+ disableRefresh: true,
+ });
const mockRequest = ({
header: () => 'XMLHttpRequest',
diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts
index aa96b0f38b..0f8f185015 100644
--- a/plugins/auth-backend/src/providers/github/provider.ts
+++ b/plugins/auth-backend/src/providers/github/provider.ts
@@ -75,7 +75,7 @@ export function createGithubProvider(
for (const [env, envConfig] of Object.entries(providerConfig)) {
const config = (envConfig as unknown) as OAuthProviderConfig;
const { secure, appOrigin } = config;
- const callbackURLParam = env === 'development' ? '?env=development' : '';
+ const callbackURLParam = `?env=${env}`;
const opts = {
clientID: config.clientId,
clientSecret: config.clientSecret,
diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts
index b1d82266fc..971f588dce 100644
--- a/plugins/auth-backend/src/providers/google/provider.ts
+++ b/plugins/auth-backend/src/providers/google/provider.ts
@@ -122,7 +122,7 @@ export function createGoogleProvider(
for (const [env, envConfig] of Object.entries(providerConfig)) {
const config = (envConfig as unknown) as OAuthProviderConfig;
const { secure, appOrigin } = config;
- const callbackURLParam = env === 'development' ? '?env=development' : '';
+ const callbackURLParam = `?env=${env}`;
const opts = {
clientID: config.clientId,
clientSecret: config.clientSecret,
From 51b780b061977f4ae26d7762fa59f7e2ba9ebb00 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?=
Date: Fri, 12 Jun 2020 15:16:16 +0200
Subject: [PATCH 29/63] feat(catalog-backend): support wildcard matching on
entities
---
.../src/database/CommonDatabase.ts | 46 +++++++++++++++----
1 file changed, 36 insertions(+), 10 deletions(-)
diff --git a/plugins/catalog-backend/src/database/CommonDatabase.ts b/plugins/catalog-backend/src/database/CommonDatabase.ts
index 43eb66e9d7..13439074ce 100644
--- a/plugins/catalog-backend/src/database/CommonDatabase.ts
+++ b/plugins/catalog-backend/src/database/CommonDatabase.ts
@@ -283,22 +283,48 @@ export class CommonDatabase implements Database {
let builder = tx('entities');
for (const [index, filter] of (filters ?? []).entries()) {
+ const key = filter.key.replace('*', '%');
+ const keyOp = filter.key.includes('*') ? 'like' : '=';
+
+ let matchNulls = false;
+ const matchIn: string[] = [];
+ const matchLike: string[] = [];
+
+ for (const value of filter.values) {
+ if (!value) {
+ matchNulls = true;
+ } else if (value.includes('*')) {
+ matchLike.push(value.replace('*', '%'));
+ } else {
+ matchIn.push(value);
+ }
+ }
+
builder = builder
- .leftOuterJoin(`entities_search as t${index}`, function join() {
- this.on('entities.id', '=', `t${index}.entity_id`).onIn(
- `t${index}.value`,
- filter.values.filter(x => x),
- );
- if (filter.values.some(x => !x)) {
- this.orOnNull(`t${index}.value`);
- }
+ .leftOuterJoin(`entities_search as t${index}`, function joins() {
+ this.on('entities.id', '=', `t${index}.entity_id`);
+ this.andOn(`t${index}.key`, keyOp, tx.raw('?', [key]));
})
- .where(`t${index}.key`, '=', filter.key);
+ .where(function rules() {
+ if (matchIn.length) {
+ this.orWhereIn(`t${index}.value`, matchIn);
+ }
+ if (matchLike.length) {
+ for (const x of matchLike) {
+ this.orWhere(`t${index}.value`, 'like', tx.raw('?', [x]));
+ }
+ }
+ if (matchNulls) {
+ this.orWhereNull(`t${index}.value`);
+ }
+ });
}
const rows = await builder
- .orderBy('namespace', 'name')
.select('entities.*')
+ .orderBy('kind', 'asc')
+ .orderBy('namespace', 'asc')
+ .orderBy('name', 'asc')
.groupBy('id');
return rows.map(row => toEntityResponse(row));
From da6f33a13fd1d04932e9ef3d99fb654b4846f9b7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?=
Date: Fri, 12 Jun 2020 15:36:19 +0200
Subject: [PATCH 30/63] chore(backend): make the sentry handling work better
---
packages/backend/package.json | 2 +-
packages/backend/src/index.ts | 5 ++++-
plugins/sentry-backend/src/index.ts | 1 +
3 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/packages/backend/package.json b/packages/backend/package.json
index 4e2610380c..8629ea64ca 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -10,7 +10,7 @@
},
"scripts": {
"build": "tsc",
- "start": "backstage-cli watch-deps --build -- tsc-watch --onFirstSuccess \\\"nodemon -r esm\\\"",
+ "start": "backstage-cli watch-deps --build -- tsc-watch --onFirstSuccess \\\"cross-env NODE_ENV=development nodemon -r esm\\\"",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"clean": "backstage-cli clean",
diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts
index 0f0733a8ec..d583db672b 100644
--- a/packages/backend/src/index.ts
+++ b/packages/backend/src/index.ts
@@ -65,4 +65,7 @@ async function main() {
});
}
-main();
+main().catch(error => {
+ console.error(`Backend failed to start up, ${error}`);
+ process.exit(1);
+});
diff --git a/plugins/sentry-backend/src/index.ts b/plugins/sentry-backend/src/index.ts
index 28c43aee33..7612c392a2 100644
--- a/plugins/sentry-backend/src/index.ts
+++ b/plugins/sentry-backend/src/index.ts
@@ -13,4 +13,5 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
export * from './service/router';
From fcc514e90912922ea4248e24b1266caede12a8a0 Mon Sep 17 00:00:00 2001
From: Patrik Oldsberg
Date: Fri, 12 Jun 2020 15:52:27 +0200
Subject: [PATCH 31/63] packages/config-loader: added secret reading option +
initial validation and file handler
---
packages/config-loader/package.json | 6 +-
packages/config-loader/src/loader.ts | 85 +++++++++++++++++++++++++---
packages/config-loader/src/types.ts | 3 +
3 files changed, 83 insertions(+), 11 deletions(-)
diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json
index 7018dc40a8..de6b26363a 100644
--- a/packages/config-loader/package.json
+++ b/packages/config-loader/package.json
@@ -32,11 +32,13 @@
"dependencies": {
"@backstage/config": "^0.1.1-alpha.7",
"fs-extra": "^9.0.0",
- "yaml": "^1.9.2"
+ "yaml": "^1.9.2",
+ "yup": "^0.28.5"
},
"devDependencies": {
"@types/jest": "^25.2.2",
- "@types/node": "^12.0.0"
+ "@types/node": "^12.0.0",
+ "@types/yup": "^0.28.2"
},
"files": [
"dist/**/*.{js,d.ts}"
diff --git a/packages/config-loader/src/loader.ts b/packages/config-loader/src/loader.ts
index 6bdc954e04..23bb8799cf 100644
--- a/packages/config-loader/src/loader.ts
+++ b/packages/config-loader/src/loader.ts
@@ -15,8 +15,9 @@
*/
import fs from 'fs-extra';
+import * as yup from 'yup';
import yaml from 'yaml';
-import { resolve as resolvePath } from 'path';
+import { resolve as resolvePath, dirname } from 'path';
import { AppConfig, JsonObject, JsonValue } from '@backstage/config';
import { findRootPath } from './paths';
import { LoadConfigOptions } from './types';
@@ -79,6 +80,11 @@ export function readEnv(env: {
type ReadFileFunc = (path: string) => Promise;
+type ReaderContext = {
+ shouldReadSecrets: boolean;
+ readFile: ReadFileFunc;
+};
+
function isObject(obj: JsonValue | undefined): obj is JsonObject {
if (typeof obj !== 'object') {
return false;
@@ -88,8 +94,54 @@ function isObject(obj: JsonValue | undefined): obj is JsonObject {
return obj !== null;
}
-export async function readConfigFile(filePath: string, readFile: ReadFileFunc) {
- const configYaml = await readFile(filePath);
+type FileSecret = {
+ file: string;
+};
+
+type Secret = FileSecret;
+
+const secretLoaderSchemas = {
+ file: yup.object({
+ file: yup.string().required(),
+ }),
+};
+
+const secretSchema = yup.lazy