From 709a7352b21da89f0b0a654ea38ceb1043517b3d Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 22 Jul 2020 14:51:15 +0200 Subject: [PATCH 01/85] chore(cli): create backend plugin start --- .../default-backend-plugin/.eslintrc.js | 3 + .../default-backend-plugin/README.md.hbs | 13 +++ .../default-backend-plugin/dev/index.tsx | 20 ++++ .../default-backend-plugin/package.json.hbs | 46 ++++++++ .../ExampleComponent.test.tsx.hbs | 34 ++++++ .../ExampleComponent/ExampleComponent.tsx.hbs | 57 +++++++++ .../src/components/ExampleComponent/index.ts | 17 +++ .../ExampleFetchComponent.test.tsx.hbs | 28 +++++ .../ExampleFetchComponent.tsx.hbs | 108 ++++++++++++++++++ .../components/ExampleFetchComponent/index.ts | 17 +++ .../default-backend-plugin/src/index.ts | 17 +++ .../src/plugin.test.ts.hbs | 23 ++++ .../default-backend-plugin/src/plugin.ts.hbs | 30 +++++ .../default-backend-plugin/src/setupTests.ts | 19 +++ 14 files changed, 432 insertions(+) create mode 100644 packages/cli/templates/default-backend-plugin/.eslintrc.js create mode 100644 packages/cli/templates/default-backend-plugin/README.md.hbs create mode 100644 packages/cli/templates/default-backend-plugin/dev/index.tsx create mode 100644 packages/cli/templates/default-backend-plugin/package.json.hbs create mode 100644 packages/cli/templates/default-backend-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs create mode 100644 packages/cli/templates/default-backend-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs create mode 100644 packages/cli/templates/default-backend-plugin/src/components/ExampleComponent/index.ts create mode 100644 packages/cli/templates/default-backend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs create mode 100644 packages/cli/templates/default-backend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx.hbs create mode 100644 packages/cli/templates/default-backend-plugin/src/components/ExampleFetchComponent/index.ts create mode 100644 packages/cli/templates/default-backend-plugin/src/index.ts create mode 100644 packages/cli/templates/default-backend-plugin/src/plugin.test.ts.hbs create mode 100644 packages/cli/templates/default-backend-plugin/src/plugin.ts.hbs create mode 100644 packages/cli/templates/default-backend-plugin/src/setupTests.ts diff --git a/packages/cli/templates/default-backend-plugin/.eslintrc.js b/packages/cli/templates/default-backend-plugin/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/packages/cli/templates/default-backend-plugin/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/packages/cli/templates/default-backend-plugin/README.md.hbs b/packages/cli/templates/default-backend-plugin/README.md.hbs new file mode 100644 index 0000000000..d6f2d65134 --- /dev/null +++ b/packages/cli/templates/default-backend-plugin/README.md.hbs @@ -0,0 +1,13 @@ +# {{id}} + +Welcome to the {{id}} plugin! + +_This plugin was created through the Backstage CLI_ + +## Getting started + +Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/{{id}}](http://localhost:3000/{{id}}). + +You can also serve the plugin in isolation by running `yarn start` in the plugin directory. +This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. +It is only meant for local development, and the setup for it can be found inside the [/dev](/dev) directory. diff --git a/packages/cli/templates/default-backend-plugin/dev/index.tsx b/packages/cli/templates/default-backend-plugin/dev/index.tsx new file mode 100644 index 0000000000..812a5585d4 --- /dev/null +++ b/packages/cli/templates/default-backend-plugin/dev/index.tsx @@ -0,0 +1,20 @@ +/* + * 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 { createDevApp } from '@backstage/dev-utils'; +import { plugin } from '../src/plugin'; + +createDevApp().registerPlugin(plugin).render(); diff --git a/packages/cli/templates/default-backend-plugin/package.json.hbs b/packages/cli/templates/default-backend-plugin/package.json.hbs new file mode 100644 index 0000000000..c0ba6676c5 --- /dev/null +++ b/packages/cli/templates/default-backend-plugin/package.json.hbs @@ -0,0 +1,46 @@ +{ +"name": "@backstage/plugin-{{id}}", +"version": "{{version}}", +"main": "src/index.ts", +"types": "src/index.ts", +"license": "Apache-2.0", +"private": true, +"publishConfig": { +"access": "public", +"main": "dist/index.esm.js", +"types": "dist/index.d.ts" +}, +"scripts": { +"build": "backstage-cli backend: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", +"@material-ui/lab": "4.0.0-alpha.45", +"react": "^16.13.1", +"react-dom": "^16.13.1", +"react-use": "^14.2.0" +}, +"devDependencies": { +"@backstage/cli": "^{{version}}", +"@backstage/dev-utils": "^{{version}}", +"@testing-library/jest-dom": "^5.10.1", +"@testing-library/react": "^10.4.1", +"@testing-library/user-event": "^12.0.7", +"@types/jest": "^25.2.2", +"@types/node": "^12.0.0", +"jest-fetch-mock": "^3.0.3" +}, +"files": [ +"dist" +] +} diff --git a/packages/cli/templates/default-backend-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs b/packages/cli/templates/default-backend-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs new file mode 100644 index 0000000000..dff57e66a4 --- /dev/null +++ b/packages/cli/templates/default-backend-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs @@ -0,0 +1,34 @@ +/* + * 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 React from 'react'; +import { render } from '@testing-library/react'; +import mockFetch from 'jest-fetch-mock'; +import ExampleComponent from './ExampleComponent'; +import { ThemeProvider } from '@material-ui/core'; +import { lightTheme } from '@backstage/theme'; + +describe('ExampleComponent', () => { + it('should render', () => { + mockFetch.mockResponse(() => new Promise(() => {})); + const rendered = render( + + + , + ); + expect(rendered.getByText('Welcome to {{ id }}!')).toBeInTheDocument(); + }); +}); diff --git a/packages/cli/templates/default-backend-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs b/packages/cli/templates/default-backend-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs new file mode 100644 index 0000000000..34e37f3e7b --- /dev/null +++ b/packages/cli/templates/default-backend-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs @@ -0,0 +1,57 @@ +/* + * 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 React, { FC } from 'react'; +import { Typography, Grid } from '@material-ui/core'; +import { + InfoCard, + Header, + Page, + pageTheme, + Content, + ContentHeader, + HeaderLabel, + SupportButton, +} from '@backstage/core'; +import ExampleFetchComponent from '../ExampleFetchComponent'; + +const ExampleComponent: FC<{}> = () => ( + +
+ + +
+ + + A description of your plugin goes here. + + + + + + All content should be wrapped in a card like this. + + + + + + + + +
+); + +export default ExampleComponent; diff --git a/packages/cli/templates/default-backend-plugin/src/components/ExampleComponent/index.ts b/packages/cli/templates/default-backend-plugin/src/components/ExampleComponent/index.ts new file mode 100644 index 0000000000..e785d45082 --- /dev/null +++ b/packages/cli/templates/default-backend-plugin/src/components/ExampleComponent/index.ts @@ -0,0 +1,17 @@ +/* + * 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 { default } from './ExampleComponent'; diff --git a/packages/cli/templates/default-backend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs b/packages/cli/templates/default-backend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs new file mode 100644 index 0000000000..7fecdc6f11 --- /dev/null +++ b/packages/cli/templates/default-backend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs @@ -0,0 +1,28 @@ +/* + * 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 React from 'react'; +import { render } from '@testing-library/react'; +import mockFetch from 'jest-fetch-mock'; +import ExampleFetchComponent from './ExampleFetchComponent'; + +describe('ExampleFetchComponent', () => { + it('should render', async () => { + mockFetch.mockResponse(() => new Promise(() => {})); + const rendered = render(); + expect(await rendered.findByTestId('progress')).toBeInTheDocument(); + }); +}); diff --git a/packages/cli/templates/default-backend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx.hbs b/packages/cli/templates/default-backend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx.hbs new file mode 100644 index 0000000000..2dd54f726e --- /dev/null +++ b/packages/cli/templates/default-backend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx.hbs @@ -0,0 +1,108 @@ +/* + * 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 React, { FC } from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import { Table, TableColumn, Progress } from '@backstage/core'; +import Alert from '@material-ui/lab/Alert'; +import { useAsync } from 'react-use'; + +const useStyles = makeStyles({ + avatar: { + height: 32, + width: 32, + borderRadius: '50%', + }, +}); + +type User = { + gender: string; // "male" + name: { + title: string; // "Mr", + first: string; // "Duane", + last: string; // "Reed" + }; + location: object; // {street: {number: 5060, name: "Hickory Creek Dr"}, city: "Albany", state: "New South Wales",…} + email: string; // "duane.reed@example.com" + login: object; // {uuid: "4b785022-9a23-4ab9-8a23-cb3fb43969a9", username: "blackdog796", password: "patch",…} + dob: object; // {date: "1983-06-22T12:30:23.016Z", age: 37} + registered: object; // {date: "2006-06-13T18:48:28.037Z", age: 14} + phone: string; // "07-2154-5651" + cell: string; // "0405-592-879" + id: { + name: string; // "TFN", + value: string; // "796260432" + }; + picture: { medium: string }; // {medium: "https://randomuser.me/api/portraits/men/95.jpg",…} + nat: string; // "AU" +}; + +type DenseTableProps = { + users: User[]; +}; + +export const DenseTable: FC = ({ users }) => { + const classes = useStyles(); + + const columns: TableColumn[] = [ + { title: 'Avatar', field: 'avatar' }, + { title: 'Name', field: 'name' }, + { title: 'Email', field: 'email' }, + { title: 'Nationality', field: 'nationality' }, + ]; + + const data = users.map((user) => { + return { + avatar: ( + {user.name.first} + ), + name: `${user.name.first} ${user.name.last}`, + email: user.email, + nationality: user.nat, + }; + }); + + return ( + + ); +}; + +const ExampleFetchComponent: FC<{}> = () => { + const { value, loading, error } = useAsync(async (): Promise => { + const response = await fetch('https://randomuser.me/api/?results=20'); + const data = await response.json(); + return data.results; + }, []); + + if (loading) { + return ; + } else if (error) { + return {error.message}; + } + + return ; +}; + +export default ExampleFetchComponent; diff --git a/packages/cli/templates/default-backend-plugin/src/components/ExampleFetchComponent/index.ts b/packages/cli/templates/default-backend-plugin/src/components/ExampleFetchComponent/index.ts new file mode 100644 index 0000000000..28482f9fe1 --- /dev/null +++ b/packages/cli/templates/default-backend-plugin/src/components/ExampleFetchComponent/index.ts @@ -0,0 +1,17 @@ +/* + * 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 { default } from './ExampleFetchComponent'; diff --git a/packages/cli/templates/default-backend-plugin/src/index.ts b/packages/cli/templates/default-backend-plugin/src/index.ts new file mode 100644 index 0000000000..3a0a0fe2d3 --- /dev/null +++ b/packages/cli/templates/default-backend-plugin/src/index.ts @@ -0,0 +1,17 @@ +/* + * 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 { plugin } from './plugin'; diff --git a/packages/cli/templates/default-backend-plugin/src/plugin.test.ts.hbs b/packages/cli/templates/default-backend-plugin/src/plugin.test.ts.hbs new file mode 100644 index 0000000000..e34204c9ae --- /dev/null +++ b/packages/cli/templates/default-backend-plugin/src/plugin.test.ts.hbs @@ -0,0 +1,23 @@ +/* + * 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 { plugin } from './plugin'; + +describe('{{ id }}', () => { + it('should export plugin', () => { + expect(plugin).toBeDefined(); + }); +}); diff --git a/packages/cli/templates/default-backend-plugin/src/plugin.ts.hbs b/packages/cli/templates/default-backend-plugin/src/plugin.ts.hbs new file mode 100644 index 0000000000..ad5db2e711 --- /dev/null +++ b/packages/cli/templates/default-backend-plugin/src/plugin.ts.hbs @@ -0,0 +1,30 @@ +/* + * 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 { createPlugin, createRouteRef } from '@backstage/core'; +import ExampleComponent from './components/ExampleComponent'; + +export const rootRouteRef = createRouteRef({ +path: '/{{ id }}', +title: '{{ id }}', +}); + +export const plugin = createPlugin({ +id: '{{ id }}', +register({ router }) { +router.addRoute(rootRouteRef, ExampleComponent); +}, +}); diff --git a/packages/cli/templates/default-backend-plugin/src/setupTests.ts b/packages/cli/templates/default-backend-plugin/src/setupTests.ts new file mode 100644 index 0000000000..8553642152 --- /dev/null +++ b/packages/cli/templates/default-backend-plugin/src/setupTests.ts @@ -0,0 +1,19 @@ +/* + * 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 '@testing-library/jest-dom'; + +require('jest-fetch-mock').enableMocks(); From b468631bc2c2b99d177c8697c988e055c1650b8c Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 22 Jul 2020 15:00:46 +0200 Subject: [PATCH 02/85] chore(cli): starting to move some more boilerplate over --- .../default-backend-plugin/dev/index.tsx | 20 ----- .../default-backend-plugin/package.json.hbs | 79 ++++++++----------- .../scripts/mock-data.sh | 2 + .../templates/default-plugin/package.json.hbs | 2 +- 4 files changed, 38 insertions(+), 65 deletions(-) delete mode 100644 packages/cli/templates/default-backend-plugin/dev/index.tsx create mode 100755 packages/cli/templates/default-backend-plugin/scripts/mock-data.sh diff --git a/packages/cli/templates/default-backend-plugin/dev/index.tsx b/packages/cli/templates/default-backend-plugin/dev/index.tsx deleted file mode 100644 index 812a5585d4..0000000000 --- a/packages/cli/templates/default-backend-plugin/dev/index.tsx +++ /dev/null @@ -1,20 +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 { createDevApp } from '@backstage/dev-utils'; -import { plugin } from '../src/plugin'; - -createDevApp().registerPlugin(plugin).render(); diff --git a/packages/cli/templates/default-backend-plugin/package.json.hbs b/packages/cli/templates/default-backend-plugin/package.json.hbs index c0ba6676c5..51be95a3ba 100644 --- a/packages/cli/templates/default-backend-plugin/package.json.hbs +++ b/packages/cli/templates/default-backend-plugin/package.json.hbs @@ -1,46 +1,37 @@ { -"name": "@backstage/plugin-{{id}}", -"version": "{{version}}", -"main": "src/index.ts", -"types": "src/index.ts", -"license": "Apache-2.0", -"private": true, -"publishConfig": { -"access": "public", -"main": "dist/index.esm.js", -"types": "dist/index.d.ts" -}, -"scripts": { -"build": "backstage-cli backend: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", -"@material-ui/lab": "4.0.0-alpha.45", -"react": "^16.13.1", -"react-dom": "^16.13.1", -"react-use": "^14.2.0" -}, -"devDependencies": { -"@backstage/cli": "^{{version}}", -"@backstage/dev-utils": "^{{version}}", -"@testing-library/jest-dom": "^5.10.1", -"@testing-library/react": "^10.4.1", -"@testing-library/user-event": "^12.0.7", -"@types/jest": "^25.2.2", -"@types/node": "^12.0.0", -"jest-fetch-mock": "^3.0.3" -}, -"files": [ -"dist" -] + "name": "@backstage/plugin-{{id}}-backend", + "version": "{{version}}", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": true, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "start": "backstage-cli backend:dev", + "build": "backstage-cli backend:build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean", + "mock-data": "./scripts/mock-data.sh" + }, + "dependencies": { + "@backstage/backend-common": "^{{version}}", + "@types/express": "^4.17.6", + "express": "^4.17.1", + "express-promise-router": "^3.0.3" + }, + "devDependencies": { + "@backstage/cli": "^{{version}}", + "@types/supertest": "^2.0.8", + "supertest": "^4.0.2" + }, + "files": [ + "dist" + ] } diff --git a/packages/cli/templates/default-backend-plugin/scripts/mock-data.sh b/packages/cli/templates/default-backend-plugin/scripts/mock-data.sh new file mode 100755 index 0000000000..ff30921715 --- /dev/null +++ b/packages/cli/templates/default-backend-plugin/scripts/mock-data.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +echo "use this script to load your service with some mock data if needed!" diff --git a/packages/cli/templates/default-plugin/package.json.hbs b/packages/cli/templates/default-plugin/package.json.hbs index da611a13b7..b457bc41fe 100644 --- a/packages/cli/templates/default-plugin/package.json.hbs +++ b/packages/cli/templates/default-plugin/package.json.hbs @@ -38,7 +38,7 @@ "@testing-library/user-event": "^12.0.7", "@types/jest": "^25.2.2", "@types/node": "^12.0.0", - "jest-fetch-mock": "^3.0.3" + "msw": "^0.19.5" }, "files": [ "dist" From 1516a0872150b497ab21d66a89cd28008681467f Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 22 Jul 2020 15:32:53 +0200 Subject: [PATCH 03/85] chore(cli): removing some other stuff to make way for more things --- .../ExampleComponent.test.tsx.hbs | 34 ------ .../ExampleComponent/ExampleComponent.tsx.hbs | 57 --------- .../src/components/ExampleComponent/index.ts | 17 --- .../ExampleFetchComponent.test.tsx.hbs | 28 ----- .../ExampleFetchComponent.tsx.hbs | 108 ------------------ .../components/ExampleFetchComponent/index.ts | 17 --- .../default-backend-plugin/src/index.ts | 17 --- .../src/plugin.test.ts.hbs | 23 ---- .../default-backend-plugin/src/plugin.ts.hbs | 30 ----- 9 files changed, 331 deletions(-) delete mode 100644 packages/cli/templates/default-backend-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs delete mode 100644 packages/cli/templates/default-backend-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs delete mode 100644 packages/cli/templates/default-backend-plugin/src/components/ExampleComponent/index.ts delete mode 100644 packages/cli/templates/default-backend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs delete mode 100644 packages/cli/templates/default-backend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx.hbs delete mode 100644 packages/cli/templates/default-backend-plugin/src/components/ExampleFetchComponent/index.ts delete mode 100644 packages/cli/templates/default-backend-plugin/src/index.ts delete mode 100644 packages/cli/templates/default-backend-plugin/src/plugin.test.ts.hbs delete mode 100644 packages/cli/templates/default-backend-plugin/src/plugin.ts.hbs diff --git a/packages/cli/templates/default-backend-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs b/packages/cli/templates/default-backend-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs deleted file mode 100644 index dff57e66a4..0000000000 --- a/packages/cli/templates/default-backend-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs +++ /dev/null @@ -1,34 +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 React from 'react'; -import { render } from '@testing-library/react'; -import mockFetch from 'jest-fetch-mock'; -import ExampleComponent from './ExampleComponent'; -import { ThemeProvider } from '@material-ui/core'; -import { lightTheme } from '@backstage/theme'; - -describe('ExampleComponent', () => { - it('should render', () => { - mockFetch.mockResponse(() => new Promise(() => {})); - const rendered = render( - - - , - ); - expect(rendered.getByText('Welcome to {{ id }}!')).toBeInTheDocument(); - }); -}); diff --git a/packages/cli/templates/default-backend-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs b/packages/cli/templates/default-backend-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs deleted file mode 100644 index 34e37f3e7b..0000000000 --- a/packages/cli/templates/default-backend-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs +++ /dev/null @@ -1,57 +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 React, { FC } from 'react'; -import { Typography, Grid } from '@material-ui/core'; -import { - InfoCard, - Header, - Page, - pageTheme, - Content, - ContentHeader, - HeaderLabel, - SupportButton, -} from '@backstage/core'; -import ExampleFetchComponent from '../ExampleFetchComponent'; - -const ExampleComponent: FC<{}> = () => ( - -
- - -
- - - A description of your plugin goes here. - - - - - - All content should be wrapped in a card like this. - - - - - - - - -
-); - -export default ExampleComponent; diff --git a/packages/cli/templates/default-backend-plugin/src/components/ExampleComponent/index.ts b/packages/cli/templates/default-backend-plugin/src/components/ExampleComponent/index.ts deleted file mode 100644 index e785d45082..0000000000 --- a/packages/cli/templates/default-backend-plugin/src/components/ExampleComponent/index.ts +++ /dev/null @@ -1,17 +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 { default } from './ExampleComponent'; diff --git a/packages/cli/templates/default-backend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs b/packages/cli/templates/default-backend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs deleted file mode 100644 index 7fecdc6f11..0000000000 --- a/packages/cli/templates/default-backend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs +++ /dev/null @@ -1,28 +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 React from 'react'; -import { render } from '@testing-library/react'; -import mockFetch from 'jest-fetch-mock'; -import ExampleFetchComponent from './ExampleFetchComponent'; - -describe('ExampleFetchComponent', () => { - it('should render', async () => { - mockFetch.mockResponse(() => new Promise(() => {})); - const rendered = render(); - expect(await rendered.findByTestId('progress')).toBeInTheDocument(); - }); -}); diff --git a/packages/cli/templates/default-backend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx.hbs b/packages/cli/templates/default-backend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx.hbs deleted file mode 100644 index 2dd54f726e..0000000000 --- a/packages/cli/templates/default-backend-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx.hbs +++ /dev/null @@ -1,108 +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 React, { FC } from 'react'; -import { makeStyles } from '@material-ui/core/styles'; -import { Table, TableColumn, Progress } from '@backstage/core'; -import Alert from '@material-ui/lab/Alert'; -import { useAsync } from 'react-use'; - -const useStyles = makeStyles({ - avatar: { - height: 32, - width: 32, - borderRadius: '50%', - }, -}); - -type User = { - gender: string; // "male" - name: { - title: string; // "Mr", - first: string; // "Duane", - last: string; // "Reed" - }; - location: object; // {street: {number: 5060, name: "Hickory Creek Dr"}, city: "Albany", state: "New South Wales",…} - email: string; // "duane.reed@example.com" - login: object; // {uuid: "4b785022-9a23-4ab9-8a23-cb3fb43969a9", username: "blackdog796", password: "patch",…} - dob: object; // {date: "1983-06-22T12:30:23.016Z", age: 37} - registered: object; // {date: "2006-06-13T18:48:28.037Z", age: 14} - phone: string; // "07-2154-5651" - cell: string; // "0405-592-879" - id: { - name: string; // "TFN", - value: string; // "796260432" - }; - picture: { medium: string }; // {medium: "https://randomuser.me/api/portraits/men/95.jpg",…} - nat: string; // "AU" -}; - -type DenseTableProps = { - users: User[]; -}; - -export const DenseTable: FC = ({ users }) => { - const classes = useStyles(); - - const columns: TableColumn[] = [ - { title: 'Avatar', field: 'avatar' }, - { title: 'Name', field: 'name' }, - { title: 'Email', field: 'email' }, - { title: 'Nationality', field: 'nationality' }, - ]; - - const data = users.map((user) => { - return { - avatar: ( - {user.name.first} - ), - name: `${user.name.first} ${user.name.last}`, - email: user.email, - nationality: user.nat, - }; - }); - - return ( -
- ); -}; - -const ExampleFetchComponent: FC<{}> = () => { - const { value, loading, error } = useAsync(async (): Promise => { - const response = await fetch('https://randomuser.me/api/?results=20'); - const data = await response.json(); - return data.results; - }, []); - - if (loading) { - return ; - } else if (error) { - return {error.message}; - } - - return ; -}; - -export default ExampleFetchComponent; diff --git a/packages/cli/templates/default-backend-plugin/src/components/ExampleFetchComponent/index.ts b/packages/cli/templates/default-backend-plugin/src/components/ExampleFetchComponent/index.ts deleted file mode 100644 index 28482f9fe1..0000000000 --- a/packages/cli/templates/default-backend-plugin/src/components/ExampleFetchComponent/index.ts +++ /dev/null @@ -1,17 +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 { default } from './ExampleFetchComponent'; diff --git a/packages/cli/templates/default-backend-plugin/src/index.ts b/packages/cli/templates/default-backend-plugin/src/index.ts deleted file mode 100644 index 3a0a0fe2d3..0000000000 --- a/packages/cli/templates/default-backend-plugin/src/index.ts +++ /dev/null @@ -1,17 +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 { plugin } from './plugin'; diff --git a/packages/cli/templates/default-backend-plugin/src/plugin.test.ts.hbs b/packages/cli/templates/default-backend-plugin/src/plugin.test.ts.hbs deleted file mode 100644 index e34204c9ae..0000000000 --- a/packages/cli/templates/default-backend-plugin/src/plugin.test.ts.hbs +++ /dev/null @@ -1,23 +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 { plugin } from './plugin'; - -describe('{{ id }}', () => { - it('should export plugin', () => { - expect(plugin).toBeDefined(); - }); -}); diff --git a/packages/cli/templates/default-backend-plugin/src/plugin.ts.hbs b/packages/cli/templates/default-backend-plugin/src/plugin.ts.hbs deleted file mode 100644 index ad5db2e711..0000000000 --- a/packages/cli/templates/default-backend-plugin/src/plugin.ts.hbs +++ /dev/null @@ -1,30 +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 { createPlugin, createRouteRef } from '@backstage/core'; -import ExampleComponent from './components/ExampleComponent'; - -export const rootRouteRef = createRouteRef({ -path: '/{{ id }}', -title: '{{ id }}', -}); - -export const plugin = createPlugin({ -id: '{{ id }}', -register({ router }) { -router.addRoute(rootRouteRef, ExampleComponent); -}, -}); From 444b8f768c2a3dc80b0548bf12924cff6052168d Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 23 Jul 2020 10:41:46 +0200 Subject: [PATCH 04/85] chore(cli): finish the template for the backend plugin --- .../default-backend-plugin/README.md.hbs | 5 +- .../default-backend-plugin/package.json.hbs | 7 ++- .../default-backend-plugin/src/index.ts | 17 +++++++ .../default-backend-plugin/src/run.ts.hbs | 33 +++++++++++++ .../src/service/router.test.ts | 45 ++++++++++++++++++ .../src/service/router.ts | 40 ++++++++++++++++ .../src/service/standaloneServer.ts.hbs | 47 +++++++++++++++++++ 7 files changed, 190 insertions(+), 4 deletions(-) create mode 100644 packages/cli/templates/default-backend-plugin/src/index.ts create mode 100644 packages/cli/templates/default-backend-plugin/src/run.ts.hbs create mode 100644 packages/cli/templates/default-backend-plugin/src/service/router.test.ts create mode 100644 packages/cli/templates/default-backend-plugin/src/service/router.ts create mode 100644 packages/cli/templates/default-backend-plugin/src/service/standaloneServer.ts.hbs diff --git a/packages/cli/templates/default-backend-plugin/README.md.hbs b/packages/cli/templates/default-backend-plugin/README.md.hbs index d6f2d65134..5c34b40360 100644 --- a/packages/cli/templates/default-backend-plugin/README.md.hbs +++ b/packages/cli/templates/default-backend-plugin/README.md.hbs @@ -1,12 +1,13 @@ # {{id}} -Welcome to the {{id}} plugin! +Welcome to the {{id}} backend plugin! _This plugin was created through the Backstage CLI_ ## Getting started -Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/{{id}}](http://localhost:3000/{{id}}). +Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn +start` in the root directory, and then navigating to [/{{id}}](http://localhost:3000/{{id}}). You can also serve the plugin in isolation by running `yarn start` in the plugin directory. This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. diff --git a/packages/cli/templates/default-backend-plugin/package.json.hbs b/packages/cli/templates/default-backend-plugin/package.json.hbs index 51be95a3ba..4d347107de 100644 --- a/packages/cli/templates/default-backend-plugin/package.json.hbs +++ b/packages/cli/templates/default-backend-plugin/package.json.hbs @@ -24,12 +24,15 @@ "@backstage/backend-common": "^{{version}}", "@types/express": "^4.17.6", "express": "^4.17.1", - "express-promise-router": "^3.0.3" + "express-promise-router": "^3.0.3", + "yn": "^4.0.0", + "whatwg-fetch": "^3.2.0" }, "devDependencies": { "@backstage/cli": "^{{version}}", "@types/supertest": "^2.0.8", - "supertest": "^4.0.2" + "supertest": "^4.0.2", + "msw": "^0.19.5" }, "files": [ "dist" diff --git a/packages/cli/templates/default-backend-plugin/src/index.ts b/packages/cli/templates/default-backend-plugin/src/index.ts new file mode 100644 index 0000000000..7612c392a2 --- /dev/null +++ b/packages/cli/templates/default-backend-plugin/src/index.ts @@ -0,0 +1,17 @@ +/* + * 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 * from './service/router'; diff --git a/packages/cli/templates/default-backend-plugin/src/run.ts.hbs b/packages/cli/templates/default-backend-plugin/src/run.ts.hbs new file mode 100644 index 0000000000..b96989e4b8 --- /dev/null +++ b/packages/cli/templates/default-backend-plugin/src/run.ts.hbs @@ -0,0 +1,33 @@ +/* + * 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 { getRootLogger } from '@backstage/backend-common'; +import yn from 'yn'; +import { startStandaloneServer } from './service/standaloneServer'; + +const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7000; +const enableCors = yn(process.env.PLUGIN_CORS, { default: false }); +const logger = getRootLogger(); + +startStandaloneServer({ port, enableCors, logger }).catch(err => { + logger.error(err); + process.exit(1); +}); + +process.on('SIGINT', () => { + logger.info('CTRL+C pressed; exiting.'); + process.exit(0); +}); diff --git a/packages/cli/templates/default-backend-plugin/src/service/router.test.ts b/packages/cli/templates/default-backend-plugin/src/service/router.test.ts new file mode 100644 index 0000000000..fba6b0ccdf --- /dev/null +++ b/packages/cli/templates/default-backend-plugin/src/service/router.test.ts @@ -0,0 +1,45 @@ +/* + * 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 { getVoidLogger } from '@backstage/backend-common'; +import express from 'express'; +import request from 'supertest'; + +import { createRouter } from './router'; + +describe('createRouter', () => { + let app: express.Express; + + beforeAll(async () => { + const router = await createRouter({ + logger: getVoidLogger(), + }); + app = express().use(router); + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('GET /ping', () => { + it('returns ok', async () => { + const response = await request(app).get('/ping'); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ status: 'ok' }); + }); + }); +}); diff --git a/packages/cli/templates/default-backend-plugin/src/service/router.ts b/packages/cli/templates/default-backend-plugin/src/service/router.ts new file mode 100644 index 0000000000..3ea8219365 --- /dev/null +++ b/packages/cli/templates/default-backend-plugin/src/service/router.ts @@ -0,0 +1,40 @@ +/* + * 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 { errorHandler } from '@backstage/backend-common'; +import express from 'express'; +import Router from 'express-promise-router'; +import { Logger } from 'winston'; + +export interface RouterOptions { + logger: Logger; +} + +export async function createRouter( + options: RouterOptions, +): Promise { + const { logger } = options; + + const router = Router(); + router.use(express.json()); + + router.get('/health', (_, response) => { + logger.info('PONG!'); + response.send({ status: 'ok' }); + }); + router.use(errorHandler()); + return router; +} diff --git a/packages/cli/templates/default-backend-plugin/src/service/standaloneServer.ts.hbs b/packages/cli/templates/default-backend-plugin/src/service/standaloneServer.ts.hbs new file mode 100644 index 0000000000..6e38965246 --- /dev/null +++ b/packages/cli/templates/default-backend-plugin/src/service/standaloneServer.ts.hbs @@ -0,0 +1,47 @@ +/* + * 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 { createServiceBuilder } from '@backstage/backend-common'; +import { Server } from 'http'; +import { Logger } from 'winston'; +import { createRouter } from './router'; + +export interface ServerOptions { + port: number; + enableCors: boolean; + logger: Logger; +} + +export async function startStandaloneServer( + options: ServerOptions, +): Promise { + const logger = options.logger.child({ service: '{{id}}-backend' }); + logger.debug('Starting application server...'); + const router = await createRouter({ + logger, + }); + + const service = createServiceBuilder(module) + .enableCors({ origin: 'http://localhost:3000' }) + .addRouter('/{{id}}', router); + + return await service.start().catch(err => { + logger.error(err); + process.exit(1); + }); +} + +module.hot?.accept(); From dd9cc6a666df8ef972e43dfe02ce8a60d06e091c Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 23 Jul 2020 10:42:55 +0200 Subject: [PATCH 05/85] chore(cli): polyfill whatwg-fetch in tests --- .../cli/templates/default-backend-plugin/src/setupTests.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/cli/templates/default-backend-plugin/src/setupTests.ts b/packages/cli/templates/default-backend-plugin/src/setupTests.ts index 8553642152..aa5bc44d68 100644 --- a/packages/cli/templates/default-backend-plugin/src/setupTests.ts +++ b/packages/cli/templates/default-backend-plugin/src/setupTests.ts @@ -13,7 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import '@testing-library/jest-dom'; - -require('jest-fetch-mock').enableMocks(); +require('whatwg-fetch'); From ffa1b38a7380b3c3aefe56b420971963eb74ac4a Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 23 Jul 2020 10:43:39 +0200 Subject: [PATCH 06/85] chore(cli): polyfill fetch in the server --- packages/cli/templates/default-backend-plugin/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/cli/templates/default-backend-plugin/src/index.ts b/packages/cli/templates/default-backend-plugin/src/index.ts index 7612c392a2..7332f92d68 100644 --- a/packages/cli/templates/default-backend-plugin/src/index.ts +++ b/packages/cli/templates/default-backend-plugin/src/index.ts @@ -14,4 +14,5 @@ * limitations under the License. */ +require('whatwg-fetch'); export * from './service/router'; From 93306ace29cfb30b2c3db76581ff649bb0ae8bb8 Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Thu, 23 Jul 2020 11:53:50 +0200 Subject: [PATCH 07/85] chore(cli): update eslint rules --- packages/cli/templates/default-backend-plugin/.eslintrc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/templates/default-backend-plugin/.eslintrc.js b/packages/cli/templates/default-backend-plugin/.eslintrc.js index 13573efa9c..16a033dbc6 100644 --- a/packages/cli/templates/default-backend-plugin/.eslintrc.js +++ b/packages/cli/templates/default-backend-plugin/.eslintrc.js @@ -1,3 +1,3 @@ module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], + extends: [require.resolve('@backstage/cli/config/eslint.backend')], }; From 407f62e40ac26824e37fdd54c4cfcb2c7b3efea4 Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Thu, 23 Jul 2020 11:54:25 +0200 Subject: [PATCH 08/85] chore(cli): need to make a module --- packages/cli/templates/default-backend-plugin/src/setupTests.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/cli/templates/default-backend-plugin/src/setupTests.ts b/packages/cli/templates/default-backend-plugin/src/setupTests.ts index aa5bc44d68..ac41ef995a 100644 --- a/packages/cli/templates/default-backend-plugin/src/setupTests.ts +++ b/packages/cli/templates/default-backend-plugin/src/setupTests.ts @@ -14,3 +14,5 @@ * limitations under the License. */ require('whatwg-fetch'); + +export {}; From 168f1df9359fc9a1cb960fb9a7de2248bc0cef0e Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Thu, 23 Jul 2020 11:56:35 +0200 Subject: [PATCH 09/85] chore(cli): fixing dependenceis:Wq --- .../cli/templates/default-backend-plugin/package.json.hbs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/cli/templates/default-backend-plugin/package.json.hbs b/packages/cli/templates/default-backend-plugin/package.json.hbs index 4d347107de..a2328223db 100644 --- a/packages/cli/templates/default-backend-plugin/package.json.hbs +++ b/packages/cli/templates/default-backend-plugin/package.json.hbs @@ -25,8 +25,9 @@ "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^3.0.3", - "yn": "^4.0.0", - "whatwg-fetch": "^3.2.0" + "whatwg-fetch": "^3.2.0", + "winston": "^3.3.3", + "yn": "^4.0.0" }, "devDependencies": { "@backstage/cli": "^{{version}}", From f6296c078f6eace6ccbf30fbbb45e4a4781ead67 Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Thu, 23 Jul 2020 12:00:48 +0200 Subject: [PATCH 10/85] chore(cli): fixing winston dependency --- packages/cli/templates/default-backend-plugin/package.json.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/templates/default-backend-plugin/package.json.hbs b/packages/cli/templates/default-backend-plugin/package.json.hbs index a2328223db..92f0a9e10f 100644 --- a/packages/cli/templates/default-backend-plugin/package.json.hbs +++ b/packages/cli/templates/default-backend-plugin/package.json.hbs @@ -26,7 +26,7 @@ "express": "^4.17.1", "express-promise-router": "^3.0.3", "whatwg-fetch": "^3.2.0", - "winston": "^3.3.3", + "winston": "^3.2.1", "yn": "^4.0.0" }, "devDependencies": { From 7197ceba043ca96e23c22f8cdb0bd690f679053a Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Thu, 23 Jul 2020 12:02:33 +0200 Subject: [PATCH 11/85] chore(cli): updating endpoint for test --- .../default-backend-plugin/src/service/router.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/templates/default-backend-plugin/src/service/router.test.ts b/packages/cli/templates/default-backend-plugin/src/service/router.test.ts index fba6b0ccdf..0aaeafa379 100644 --- a/packages/cli/templates/default-backend-plugin/src/service/router.test.ts +++ b/packages/cli/templates/default-backend-plugin/src/service/router.test.ts @@ -34,9 +34,9 @@ describe('createRouter', () => { jest.resetAllMocks(); }); - describe('GET /ping', () => { + describe('GET /health', () => { it('returns ok', async () => { - const response = await request(app).get('/ping'); + const response = await request(app).get('/health'); expect(response.status).toEqual(200); expect(response.body).toEqual({ status: 'ok' }); From b958118d7497b1e9990ed2b9c471eb4310cb4e18 Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Thu, 23 Jul 2020 20:22:50 +0200 Subject: [PATCH 12/85] chore(cli): updating issues with wrong package version --- packages/cli/templates/default-backend-plugin/package.json.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/templates/default-backend-plugin/package.json.hbs b/packages/cli/templates/default-backend-plugin/package.json.hbs index 92f0a9e10f..1cf9ab1c5b 100644 --- a/packages/cli/templates/default-backend-plugin/package.json.hbs +++ b/packages/cli/templates/default-backend-plugin/package.json.hbs @@ -25,7 +25,7 @@ "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^3.0.3", - "whatwg-fetch": "^3.2.0", + "whatwg-fetch": "^2.0.0", "winston": "^3.2.1", "yn": "^4.0.0" }, From 1ae5a74c3ae8eb01c917d51d9631cccd08745c3c Mon Sep 17 00:00:00 2001 From: "J Shamsul Bahri (jibone))" Date: Fri, 4 Sep 2020 13:56:54 +0800 Subject: [PATCH 13/85] WIP: Got the SAML login working TODO: Cleanup, remove hardcoded url. --- app-config.yaml | 136 +++++++++--------- packages/app/src/App.tsx | 5 +- packages/app/src/apis.ts | 10 ++ packages/app/src/identityProviders.ts | 7 + .../core-api/src/apis/definitions/auth.ts | 23 +++ .../src/apis/implementations/auth/index.ts | 1 + .../implementations/auth/saml/SamlAuth.ts | 126 ++++++++++++++++ .../apis/implementations/auth/saml/index.ts | 16 +++ .../apis/implementations/auth/saml/types.ts | 22 +++ packages/core-api/src/app/AppIdentity.ts | 4 + .../lib/AuthConnector/SamlAuthConnector.ts | 105 ++++++++++++++ .../core-api/src/lib/AuthConnector/index.ts | 1 + .../SamlAuthSessionManager.ts | 64 +++++++++ .../SamlAuthSessionStore.ts | 96 +++++++++++++ .../src/lib/AuthSessionManager/index.ts | 2 + .../src/lib/AuthSessionManager/types.ts | 7 + .../core/src/layout/Sidebar/UserSettings.tsx | 8 +- .../src/layout/SignInPage/commonProvider.tsx | 5 + .../core/src/layout/SignInPage/providers.tsx | 2 + .../src/layout/SignInPage/samlProvider.tsx | 92 ++++++++++++ packages/core/src/layout/SignInPage/types.ts | 9 +- .../auth-backend/src/providers/factories.ts | 1 + .../src/providers/saml/provider.ts | 29 +++- .../src/ingestion/HigherOrderOperations.ts | 6 +- yarn.lock | 5 +- 25 files changed, 698 insertions(+), 84 deletions(-) create mode 100644 packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts create mode 100644 packages/core-api/src/apis/implementations/auth/saml/index.ts create mode 100644 packages/core-api/src/apis/implementations/auth/saml/types.ts create mode 100644 packages/core-api/src/lib/AuthConnector/SamlAuthConnector.ts create mode 100644 packages/core-api/src/lib/AuthSessionManager/SamlAuthSessionManager.ts create mode 100644 packages/core-api/src/lib/AuthSessionManager/SamlAuthSessionStore.ts create mode 100644 packages/core/src/layout/SignInPage/samlProvider.tsx diff --git a/app-config.yaml b/app-config.yaml index f578fb3fca..e65df93969 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -28,72 +28,74 @@ sentry: auth: providers: - google: - development: - appOrigin: "http://localhost:3000/" - secure: false - clientId: - $secret: - env: AUTH_GOOGLE_CLIENT_ID - clientSecret: - $secret: - env: AUTH_GOOGLE_CLIENT_SECRET - github: - development: - appOrigin: "http://localhost:3000/" - secure: false - clientId: - $secret: - env: AUTH_GITHUB_CLIENT_ID - clientSecret: - $secret: - env: AUTH_GITHUB_CLIENT_SECRET - enterpriseInstanceUrl: - $secret: - env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL - gitlab: - development: - appOrigin: "http://localhost:3000/" - secure: false - clientId: - $secret: - env: AUTH_GITLAB_CLIENT_ID - clientSecret: - $secret: - env: AUTH_GITLAB_CLIENT_SECRET - audience: - $secret: - env: GITLAB_BASE_URL - # saml: + # google: # development: - # entryPoint: "http://localhost:7001/" - # issuer: "passport-saml" - okta: + # appOrigin: 'http://localhost:3000/' + # secure: false + # clientId: + # $secret: + # env: AUTH_GOOGLE_CLIENT_ID + # clientSecret: + # $secret: + # env: AUTH_GOOGLE_CLIENT_SECRET + # github: + # development: + # appOrigin: 'http://localhost:3000/' + # secure: false + # clientId: + # $secret: + # env: AUTH_GITHUB_CLIENT_ID + # clientSecret: + # $secret: + # env: AUTH_GITHUB_CLIENT_SECRET + # enterpriseInstanceUrl: + # $secret: + # env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL + # gitlab: + # development: + # appOrigin: 'http://localhost:3000/' + # secure: false + # clientId: + # $secret: + # env: AUTH_GITLAB_CLIENT_ID + # clientSecret: + # $secret: + # env: AUTH_GITLAB_CLIENT_SECRET + # audience: + # $secret: + # env: GITLAB_BASE_URL + saml: development: - appOrigin: "http://localhost:3000/" - secure: false - clientId: - $secret: - env: AUTH_OKTA_CLIENT_ID - clientSecret: - $secret: - env: AUTH_OKTA_CLIENT_SECRET - audience: - $secret: - env: AUTH_OKTA_AUDIENCE - oauth2: - development: - appOrigin: "http://localhost:3000/" - secure: false - clientId: - $secret: - env: AUTH_OAUTH2_CLIENT_ID - clientSecret: - $secret: - env: AUTH_OAUTH2_CLIENT_SECRET - authorizationURL: - $secret: - env: AUTH_OAUTH2_AUTH_URL - tokenURL: - $secret: - env: AUTH_OAUTH2_TOKEN_URL + entryPoint: 'https://sso.jumpcloud.com/saml2/backstage-test' + issuer: 'passport-saml' + # entryPoint: 'http://localhost:7001/' + # issuer: 'passport-saml' + # okta + # development: + # appOrigin: 'http://localhost:3000/' + # secure: false + # clientId: + # $secret: + # env: AUTH_OKTA_CLIENT_ID + # clientSecret: + # $secret: + # env: AUTH_OKTA_CLIENT_SECRET + # audience: + # $secret: + # env: AUTH_OKTA_AUDIENCE + # oauth2: + # development: + # appOrigin: 'http://localhost:3000/' + # secure: false + # clientId: + # $secret: + # env: AUTH_OAUTH2_CLIENT_ID + # clientSecret: + # $secret: + # env: AUTH_OAUTH2_CLIENT_SECRET + # authorizationURL: + # $secret: + # env: AUTH_OAUTH2_AUTH_URL + # tokenURL: + # $secret: + # env: AUTH_OAUTH2_TOKEN_URL diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 9a9fdfe5c4..29e371b469 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -33,7 +33,10 @@ const app = createApp({ components: { SignInPage: props => { return ( - + ); }, }, diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index cac3d6e171..0c097450bc 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -38,6 +38,8 @@ import { gitlabAuthApiRef, storageApiRef, WebStorage, + SamlAuth, + samlAuthApiRef, } from '@backstage/core'; import { @@ -139,6 +141,14 @@ export const apis = (config: ConfigApi) => { }), ); + builder.add( + samlAuthApiRef, + SamlAuth.create({ + apiOrigin: backendUrl, + basePath: '/auth/', + }), + ); + builder.add( techRadarApiRef, new TechRadar({ diff --git a/packages/app/src/identityProviders.ts b/packages/app/src/identityProviders.ts index e80b1d6fea..e229bb7671 100644 --- a/packages/app/src/identityProviders.ts +++ b/packages/app/src/identityProviders.ts @@ -19,6 +19,7 @@ import { gitlabAuthApiRef, oktaAuthApiRef, githubAuthApiRef, + samlAuthApiRef, } from '@backstage/core'; export const providers = [ @@ -46,4 +47,10 @@ export const providers = [ message: 'Sign In using Okta', apiRef: oktaAuthApiRef, }, + { + id: 'saml-auth-provider', + title: 'SAML', + message: 'Sign In using SAML', + apiRef: samlAuthApiRef, + }, ]; diff --git a/packages/core-api/src/apis/definitions/auth.ts b/packages/core-api/src/apis/definitions/auth.ts index ec7aba81c4..6dfe9595b0 100644 --- a/packages/core-api/src/apis/definitions/auth.ts +++ b/packages/core-api/src/apis/definitions/auth.ts @@ -97,6 +97,21 @@ export type OAuthApi = { logout(): Promise; }; +/** + * This API provides access to SAML 2 credentials. Verify user access with identity provider. + */ +export type SamlApi = { + // Not sure what Promise call back should have. + getBackstageIdentity( + options?: AuthRequestOptions, + ): Promise; + + getProfile(options?: AuthRequestOptions): Promise; + + // Not sure if this is needed. + logout(): Promise; +}; + /** * This API provides access to OpenID Connect credentials. It lets you request ID tokens, * which can be passed to backend services to prove the user's identity. @@ -273,3 +288,11 @@ export const oauth2ApiRef = createApiRef< id: 'core.auth.oauth2', description: 'Example of how to use oauth2 custom provider', }); + +/** + * Provides authentication for saml based identity providers + */ +export const samlAuthApiRef = createApiRef({ + id: 'core.auth.saml', + description: 'provides authentication towards saml based provider', +}); diff --git a/packages/core-api/src/apis/implementations/auth/index.ts b/packages/core-api/src/apis/implementations/auth/index.ts index 786e9fa771..0906e34149 100644 --- a/packages/core-api/src/apis/implementations/auth/index.ts +++ b/packages/core-api/src/apis/implementations/auth/index.ts @@ -19,3 +19,4 @@ export * from './gitlab'; export * from './google'; export * from './oauth2'; export * from './okta'; +export * from './saml'; diff --git a/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts b/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts new file mode 100644 index 0000000000..75aefda7f5 --- /dev/null +++ b/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts @@ -0,0 +1,126 @@ +/* + * 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 SamlIcon from '@material-ui/icons/AcUnit'; +import { SamlAuthConnector } from '../../../../lib/AuthConnector'; +import { SessionManager } from '../../../../lib/AuthSessionManager/types'; +import { Observable } from '../../../../types'; +import { + ProfileInfo, + BackstageIdentity, + SessionState, + AuthRequestOptions, + SamlApi, +} from '../../../definitions/auth'; +import { AuthProvider } from '../../../definitions'; +import { SamlSession } from './types'; +import { + SamlAuthSessionManager, + SamlAuthSessionStore, +} from '../../../../lib/AuthSessionManager'; + +type CreateOptions = { + apiOrigin: string; + basePath: string; + + // oauthRequestApi?: OAuthRequestApi; + + environment?: string; + provider?: AuthProvider & { id: string }; +}; + +export type SamlAuthResponse = { + userId: string; + profile: ProfileInfo; + backstageIdentity: BackstageIdentity; +}; + +const DEFAULT_PROVIDER = { + id: 'saml', + title: 'SAML', + icon: SamlIcon, +}; + +class SamlAuth implements SamlApi { + static create({ + apiOrigin, + basePath, + environment = 'development', + provider = DEFAULT_PROVIDER, + }: CreateOptions) { + // eslint-disable-next-line no-console + console.log('this is from SamlAuth'); + + const connector = new SamlAuthConnector({ + apiOrigin, + basePath, + environment, + provider, + }); + + const sessionManager = new SamlAuthSessionManager({ + connector, + }); + + const authSessionStore = new SamlAuthSessionStore({ + manager: sessionManager, + storageKey: 'samlSession', + }); + + // return new SamlAuth(authSessionStore); + return new SamlAuth(authSessionStore); + } + + sessionState$(): Observable { + return this.sessionManager.sessionState$(); + } + // constructor(private readonly sessionManager: any) {} + // eslint-disable-next-line @typescript-eslint/no-useless-constructor + constructor(private readonly sessionManager: SessionManager) { + // eslint-disable-next-line no-console + console.log('this is the constructor'); + } + + async getBackstageIdentity( + options: AuthRequestOptions, + ): Promise { + // eslint-disable-next-line no-console + console.log('===> Saml getBackstageIdentity()'); + const session = await this.sessionManager.getSession(options); + // eslint-disable-next-line no-console + console.log('this this thish lkkdfkkfkfji'); + // eslint-disable-next-line no-console + console.log(session); + return session?.backstageIdentity; + } + + async getProfile(options: AuthRequestOptions = {}) { + // eslint-disable-next-line no-console + console.log('==> samlauth getprofile()'); + const session = await this.sessionManager.getSession(options); + // eslint-disable-next-line no-console + console.log('+++ this is the session from getProfile()'); + // eslint-disable-next-line no-console + console.log(session); + return session?.profile; + } + + async logout() { + await this.sessionManager.removeSession(); + } +} + +export default SamlAuth; diff --git a/packages/core-api/src/apis/implementations/auth/saml/index.ts b/packages/core-api/src/apis/implementations/auth/saml/index.ts new file mode 100644 index 0000000000..c2436ab435 --- /dev/null +++ b/packages/core-api/src/apis/implementations/auth/saml/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { default as SamlAuth } from './SamlAuth'; diff --git a/packages/core-api/src/apis/implementations/auth/saml/types.ts b/packages/core-api/src/apis/implementations/auth/saml/types.ts new file mode 100644 index 0000000000..296f70b0ea --- /dev/null +++ b/packages/core-api/src/apis/implementations/auth/saml/types.ts @@ -0,0 +1,22 @@ +/* + * 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 { ProfileInfo, BackstageIdentity } from '../../../definitions'; + +export type SamlSession = { + userId: string; + profile: ProfileInfo; + backstageIdentity: BackstageIdentity; +}; diff --git a/packages/core-api/src/app/AppIdentity.ts b/packages/core-api/src/app/AppIdentity.ts index 69ee5d28ac..1a6cdac3e6 100644 --- a/packages/core-api/src/app/AppIdentity.ts +++ b/packages/core-api/src/app/AppIdentity.ts @@ -67,6 +67,10 @@ export class AppIdentity implements IdentityApi { // This is indirectly called by the sign-in page to continue into the app. setSignInResult(result: SignInResult) { + // eslint-disable-next-line no-console + console.log('this is from setSignInResult'); + // eslint-disable-next-line no-console + console.log(result); if (this.hasIdentity) { return; } diff --git a/packages/core-api/src/lib/AuthConnector/SamlAuthConnector.ts b/packages/core-api/src/lib/AuthConnector/SamlAuthConnector.ts new file mode 100644 index 0000000000..9dc0fca0a0 --- /dev/null +++ b/packages/core-api/src/lib/AuthConnector/SamlAuthConnector.ts @@ -0,0 +1,105 @@ +/* + * 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 { + AuthProvider, + ProfileInfo, + BackstageIdentity, +} from '../../apis/definitions'; +import { AuthConnector } from './types'; +import { showLoginPopup } from '../loginPopup'; + +const DEFAULT_BASE_PATH = '/api/auth'; + +type Options = { + apiOrigin?: string; + basePath?: string; + environment?: string; + provider: AuthProvider & { id: string }; +}; + +export type SamlResponse = { + userId: string; + profile: ProfileInfo; + backstageIdentity: BackstageIdentity; +}; + +export class SamlAuthConnector implements AuthConnector { + private readonly apiOrigin: string; + private readonly basePath: string; + private readonly environment: string | undefined; + private readonly provider: AuthProvider & { id: string }; + + constructor(options: Options) { + const { + apiOrigin = window.location.origin, + basePath = DEFAULT_BASE_PATH, + environment, + provider, + } = options; + + this.apiOrigin = apiOrigin; + this.basePath = basePath; + this.environment = environment; + this.provider = provider; + } + + async createSession(): Promise { + // eslint-disable-next-line no-console + console.log('==> from SamlAuthConnector createSession'); + + const payload = await showLoginPopup({ + url: 'http://localhost:7000/auth/saml/start', // FIXME: this should be from app.config or somewhere + name: 'SAML Login', // FIXME: change this to provider name? and not hardcode the name + origin: this.apiOrigin, + width: 450, + height: 730, + }); + + // eslint-disable-next-line no-console + console.log(payload); + + return { + ...payload, + id: payload.profile.email, + }; + } + + // TODO: do we need this for SAML? + async refreshSession(): Promise { + // eslint-disable-next-line no-console + console.log('==> this is refresh session'); + } + + async removeSession(): Promise { + // eslint-disable-next-line no-console + console.log('this removes the session'); + const res = await fetch('http://localhost:7000/auth/saml/logout', { + method: 'POST', + headers: { + 'x-requested-with': 'XMLHttpRequest', + }, + credentials: 'include', + }).catch(error => { + throw new Error(`Logout request failed, ${error}`); + }); + + if (!res.ok) { + const error: any = new Error(`Logout request failed, ${res.statusText}`); + error.status = res.status; + throw error; + } + } +} diff --git a/packages/core-api/src/lib/AuthConnector/index.ts b/packages/core-api/src/lib/AuthConnector/index.ts index db5c582328..84f5067443 100644 --- a/packages/core-api/src/lib/AuthConnector/index.ts +++ b/packages/core-api/src/lib/AuthConnector/index.ts @@ -15,4 +15,5 @@ */ export { DefaultAuthConnector } from './DefaultAuthConnector'; +export { SamlAuthConnector } from './SamlAuthConnector'; export * from './types'; diff --git a/packages/core-api/src/lib/AuthSessionManager/SamlAuthSessionManager.ts b/packages/core-api/src/lib/AuthSessionManager/SamlAuthSessionManager.ts new file mode 100644 index 0000000000..9439684cf4 --- /dev/null +++ b/packages/core-api/src/lib/AuthSessionManager/SamlAuthSessionManager.ts @@ -0,0 +1,64 @@ +/* + * 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 { SessionManager, GetSessionOptions } from './types'; +import { SamlAuthConnector, SamlResponse } from '../AuthConnector/SamlAuthConnector'; +import { SessionStateTracker } from './SessionStateTracker'; + + +type Options = { + connector: SamlAuthConnector; +}; + +export class SamlAuthSessionManager implements SessionManager { + private readonly connector: SamlAuthConnector; + private readonly stateTracker = new SessionStateTracker(); + + private currentSession: any | undefined; // FIXME: proper typing here + + constructor(options: Options) { + const { connector } = options; + + this.connector = connector; + // this.helper = new SessionScopeHelper + } + + async getSession(options: GetSessionOptions): Promise { + // eslint-disable-next-line no-console + console.log('==> this is from SamlAuthSessionManager getSession()'); + if (this.currentSession) { + return this.currentSession; + } + + if (options.optional) { + return undefined; + } + + this.currentSession = await this.connector.createSession(); + this.stateTracker.setIsSignedIn(true); + return this.currentSession; + } + + async removeSession() { + this.currentSession = undefined; + await this.connector.removeSession(); + this.stateTracker.setIsSignedIn(false); + } + + sessionState$() { + return this.stateTracker.sessionState$(); + } +} diff --git a/packages/core-api/src/lib/AuthSessionManager/SamlAuthSessionStore.ts b/packages/core-api/src/lib/AuthSessionManager/SamlAuthSessionStore.ts new file mode 100644 index 0000000000..53fba09b54 --- /dev/null +++ b/packages/core-api/src/lib/AuthSessionManager/SamlAuthSessionStore.ts @@ -0,0 +1,96 @@ +/* + * 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 { SamlAuthSessionManager, GetSessionOptions } from './types'; + +type Options = { + manager: SamlAuthSessionManager; + storageKey: string; +}; + +export class SamlAuthSessionStore implements SamlAuthSessionManager { + private readonly manager: SamlAuthSessionManager; + private readonly storageKey: string; + + constructor(options: Options) { + const { manager, storageKey } = options; + + this.manager = manager; + this.storageKey = storageKey; + } + + async getSession(options: GetSessionOptions): Promise { + // eslint-disable-next-line no-console + console.log('==>> this is from SamlAuthSessionStore getSession()'); + const session = this.loadSession(); + + if (session) { + return session!; + } + + const newSession = await this.manager.getSession(options); + this.saveSession(newSession); + return newSession; + } + + async removeSession() { + localStorage.removeItem(this.storageKey); + await this.manager.removeSession(); + } + + sessionState$() { + return this.manager.sessionState$(); + } + + private saveSession(session: T | undefined) { + if (session === undefined) { + localStorage.removeItem(this.storageKey); + } else { + localStorage.setItem( + this.storageKey, + JSON.stringify(session, (_key, value) => { + if (value instanceof Set) { + return { + __type: 'Set', + __value: Array.from(value), + }; + } + return value; + }), + ); + } + } + + private loadSession(): T | undefined { + try { + const sessionJson = localStorage.getItem(this.storageKey); + if (sessionJson) { + const session = JSON.parse(sessionJson, (_key, value) => { + if (value?.__type === 'Set') { + return new Set(value.__value); + } + return value; + }); + return session; + } + + return undefined; + } catch (error) { + localStorage.removeItem(this.storageKey); + return undefined; + } + } +} diff --git a/packages/core-api/src/lib/AuthSessionManager/index.ts b/packages/core-api/src/lib/AuthSessionManager/index.ts index 5f4dde8662..a452ac0055 100644 --- a/packages/core-api/src/lib/AuthSessionManager/index.ts +++ b/packages/core-api/src/lib/AuthSessionManager/index.ts @@ -16,5 +16,7 @@ export { RefreshingAuthSessionManager } from './RefreshingAuthSessionManager'; export { StaticAuthSessionManager } from './StaticAuthSessionManager'; +export { SamlAuthSessionManager } from './SamlAuthSessionManager'; export { AuthSessionStore } from './AuthSessionStore'; +export { SamlAuthSessionStore } from './SamlAuthSessionStore'; export * from './types'; diff --git a/packages/core-api/src/lib/AuthSessionManager/types.ts b/packages/core-api/src/lib/AuthSessionManager/types.ts index 804c7121e1..1be439a840 100644 --- a/packages/core-api/src/lib/AuthSessionManager/types.ts +++ b/packages/core-api/src/lib/AuthSessionManager/types.ts @@ -36,6 +36,13 @@ export type SessionManager = { sessionState$(): Observable; }; +// FIXME do we need this? +export type SamlAuthSessionManager = { + getSession(options: GetSessionOptions): Promise; + removeSession(): Promise; + sessionState$(): Observable; +}; + /** * A function called to determine the scopes of a session. */ diff --git a/packages/core/src/layout/Sidebar/UserSettings.tsx b/packages/core/src/layout/Sidebar/UserSettings.tsx index e69d63186d..b08a3b82d8 100644 --- a/packages/core/src/layout/Sidebar/UserSettings.tsx +++ b/packages/core/src/layout/Sidebar/UserSettings.tsx @@ -49,6 +49,12 @@ export function SidebarUserSettings() { if (!sidebarOpen && open) setOpen(false); }, [open, sidebarOpen]); + // FIXME: Change this and remove the session storage stuff + const handleLogout = () => { + identityApi.logout(); + window.sessionStorage.clear(); + }; + return ( <> @@ -91,7 +97,7 @@ export function SidebarUserSettings() { identityApi.logout()} + onClick={handleLogout} /> diff --git a/packages/core/src/layout/SignInPage/commonProvider.tsx b/packages/core/src/layout/SignInPage/commonProvider.tsx index f9611342a2..88a53ec358 100644 --- a/packages/core/src/layout/SignInPage/commonProvider.tsx +++ b/packages/core/src/layout/SignInPage/commonProvider.tsx @@ -36,6 +36,11 @@ const Component: ProviderComponent = ({ config, onResult }) => { instantPopup: true, }); + // eslint-disable-next-line no-console + console.log('====++++++++++++++++++++++++++> handleLogin for commonProvider'); + // eslint-disable-next-line no-console + console.log(identity); + const profile = await authApi.getProfile(); onResult({ userId: identity!.id, diff --git a/packages/core/src/layout/SignInPage/providers.tsx b/packages/core/src/layout/SignInPage/providers.tsx index ff60eba228..436078e49c 100644 --- a/packages/core/src/layout/SignInPage/providers.tsx +++ b/packages/core/src/layout/SignInPage/providers.tsx @@ -26,6 +26,7 @@ import { SignInConfig, IdentityProviders, SignInProvider } from './types'; import { commonProvider } from './commonProvider'; import { guestProvider } from './guestProvider'; import { customProvider } from './customProvider'; +import { samlProvider } from './samlProvider'; const PROVIDER_STORAGE_KEY = '@backstage/core:SignInPage:provider'; @@ -41,6 +42,7 @@ const signInProviders: { [key: string]: SignInProvider } = { guest: guestProvider, custom: customProvider, common: commonProvider, + saml: samlProvider, }; function validateIDs(id: string, providers: SignInProviderType): void { diff --git a/packages/core/src/layout/SignInPage/samlProvider.tsx b/packages/core/src/layout/SignInPage/samlProvider.tsx new file mode 100644 index 0000000000..e9673bca8c --- /dev/null +++ b/packages/core/src/layout/SignInPage/samlProvider.tsx @@ -0,0 +1,92 @@ +/* + * 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 React from 'react'; +import { Grid, Typography, Button } from '@material-ui/core'; +import { ProviderComponent, ProviderLoader, SignInProvider } from './types'; +import { useApi, errorApiRef /* samlAuthApiRef */ } from '@backstage/core-api'; +import { InfoCard } from '../InfoCard/InfoCard'; + +const Component: ProviderComponent = ({ onResult }) => { + // const samlApi = useApi(samlAuthApiRef); + const errorApi = useApi(errorApiRef); + + const handleSSORedirect = () => { + // FIXME: this shoudl be from identity API or something. + window.open('http://localhost:7000/auth/saml/start'); + }; + + const receiveMessage = (event: any) => { + // FIXME: Should use the backstage identity API and not create this session storage. + window.sessionStorage.setItem( + 'helixSession', + JSON.stringify(event.data.response), + ); + + onResult({ + userId: event.data.response.backstageIdentity, + profile: { + email: event.data.response.profile.email, + displayName: event.data.response.profile.displayName, + }, + }); + }; + + const handleLogin = async () => { + try { + // FIXME: this should be handle through backstage API or something. + handleSSORedirect(); + window.addEventListener('message', receiveMessage, false); + } catch (error) { + errorApi.post(error); + } + }; + + return ( + + + Sign In + + } + > + Sign In using SAML lolololol + + + ); +}; + +const loader: ProviderLoader = async () => { + // FIXME: should get the profile from identiy API not from storage session. + const sessionRaw = window.sessionStorage.getItem('helixSession'); + + if (!sessionRaw) { + return undefined; + } + + const session = JSON.parse(sessionRaw); + + return { + userId: session.backstageIdentity, + profile: { + email: session.profile.email, + displayName: session.profile.displayName, + }, + }; +}; + +export const samlProvider: SignInProvider = { Component, loader }; diff --git a/packages/core/src/layout/SignInPage/types.ts b/packages/core/src/layout/SignInPage/types.ts index 9c501e8095..74463f93b4 100644 --- a/packages/core/src/layout/SignInPage/types.ts +++ b/packages/core/src/layout/SignInPage/types.ts @@ -24,18 +24,19 @@ import { ProfileInfoApi, BackstageIdentityApi, SessionStateApi, + SamlApi, } from '@backstage/core-api'; export type SignInConfig = { id: string; title: string; message: string; - apiRef: ApiRef< - OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionStateApi - >; + apiRef: + | ApiRef + | ApiRef; }; -export type IdentityProviders = ('guest' | 'custom' | SignInConfig)[]; +export type IdentityProviders = ('guest' | 'custom' | 'saml' | SignInConfig)[]; export type ProviderComponent = ComponentType< SignInPageProps & { config: SignInConfig } diff --git a/plugins/auth-backend/src/providers/factories.ts b/plugins/auth-backend/src/providers/factories.ts index 701eef81af..3311bfc836 100644 --- a/plugins/auth-backend/src/providers/factories.ts +++ b/plugins/auth-backend/src/providers/factories.ts @@ -62,6 +62,7 @@ export const createAuthProviderRouter = ( for (const env of envs) { const envConfig = providerConfig.getConfig(env); + console.log(envConfig); const provider = factory(globalConfig, env, envConfig, logger, issuer); if (provider) { envProviders[env] = provider; diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index 1ccee1a9cb..fe6bb87b6d 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -55,8 +55,21 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { // for non-oauth auth flows. // TODO: This flow doesn't issue an identity token that can be used to validate // the identity of the user in other backends, which we need in some form. + console.log('===>'); + console.log('===>'); + console.log('===>'); + console.log('===>'); + console.log('===>'); + console.log('===>'); + console.log('===>'); + console.log('===>'); + console.log('===>'); + console.log('===>'); + console.log('===>'); + console.log('===> SamlAuthProvider constructor'); + console.log(profile); done(undefined, { - userId: profile.ID!, + userId: profile.nameID!, profile: { email: profile.email!, displayName: profile.displayName as string, @@ -76,9 +89,9 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { ): Promise { try { const { - response: { userId, profile }, + response: { userId, profile }, } = await executeFrameHandlerStrategy(req, this.strategy); - + const id = userId; const idToken = await this.tokenIssuer.issueToken({ claims: { sub: id }, @@ -107,8 +120,14 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { res.send('noop'); } - identifyEnv(): string | undefined { - return undefined; + identifyEnv(req: express.Request): string | undefined { + const reqEnv = req.query.env?.toString(); + if (reqEnv) { + return reqEnv; + } + + // FIXME : do we want to always to return 'development' ? + return 'development'; } } diff --git a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts index 0224dc7f1d..2b446c9cd6 100644 --- a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts +++ b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts @@ -116,14 +116,14 @@ export class HigherOrderOperations implements HigherOrderOperation { * Entities are read from their respective sources, are parsed and validated * according to the entity policy, and get inserted or updated in the catalog. * Entities that have disappeared from their location are left orphaned, - * without changes. + * without changes.i */ async refreshAllLocations(): Promise { const startTimestamp = new Date().valueOf(); - this.logger.info('Beginning locations refresh'); + // this.logger.info('Beginning locations refresh'); const locations = await this.locationsCatalog.locations(); - this.logger.info(`Visiting ${locations.length} locations`); + // this.logger.info(`Visiting ${locations.length} locations`); for (const { data: location } of locations) { this.logger.debug( diff --git a/yarn.lock b/yarn.lock index 0354b1844f..f873b2cdaf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9848,9 +9848,8 @@ file-loader@^4.2.0: loader-utils "^1.2.3" schema-utils "^2.5.0" -"file-saver@github:eligrey/FileSaver.js#1.3.8": +file-saver@eligrey/FileSaver.js#1.3.8: version "1.3.8" - uid e865e37af9f9947ddcced76b549e27dc45c1cb2e resolved "https://codeload.github.com/eligrey/FileSaver.js/tar.gz/e865e37af9f9947ddcced76b549e27dc45c1cb2e" file-system-cache@^1.0.5: @@ -12977,7 +12976,7 @@ jspdf@1.5.3: integrity sha512-J9X76xnncMw+wIqb15HeWfPMqPwYxSpPY8yWPJ7rAZN/ZDzFkjCSZObryCyUe8zbrVRNiuCnIeQteCzMn7GnWw== dependencies: canvg "1.5.3" - file-saver "github:eligrey/FileSaver.js#1.3.8" + file-saver eligrey/FileSaver.js#1.3.8 html2canvas "1.0.0-alpha.12" omggif "1.0.7" promise-polyfill "8.1.0" From 667b42cfb7dbaffbb9488512b4a79847f567d773 Mon Sep 17 00:00:00 2001 From: "J Shamsul Bahri (jibone))" Date: Fri, 4 Sep 2020 16:18:32 +0800 Subject: [PATCH 14/85] Remove console.log debug stuff. --- packages/app/src/App.tsx | 2 +- .../core-api/src/apis/definitions/auth.ts | 3 + .../implementations/auth/saml/SamlAuth.ts | 32 ++----- packages/core-api/src/app/AppIdentity.ts | 4 - .../lib/AuthConnector/SamlAuthConnector.ts | 21 ++--- .../SamlAuthSessionManager.ts | 11 +-- .../SamlAuthSessionStore.ts | 2 - .../src/layout/SignInPage/commonProvider.tsx | 5 - .../core/src/layout/SignInPage/providers.tsx | 2 - .../src/layout/SignInPage/samlProvider.tsx | 92 ------------------- packages/core/src/layout/SignInPage/types.ts | 8 +- .../src/providers/saml/provider.ts | 11 --- 12 files changed, 27 insertions(+), 166 deletions(-) delete mode 100644 packages/core/src/layout/SignInPage/samlProvider.tsx diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 29e371b469..6ff4da0fe3 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -35,7 +35,7 @@ const app = createApp({ return ( ); }, diff --git a/packages/core-api/src/apis/definitions/auth.ts b/packages/core-api/src/apis/definitions/auth.ts index 6dfe9595b0..8f0a4b6595 100644 --- a/packages/core-api/src/apis/definitions/auth.ts +++ b/packages/core-api/src/apis/definitions/auth.ts @@ -108,6 +108,9 @@ export type SamlApi = { getProfile(options?: AuthRequestOptions): Promise; + // FIXME: is this needed? + getAccessToken(options?: AuthRequestOptions): Promise; + // Not sure if this is needed. logout(): Promise; }; diff --git a/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts b/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts index 75aefda7f5..cb93dcea97 100644 --- a/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts @@ -35,9 +35,6 @@ import { type CreateOptions = { apiOrigin: string; basePath: string; - - // oauthRequestApi?: OAuthRequestApi; - environment?: string; provider?: AuthProvider & { id: string }; }; @@ -61,9 +58,6 @@ class SamlAuth implements SamlApi { environment = 'development', provider = DEFAULT_PROVIDER, }: CreateOptions) { - // eslint-disable-next-line no-console - console.log('this is from SamlAuth'); - const connector = new SamlAuthConnector({ apiOrigin, basePath, @@ -87,37 +81,27 @@ class SamlAuth implements SamlApi { sessionState$(): Observable { return this.sessionManager.sessionState$(); } - // constructor(private readonly sessionManager: any) {} - // eslint-disable-next-line @typescript-eslint/no-useless-constructor - constructor(private readonly sessionManager: SessionManager) { - // eslint-disable-next-line no-console - console.log('this is the constructor'); - } + + constructor(private readonly sessionManager: SessionManager) {} async getBackstageIdentity( options: AuthRequestOptions, ): Promise { - // eslint-disable-next-line no-console - console.log('===> Saml getBackstageIdentity()'); const session = await this.sessionManager.getSession(options); - // eslint-disable-next-line no-console - console.log('this this thish lkkdfkkfkfji'); - // eslint-disable-next-line no-console - console.log(session); return session?.backstageIdentity; } async getProfile(options: AuthRequestOptions = {}) { - // eslint-disable-next-line no-console - console.log('==> samlauth getprofile()'); const session = await this.sessionManager.getSession(options); - // eslint-disable-next-line no-console - console.log('+++ this is the session from getProfile()'); - // eslint-disable-next-line no-console - console.log(session); return session?.profile; } + // FIXME: Is this needed?... + async getAccessToken(options: AuthRequestOptions) { + const session = await this.sessionManager.getSession(options); + return session?.userId ?? ''; + } + async logout() { await this.sessionManager.removeSession(); } diff --git a/packages/core-api/src/app/AppIdentity.ts b/packages/core-api/src/app/AppIdentity.ts index 1a6cdac3e6..69ee5d28ac 100644 --- a/packages/core-api/src/app/AppIdentity.ts +++ b/packages/core-api/src/app/AppIdentity.ts @@ -67,10 +67,6 @@ export class AppIdentity implements IdentityApi { // This is indirectly called by the sign-in page to continue into the app. setSignInResult(result: SignInResult) { - // eslint-disable-next-line no-console - console.log('this is from setSignInResult'); - // eslint-disable-next-line no-console - console.log(result); if (this.hasIdentity) { return; } diff --git a/packages/core-api/src/lib/AuthConnector/SamlAuthConnector.ts b/packages/core-api/src/lib/AuthConnector/SamlAuthConnector.ts index 9dc0fca0a0..4a6babe224 100644 --- a/packages/core-api/src/lib/AuthConnector/SamlAuthConnector.ts +++ b/packages/core-api/src/lib/AuthConnector/SamlAuthConnector.ts @@ -36,7 +36,8 @@ export type SamlResponse = { backstageIdentity: BackstageIdentity; }; -export class SamlAuthConnector implements AuthConnector { +export class SamlAuthConnector + implements AuthConnector { private readonly apiOrigin: string; private readonly basePath: string; private readonly environment: string | undefined; @@ -57,35 +58,25 @@ export class SamlAuthConnector implements AuthConnector { - // eslint-disable-next-line no-console - console.log('==> from SamlAuthConnector createSession'); - const payload = await showLoginPopup({ - url: 'http://localhost:7000/auth/saml/start', // FIXME: this should be from app.config or somewhere + url: 'http://localhost:7000/auth/saml/start', // FIXME: remove hardcoded here. should do something like buildUrl name: 'SAML Login', // FIXME: change this to provider name? and not hardcode the name origin: this.apiOrigin, width: 450, height: 730, }); - // eslint-disable-next-line no-console - console.log(payload); - return { ...payload, id: payload.profile.email, }; } - // TODO: do we need this for SAML? - async refreshSession(): Promise { - // eslint-disable-next-line no-console - console.log('==> this is refresh session'); - } + // FIXME: do we need this for SAML? + async refreshSession(): Promise {} async removeSession(): Promise { - // eslint-disable-next-line no-console - console.log('this removes the session'); + // FIXME: remove hardcoded url here... should do something like buildUrl const res = await fetch('http://localhost:7000/auth/saml/logout', { method: 'POST', headers: { diff --git a/packages/core-api/src/lib/AuthSessionManager/SamlAuthSessionManager.ts b/packages/core-api/src/lib/AuthSessionManager/SamlAuthSessionManager.ts index 9439684cf4..8a21bc05b9 100644 --- a/packages/core-api/src/lib/AuthSessionManager/SamlAuthSessionManager.ts +++ b/packages/core-api/src/lib/AuthSessionManager/SamlAuthSessionManager.ts @@ -15,10 +15,12 @@ */ import { SessionManager, GetSessionOptions } from './types'; -import { SamlAuthConnector, SamlResponse } from '../AuthConnector/SamlAuthConnector'; +import { + SamlAuthConnector, + SamlResponse, +} from '../AuthConnector/SamlAuthConnector'; import { SessionStateTracker } from './SessionStateTracker'; - type Options = { connector: SamlAuthConnector; }; @@ -27,18 +29,15 @@ export class SamlAuthSessionManager implements SessionManager { private readonly connector: SamlAuthConnector; private readonly stateTracker = new SessionStateTracker(); - private currentSession: any | undefined; // FIXME: proper typing here + private currentSession: any | undefined; // FIXME: proper typing here? constructor(options: Options) { const { connector } = options; this.connector = connector; - // this.helper = new SessionScopeHelper } async getSession(options: GetSessionOptions): Promise { - // eslint-disable-next-line no-console - console.log('==> this is from SamlAuthSessionManager getSession()'); if (this.currentSession) { return this.currentSession; } diff --git a/packages/core-api/src/lib/AuthSessionManager/SamlAuthSessionStore.ts b/packages/core-api/src/lib/AuthSessionManager/SamlAuthSessionStore.ts index 53fba09b54..3afb07c4df 100644 --- a/packages/core-api/src/lib/AuthSessionManager/SamlAuthSessionStore.ts +++ b/packages/core-api/src/lib/AuthSessionManager/SamlAuthSessionStore.ts @@ -33,8 +33,6 @@ export class SamlAuthSessionStore implements SamlAuthSessionManager { } async getSession(options: GetSessionOptions): Promise { - // eslint-disable-next-line no-console - console.log('==>> this is from SamlAuthSessionStore getSession()'); const session = this.loadSession(); if (session) { diff --git a/packages/core/src/layout/SignInPage/commonProvider.tsx b/packages/core/src/layout/SignInPage/commonProvider.tsx index 88a53ec358..f9611342a2 100644 --- a/packages/core/src/layout/SignInPage/commonProvider.tsx +++ b/packages/core/src/layout/SignInPage/commonProvider.tsx @@ -36,11 +36,6 @@ const Component: ProviderComponent = ({ config, onResult }) => { instantPopup: true, }); - // eslint-disable-next-line no-console - console.log('====++++++++++++++++++++++++++> handleLogin for commonProvider'); - // eslint-disable-next-line no-console - console.log(identity); - const profile = await authApi.getProfile(); onResult({ userId: identity!.id, diff --git a/packages/core/src/layout/SignInPage/providers.tsx b/packages/core/src/layout/SignInPage/providers.tsx index 436078e49c..ff60eba228 100644 --- a/packages/core/src/layout/SignInPage/providers.tsx +++ b/packages/core/src/layout/SignInPage/providers.tsx @@ -26,7 +26,6 @@ import { SignInConfig, IdentityProviders, SignInProvider } from './types'; import { commonProvider } from './commonProvider'; import { guestProvider } from './guestProvider'; import { customProvider } from './customProvider'; -import { samlProvider } from './samlProvider'; const PROVIDER_STORAGE_KEY = '@backstage/core:SignInPage:provider'; @@ -42,7 +41,6 @@ const signInProviders: { [key: string]: SignInProvider } = { guest: guestProvider, custom: customProvider, common: commonProvider, - saml: samlProvider, }; function validateIDs(id: string, providers: SignInProviderType): void { diff --git a/packages/core/src/layout/SignInPage/samlProvider.tsx b/packages/core/src/layout/SignInPage/samlProvider.tsx deleted file mode 100644 index e9673bca8c..0000000000 --- a/packages/core/src/layout/SignInPage/samlProvider.tsx +++ /dev/null @@ -1,92 +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 React from 'react'; -import { Grid, Typography, Button } from '@material-ui/core'; -import { ProviderComponent, ProviderLoader, SignInProvider } from './types'; -import { useApi, errorApiRef /* samlAuthApiRef */ } from '@backstage/core-api'; -import { InfoCard } from '../InfoCard/InfoCard'; - -const Component: ProviderComponent = ({ onResult }) => { - // const samlApi = useApi(samlAuthApiRef); - const errorApi = useApi(errorApiRef); - - const handleSSORedirect = () => { - // FIXME: this shoudl be from identity API or something. - window.open('http://localhost:7000/auth/saml/start'); - }; - - const receiveMessage = (event: any) => { - // FIXME: Should use the backstage identity API and not create this session storage. - window.sessionStorage.setItem( - 'helixSession', - JSON.stringify(event.data.response), - ); - - onResult({ - userId: event.data.response.backstageIdentity, - profile: { - email: event.data.response.profile.email, - displayName: event.data.response.profile.displayName, - }, - }); - }; - - const handleLogin = async () => { - try { - // FIXME: this should be handle through backstage API or something. - handleSSORedirect(); - window.addEventListener('message', receiveMessage, false); - } catch (error) { - errorApi.post(error); - } - }; - - return ( - - - Sign In - - } - > - Sign In using SAML lolololol - - - ); -}; - -const loader: ProviderLoader = async () => { - // FIXME: should get the profile from identiy API not from storage session. - const sessionRaw = window.sessionStorage.getItem('helixSession'); - - if (!sessionRaw) { - return undefined; - } - - const session = JSON.parse(sessionRaw); - - return { - userId: session.backstageIdentity, - profile: { - email: session.profile.email, - displayName: session.profile.displayName, - }, - }; -}; - -export const samlProvider: SignInProvider = { Component, loader }; diff --git a/packages/core/src/layout/SignInPage/types.ts b/packages/core/src/layout/SignInPage/types.ts index 74463f93b4..513fe9f674 100644 --- a/packages/core/src/layout/SignInPage/types.ts +++ b/packages/core/src/layout/SignInPage/types.ts @@ -36,7 +36,7 @@ export type SignInConfig = { | ApiRef; }; -export type IdentityProviders = ('guest' | 'custom' | 'saml' | SignInConfig)[]; +export type IdentityProviders = ('guest' | 'custom' | SignInConfig)[]; export type ProviderComponent = ComponentType< SignInPageProps & { config: SignInConfig } @@ -44,9 +44,9 @@ export type ProviderComponent = ComponentType< export type ProviderLoader = ( apis: ApiHolder, - apiRef: ApiRef< - OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionStateApi - >, + apiRef: + | ApiRef + | ApiRef, ) => Promise; export type SignInProvider = { diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index fe6bb87b6d..f790228107 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -55,17 +55,6 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { // for non-oauth auth flows. // TODO: This flow doesn't issue an identity token that can be used to validate // the identity of the user in other backends, which we need in some form. - console.log('===>'); - console.log('===>'); - console.log('===>'); - console.log('===>'); - console.log('===>'); - console.log('===>'); - console.log('===>'); - console.log('===>'); - console.log('===>'); - console.log('===>'); - console.log('===>'); console.log('===> SamlAuthProvider constructor'); console.log(profile); done(undefined, { From 66b816cdf00153e215515efd140174dc4de32164 Mon Sep 17 00:00:00 2001 From: "J Shamsul Bahri (jibone))" Date: Fri, 4 Sep 2020 18:11:02 +0800 Subject: [PATCH 15/85] clean up config --- app-config.yaml | 134 ++++++++++++++++++++++++------------------------ 1 file changed, 66 insertions(+), 68 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index e65df93969..d6c745a6c2 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -28,74 +28,72 @@ sentry: auth: providers: - # google: - # development: - # appOrigin: 'http://localhost:3000/' - # secure: false - # clientId: - # $secret: - # env: AUTH_GOOGLE_CLIENT_ID - # clientSecret: - # $secret: - # env: AUTH_GOOGLE_CLIENT_SECRET - # github: - # development: - # appOrigin: 'http://localhost:3000/' - # secure: false - # clientId: - # $secret: - # env: AUTH_GITHUB_CLIENT_ID - # clientSecret: - # $secret: - # env: AUTH_GITHUB_CLIENT_SECRET - # enterpriseInstanceUrl: - # $secret: - # env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL - # gitlab: - # development: - # appOrigin: 'http://localhost:3000/' - # secure: false - # clientId: - # $secret: - # env: AUTH_GITLAB_CLIENT_ID - # clientSecret: - # $secret: - # env: AUTH_GITLAB_CLIENT_SECRET - # audience: - # $secret: - # env: GITLAB_BASE_URL + google: + development: + appOrigin: 'http://localhost:3000/' + secure: false + clientId: + $secret: + env: AUTH_GOOGLE_CLIENT_ID + clientSecret: + $secret: + env: AUTH_GOOGLE_CLIENT_SECRET + github: + development: + appOrigin: 'http://localhost:3000/' + secure: false + clientId: + $secret: + env: AUTH_GITHUB_CLIENT_ID + clientSecret: + $secret: + env: AUTH_GITHUB_CLIENT_SECRET + enterpriseInstanceUrl: + $secret: + env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL + gitlab: + development: + appOrigin: 'http://localhost:3000/' + secure: false + clientId: + $secret: + env: AUTH_GITLAB_CLIENT_ID + clientSecret: + $secret: + env: AUTH_GITLAB_CLIENT_SECRET + audience: + $secret: + env: GITLAB_BASE_URL saml: development: - entryPoint: 'https://sso.jumpcloud.com/saml2/backstage-test' + entryPoint: 'http://localhost:7001/' issuer: 'passport-saml' - # entryPoint: 'http://localhost:7001/' - # issuer: 'passport-saml' - # okta - # development: - # appOrigin: 'http://localhost:3000/' - # secure: false - # clientId: - # $secret: - # env: AUTH_OKTA_CLIENT_ID - # clientSecret: - # $secret: - # env: AUTH_OKTA_CLIENT_SECRET - # audience: - # $secret: - # env: AUTH_OKTA_AUDIENCE - # oauth2: - # development: - # appOrigin: 'http://localhost:3000/' - # secure: false - # clientId: - # $secret: - # env: AUTH_OAUTH2_CLIENT_ID - # clientSecret: - # $secret: - # env: AUTH_OAUTH2_CLIENT_SECRET - # authorizationURL: - # $secret: - # env: AUTH_OAUTH2_AUTH_URL - # tokenURL: - # $secret: - # env: AUTH_OAUTH2_TOKEN_URL + okta: + development: + appOrigin: 'http://localhost:3000/' + secure: false + clientId: + $secret: + env: AUTH_OKTA_CLIENT_ID + clientSecret: + $secret: + env: AUTH_OKTA_CLIENT_SECRET + audience: + $secret: + env: AUTH_OKTA_AUDIENCE + oauth2: + development: + appOrigin: 'http://localhost:3000/' + secure: false + clientId: + $secret: + env: AUTH_OAUTH2_CLIENT_ID + clientSecret: + $secret: + env: AUTH_OAUTH2_CLIENT_SECRET + authorizationURL: + $secret: + env: AUTH_OAUTH2_AUTH_URL + tokenURL: + $secret: + env: AUTH_OAUTH2_TOKEN_URL From 1cd640dc16c1169e85813fb8e041d271105de100 Mon Sep 17 00:00:00 2001 From: "J Shamsul Bahri (jibone))" Date: Fri, 4 Sep 2020 18:33:13 +0800 Subject: [PATCH 16/85] fix merge conflicts --- app-config.yaml | 34 ++----------------- packages/app/src/App.tsx | 3 -- packages/app/src/identityProviders.ts | 3 -- .../src/apis/implementations/auth/index.ts | 3 -- .../auth-backend/src/providers/factories.ts | 22 ------------ 5 files changed, 2 insertions(+), 63 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index 7eb6023f61..2620f6b9b7 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -92,11 +92,6 @@ auth: providers: google: development: -<<<<<<< HEAD - appOrigin: 'http://localhost:3000/' - secure: false -======= ->>>>>>> master clientId: $secret: env: AUTH_GOOGLE_CLIENT_ID @@ -105,11 +100,6 @@ auth: env: AUTH_GOOGLE_CLIENT_SECRET github: development: -<<<<<<< HEAD - appOrigin: 'http://localhost:3000/' - secure: false -======= ->>>>>>> master clientId: $secret: env: AUTH_GITHUB_CLIENT_ID @@ -121,11 +111,6 @@ auth: env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL gitlab: development: -<<<<<<< HEAD - appOrigin: 'http://localhost:3000/' - secure: false -======= ->>>>>>> master clientId: $secret: env: AUTH_GITLAB_CLIENT_ID @@ -136,20 +121,10 @@ auth: $secret: env: GITLAB_BASE_URL saml: -<<<<<<< HEAD - development: - entryPoint: 'http://localhost:7001/' - issuer: 'passport-saml' + entryPoint: 'http://localhost:7001/' + issuer: 'passport-saml' okta: development: - appOrigin: 'http://localhost:3000/' - secure: false -======= - entryPoint: "http://localhost:7001/" - issuer: "passport-saml" - okta: - development: ->>>>>>> master clientId: $secret: env: AUTH_OKTA_CLIENT_ID @@ -161,11 +136,6 @@ auth: env: AUTH_OKTA_AUDIENCE oauth2: development: -<<<<<<< HEAD - appOrigin: 'http://localhost:3000/' - secure: false -======= ->>>>>>> master clientId: $secret: env: AUTH_OAUTH2_CLIENT_ID diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 53f7d93d3c..cb38726716 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -40,11 +40,8 @@ const app = createApp({ >>>>>> master /> ); }, diff --git a/packages/app/src/identityProviders.ts b/packages/app/src/identityProviders.ts index 767229eed4..388bc8d8b6 100644 --- a/packages/app/src/identityProviders.ts +++ b/packages/app/src/identityProviders.ts @@ -19,11 +19,8 @@ import { gitlabAuthApiRef, oktaAuthApiRef, githubAuthApiRef, -<<<<<<< HEAD samlAuthApiRef, -======= microsoftAuthApiRef, ->>>>>>> master } from '@backstage/core'; export const providers = [ diff --git a/packages/core-api/src/apis/implementations/auth/index.ts b/packages/core-api/src/apis/implementations/auth/index.ts index 9c6b8ff565..39223e8e15 100644 --- a/packages/core-api/src/apis/implementations/auth/index.ts +++ b/packages/core-api/src/apis/implementations/auth/index.ts @@ -19,9 +19,6 @@ export * from './gitlab'; export * from './google'; export * from './oauth2'; export * from './okta'; -<<<<<<< HEAD export * from './saml'; -======= export * from './auth0'; export * from './microsoft'; ->>>>>>> master diff --git a/plugins/auth-backend/src/providers/factories.ts b/plugins/auth-backend/src/providers/factories.ts index 948cc3eb8f..6c83dffda3 100644 --- a/plugins/auth-backend/src/providers/factories.ts +++ b/plugins/auth-backend/src/providers/factories.ts @@ -53,29 +53,7 @@ export const createAuthProviderRouter = ( const router = Router(); -<<<<<<< HEAD - for (const env of envs) { - const envConfig = providerConfig.getConfig(env); - console.log(envConfig); - const provider = factory(globalConfig, env, envConfig, logger, issuer); - if (provider) { - envProviders[env] = provider; - envIdentifier = provider.identifyEnv; - } - } - - if (typeof envIdentifier === 'undefined') { - throw Error(`No envIdentifier provided for '${providerId}'`); - } - - const handler = new EnvironmentHandler( - providerId, - envProviders, - envIdentifier, - ); -======= const handler = factory({ globalConfig, config, logger, tokenIssuer }); ->>>>>>> master router.get('/start', handler.start.bind(handler)); router.get('/handler/frame', handler.frameHandler.bind(handler)); From 41edd22db2dccdf99a8e4c2e25a03f4a29229256 Mon Sep 17 00:00:00 2001 From: "J Shamsul Bahri (jibone))" Date: Fri, 4 Sep 2020 18:44:30 +0800 Subject: [PATCH 17/85] Remove commented out log code. --- .../catalog-backend/src/ingestion/HigherOrderOperations.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts index 2b446c9cd6..0224dc7f1d 100644 --- a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts +++ b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts @@ -116,14 +116,14 @@ export class HigherOrderOperations implements HigherOrderOperation { * Entities are read from their respective sources, are parsed and validated * according to the entity policy, and get inserted or updated in the catalog. * Entities that have disappeared from their location are left orphaned, - * without changes.i + * without changes. */ async refreshAllLocations(): Promise { const startTimestamp = new Date().valueOf(); - // this.logger.info('Beginning locations refresh'); + this.logger.info('Beginning locations refresh'); const locations = await this.locationsCatalog.locations(); - // this.logger.info(`Visiting ${locations.length} locations`); + this.logger.info(`Visiting ${locations.length} locations`); for (const { data: location } of locations) { this.logger.debug( From cc2afdb9e327911bbd0d749274e104b087543abd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Sep 2020 04:19:00 +0000 Subject: [PATCH 18/85] chore(deps): bump @types/webpack-env from 1.15.2 to 1.15.3 Bumps [@types/webpack-env](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/webpack-env) from 1.15.2 to 1.15.3. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/webpack-env) Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 449148e487..a59bbf3ac2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4865,9 +4865,9 @@ "@types/webpack" "*" "@types/webpack-env@^1.15.2": - version "1.15.2" - resolved "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.15.2.tgz#927997342bb9f4a5185a86e6579a0a18afc33b0a" - integrity sha512-67ZgZpAlhIICIdfQrB5fnDvaKFcDxpKibxznfYRVAT4mQE41Dido/3Ty+E3xGBmTogc5+0Qb8tWhna+5B8z1iQ== + version "1.15.3" + resolved "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.15.3.tgz#fb602cd4c2f0b7c0fb857e922075fdf677d25d84" + integrity sha512-5oiXqR7kwDGZ6+gmzIO2lTC+QsriNuQXZDWNYRV3l2XRN/zmPgnC21DLSx2D05zvD8vnXW6qUg7JnXZ4I6qLVQ== "@types/webpack-node-externals@^2.5.0": version "2.5.0" From 0d07847859f1c6e664578ede645679b19a862be2 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 21 Sep 2020 00:46:02 +0200 Subject: [PATCH 19/85] feat(cli): adding the backend flag to the cli --- packages/cli/src/commands/create-plugin/createPlugin.ts | 9 ++++++--- packages/cli/src/commands/index.ts | 4 ++++ .../templates/default-backend-plugin/package.json.hbs | 6 +++--- .../default-backend-plugin/scripts/mock-data.sh | 2 -- .../cli/templates/default-backend-plugin/src/index.ts | 1 - .../templates/default-backend-plugin/src/setupTests.ts | 1 - 6 files changed, 13 insertions(+), 10 deletions(-) delete mode 100755 packages/cli/templates/default-backend-plugin/scripts/mock-data.sh diff --git a/packages/cli/src/commands/create-plugin/createPlugin.ts b/packages/cli/src/commands/create-plugin/createPlugin.ts index 5e6882a4a0..8f75728544 100644 --- a/packages/cli/src/commands/create-plugin/createPlugin.ts +++ b/packages/cli/src/commands/create-plugin/createPlugin.ts @@ -175,7 +175,7 @@ export async function movePlugin( }); } -export default async () => { +export default async ({ backend }: { backend: boolean }) => { const codeownersPath = await getCodeownersFilePath(paths.targetRoot); const questions: Question[] = [ @@ -223,7 +223,9 @@ export default async () => { const answers: Answers = await inquirer.prompt(questions); const appPackage = paths.resolveTargetRoot('packages/app'); - const templateDir = paths.resolveOwn('templates/default-plugin'); + const templateDir = paths.resolveOwn( + backend ? 'templates/default-backend-plugin' : 'templates/default-plugin', + ); const tempDir = resolvePath(os.tmpdir(), answers.id); const pluginDir = paths.resolveTargetRoot('plugins', answers.id); const ownerIds = parseOwnerIds(answers.owner); @@ -240,6 +242,7 @@ export default async () => { await createTemporaryPluginFolder(tempDir); Task.section('Preparing files'); + await templatingTask(templateDir, tempDir, { ...answers, version, @@ -252,7 +255,7 @@ export default async () => { Task.section('Building the plugin'); await buildPlugin(pluginDir); - if (await fs.pathExists(appPackage)) { + if ((await fs.pathExists(appPackage)) && !backend) { Task.section('Adding plugin as dependency in app'); await addPluginDependencyToApp(paths.targetRoot, answers.id, version); diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 4e031cdbc1..a0b898db75 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -61,6 +61,10 @@ export function registerCommands(program: CommanderStatic) { program .command('create-plugin') + .option( + '--backend', + 'Create plugin with the backend dependencies as default', + ) .description('Creates a new plugin in the current repository') .action( lazy(() => import('./create-plugin/createPlugin').then(m => m.default)), diff --git a/packages/cli/templates/default-backend-plugin/package.json.hbs b/packages/cli/templates/default-backend-plugin/package.json.hbs index 1cf9ab1c5b..8a1d5a9522 100644 --- a/packages/cli/templates/default-backend-plugin/package.json.hbs +++ b/packages/cli/templates/default-backend-plugin/package.json.hbs @@ -17,16 +17,16 @@ "test": "backstage-cli test", "prepack": "backstage-cli prepack", "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean", - "mock-data": "./scripts/mock-data.sh" + "clean": "backstage-cli clean" }, "dependencies": { "@backstage/backend-common": "^{{version}}", + "@backstage/config": "^{{version}}", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^3.0.3", - "whatwg-fetch": "^2.0.0", "winston": "^3.2.1", + "node-fetch": "^2.6.0", "yn": "^4.0.0" }, "devDependencies": { diff --git a/packages/cli/templates/default-backend-plugin/scripts/mock-data.sh b/packages/cli/templates/default-backend-plugin/scripts/mock-data.sh deleted file mode 100755 index ff30921715..0000000000 --- a/packages/cli/templates/default-backend-plugin/scripts/mock-data.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env bash -echo "use this script to load your service with some mock data if needed!" diff --git a/packages/cli/templates/default-backend-plugin/src/index.ts b/packages/cli/templates/default-backend-plugin/src/index.ts index 7332f92d68..7612c392a2 100644 --- a/packages/cli/templates/default-backend-plugin/src/index.ts +++ b/packages/cli/templates/default-backend-plugin/src/index.ts @@ -14,5 +14,4 @@ * limitations under the License. */ -require('whatwg-fetch'); export * from './service/router'; diff --git a/packages/cli/templates/default-backend-plugin/src/setupTests.ts b/packages/cli/templates/default-backend-plugin/src/setupTests.ts index ac41ef995a..ba33cf996b 100644 --- a/packages/cli/templates/default-backend-plugin/src/setupTests.ts +++ b/packages/cli/templates/default-backend-plugin/src/setupTests.ts @@ -13,6 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -require('whatwg-fetch'); export {}; From 8953211c5bc77711058654dd7395f613a14dad64 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 21 Sep 2020 01:08:04 +0200 Subject: [PATCH 20/85] chore(cli): fixed the failing tests --- packages/app/package.json | 1 + packages/app/src/plugins.ts | 1 + packages/cli/src/commands/create-plugin/createPlugin.ts | 1 + .../ExampleComponent/ExampleComponent.test.tsx.hbs | 9 ++++----- .../ExampleFetchComponent.test.tsx.hbs | 2 -- packages/cli/templates/default-plugin/src/setupTests.ts | 2 -- 6 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/app/package.json b/packages/app/package.json index cb61e1ed8c..84a8dd361c 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -8,6 +8,7 @@ "@backstage/cli": "^0.1.1-alpha.22", "@backstage/core": "^0.1.1-alpha.22", "@backstage/plugin-api-docs": "^0.1.1-alpha.22", + "@backstage/plugin-asd": "^0.1.1-alpha.22", "@backstage/plugin-catalog": "^0.1.1-alpha.22", "@backstage/plugin-circleci": "^0.1.1-alpha.22", "@backstage/plugin-explore": "^0.1.1-alpha.22", diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts index b36d0323e0..f7a3834bfc 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -33,3 +33,4 @@ export { plugin as Jenkins } from '@backstage/plugin-jenkins'; export { plugin as ApiDocs } from '@backstage/plugin-api-docs'; export { plugin as GithubPullRequests } from '@roadiehq/backstage-plugin-github-pull-requests'; export { plugin as GcpProjects } from '@backstage/plugin-gcp-projects'; +export { plugin as Asd } from '@backstage/plugin-asd'; diff --git a/packages/cli/src/commands/create-plugin/createPlugin.ts b/packages/cli/src/commands/create-plugin/createPlugin.ts index 8f75728544..b42edc99ec 100644 --- a/packages/cli/src/commands/create-plugin/createPlugin.ts +++ b/packages/cli/src/commands/create-plugin/createPlugin.ts @@ -176,6 +176,7 @@ export async function movePlugin( } export default async ({ backend }: { backend: boolean }) => { + console.warn(backend); const codeownersPath = await getCodeownersFilePath(paths.targetRoot); const questions: Question[] = [ diff --git a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs index 37759313ee..4d52fa82b5 100644 --- a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs +++ b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs @@ -1,18 +1,17 @@ import React from 'react'; import { render } from '@testing-library/react'; -import mockFetch from 'jest-fetch-mock'; import ExampleComponent from './ExampleComponent'; import { ThemeProvider } from '@material-ui/core'; import { lightTheme } from '@backstage/theme'; describe('ExampleComponent', () => { it('should render', () => { - mockFetch.mockResponse(() => new Promise(() => {})); const rendered = render( - + , - ); - expect(rendered.getByText('Welcome to {{ id }}!')).toBeInTheDocument(); + ); + expect(rendered.getByText('Welcome to {{ id }}!')).toBeInTheDocument(); }); }); + \ No newline at end of file diff --git a/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs b/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs index cc61c215cd..aad4fac10a 100644 --- a/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs +++ b/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs @@ -1,11 +1,9 @@ import React from 'react'; import { render } from '@testing-library/react'; -import mockFetch from 'jest-fetch-mock'; import ExampleFetchComponent from './ExampleFetchComponent'; describe('ExampleFetchComponent', () => { it('should render', async () => { - mockFetch.mockResponse(() => new Promise(() => {})); const rendered = render(); expect(await rendered.findByTestId('progress')).toBeInTheDocument(); }); diff --git a/packages/cli/templates/default-plugin/src/setupTests.ts b/packages/cli/templates/default-plugin/src/setupTests.ts index 3fa8703ac4..7b0828bfa8 100644 --- a/packages/cli/templates/default-plugin/src/setupTests.ts +++ b/packages/cli/templates/default-plugin/src/setupTests.ts @@ -1,3 +1 @@ import '@testing-library/jest-dom'; - -require('jest-fetch-mock').enableMocks(); From 9f8f07d483e54b5c2d1c85775ce795bcbf654f3d Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 21 Sep 2020 01:13:57 +0200 Subject: [PATCH 21/85] chore(cli): fixing issues --- packages/app/package.json | 1 - packages/app/src/plugins.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/app/package.json b/packages/app/package.json index 84a8dd361c..cb61e1ed8c 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -8,7 +8,6 @@ "@backstage/cli": "^0.1.1-alpha.22", "@backstage/core": "^0.1.1-alpha.22", "@backstage/plugin-api-docs": "^0.1.1-alpha.22", - "@backstage/plugin-asd": "^0.1.1-alpha.22", "@backstage/plugin-catalog": "^0.1.1-alpha.22", "@backstage/plugin-circleci": "^0.1.1-alpha.22", "@backstage/plugin-explore": "^0.1.1-alpha.22", diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts index f7a3834bfc..b36d0323e0 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -33,4 +33,3 @@ export { plugin as Jenkins } from '@backstage/plugin-jenkins'; export { plugin as ApiDocs } from '@backstage/plugin-api-docs'; export { plugin as GithubPullRequests } from '@roadiehq/backstage-plugin-github-pull-requests'; export { plugin as GcpProjects } from '@backstage/plugin-gcp-projects'; -export { plugin as Asd } from '@backstage/plugin-asd'; From bb8bc7dd42d1d07bd47a40e58351ad932e558b71 Mon Sep 17 00:00:00 2001 From: "J Shamsul Bahri (jibone))" Date: Mon, 21 Sep 2020 15:00:56 +0800 Subject: [PATCH 22/85] merge master and start using discoveryApi --- app-config.yaml | 5 +-- .../core-api/src/apis/definitions/auth.ts | 2 +- .../implementations/auth/saml/SamlAuth.ts | 12 +++---- .../lib/AuthConnector/SamlAuthConnector.ts | 32 ++++++++----------- packages/core/src/api-wrappers/defaultApis.ts | 9 ++++++ 5 files changed, 31 insertions(+), 29 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index 5b704f9b8e..562a2b0e2e 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -131,8 +131,9 @@ auth: $secret: env: GITLAB_BASE_URL saml: - entryPoint: 'http://localhost:7001/' - issuer: 'passport-saml' + development: + entryPoint: 'http://localhost:7001/' + issuer: 'passport-saml' okta: development: clientId: diff --git a/packages/core-api/src/apis/definitions/auth.ts b/packages/core-api/src/apis/definitions/auth.ts index ec99bae6b2..933a78b693 100644 --- a/packages/core-api/src/apis/definitions/auth.ts +++ b/packages/core-api/src/apis/definitions/auth.ts @@ -332,5 +332,5 @@ export const oauth2ApiRef = createApiRef< */ export const samlAuthApiRef = createApiRef({ id: 'core.auth.saml', - description: 'provides authentication towards saml based provider', + description: 'Example of how to use SAML custom provider', }); diff --git a/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts b/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts index cb93dcea97..aeede356ef 100644 --- a/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts @@ -25,7 +25,7 @@ import { AuthRequestOptions, SamlApi, } from '../../../definitions/auth'; -import { AuthProvider } from '../../../definitions'; +import { AuthProvider, DiscoveryApi } from '../../../definitions'; import { SamlSession } from './types'; import { SamlAuthSessionManager, @@ -33,8 +33,7 @@ import { } from '../../../../lib/AuthSessionManager'; type CreateOptions = { - apiOrigin: string; - basePath: string; + discoveryApi: DiscoveryApi; environment?: string; provider?: AuthProvider & { id: string }; }; @@ -53,14 +52,12 @@ const DEFAULT_PROVIDER = { class SamlAuth implements SamlApi { static create({ - apiOrigin, - basePath, + discoveryApi, environment = 'development', provider = DEFAULT_PROVIDER, }: CreateOptions) { const connector = new SamlAuthConnector({ - apiOrigin, - basePath, + discoveryApi, environment, provider, }); @@ -74,7 +71,6 @@ class SamlAuth implements SamlApi { storageKey: 'samlSession', }); - // return new SamlAuth(authSessionStore); return new SamlAuth(authSessionStore); } diff --git a/packages/core-api/src/lib/AuthConnector/SamlAuthConnector.ts b/packages/core-api/src/lib/AuthConnector/SamlAuthConnector.ts index 4a6babe224..47d8f25820 100644 --- a/packages/core-api/src/lib/AuthConnector/SamlAuthConnector.ts +++ b/packages/core-api/src/lib/AuthConnector/SamlAuthConnector.ts @@ -17,15 +17,13 @@ import { AuthProvider, ProfileInfo, BackstageIdentity, + DiscoveryApi, } from '../../apis/definitions'; import { AuthConnector } from './types'; import { showLoginPopup } from '../loginPopup'; -const DEFAULT_BASE_PATH = '/api/auth'; - type Options = { - apiOrigin?: string; - basePath?: string; + discoveryApi: DiscoveryApi; environment?: string; provider: AuthProvider & { id: string }; }; @@ -38,30 +36,24 @@ export type SamlResponse = { export class SamlAuthConnector implements AuthConnector { - private readonly apiOrigin: string; - private readonly basePath: string; + private readonly discoveryApi: DiscoveryApi; private readonly environment: string | undefined; private readonly provider: AuthProvider & { id: string }; constructor(options: Options) { - const { - apiOrigin = window.location.origin, - basePath = DEFAULT_BASE_PATH, - environment, - provider, - } = options; + const { discoveryApi, environment, provider } = options; - this.apiOrigin = apiOrigin; - this.basePath = basePath; + this.discoveryApi = discoveryApi; this.environment = environment; this.provider = provider; } async createSession(): Promise { + const popupUrl = await this.buildUrl('/start'); const payload = await showLoginPopup({ - url: 'http://localhost:7000/auth/saml/start', // FIXME: remove hardcoded here. should do something like buildUrl + url: popupUrl, name: 'SAML Login', // FIXME: change this to provider name? and not hardcode the name - origin: this.apiOrigin, + origin: new URL(popupUrl).origin, width: 450, height: 730, }); @@ -76,8 +68,7 @@ export class SamlAuthConnector async refreshSession(): Promise {} async removeSession(): Promise { - // FIXME: remove hardcoded url here... should do something like buildUrl - const res = await fetch('http://localhost:7000/auth/saml/logout', { + const res = await fetch(await this.buildUrl('/logout'), { method: 'POST', headers: { 'x-requested-with': 'XMLHttpRequest', @@ -93,4 +84,9 @@ export class SamlAuthConnector throw error; } } + + private async buildUrl(path: string): Promise { + const baseUrl = await this.discoveryApi.getBaseUrl('auth'); + return `${baseUrl}/${this.provider.id}${path}?env=${this.environment}`; + } } diff --git a/packages/core/src/api-wrappers/defaultApis.ts b/packages/core/src/api-wrappers/defaultApis.ts index a3f0cb0251..dedadf0456 100644 --- a/packages/core/src/api-wrappers/defaultApis.ts +++ b/packages/core/src/api-wrappers/defaultApis.ts @@ -44,6 +44,8 @@ import { createApiFactory, configApiRef, UrlPatternDiscovery, + samlAuthApiRef, + SamlAuth, } from '@backstage/core-api'; export const defaultApis = [ @@ -132,4 +134,11 @@ export const defaultApis = [ factory: ({ discoveryApi, oauthRequestApi }) => OAuth2.create({ discoveryApi, oauthRequestApi }), }), + createApiFactory({ + api: samlAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + }, + factory: ({ discoveryApi }) => SamlAuth.create({ discoveryApi }), + }), ]; From 5288ce0deeddfce29857a39a01c71864fc84b296 Mon Sep 17 00:00:00 2001 From: "J Shamsul Bahri (jibone))" Date: Mon, 21 Sep 2020 17:43:05 +0800 Subject: [PATCH 23/85] Remove getAccessToken func --- app-config.yaml | 5 ++--- packages/core-api/src/apis/definitions/auth.ts | 4 ---- .../src/apis/implementations/auth/saml/SamlAuth.ts | 6 ------ .../core-api/src/lib/AuthConnector/SamlAuthConnector.ts | 9 ++------- 4 files changed, 4 insertions(+), 20 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index 6fda32f175..60ecb813f7 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -160,9 +160,8 @@ auth: $secret: env: GITLAB_BASE_URL saml: - development: - entryPoint: 'http://localhost:7001/' - issuer: 'passport-saml' + entryPoint: 'http://localhost:7001/' + issuer: 'passport-saml' okta: development: clientId: diff --git a/packages/core-api/src/apis/definitions/auth.ts b/packages/core-api/src/apis/definitions/auth.ts index 933a78b693..b536d74801 100644 --- a/packages/core-api/src/apis/definitions/auth.ts +++ b/packages/core-api/src/apis/definitions/auth.ts @@ -101,16 +101,12 @@ export type OAuthApi = { * This API provides access to SAML 2 credentials. Verify user access with identity provider. */ export type SamlApi = { - // Not sure what Promise call back should have. getBackstageIdentity( options?: AuthRequestOptions, ): Promise; getProfile(options?: AuthRequestOptions): Promise; - // FIXME: is this needed? - getAccessToken(options?: AuthRequestOptions): Promise; - // Not sure if this is needed. logout(): Promise; }; diff --git a/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts b/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts index aeede356ef..c06a6274fd 100644 --- a/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts @@ -92,12 +92,6 @@ class SamlAuth implements SamlApi { return session?.profile; } - // FIXME: Is this needed?... - async getAccessToken(options: AuthRequestOptions) { - const session = await this.sessionManager.getSession(options); - return session?.userId ?? ''; - } - async logout() { await this.sessionManager.removeSession(); } diff --git a/packages/core-api/src/lib/AuthConnector/SamlAuthConnector.ts b/packages/core-api/src/lib/AuthConnector/SamlAuthConnector.ts index 47d8f25820..30de81536d 100644 --- a/packages/core-api/src/lib/AuthConnector/SamlAuthConnector.ts +++ b/packages/core-api/src/lib/AuthConnector/SamlAuthConnector.ts @@ -19,7 +19,6 @@ import { BackstageIdentity, DiscoveryApi, } from '../../apis/definitions'; -import { AuthConnector } from './types'; import { showLoginPopup } from '../loginPopup'; type Options = { @@ -34,8 +33,7 @@ export type SamlResponse = { backstageIdentity: BackstageIdentity; }; -export class SamlAuthConnector - implements AuthConnector { +export class SamlAuthConnector { private readonly discoveryApi: DiscoveryApi; private readonly environment: string | undefined; private readonly provider: AuthProvider & { id: string }; @@ -52,7 +50,7 @@ export class SamlAuthConnector const popupUrl = await this.buildUrl('/start'); const payload = await showLoginPopup({ url: popupUrl, - name: 'SAML Login', // FIXME: change this to provider name? and not hardcode the name + name: `${this.provider.title} Login`, origin: new URL(popupUrl).origin, width: 450, height: 730, @@ -64,9 +62,6 @@ export class SamlAuthConnector }; } - // FIXME: do we need this for SAML? - async refreshSession(): Promise {} - async removeSession(): Promise { const res = await fetch(await this.buildUrl('/logout'), { method: 'POST', From 70dc0627aa770680c7abb57dc01f2a58491e6744 Mon Sep 17 00:00:00 2001 From: "J Shamsul Bahri (jibone))" Date: Mon, 21 Sep 2020 17:53:18 +0800 Subject: [PATCH 24/85] remove the console.log --- plugins/auth-backend/src/providers/saml/provider.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index aac42d6bd9..2163d50f37 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -53,8 +53,6 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { // for non-oauth auth flows. // TODO: This flow doesn't issue an identity token that can be used to validate // the identity of the user in other backends, which we need in some form. - console.log('===> SamlAuthProvider constructor'); - console.log(profile); done(undefined, { userId: profile.nameID!, profile: { @@ -76,9 +74,9 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { ): Promise { try { const { - response: { userId, profile }, + response: { userId, profile }, } = await executeFrameHandlerStrategy(req, this.strategy); - + const id = userId; const idToken = await this.tokenIssuer.issueToken({ claims: { sub: id }, From f77333c35171a38c2cfc79376a78c390affe0ff4 Mon Sep 17 00:00:00 2001 From: Tim Jacomb Date: Mon, 21 Sep 2020 11:01:22 +0100 Subject: [PATCH 25/85] Update docker base image to buster for node --- contrib/docker/multi-stage-frontend/Dockerfile | 2 +- packages/backend/Dockerfile | 2 +- .../templates/default-app/packages/backend/Dockerfile | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contrib/docker/multi-stage-frontend/Dockerfile b/contrib/docker/multi-stage-frontend/Dockerfile index 0c492478d1..3190687c52 100644 --- a/contrib/docker/multi-stage-frontend/Dockerfile +++ b/contrib/docker/multi-stage-frontend/Dockerfile @@ -1,4 +1,4 @@ -FROM node:12 AS build +FROM node:12-buster AS build RUN mkdir /app COPY . /app diff --git a/packages/backend/Dockerfile b/packages/backend/Dockerfile index a7bd814b19..93929ceb86 100644 --- a/packages/backend/Dockerfile +++ b/packages/backend/Dockerfile @@ -1,4 +1,4 @@ -FROM node:12 +FROM node:12-buster WORKDIR /usr/src/app diff --git a/packages/create-app/templates/default-app/packages/backend/Dockerfile b/packages/create-app/templates/default-app/packages/backend/Dockerfile index a7bd814b19..93929ceb86 100644 --- a/packages/create-app/templates/default-app/packages/backend/Dockerfile +++ b/packages/create-app/templates/default-app/packages/backend/Dockerfile @@ -1,4 +1,4 @@ -FROM node:12 +FROM node:12-buster WORKDIR /usr/src/app From e2d719f502eaf166f6473ef14a5d9d02c9e0495c Mon Sep 17 00:00:00 2001 From: "J Shamsul Bahri (jibone))" Date: Mon, 21 Sep 2020 18:48:13 +0800 Subject: [PATCH 26/85] switch to use generic sessionmanager and storage --- .../implementations/auth/saml/SamlAuth.ts | 8 +- .../AuthSessionManager/AuthSessionStore.ts | 2 +- .../SamlAuthSessionManager.ts | 63 ------------- .../SamlAuthSessionStore.ts | 94 ------------------- .../StaticAuthSessionManager.ts | 2 +- .../src/lib/AuthSessionManager/common.ts | 7 +- .../src/lib/AuthSessionManager/index.ts | 2 - 7 files changed, 11 insertions(+), 167 deletions(-) delete mode 100644 packages/core-api/src/lib/AuthSessionManager/SamlAuthSessionManager.ts delete mode 100644 packages/core-api/src/lib/AuthSessionManager/SamlAuthSessionStore.ts diff --git a/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts b/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts index c06a6274fd..6e0bd0af10 100644 --- a/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts @@ -28,8 +28,8 @@ import { import { AuthProvider, DiscoveryApi } from '../../../definitions'; import { SamlSession } from './types'; import { - SamlAuthSessionManager, - SamlAuthSessionStore, + AuthSessionStore, + StaticAuthSessionManager, } from '../../../../lib/AuthSessionManager'; type CreateOptions = { @@ -62,11 +62,11 @@ class SamlAuth implements SamlApi { provider, }); - const sessionManager = new SamlAuthSessionManager({ + const sessionManager = new StaticAuthSessionManager({ connector, }); - const authSessionStore = new SamlAuthSessionStore({ + const authSessionStore = new AuthSessionStore({ manager: sessionManager, storageKey: 'samlSession', }); diff --git a/packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.ts b/packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.ts index f2b8558f74..e82557b1ce 100644 --- a/packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.ts +++ b/packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.ts @@ -28,7 +28,7 @@ type Options = { /** Storage key to use to store sessions */ storageKey: string; /** Used to get the scope of the session */ - sessionScopes: SessionScopesFunc; + sessionScopes?: SessionScopesFunc; /** Used to check if the session needs to be refreshed, defaults to never refresh */ sessionShouldRefresh?: SessionShouldRefreshFunc; }; diff --git a/packages/core-api/src/lib/AuthSessionManager/SamlAuthSessionManager.ts b/packages/core-api/src/lib/AuthSessionManager/SamlAuthSessionManager.ts deleted file mode 100644 index 8a21bc05b9..0000000000 --- a/packages/core-api/src/lib/AuthSessionManager/SamlAuthSessionManager.ts +++ /dev/null @@ -1,63 +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 { SessionManager, GetSessionOptions } from './types'; -import { - SamlAuthConnector, - SamlResponse, -} from '../AuthConnector/SamlAuthConnector'; -import { SessionStateTracker } from './SessionStateTracker'; - -type Options = { - connector: SamlAuthConnector; -}; - -export class SamlAuthSessionManager implements SessionManager { - private readonly connector: SamlAuthConnector; - private readonly stateTracker = new SessionStateTracker(); - - private currentSession: any | undefined; // FIXME: proper typing here? - - constructor(options: Options) { - const { connector } = options; - - this.connector = connector; - } - - async getSession(options: GetSessionOptions): Promise { - if (this.currentSession) { - return this.currentSession; - } - - if (options.optional) { - return undefined; - } - - this.currentSession = await this.connector.createSession(); - this.stateTracker.setIsSignedIn(true); - return this.currentSession; - } - - async removeSession() { - this.currentSession = undefined; - await this.connector.removeSession(); - this.stateTracker.setIsSignedIn(false); - } - - sessionState$() { - return this.stateTracker.sessionState$(); - } -} diff --git a/packages/core-api/src/lib/AuthSessionManager/SamlAuthSessionStore.ts b/packages/core-api/src/lib/AuthSessionManager/SamlAuthSessionStore.ts deleted file mode 100644 index 3afb07c4df..0000000000 --- a/packages/core-api/src/lib/AuthSessionManager/SamlAuthSessionStore.ts +++ /dev/null @@ -1,94 +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 { SamlAuthSessionManager, GetSessionOptions } from './types'; - -type Options = { - manager: SamlAuthSessionManager; - storageKey: string; -}; - -export class SamlAuthSessionStore implements SamlAuthSessionManager { - private readonly manager: SamlAuthSessionManager; - private readonly storageKey: string; - - constructor(options: Options) { - const { manager, storageKey } = options; - - this.manager = manager; - this.storageKey = storageKey; - } - - async getSession(options: GetSessionOptions): Promise { - const session = this.loadSession(); - - if (session) { - return session!; - } - - const newSession = await this.manager.getSession(options); - this.saveSession(newSession); - return newSession; - } - - async removeSession() { - localStorage.removeItem(this.storageKey); - await this.manager.removeSession(); - } - - sessionState$() { - return this.manager.sessionState$(); - } - - private saveSession(session: T | undefined) { - if (session === undefined) { - localStorage.removeItem(this.storageKey); - } else { - localStorage.setItem( - this.storageKey, - JSON.stringify(session, (_key, value) => { - if (value instanceof Set) { - return { - __type: 'Set', - __value: Array.from(value), - }; - } - return value; - }), - ); - } - } - - private loadSession(): T | undefined { - try { - const sessionJson = localStorage.getItem(this.storageKey); - if (sessionJson) { - const session = JSON.parse(sessionJson, (_key, value) => { - if (value?.__type === 'Set') { - return new Set(value.__value); - } - return value; - }); - return session; - } - - return undefined; - } catch (error) { - localStorage.removeItem(this.storageKey); - return undefined; - } - } -} diff --git a/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.ts b/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.ts index e59c11421c..e02b600828 100644 --- a/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.ts +++ b/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.ts @@ -23,7 +23,7 @@ type Options = { /** The connector used for acting on the auth session */ connector: AuthConnector; /** Used to get the scope of the session */ - sessionScopes: (session: T) => Set; + sessionScopes?: (session: T) => Set; /** The default scopes that should always be present in a session, defaults to none. */ defaultScopes?: Set; }; diff --git a/packages/core-api/src/lib/AuthSessionManager/common.ts b/packages/core-api/src/lib/AuthSessionManager/common.ts index 83b81bc81a..ff2897535d 100644 --- a/packages/core-api/src/lib/AuthSessionManager/common.ts +++ b/packages/core-api/src/lib/AuthSessionManager/common.ts @@ -29,7 +29,7 @@ export function hasScopes( } type ScopeHelperOptions = { - sessionScopes: SessionScopesFunc; + sessionScopes: SessionScopesFunc | undefined; defaultScopes?: Set; }; @@ -46,13 +46,16 @@ export class SessionScopeHelper { if (!scopes) { return true; } + if (this.options.sessionScopes === undefined) { + return true; + } const sessionScopes = this.options.sessionScopes(session); return hasScopes(sessionScopes, scopes); } getExtendedScope(session: T | undefined, scopes?: Set) { const newScope = new Set(this.options.defaultScopes); - if (session) { + if (session && this.options.sessionScopes !== undefined) { const sessionScopes = this.options.sessionScopes(session); for (const scope of sessionScopes) { newScope.add(scope); diff --git a/packages/core-api/src/lib/AuthSessionManager/index.ts b/packages/core-api/src/lib/AuthSessionManager/index.ts index a452ac0055..5f4dde8662 100644 --- a/packages/core-api/src/lib/AuthSessionManager/index.ts +++ b/packages/core-api/src/lib/AuthSessionManager/index.ts @@ -16,7 +16,5 @@ export { RefreshingAuthSessionManager } from './RefreshingAuthSessionManager'; export { StaticAuthSessionManager } from './StaticAuthSessionManager'; -export { SamlAuthSessionManager } from './SamlAuthSessionManager'; export { AuthSessionStore } from './AuthSessionStore'; -export { SamlAuthSessionStore } from './SamlAuthSessionStore'; export * from './types'; From eccef941eabfabcd26d10108cc9b983c495d519c Mon Sep 17 00:00:00 2001 From: "J Shamsul Bahri (jibone))" Date: Mon, 21 Sep 2020 18:51:24 +0800 Subject: [PATCH 27/85] remove type that is not used --- packages/core-api/src/lib/AuthSessionManager/types.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/core-api/src/lib/AuthSessionManager/types.ts b/packages/core-api/src/lib/AuthSessionManager/types.ts index 1be439a840..804c7121e1 100644 --- a/packages/core-api/src/lib/AuthSessionManager/types.ts +++ b/packages/core-api/src/lib/AuthSessionManager/types.ts @@ -36,13 +36,6 @@ export type SessionManager = { sessionState$(): Observable; }; -// FIXME do we need this? -export type SamlAuthSessionManager = { - getSession(options: GetSessionOptions): Promise; - removeSession(): Promise; - sessionState$(): Observable; -}; - /** * A function called to determine the scopes of a session. */ From cd0f2bd3db9c389b4b1eea2b2b52e4892de3b5cf Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 21 Sep 2020 15:48:55 +0200 Subject: [PATCH 28/85] chore(cli/backend): fixing some drit --- packages/cli/src/commands/create-plugin/createPlugin.ts | 1 - packages/cli/templates/default-backend-plugin/package.json.hbs | 2 +- .../components/ExampleComponent/ExampleComponent.test.tsx.hbs | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/commands/create-plugin/createPlugin.ts b/packages/cli/src/commands/create-plugin/createPlugin.ts index b42edc99ec..8f75728544 100644 --- a/packages/cli/src/commands/create-plugin/createPlugin.ts +++ b/packages/cli/src/commands/create-plugin/createPlugin.ts @@ -176,7 +176,6 @@ export async function movePlugin( } export default async ({ backend }: { backend: boolean }) => { - console.warn(backend); const codeownersPath = await getCodeownersFilePath(paths.targetRoot); const questions: Question[] = [ diff --git a/packages/cli/templates/default-backend-plugin/package.json.hbs b/packages/cli/templates/default-backend-plugin/package.json.hbs index 8a1d5a9522..fd2c57f87d 100644 --- a/packages/cli/templates/default-backend-plugin/package.json.hbs +++ b/packages/cli/templates/default-backend-plugin/package.json.hbs @@ -7,7 +7,7 @@ "private": true, "publishConfig": { "access": "public", - "main": "dist/index.esm.js", + "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, "scripts": { diff --git a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs index 4d52fa82b5..f49457490f 100644 --- a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs +++ b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs @@ -8,7 +8,7 @@ describe('ExampleComponent', () => { it('should render', () => { const rendered = render( - + , ); expect(rendered.getByText('Welcome to {{ id }}!')).toBeInTheDocument(); From 6f70008386fb128a90b88c756995a1dc402520f3 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 21 Sep 2020 23:37:35 +0200 Subject: [PATCH 29/85] chore(cli): fixing issues with the new dependency --- packages/cli/src/commands/create-plugin/createPlugin.ts | 2 +- packages/cli/templates/default-backend-plugin/package.json.hbs | 2 +- packages/cli/templates/default-plugin/package.json.hbs | 2 +- plugins/api-docs/package.json | 3 ++- plugins/app-backend/package.json | 2 +- plugins/auth-backend/package.json | 3 ++- plugins/catalog-graphql/package.json | 2 +- plugins/circleci/package.json | 3 ++- plugins/explore/package.json | 3 ++- plugins/gcp-projects/package.json | 3 ++- plugins/github-actions/package.json | 3 ++- plugins/gitops-profiles/package.json | 3 ++- plugins/graphiql/package.json | 3 ++- plugins/identity-backend/package.json | 3 ++- plugins/jenkins/package.json | 3 ++- plugins/lighthouse/package.json | 3 ++- plugins/newrelic/package.json | 3 ++- plugins/register-component/package.json | 3 ++- plugins/rollbar/package.json | 3 ++- plugins/scaffolder/package.json | 3 ++- plugins/sentry/package.json | 3 ++- plugins/tech-radar/package.json | 3 ++- plugins/techdocs/package.json | 3 ++- plugins/welcome/package.json | 3 ++- 24 files changed, 43 insertions(+), 24 deletions(-) diff --git a/packages/cli/src/commands/create-plugin/createPlugin.ts b/packages/cli/src/commands/create-plugin/createPlugin.ts index 37c026ecae..cbaf956bdb 100644 --- a/packages/cli/src/commands/create-plugin/createPlugin.ts +++ b/packages/cli/src/commands/create-plugin/createPlugin.ts @@ -272,7 +272,7 @@ export default async (cmd: Command) => { Task.section('Building the plugin'); await buildPlugin(pluginDir); - if ((await fs.pathExists(appPackage)) && !backend) { + if ((await fs.pathExists(appPackage)) && !cmd.backend) { Task.section('Adding plugin as dependency in app'); await addPluginDependencyToApp(paths.targetRoot, name, version); diff --git a/packages/cli/templates/default-backend-plugin/package.json.hbs b/packages/cli/templates/default-backend-plugin/package.json.hbs index 2ecdbcef01..a4b6e7aaf9 100644 --- a/packages/cli/templates/default-backend-plugin/package.json.hbs +++ b/packages/cli/templates/default-backend-plugin/package.json.hbs @@ -36,7 +36,7 @@ "@backstage/cli": "^{{version}}", "@types/supertest": "^2.0.8", "supertest": "^4.0.2", - "msw": "^0.19.5" + "msw": "^0.20.5" }, "files": [ "dist" diff --git a/packages/cli/templates/default-plugin/package.json.hbs b/packages/cli/templates/default-plugin/package.json.hbs index 7597d3dc28..0d26339843 100644 --- a/packages/cli/templates/default-plugin/package.json.hbs +++ b/packages/cli/templates/default-plugin/package.json.hbs @@ -41,7 +41,7 @@ "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", - "msw": "^0.19.5" + "msw": "^0.20.5" }, "files": [ "dist" diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 378ceb2d08..254dc9aba0 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -49,7 +49,8 @@ "@types/node": "^12.0.0", "@types/react": "^16.9", "@types/swagger-ui-react": "^3.23.3", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5" }, "files": [ "dist" diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index c4738c7105..2854425e75 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -32,7 +32,7 @@ "devDependencies": { "@backstage/cli": "^0.1.1-alpha.22", "@types/supertest": "^2.0.8", - "msw": "^0.19.5", + "msw": "^0.20.5", "supertest": "^4.0.2" }, "files": [ diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 91fe08c0fa..aff6470e71 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -58,7 +58,8 @@ "@types/passport-google-oauth20": "^2.0.3", "@types/passport-microsoft": "^0.0.0", "@types/passport-saml": "^1.1.2", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5" }, "files": [ "dist", diff --git a/plugins/catalog-graphql/package.json b/plugins/catalog-graphql/package.json index c508215f29..505f7bb4df 100644 --- a/plugins/catalog-graphql/package.json +++ b/plugins/catalog-graphql/package.json @@ -40,7 +40,7 @@ "@graphql-codegen/typescript-resolvers": "^1.17.7", "ts-node": "^8.10.2", "eslint-plugin-graphql": "^4.0.0", - "msw": "^0.19.5", + "msw": "^0.20.5", "supertest": "^4.0.2" }, "files": [ diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index 10278fc065..47600d5452 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -46,7 +46,8 @@ "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "@types/react-lazylog": "^4.5.0", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5" }, "files": [ "dist" diff --git a/plugins/explore/package.json b/plugins/explore/package.json index c889f96153..f094c4f908 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -41,7 +41,8 @@ "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5" }, "files": [ "dist" diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index b71cebf6e4..fa72f0e66d 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -39,7 +39,8 @@ "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5" }, "files": [ "dist" diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index 9d1538ce99..1ee1e44074 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -47,7 +47,8 @@ "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5" }, "files": [ "dist" diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index cb25167ba6..7805f35f85 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -39,7 +39,8 @@ "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5" }, "files": [ "dist" diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index e6122eb38b..5747ff5726 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -53,7 +53,8 @@ "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "jest-fetch-mock": "^3.0.3", - "react-router-dom": "6.0.0-beta.0" + "react-router-dom": "6.0.0-beta.0", + "msw": "^0.20.5" }, "files": [ "dist" diff --git a/plugins/identity-backend/package.json b/plugins/identity-backend/package.json index a34049fb91..ab006d3ecc 100644 --- a/plugins/identity-backend/package.json +++ b/plugins/identity-backend/package.json @@ -34,7 +34,8 @@ }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.22", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5" }, "files": [ "dist" diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index 398161f270..07ec728b34 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -44,7 +44,8 @@ "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "@types/testing-library__jest-dom": "^5.9.1", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5" }, "files": [ "dist" diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index c5522ae67a..ea73010491 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -42,7 +42,8 @@ "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5" }, "files": [ "dist" diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index 0cadf440f7..63a6d0a1b4 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -38,7 +38,8 @@ "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5" }, "files": [ "dist" diff --git a/plugins/register-component/package.json b/plugins/register-component/package.json index 8ccaae9914..c2d9709643 100644 --- a/plugins/register-component/package.json +++ b/plugins/register-component/package.json @@ -43,7 +43,8 @@ "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5" }, "files": [ "dist" diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index cb37c6187a..c2154a2c85 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -47,7 +47,8 @@ "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "@types/react": "^16.9", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5" }, "files": [ "dist" diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index ab95e83201..21b95a6aaf 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -49,7 +49,8 @@ "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5" }, "files": [ "dist" diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index 6e7daea15c..1fd836b83f 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -43,7 +43,8 @@ "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5" }, "files": [ "dist" diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index f25ec752e1..36318be766 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -45,7 +45,8 @@ "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "@types/react": "^16.9", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5" }, "files": [ "dist" diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index d2f99cb824..d467f8ef5f 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -48,7 +48,8 @@ "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "canvas": "^2.6.1", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5" }, "files": [ "dist" diff --git a/plugins/welcome/package.json b/plugins/welcome/package.json index 741a680cd9..7784d8ff97 100644 --- a/plugins/welcome/package.json +++ b/plugins/welcome/package.json @@ -39,7 +39,8 @@ "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5" }, "files": [ "dist" From 65664e2c30cb263c0ebcd821bd94e1d1e5310482 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 22 Sep 2020 01:54:38 +0200 Subject: [PATCH 30/85] chore(cli): fixing create again --- .../ExampleComponent.test.tsx.hbs | 18 +++++++++ .../ExampleFetchComponent.test.tsx.hbs | 16 ++++++++ packages/test-utils-core/package.json | 4 +- yarn.lock | 37 ++----------------- 4 files changed, 40 insertions(+), 35 deletions(-) diff --git a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs index f49457490f..1069790218 100644 --- a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs +++ b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs @@ -3,8 +3,26 @@ import { render } from '@testing-library/react'; import ExampleComponent from './ExampleComponent'; import { ThemeProvider } from '@material-ui/core'; import { lightTheme } from '@backstage/theme'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; + describe('ExampleComponent', () => { + const server = setupServer(); + // Enable API mocking before tests. + beforeAll(() => server.listen()) + + // Reset any runtime request handlers we may add during the tests. + afterEach(() => server.resetHandlers()) + + // Disable API mocking after the tests are done. + afterAll(() => server.close()) + + // setup mock response + beforeEach(() => { + server.use(rest.get('/*', (_, res, ctx) => res(ctx.status(200), ctx.json({})))) + }) + it('should render', () => { const rendered = render( diff --git a/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs b/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs index aad4fac10a..309fa34aee 100644 --- a/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs +++ b/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs @@ -1,8 +1,24 @@ import React from 'react'; import { render } from '@testing-library/react'; import ExampleFetchComponent from './ExampleFetchComponent'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; describe('ExampleFetchComponent', () => { + const server = setupServer(); + // Enable API mocking before tests. + beforeAll(() => server.listen()) + + // Reset any runtime request handlers we may add during the tests. + afterEach(() => server.resetHandlers()) + + // Disable API mocking after the tests are done. + afterAll(() => server.close()) + + // setup mock response + beforeEach(() => { + server.use(rest.get('/*', (_, res, ctx) => res(ctx.status(200), ctx.delay(2000), ctx.json({})))) + }) it('should render', async () => { const rendered = render(); expect(await rendered.findByTestId('progress')).toBeInTheDocument(); diff --git a/packages/test-utils-core/package.json b/packages/test-utils-core/package.json index d79acf80a8..c279656736 100644 --- a/packages/test-utils-core/package.json +++ b/packages/test-utils-core/package.json @@ -18,8 +18,8 @@ "backstage" ], "license": "Apache-2.0", - "main": "src/index.ts", - "types": "src/index.ts", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts", "scripts": { "build": "backstage-cli build --outputs types,esm", "lint": "backstage-cli lint", diff --git a/yarn.lock b/yarn.lock index 0cdce51f95..df2c58400e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3287,7 +3287,7 @@ dependencies: "@types/node" ">= 8" -"@open-draft/until@^1.0.0", "@open-draft/until@^1.0.3": +"@open-draft/until@^1.0.3": version "1.0.3" resolved "https://registry.npmjs.org/@open-draft/until/-/until-1.0.3.tgz#db9cc719191a62e7d9200f6e7bab21c5b848adca" integrity sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q== @@ -4507,11 +4507,6 @@ dependencies: "@types/express" "*" -"@types/cookie@^0.3.3": - version "0.3.3" - resolved "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz#85bc74ba782fb7aa3a514d11767832b0e3bc6803" - integrity sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow== - "@types/cookie@^0.4.0": version "0.4.0" resolved "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.0.tgz#14f854c0f93d326e39da6e3b6f34f7d37513d108" @@ -11817,7 +11812,7 @@ graphql-upload@^8.0.2: http-errors "^1.7.3" object-path "^0.11.4" -graphql@15.3.0, graphql@^15.0.0, graphql@^15.3.0: +graphql@15.3.0, graphql@^15.3.0: version "15.3.0" resolved "https://registry.npmjs.org/graphql/-/graphql-15.3.0.tgz#3ad2b0caab0d110e3be4a5a9b2aa281e362b5278" integrity sha512-GTCJtzJmkFLWRfFJuoo9RWWa/FfamUHgiFosxi/X1Ani4AVWbeyBenZTNX6dM+7WSbbFfTo/25eh0LLkwHMw2w== @@ -11993,7 +11988,7 @@ he@^1.1.0, he@^1.2.0: resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== -headers-utils@^1.1.9, headers-utils@^1.2.0: +headers-utils@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/headers-utils/-/headers-utils-1.2.0.tgz#5e10d1bc9d2bccf789547afca5b991a3167241e8" integrity sha512-4/BMXcWrJErw7JpM87gF8MNEXcIMLzepYZjNRv/P9ctgupl2Ywa3u1PgHtNhSRq84bHH9Ndlkdy7bSi+bZ9I9A== @@ -15555,22 +15550,6 @@ ms@^2.0.0, ms@^2.1.1: resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -msw@^0.19.5: - version "0.19.5" - resolved "https://registry.npmjs.org/msw/-/msw-0.19.5.tgz#7d2a1a852ccf1644d3db6735d69fff6777aac33f" - integrity sha512-J5eQ++gDVZoHPC8gVXtWcakLjgmPipvFj/sEnlRV/WViXuiq2CamSqO3Wbh6H8bAmj+k2vUWCfcVT1HjMdKB2Q== - dependencies: - "@open-draft/until" "^1.0.0" - "@types/cookie" "^0.3.3" - chalk "^4.0.0" - cookie "^0.4.1" - graphql "^15.0.0" - headers-utils "^1.1.9" - node-match-path "^0.4.2" - node-request-interceptor "^0.2.5" - statuses "^2.0.0" - yargs "^15.3.1" - msw@^0.20.5: version "0.20.5" resolved "https://registry.npmjs.org/msw/-/msw-0.20.5.tgz#b6141080c0d8b17c451d9ca36c28cc47b4ac487a" @@ -15830,7 +15809,7 @@ node-libs-browser@^2.2.1: util "^0.11.0" vm-browserify "^1.0.1" -node-match-path@^0.4.2, node-match-path@^0.4.4: +node-match-path@^0.4.4: version "0.4.4" resolved "https://registry.npmjs.org/node-match-path/-/node-match-path-0.4.4.tgz#516a10926093c0cc6f237d020685b593b19baebb" integrity sha512-pBq9gp7TG0r0VXuy/oeZmQsjBSnYQo7G886Ly/B3azRwZuEtHCY155dzmfoKWcDPGgyfIGD8WKVC7h3+6y7yTg== @@ -15889,14 +15868,6 @@ node-releases@^1.1.52, node-releases@^1.1.58: resolved "https://registry.npmjs.org/node-releases/-/node-releases-1.1.60.tgz#6948bdfce8286f0b5d0e5a88e8384e954dfe7084" integrity sha512-gsO4vjEdQaTusZAEebUWp2a5d7dF5DYoIpDG7WySnk7BuZDW+GPpHXoXXuYawRBr/9t5q54tirPz79kFIWg4dA== -node-request-interceptor@^0.2.5: - version "0.2.6" - resolved "https://registry.npmjs.org/node-request-interceptor/-/node-request-interceptor-0.2.6.tgz#541278d7033bb6a8befb5dd793f83428cf6446a2" - integrity sha512-aJW1tPSM7nzuZFRe+C/KSz22GJO3CVFMxHHmMGX8Z+tjP7TCIVbzeckLFVfJG68BdVgrdOOP7Ejc57ag820eyA== - dependencies: - debug "^4.1.1" - headers-utils "^1.2.0" - node-request-interceptor@^0.3.5: version "0.3.5" resolved "https://registry.npmjs.org/node-request-interceptor/-/node-request-interceptor-0.3.5.tgz#4b26159617829c9a70643012c0fdc3ae4c78ae43" From 23275680fa3693ad68991bd1b064e3713fd1b8fe Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 22 Sep 2020 02:53:27 +0200 Subject: [PATCH 31/85] chore(cli): revert some core testing utils --- packages/test-utils-core/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/test-utils-core/package.json b/packages/test-utils-core/package.json index c279656736..d79acf80a8 100644 --- a/packages/test-utils-core/package.json +++ b/packages/test-utils-core/package.json @@ -18,8 +18,8 @@ "backstage" ], "license": "Apache-2.0", - "main": "dist/index.esm.js", - "types": "dist/index.d.ts", + "main": "src/index.ts", + "types": "src/index.ts", "scripts": { "build": "backstage-cli build --outputs types,esm", "lint": "backstage-cli lint", From a9adf229ff8ae800db0ed8a1efeb5d02f05b363a Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 22 Sep 2020 03:25:08 +0200 Subject: [PATCH 32/85] chore(cli): fixing cli issues --- .../ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs b/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs index 309fa34aee..c584289077 100644 --- a/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs +++ b/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs @@ -17,7 +17,7 @@ describe('ExampleFetchComponent', () => { // setup mock response beforeEach(() => { - server.use(rest.get('/*', (_, res, ctx) => res(ctx.status(200), ctx.delay(2000), ctx.json({})))) + server.use(rest.get('https://randomuser.me/*', (_, res, ctx) => res(ctx.status(200), ctx.delay(2000), ctx.json({})))) }) it('should render', async () => { const rendered = render(); From b60086db6b5968be2f87b68f338d8c28a6aa7046 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 22 Sep 2020 03:56:16 +0200 Subject: [PATCH 33/85] chore(cli): updating nodefetch --- packages/cli/templates/default-backend-plugin/package.json.hbs | 2 +- .../cli/templates/default-backend-plugin/src/setupTests.ts | 1 + packages/cli/templates/default-plugin/package.json.hbs | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/cli/templates/default-backend-plugin/package.json.hbs b/packages/cli/templates/default-backend-plugin/package.json.hbs index a4b6e7aaf9..40e612609a 100644 --- a/packages/cli/templates/default-backend-plugin/package.json.hbs +++ b/packages/cli/templates/default-backend-plugin/package.json.hbs @@ -29,7 +29,7 @@ "express": "^4.17.1", "express-promise-router": "^3.0.3", "winston": "^3.2.1", - "node-fetch": "^2.6.0", + "node-fetch": "^2.6.1", "yn": "^4.0.0" }, "devDependencies": { diff --git a/packages/cli/templates/default-backend-plugin/src/setupTests.ts b/packages/cli/templates/default-backend-plugin/src/setupTests.ts index ba33cf996b..a5907fd52f 100644 --- a/packages/cli/templates/default-backend-plugin/src/setupTests.ts +++ b/packages/cli/templates/default-backend-plugin/src/setupTests.ts @@ -15,3 +15,4 @@ */ export {}; +global.fetch = require('node-fetch'); diff --git a/packages/cli/templates/default-plugin/package.json.hbs b/packages/cli/templates/default-plugin/package.json.hbs index 0d26339843..e024884547 100644 --- a/packages/cli/templates/default-plugin/package.json.hbs +++ b/packages/cli/templates/default-plugin/package.json.hbs @@ -41,7 +41,8 @@ "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", - "msw": "^0.20.5" + "msw": "^0.20.5", + "node-fetch": "^2.6.1" }, "files": [ "dist" From 113ac17c2e5410844892a09e8805b8dd147e0cf2 Mon Sep 17 00:00:00 2001 From: "J Shamsul Bahri (jibone))" Date: Tue, 22 Sep 2020 12:23:09 +0800 Subject: [PATCH 34/85] fix tsc error --- packages/core-api/src/lib/AuthConnector/SamlAuthConnector.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/core-api/src/lib/AuthConnector/SamlAuthConnector.ts b/packages/core-api/src/lib/AuthConnector/SamlAuthConnector.ts index 30de81536d..e8ab412756 100644 --- a/packages/core-api/src/lib/AuthConnector/SamlAuthConnector.ts +++ b/packages/core-api/src/lib/AuthConnector/SamlAuthConnector.ts @@ -62,6 +62,8 @@ export class SamlAuthConnector { }; } + async refreshSession(): Promise {} + async removeSession(): Promise { const res = await fetch(await this.buildUrl('/logout'), { method: 'POST', From 40a6a4ee23c0bd0e864b49027fe7621fffb71e01 Mon Sep 17 00:00:00 2001 From: "J Shamsul Bahri (jibone))" Date: Tue, 22 Sep 2020 14:47:16 +0800 Subject: [PATCH 35/85] add sidebar option --- .../src/apis/implementations/auth/saml/SamlAuth.ts | 5 +++++ .../core/src/layout/Sidebar/DefaultProviderSettings.tsx | 8 ++++++++ .../src/layout/Sidebar/Settings/OIDCProviderSettings.tsx | 2 +- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts b/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts index 6e0bd0af10..2c8c98ecf6 100644 --- a/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts @@ -87,6 +87,11 @@ class SamlAuth implements SamlApi { return session?.backstageIdentity; } + async getIdToken(options: AuthRequestOptions = {}) { + const session = await this.sessionManager.getSession(options); + return session?.backstageIdentity ?? ''; + } + async getProfile(options: AuthRequestOptions = {}) { const session = await this.sessionManager.getSession(options); return session?.profile; diff --git a/packages/core/src/layout/Sidebar/DefaultProviderSettings.tsx b/packages/core/src/layout/Sidebar/DefaultProviderSettings.tsx index 293cefb19a..246ce722a1 100644 --- a/packages/core/src/layout/Sidebar/DefaultProviderSettings.tsx +++ b/packages/core/src/layout/Sidebar/DefaultProviderSettings.tsx @@ -21,6 +21,7 @@ import { oauth2ApiRef, oktaAuthApiRef, microsoftAuthApiRef, + samlAuthApiRef, useApi, } from '@backstage/core-api'; import Star from '@material-ui/icons/Star'; @@ -69,6 +70,13 @@ export const DefaultProviderSettings = () => { icon={Star} /> )} + {providers.includes('saml') && ( + + )} {providers.includes('oauth2') && ( ; + apiRef: ApiRef | ApiRef; }; export const OIDCProviderSettings: FC = ({ From 60fdb3eda1ff6604fb4510a0476823faf41051bf Mon Sep 17 00:00:00 2001 From: "J Shamsul Bahri (jibone))" Date: Tue, 22 Sep 2020 16:02:00 +0800 Subject: [PATCH 36/85] remove comment --- plugins/auth-backend/src/providers/saml/provider.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index 2163d50f37..82e0acbdcc 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -111,7 +111,6 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { return reqEnv; } - // FIXME : do we want to always to return 'development' ? return 'development'; } } From 0222279021d3ed0f3d83652c36abea64a37ac9a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 21 Sep 2020 13:33:36 +0200 Subject: [PATCH 37/85] feat(freben): add handling and docs for entity references --- .../adr008-default-catalog-file-name.md | 4 +- .../adr009-entity-references.md | 69 ++++ docs/features/software-catalog/references.md | 104 +++++ microsite/sidebars.json | 4 +- mkdocs.yml | 2 + packages/catalog-model/src/entity/index.ts | 2 + packages/catalog-model/src/entity/ref.test.ts | 355 ++++++++++++++++++ packages/catalog-model/src/entity/ref.ts | 146 +++++++ packages/catalog-model/src/index.ts | 8 +- packages/catalog-model/src/types.ts | 28 ++ 10 files changed, 718 insertions(+), 4 deletions(-) create mode 100644 docs/architecture-decisions/adr009-entity-references.md create mode 100644 docs/features/software-catalog/references.md create mode 100644 packages/catalog-model/src/entity/ref.test.ts create mode 100644 packages/catalog-model/src/entity/ref.ts diff --git a/docs/architecture-decisions/adr008-default-catalog-file-name.md b/docs/architecture-decisions/adr008-default-catalog-file-name.md index c57d10c0b1..d4716318bc 100644 --- a/docs/architecture-decisions/adr008-default-catalog-file-name.md +++ b/docs/architecture-decisions/adr008-default-catalog-file-name.md @@ -7,7 +7,7 @@ description: Architecture Decision Record (ADR) log on Default Catalog File Name ## Background While the spec for the catalog file format is well described in -[ADR002](./adr002-default-catalog-file-format.md), guidance was note provided as +[ADR002](./adr002-default-catalog-file-format.md), guidance was not provided as to the name of the catalog file. Following discussion in @@ -23,4 +23,4 @@ catalog-info.yaml ``` This name is a default, **not a requirement**. The catalog file will work with -Backstage irregardless of its name. +Backstage regardless of its name. diff --git a/docs/architecture-decisions/adr009-entity-references.md b/docs/architecture-decisions/adr009-entity-references.md new file mode 100644 index 0000000000..8b7984ea49 --- /dev/null +++ b/docs/architecture-decisions/adr009-entity-references.md @@ -0,0 +1,69 @@ +--- +id: adrs-adr009 +title: ADR009: Entity References +description: Architecture Decision Record (ADR) log on Entity References +--- + +## Background + +While the spec for the catalog file format is well described in +[ADR002](./adr002-default-catalog-file-format.md), guidance was not provided as +to how one is expected to express references to other entities in the catalog. +There was also some confusion on how to reference entities in URLs in the +Backstage frontend. + +Following discussion in +[Issue 1947](https://github.com/spotify/backstage/issues/1947), a decision was +made. + +## Entity References in YAML files + +The textual format, as written by humans, to reference entities by name is on +the following form, where square brackets denote optionality: + +``` +[:][/] +``` + +That is, it is composed of between one and three parts in this specific order, +without any additional encoding, with those exact separator characters. +Optionality of `kind` and `namespace` are contextual, and they may or may not +have default contextual fallback values. + +When that format is insufficient or when machine made interchange formats wish +to express such relations in a more expressive form, a nested structure on the +following form can be used: + +```yaml +kind: +namespace: +name: +``` + +Of these, only `name` is always required. Optionality of `kind` and `namespace` +are contextual, and they may or may not have default contextual fallback values. +All other possible key values in this structure are reserved for future use. + +A system or user wanting to express a full entity name that is always valid, +shall supply the entire triplet whether using the string form or the compound +form. + +A full description of the format can be found +[in the documentation](https://backstage.io/docs/features/software-catalog/references). + +## Entity References in URLs + +Where entities are referenced by name in the Backstage frontend, the URL +containing the reference shall take the following form: + +``` +:namespace/:kind/:name +``` + +All three parts are required under all circumstances. The default value for the +`namespace` in the catalog is the string `"default"`, if the entity does not +specify one explicitly in `metadata.namespace`. + +This means that we do not encourage the string form of entity references to be +used as a single URL segment, due to the use of URL-unsafe characters leading to +possible risk, confusion, and uglier URLs. diff --git a/docs/features/software-catalog/references.md b/docs/features/software-catalog/references.md new file mode 100644 index 0000000000..5093ecc49e --- /dev/null +++ b/docs/features/software-catalog/references.md @@ -0,0 +1,104 @@ +--- +id: references +title: Entity References +description: How to express references between entities +--- + +Entities commonly have a need to reference other entities. For example, a +[Component](descriptor-format.md#kind-component) entity may want to declare who +its owner is by mentioning a Group or User entity, and a User entity may want to +declare what Group entities it is a member of. This article describes how to +write those references in your yaml entity declaration files. + +Each entity in the catalog is uniquely identified by the triplet of its +[kind](descriptor-format.md#apiversion-and-kind-required), +[namespace](descriptor-format.md#namespace-optional), and +[name](descriptor-format.md#name-required). But that's a lot to type out +manually, and in a lot of circumstances, both the kind and the namespace are +fixed, or possible to deduce, or could have sane default values. So in order to +help the writer, the catalog has a few tricks up its sleeve. + +Each reference can be expressed in one of two ways: as a compact string, or as a +compound reference structure. + +## String References + +This is the most common alternative, that should be used in almost all +circumstances. + +The string is on the form `[:][/]`, that is, it is +composed of between one and three parts in this specific order, without any +additional encoding: + +- Optionally, the kind, followed by a colon +- Optionally, the namespace, followed by a forward slash +- The name + +The name is always required. Depending on the context, you may be able to leave +out the kind and/or namespace. If you do, it is contextual what values will be +used, and the relevant documentation should specify which rule applies where. +All strings are case insensitive. + +```yaml +# Example: +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: petstore + namespace: external-systems + description: Petstore +spec: + type: service + lifecycle: experimental + owner: group:pet-managers + implementsApis: + - petstore + - internal/streetlights + - hello-world +``` + +The field `spec.owner` is a reference. In this case, the string +`group:pet-managers` was given by the user. That means that the kind is `Group`, +the namespace is left out, and the name is `pet-managers`. In this context, the +namespace was chosen to fall back to the value `default` by the code that parsed +the reference, so the end result is that we expect to find another entity in the +catalog that is of kind `Group`, namespace `default` (which, actually, also can +be left out in its own yaml file because that's the default value there too), +and name `pet-managers`. + +The entries in `implementsApis` are also references. In this case, none of them +need to specify a kind since we know from the context that that's the only kind +that's supported here. The second entry specifies a namespace but the other ones +don't, and in this context, the default is to refer to the same namespace as the +originating entity (`external-systems` here). So the three references +essentially expand to `api:external-systems/petstore`, +`api:internal/streetlights`, and `api:external-systems/hello-world`. We expect +there to exist three API kind entities in the catalog matching those references. + +## Compound References + +This is a more verbose version of a reference, where each part of the +kind-namespace-name triplet is expressed as a field in a structure. This format +can be used where necessary, such as if either of the three elements contain +colons or forward slashes. Avoid using it where possible, since it is harder to +read and write for humans. + +```yaml +# Example: +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: petstore + description: Petstore +spec: + type: service + lifecycle: experimental + owner: + kind: Group + name: aegis-imports/pet-managers +``` + +In this example, the `spec.owner` has been broken apart since the name was +complex. The kind happened to be written with an uppercase letter G, which also +works. The namespace was left out just like in the string version above, which +is handled identically. diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 8fa4935a38..ad47592dfc 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -43,6 +43,7 @@ "features/software-catalog/configuration", "features/software-catalog/system-model", "features/software-catalog/descriptor-format", + "features/software-catalog/references", "features/software-catalog/well-known-annotations", "features/software-catalog/extending-the-model", "features/software-catalog/external-integrations", @@ -158,7 +159,8 @@ "architecture-decisions/adrs-adr005", "architecture-decisions/adrs-adr006", "architecture-decisions/adrs-adr007", - "architecture-decisions/adrs-adr008" + "architecture-decisions/adrs-adr008", + "architecture-decisions/adrs-adr009" ], "Contribute": ["../CONTRIBUTING"], "Support": ["overview/support"], diff --git a/mkdocs.yml b/mkdocs.yml index ba607f8a6e..91886a9dac 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -32,6 +32,7 @@ nav: - Configuration: 'features/software-catalog/configuration.md' - System model: 'features/software-catalog/system-model.md' - YAML File Format: 'features/software-catalog/descriptor-format.md' + - Entity References: 'features/software-catalog/references.md' - Well-known Annotations: 'features/software-catalog/well-known-annotations.md' - Extending the model: 'features/software-catalog/extending-the-model.md' - External integrations: 'features/software-catalog/external-integrations.md' @@ -104,6 +105,7 @@ nav: - ADR006 - Avoid React.FC and React.SFC: 'architecture-decisions/adr006-avoid-react-fc.md' - ADR007 - Use MSW for Network Request Mocking: 'architecture-decisions/adr007-use-msw-to-mock-service-requests.md' - ADR008 - Default Catalog File Name: 'architecture-decisions/adr008-default-catalog-file-name.md' + - ADR009 - Entity References: 'architecture-decisions/adr009-entity-references.md' - Contribute: '../CONTRIBUTING.md' - Support: 'overview/support.md' - FAQ: FAQ.md diff --git a/packages/catalog-model/src/entity/index.ts b/packages/catalog-model/src/entity/index.ts index 380f5458cc..12ec19667b 100644 --- a/packages/catalog-model/src/entity/index.ts +++ b/packages/catalog-model/src/entity/index.ts @@ -17,6 +17,8 @@ export { entityMetaGeneratedFields } from './Entity'; export type { Entity, EntityMeta } from './Entity'; export * from './policies'; +export { parseEntityName, parseEntityRef, serializeEntityRef } from './ref'; +export type { EntityRefContext } from './ref'; export { entityHasChanges, generateEntityEtag, diff --git a/packages/catalog-model/src/entity/ref.test.ts b/packages/catalog-model/src/entity/ref.test.ts new file mode 100644 index 0000000000..8a8b7d3f86 --- /dev/null +++ b/packages/catalog-model/src/entity/ref.test.ts @@ -0,0 +1,355 @@ +/* + * 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 { parseEntityName, parseEntityRef, serializeEntityRef } from './ref'; + +describe('ref', () => { + describe('parseEntityName', () => { + it('handles some omissions', () => { + expect(parseEntityName('a:b/c')).toEqual({ + kind: 'a', + namespace: 'b', + name: 'c', + }); + expect(() => parseEntityName('b/c')).toThrow(/kind/); + expect(parseEntityName('a:c')).toEqual({ + kind: 'a', + namespace: 'default', + name: 'c', + }); + expect(() => parseEntityName('c')).toThrow(/kind/); + }); + + it('rejects bad inputs', () => { + expect(() => parseEntityName(null as any)).toThrow(); + expect(() => parseEntityName(7 as any)).toThrow(); + expect(() => parseEntityName('a:b:c')).toThrow(); + expect(() => parseEntityName('a/b/c')).toThrow(); + expect(() => parseEntityName('a/b:c')).toThrow(); + expect(() => parseEntityName('a:b/c/d')).toThrow(); + expect(() => parseEntityName('a:b/c:d')).toThrow(); + }); + + it('rejects empty parts in strings', () => { + // one is empty + expect(() => parseEntityName(':b/c')).toThrow(); + expect(() => parseEntityName('a:/c')).toThrow(); + expect(() => parseEntityName('a:b/')).toThrow(); + // two are empty + expect(() => parseEntityName('a:/')).toThrow(); + expect(() => parseEntityName(':b/')).toThrow(); + expect(() => parseEntityName(':/c')).toThrow(); + // three are empty + expect(() => parseEntityName(':/')).toThrow(); + // one is left out, one empty + expect(() => parseEntityName('/c')).toThrow(); + expect(() => parseEntityName('b/')).toThrow(); + expect(() => parseEntityName(':c')).toThrow(); + expect(() => parseEntityName('a:')).toThrow(); + // nothing at all + expect(() => parseEntityName('')).toThrow(); + }); + + it('rejects empty parts in compounds', () => { + // one is empty + expect(() => + parseEntityName({ kind: '', namespace: 'b', name: 'c' }), + ).toThrow(); + expect(() => + parseEntityName({ kind: 'a', namespace: '', name: 'c' }), + ).toThrow(); + expect(() => + parseEntityName({ kind: 'a', namespace: 'b', name: '' }), + ).toThrow(); + // two are empty + expect(() => + parseEntityName({ kind: '', namespace: '', name: 'c' }), + ).toThrow(); + expect(() => + parseEntityName({ kind: '', namespace: 'b', name: '' }), + ).toThrow(); + expect(() => + parseEntityName({ kind: 'a', namespace: '', name: '' }), + ).toThrow(); + // three are empty + expect(() => + parseEntityName({ kind: '', namespace: '', name: '' }), + ).toThrow(); + // one is left out, one empty + expect(() => parseEntityName({ namespace: '', name: 'c' })).toThrow(); + expect(() => parseEntityName({ namespace: 'b', name: '' })).toThrow(); + expect(() => parseEntityName({ kind: '', name: 'c' })).toThrow(); + expect(() => parseEntityName({ kind: 'a', name: '' })).toThrow(); + // nothing at all + expect(() => parseEntityName({} as any)).toThrow(); + }); + + it('adds defaults where necessary to strings', () => { + expect( + parseEntityName('a:b/c', { defaultKind: 'x', defaultNamespace: 'y' }), + ).toEqual({ kind: 'a', namespace: 'b', name: 'c' }); + expect( + parseEntityName('b/c', { defaultKind: 'x', defaultNamespace: 'y' }), + ).toEqual({ kind: 'x', namespace: 'b', name: 'c' }); + expect( + parseEntityName('a:c', { defaultKind: 'x', defaultNamespace: 'y' }), + ).toEqual({ kind: 'a', namespace: 'y', name: 'c' }); + expect(parseEntityName('a:c', { defaultKind: 'x' })).toEqual({ + kind: 'a', + namespace: 'default', + name: 'c', + }); + expect( + parseEntityName('c', { defaultKind: 'x', defaultNamespace: 'y' }), + ).toEqual({ kind: 'x', namespace: 'y', name: 'c' }); + expect(parseEntityName('c', { defaultKind: 'x' })).toEqual({ + kind: 'x', + namespace: 'default', + name: 'c', + }); + }); + + it('adds defaults where necessary to compounds', () => { + expect( + parseEntityName( + { kind: 'a', namespace: 'b', name: 'c' }, + { defaultKind: 'x', defaultNamespace: 'y' }, + ), + ).toEqual({ kind: 'a', namespace: 'b', name: 'c' }); + expect( + parseEntityName( + { namespace: 'b', name: 'c' }, + { defaultKind: 'x', defaultNamespace: 'y' }, + ), + ).toEqual({ kind: 'x', namespace: 'b', name: 'c' }); + expect( + parseEntityName( + { kind: 'a', name: 'c' }, + { defaultKind: 'x', defaultNamespace: 'y' }, + ), + ).toEqual({ kind: 'a', namespace: 'y', name: 'c' }); + expect( + parseEntityName({ kind: 'a', name: 'c' }, { defaultKind: 'x' }), + ).toEqual({ kind: 'a', namespace: 'default', name: 'c' }); + expect( + parseEntityName( + { name: 'c' }, + { defaultKind: 'x', defaultNamespace: 'y' }, + ), + ).toEqual({ kind: 'x', namespace: 'y', name: 'c' }); + expect(parseEntityName({ name: 'c' }, { defaultKind: 'x' })).toEqual({ + kind: 'x', + namespace: 'default', + name: 'c', + }); + // empty strings are errors, not defaults + expect(() => + parseEntityName( + { kind: '', namespace: 'b', name: 'c' }, + { defaultKind: 'x', defaultNamespace: 'y' }, + ), + ).toThrow(/kind/); + expect(() => + parseEntityName( + { kind: 'a', namespace: '', name: 'c' }, + { defaultKind: 'x', defaultNamespace: 'y' }, + ), + ).toThrow(/namespace/); + }); + }); + + describe('parseEntityRef', () => { + it('handles some omissions', () => { + expect(parseEntityRef('a:b/c')).toEqual({ + kind: 'a', + namespace: 'b', + name: 'c', + }); + expect(parseEntityRef('b/c')).toEqual({ + kind: undefined, + namespace: 'b', + name: 'c', + }); + expect(parseEntityRef('a:c')).toEqual({ + kind: 'a', + namespace: undefined, + name: 'c', + }); + expect(parseEntityRef('c')).toEqual({ + kind: undefined, + namespace: undefined, + name: 'c', + }); + }); + + it('rejects bad inputs', () => { + expect(() => parseEntityRef(null as any)).toThrow(); + expect(() => parseEntityRef(7 as any)).toThrow(); + expect(() => parseEntityRef('a:b:c')).toThrow(); + expect(() => parseEntityRef('a/b/c')).toThrow(); + expect(() => parseEntityRef('a/b:c')).toThrow(); + expect(() => parseEntityRef('a:b/c/d')).toThrow(); + expect(() => parseEntityRef('a:b/c:d')).toThrow(); + }); + + it('rejects empty parts in strings', () => { + // one is empty + expect(() => parseEntityRef(':b/c')).toThrow(); + expect(() => parseEntityRef('a:/c')).toThrow(); + expect(() => parseEntityRef('a:b/')).toThrow(); + // two are empty + expect(() => parseEntityRef('a:/')).toThrow(); + expect(() => parseEntityRef(':b/')).toThrow(); + expect(() => parseEntityRef(':/c')).toThrow(); + // three are empty + expect(() => parseEntityRef(':/')).toThrow(); + // one is left out, one empty + expect(() => parseEntityRef('/c')).toThrow(); + expect(() => parseEntityRef('b/')).toThrow(); + expect(() => parseEntityRef(':c')).toThrow(); + expect(() => parseEntityRef('a:')).toThrow(); + // nothing at all + expect(() => parseEntityRef('')).toThrow(); + }); + + it('rejects empty parts in compounds', () => { + // one is empty + expect(() => + parseEntityRef({ kind: '', namespace: 'b', name: 'c' }), + ).toThrow(); + expect(() => + parseEntityRef({ kind: 'a', namespace: '', name: 'c' }), + ).toThrow(); + expect(() => + parseEntityRef({ kind: 'a', namespace: 'b', name: '' }), + ).toThrow(); + // two are empty + expect(() => + parseEntityRef({ kind: '', namespace: '', name: 'c' }), + ).toThrow(); + expect(() => + parseEntityRef({ kind: '', namespace: 'b', name: '' }), + ).toThrow(); + expect(() => + parseEntityRef({ kind: 'a', namespace: '', name: '' }), + ).toThrow(); + // three are empty + expect(() => + parseEntityRef({ kind: '', namespace: '', name: '' }), + ).toThrow(); + // one is left out, one empty + expect(() => parseEntityRef({ namespace: '', name: 'c' })).toThrow(); + expect(() => parseEntityRef({ namespace: 'b', name: '' })).toThrow(); + expect(() => parseEntityRef({ kind: '', name: 'c' })).toThrow(); + expect(() => parseEntityRef({ kind: 'a', name: '' })).toThrow(); + // nothing at all + expect(() => parseEntityRef({} as any)).toThrow(); + }); + + it('adds defaults where necessary to strings', () => { + expect( + parseEntityRef('a:b/c', { defaultKind: 'x', defaultNamespace: 'y' }), + ).toEqual({ kind: 'a', namespace: 'b', name: 'c' }); + expect( + parseEntityRef('b/c', { defaultKind: 'x', defaultNamespace: 'y' }), + ).toEqual({ kind: 'x', namespace: 'b', name: 'c' }); + expect( + parseEntityRef('a:c', { defaultKind: 'x', defaultNamespace: 'y' }), + ).toEqual({ kind: 'a', namespace: 'y', name: 'c' }); + expect( + parseEntityRef('c', { defaultKind: 'x', defaultNamespace: 'y' }), + ).toEqual({ kind: 'x', namespace: 'y', name: 'c' }); + }); + + it('adds defaults where necessary to compounds', () => { + expect( + parseEntityRef( + { kind: 'a', namespace: 'b', name: 'c' }, + { defaultKind: 'x', defaultNamespace: 'y' }, + ), + ).toEqual({ kind: 'a', namespace: 'b', name: 'c' }); + expect( + parseEntityRef( + { namespace: 'b', name: 'c' }, + { defaultKind: 'x', defaultNamespace: 'y' }, + ), + ).toEqual({ kind: 'x', namespace: 'b', name: 'c' }); + expect( + parseEntityRef( + { kind: 'a', name: 'c' }, + { defaultKind: 'x', defaultNamespace: 'y' }, + ), + ).toEqual({ kind: 'a', namespace: 'y', name: 'c' }); + expect( + parseEntityRef( + { name: 'c' }, + { defaultKind: 'x', defaultNamespace: 'y' }, + ), + ).toEqual({ kind: 'x', namespace: 'y', name: 'c' }); + // empty strings are errors, not defaults + expect(() => + parseEntityRef( + { kind: '', namespace: 'b', name: 'c' }, + { defaultKind: 'x', defaultNamespace: 'y' }, + ), + ).toThrow(/kind/); + expect(() => + parseEntityRef( + { kind: 'a', namespace: '', name: 'c' }, + { defaultKind: 'x', defaultNamespace: 'y' }, + ), + ).toThrow(/namespace/); + }); + }); + + describe('serializeEntityRef', () => { + it('handles partials', () => { + expect( + serializeEntityRef({ kind: 'a', namespace: 'b', name: 'c' }), + ).toEqual('a:b/c'); + expect(serializeEntityRef({ namespace: 'b', name: 'c' })).toEqual('b/c'); + expect(serializeEntityRef({ kind: 'a', name: 'c' })).toEqual('a:c'); + expect(serializeEntityRef({ name: 'c' })).toEqual('c'); + }); + + it('picks the least complex form', () => { + expect( + serializeEntityRef({ kind: 'a', namespace: 'b', name: 'c' }), + ).toEqual('a:b/c'); + expect(serializeEntityRef({ namespace: 'b', name: 'c' })).toEqual('b/c'); + expect(serializeEntityRef({ kind: 'a', name: 'c' })).toEqual('a:c'); + expect(serializeEntityRef({ name: 'c' })).toEqual('c'); + expect( + serializeEntityRef({ kind: 'a:x', namespace: 'b', name: 'c' }), + ).toEqual({ kind: 'a:x', namespace: 'b', name: 'c' }); + expect( + serializeEntityRef({ kind: 'a/x', namespace: 'b', name: 'c' }), + ).toEqual({ kind: 'a/x', namespace: 'b', name: 'c' }); + expect( + serializeEntityRef({ kind: 'a', namespace: 'b:x', name: 'c' }), + ).toEqual({ kind: 'a', namespace: 'b:x', name: 'c' }); + expect( + serializeEntityRef({ kind: 'a', namespace: 'b/x', name: 'c' }), + ).toEqual({ kind: 'a', namespace: 'b/x', name: 'c' }); + expect( + serializeEntityRef({ kind: 'a', namespace: 'b', name: 'c:x' }), + ).toEqual({ kind: 'a', namespace: 'b', name: 'c:x' }); + expect( + serializeEntityRef({ kind: 'a', namespace: 'b', name: 'c/x' }), + ).toEqual({ kind: 'a', namespace: 'b', name: 'c/x' }); + }); + }); +}); diff --git a/packages/catalog-model/src/entity/ref.ts b/packages/catalog-model/src/entity/ref.ts new file mode 100644 index 0000000000..1a579eeb7e --- /dev/null +++ b/packages/catalog-model/src/entity/ref.ts @@ -0,0 +1,146 @@ +/* + * 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 { CompoundEntityRef, EntityName, EntityRef } from '../types'; + +/** + * The context of defaults that entity reference parsing happens within. + */ +export type EntityRefContext = { + /** The default kind, if none is given in the reference */ + defaultKind?: string; + /** The default namespace, if none is given in the reference */ + defaultNamespace?: string; +}; + +/** + * Parses an entity reference, either on string or compound form, and always + * returns a complete entity name including kind, namespace and name. + * + * This function automatically assumes the default namespace "default" unless + * otherwise specified as part of the options, and will throw an error if no + * kind was specified in the input reference and no default kind was given. + * + * @param ref The reference to parse + * @param context The context of defaults that the parsing happens within + * @returns A complete entity name + */ +export function parseEntityName( + ref: EntityRef, + context: EntityRefContext = {}, +): EntityName { + const { kind, namespace, name } = parseEntityRef(ref, { + defaultNamespace: 'default', + ...context, + }); + + if (!kind) { + throw new Error( + `Entity reference ${namespace}/${name} did not contain a kind`, + ); + } + + // This is a corner case - when the ref contained a namespace but it was the + // empty string (so the default did not kick in) + if (!namespace) { + throw new Error( + `Entity reference ${kind}:${name} did not contain a namespace`, + ); + } + + return { + kind, + namespace: namespace!, + name, + }; +} + +/** + * Parses an entity reference, either on string or compound form, and returns + * a structure with a name, and optional kind and namespace. + * + * The options object can contain default values for the kind and namespace, + * that will be used if the input reference did not specify any. + * + * @param ref The reference to parse + * @param context The context of defaults that the parsing happens within + * @returns The compound form of the reference + */ +export function parseEntityRef( + ref: EntityRef, + context: EntityRefContext = {}, +): CompoundEntityRef { + if (!ref) { + throw new Error(`Entity reference must not be empty`); + } + + if (typeof ref === 'string') { + const match = /^(?:([^:/]+):)?(?:([^:/]+)\/)?([^:/]+)$/.exec(ref.trim()); + if (!match) { + throw new Error( + `Entity reference "${ref}" was not on the form [:][/]`, + ); + } + + return { + kind: match[1] ?? context.defaultKind, + namespace: match[2] ?? context.defaultNamespace, + name: match[3], + }; + } + + const { kind, namespace, name } = ref; + if (kind === '') { + throw new Error('Entity reference kinds must not be empty'); + } else if (namespace === '') { + throw new Error('Entity reference namespaces must not be empty'); + } else if (!name) { + throw new Error('Entity references must contain a name'); + } + + return { + kind: kind ?? context.defaultKind, + namespace: namespace ?? context.defaultNamespace, + name, + }; +} + +/** + * Takes an entity reference or name, and outputs an entity reference on the + * most compact form possible. I.e. if the parts do not contain any + * special/reserved characters, it outputs the string form, otherwise it + * outputs the compound form. + * + * @param ref The reference to serialize + * @returns The same reference on either string or compound form + */ +export function serializeEntityRef( + ref: CompoundEntityRef | EntityName, +): EntityRef { + const { kind, namespace, name } = ref; + if ( + kind?.includes(':') || + kind?.includes('/') || + namespace?.includes(':') || + namespace?.includes('/') || + name.includes(':') || + name.includes('/') + ) { + return { kind, namespace, name }; + } + + return `${kind ? `${kind}:` : ''}${namespace ? `${namespace}/` : ''}${name}`; +} diff --git a/packages/catalog-model/src/index.ts b/packages/catalog-model/src/index.ts index 9469319b28..16e7e3477b 100644 --- a/packages/catalog-model/src/index.ts +++ b/packages/catalog-model/src/index.ts @@ -18,5 +18,11 @@ export * from './entity'; export { EntityPolicies } from './EntityPolicies'; export * from './kinds'; export * from './location'; -export type { EntityPolicy, JSONSchema } from './types'; +export type { + CompoundEntityRef, + EntityName, + EntityPolicy, + EntityRef, + JSONSchema, +} from './types'; export * from './validation'; diff --git a/packages/catalog-model/src/types.ts b/packages/catalog-model/src/types.ts index cba6438ccb..1e0a778827 100644 --- a/packages/catalog-model/src/types.ts +++ b/packages/catalog-model/src/types.ts @@ -34,3 +34,31 @@ export type EntityPolicy = { }; export type JSONSchema = JSONSchema7 & { [key in string]?: JsonValue }; + +/** + * A complete entity name, with the full kind-namespace-name triplet. + */ +export type EntityName = { + kind: string; + namespace: string; + name: string; +}; + +/** + * A reference by name to an entity, where the kind and/or the namespace can be + * left out. + * + * Left-out parts of the reference need to be handled by the application, + * either by rejecting the reference or by falling back to default values. + */ +export type CompoundEntityRef = { + kind?: string; + namespace?: string; + name: string; +}; + +/** + * A reference by name to an entity, either as a compact string representation, + * or as a compound reference structure. + */ +export type EntityRef = string | CompoundEntityRef; From e52a6daae83e1c7798d27891dcbd03d4d306b031 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 19 Sep 2020 13:17:08 +0200 Subject: [PATCH 38/85] core-api/routing: initial RouteRefRegistry implementation --- .../src/routing/RouteRefRegistry.test.ts | 67 ++++++++++++ .../core-api/src/routing/RouteRefRegistry.ts | 103 ++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 packages/core-api/src/routing/RouteRefRegistry.test.ts create mode 100644 packages/core-api/src/routing/RouteRefRegistry.ts diff --git a/packages/core-api/src/routing/RouteRefRegistry.test.ts b/packages/core-api/src/routing/RouteRefRegistry.test.ts new file mode 100644 index 0000000000..addf7dec80 --- /dev/null +++ b/packages/core-api/src/routing/RouteRefRegistry.test.ts @@ -0,0 +1,67 @@ +/* + * 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 { RouteRefRegistry } from './RouteRefRegistry'; + +const ref1 = {}; +const ref11 = {}; +const ref12 = {}; +const ref121 = {}; +const ref2 = {}; + +describe('RouteRefRegistry', () => { + it('should be constructed with a root route', () => { + const registry = new RouteRefRegistry(); + expect(registry.resolveRoute([], [])).toBe(''); + }); + + it('should register and resolve some routes', () => { + const registry = new RouteRefRegistry(); + expect(registry.registerRoute([ref1], '1')).toBe(true); + expect(registry.registerRoute([ref1, ref11], '11')).toBe(true); + expect(registry.registerRoute([ref1, ref12], '12')).toBe(true); + expect(registry.registerRoute([ref1, ref12, ref121], '121')).toBe(true); + expect(registry.registerRoute([ref1, ref12, ref121], 'duplicate')).toBe( + false, + ); + expect(registry.registerRoute([ref1, ref12], 'duplicate')).toBe(false); + expect(registry.registerRoute([ref2], '2')).toBe(true); + expect(registry.registerRoute([ref2], 'duplicate')).toBe(false); + + expect(registry.resolveRoute([], [ref1])).toBe('1'); + expect(registry.resolveRoute([], [ref11])).toBe(undefined); + expect(registry.resolveRoute([], [ref1, ref11])).toBe('11'); + expect(registry.resolveRoute([ref1], [ref11])).toBe('11'); + expect(registry.resolveRoute([ref1], [ref2])).toBe('2'); + expect(registry.resolveRoute([ref1, ref12, ref121], [])).toBe('121'); + expect(registry.resolveRoute([ref1, ref12, ref121], [ref121])).toBe('121'); + expect(registry.resolveRoute([ref1, ref12, ref121], [ref12, ref121])).toBe( + '121', + ); + expect(registry.resolveRoute([ref1, ref12, ref121], [ref12])).toBe('12'); + expect(registry.resolveRoute([ref1, ref12, ref121], [ref1])).toBe('1'); + }); + + it('should throw when registering routes incorrectly', () => { + const registry = new RouteRefRegistry(); + expect(() => { + registry.registerRoute([ref1, ref11], '11'); + }).toThrow('Could not find parent for new routing node'); + expect(() => { + registry.registerRoute([], '11'); + }).toThrow('Must provide at least 1 route to add routing node'); + }); +}); diff --git a/packages/core-api/src/routing/RouteRefRegistry.ts b/packages/core-api/src/routing/RouteRefRegistry.ts new file mode 100644 index 0000000000..3b6cb9e768 --- /dev/null +++ b/packages/core-api/src/routing/RouteRefRegistry.ts @@ -0,0 +1,103 @@ +/* + * 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 type RouteRefResolver = { + resolveRoute(from: unknown[], to: unknown[]): string; +}; + +class Node { + readonly children = new Map(); + + constructor(readonly path: string, readonly parent: Node | undefined) {} + + /** + * Look up a node in the tree given a path. + */ + findNode(routes: unknown[]): Node | undefined { + // eslint-disable-next-line consistent-this + let node: Node | undefined = this; + + for (let i = 0; i < routes.length; i++) { + node = node?.children.get(routes[i]); + } + + return node; + } + + /** + * Assigns a path to a leaf node in the routing tree. All ancestor + * nodes of the new leaf node must already exist, or an error will be thrown. + * + * Returns true if the node was added, or false if the node already existed. + */ + addNode(routes: unknown[], path: string): boolean { + if (routes.length === 0) { + throw new Error('Must provide at least 1 route to add routing node'); + } + + const parentNode = this.findNode(routes.slice(0, -1)); + if (!parentNode) { + throw new Error('Could not find parent for new routing node'); + } + + const lastRoute = routes[routes.length - 1]; + if (parentNode.children.has(lastRoute)) { + return false; + } + + parentNode.children.set(lastRoute, new Node(path, parentNode)); + return true; + } +} + +/** + * A registry for resolving route refs into concrete string routes. + */ +export class RouteRefRegistry { + private readonly root = new Node('', undefined); + + /** + * Register a new leaf path for a sequence of routes. All ancestor + * routes must already exist. + */ + registerRoute(routes: unknown[], path: string): boolean { + return this.root.addNode(routes, path); + } + + /** + * Resolve a route from a point in the routing tree. + * + * The route referenced by `from` must exist, and is the starting + * point for the search, walking up the tree until a subtree that + * matches the routes reference in `to` are found. + * + * If `from` is empty, the search starts and ends at the root node. + * If `to` is empty, the route referenced by `from` will always be returned. + */ + resolveRoute(from: unknown[], to: unknown[]): string | undefined { + let fromNode = this.root.findNode(from); + + while (fromNode) { + const resolvedNode = fromNode.findNode(to); + if (resolvedNode) { + return resolvedNode.path; + } + fromNode = fromNode.parent; + } + + return undefined; + } +} From 558ae5593ecf1f2d823b37df1da25342cd40062a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 19 Sep 2020 14:56:27 +0200 Subject: [PATCH 39/85] core-api/routing: implement parameterized and absolute path resolution --- .../src/routing/RouteRefRegistry.test.ts | 34 +++++----- .../core-api/src/routing/RouteRefRegistry.ts | 67 ++++++++++++++++--- 2 files changed, 74 insertions(+), 27 deletions(-) diff --git a/packages/core-api/src/routing/RouteRefRegistry.test.ts b/packages/core-api/src/routing/RouteRefRegistry.test.ts index addf7dec80..7547de1eb4 100644 --- a/packages/core-api/src/routing/RouteRefRegistry.test.ts +++ b/packages/core-api/src/routing/RouteRefRegistry.test.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { RouteRefRegistry } from './RouteRefRegistry'; +import { RouteRefRegistry, resolveRoute } from './RouteRefRegistry'; -const ref1 = {}; -const ref11 = {}; -const ref12 = {}; -const ref121 = {}; -const ref2 = {}; +const ref1 = { [resolveRoute]: (path: string) => path }; +const ref11 = { [resolveRoute]: (path: string) => path }; +const ref12 = { [resolveRoute]: (path: string) => path }; +const ref121 = { [resolveRoute]: (path: string) => path }; +const ref2 = { [resolveRoute]: (path: string) => path }; describe('RouteRefRegistry', () => { it('should be constructed with a root route', () => { @@ -41,18 +41,20 @@ describe('RouteRefRegistry', () => { expect(registry.registerRoute([ref2], '2')).toBe(true); expect(registry.registerRoute([ref2], 'duplicate')).toBe(false); - expect(registry.resolveRoute([], [ref1])).toBe('1'); + expect(registry.resolveRoute([], [ref1])).toBe('/1'); expect(registry.resolveRoute([], [ref11])).toBe(undefined); - expect(registry.resolveRoute([], [ref1, ref11])).toBe('11'); - expect(registry.resolveRoute([ref1], [ref11])).toBe('11'); - expect(registry.resolveRoute([ref1], [ref2])).toBe('2'); - expect(registry.resolveRoute([ref1, ref12, ref121], [])).toBe('121'); - expect(registry.resolveRoute([ref1, ref12, ref121], [ref121])).toBe('121'); - expect(registry.resolveRoute([ref1, ref12, ref121], [ref12, ref121])).toBe( - '121', + expect(registry.resolveRoute([], [ref1, ref11])).toBe('/1/11'); + expect(registry.resolveRoute([ref1], [ref11])).toBe('/1/11'); + expect(registry.resolveRoute([ref1], [ref2])).toBe('/2'); + expect(registry.resolveRoute([ref1, ref12, ref121], [])).toBe('/1/12/121'); + expect(registry.resolveRoute([ref1, ref12, ref121], [ref121])).toBe( + '/1/12/121', ); - expect(registry.resolveRoute([ref1, ref12, ref121], [ref12])).toBe('12'); - expect(registry.resolveRoute([ref1, ref12, ref121], [ref1])).toBe('1'); + expect(registry.resolveRoute([ref1, ref12, ref121], [ref12, ref121])).toBe( + '/1/12/121', + ); + expect(registry.resolveRoute([ref1, ref12, ref121], [ref12])).toBe('/1/12'); + expect(registry.resolveRoute([ref1, ref12, ref121], [ref1])).toBe('/1'); }); it('should throw when registering routes incorrectly', () => { diff --git a/packages/core-api/src/routing/RouteRefRegistry.ts b/packages/core-api/src/routing/RouteRefRegistry.ts index 3b6cb9e768..7d6260f78a 100644 --- a/packages/core-api/src/routing/RouteRefRegistry.ts +++ b/packages/core-api/src/routing/RouteRefRegistry.ts @@ -14,21 +14,30 @@ * limitations under the License. */ +export const resolveRoute = Symbol('resolve-route'); + +type ConcreteRoute = { + [resolveRoute](path: string): string; +}; + +const rootRoute: ConcreteRoute = { + [resolveRoute]: () => '', +}; + export type RouteRefResolver = { - resolveRoute(from: unknown[], to: unknown[]): string; + resolveRoute(from: ConcreteRoute[], to: ConcreteRoute[]): string; }; class Node { - readonly children = new Map(); + readonly children = new Map(); constructor(readonly path: string, readonly parent: Node | undefined) {} /** * Look up a node in the tree given a path. */ - findNode(routes: unknown[]): Node | undefined { - // eslint-disable-next-line consistent-this - let node: Node | undefined = this; + findNode(routes: ConcreteRoute[]): Node | undefined { + let node = this as Node | undefined; for (let i = 0; i < routes.length; i++) { node = node?.children.get(routes[i]); @@ -43,7 +52,7 @@ class Node { * * Returns true if the node was added, or false if the node already existed. */ - addNode(routes: unknown[], path: string): boolean { + addNode(routes: ConcreteRoute[], path: string): boolean { if (routes.length === 0) { throw new Error('Must provide at least 1 route to add routing node'); } @@ -61,6 +70,35 @@ class Node { parentNode.children.set(lastRoute, new Node(path, parentNode)); return true; } + + /** + * Resolve an absolute URL that represents this node in the routing tree, using + * using the supplied concrete routes and ancestors of this node. + * + * The length of the provided routes array must match the depth of + * the routing tree that this node is at, or an error will be thrown. + */ + resolve(routes: ConcreteRoute[]) { + const parts = Array(routes.length); + + let node = this as Node | undefined; + for (let i = routes.length - 1; i >= 0; i--) { + if (!node) { + throw new Error('Route resolve missing required parent'); + } + + const route = routes[i]; + parts[i] = route[resolveRoute](node.path); + + node = node.parent; + } + + if (node) { + throw new Error('Route resolve did not reach root'); + } + + return parts.join('/'); + } } /** @@ -73,12 +111,12 @@ export class RouteRefRegistry { * Register a new leaf path for a sequence of routes. All ancestor * routes must already exist. */ - registerRoute(routes: unknown[], path: string): boolean { + registerRoute(routes: ConcreteRoute[], path: string): boolean { return this.root.addNode(routes, path); } /** - * Resolve a route from a point in the routing tree. + * Resolve an absolute path from a point in the routing tree. * * The route referenced by `from` must exist, and is the starting * point for the search, walking up the tree until a subtree that @@ -87,14 +125,21 @@ export class RouteRefRegistry { * If `from` is empty, the search starts and ends at the root node. * If `to` is empty, the route referenced by `from` will always be returned. */ - resolveRoute(from: unknown[], to: unknown[]): string | undefined { - let fromNode = this.root.findNode(from); + resolveRoute(from: ConcreteRoute[], to: ConcreteRoute[]): string | undefined { + // Keep track of the `from` routes and pop the last ones as we traverse up + // the routing tree. The list of concrete routes that we're passing to + // `node.resolve()` should only include the ones in the resolve path. + const concreteStack = from.slice(); + let fromNode = this.root.findNode(from); while (fromNode) { const resolvedNode = fromNode.findNode(to); if (resolvedNode) { - return resolvedNode.path; + return resolvedNode.resolve([rootRoute].concat(concreteStack, to)); } + + // Search at this level of the tree failed, move up to parent + concreteStack.pop(); fromNode = fromNode.parent; } From 988ebe7139d332b648a5a7d77d9b71be07373b26 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 19 Sep 2020 15:16:24 +0200 Subject: [PATCH 40/85] core-api/routing: make RouteRefs immutable + move out some types --- packages/core-api/src/routing/RouteRef.ts | 21 +++++++------------ .../src/routing/RouteRefRegistry.test.ts | 3 ++- .../core-api/src/routing/RouteRefRegistry.ts | 6 +----- packages/core-api/src/routing/index.ts | 3 +-- packages/core-api/src/routing/types.ts | 13 ++++++------ 5 files changed, 19 insertions(+), 27 deletions(-) diff --git a/packages/core-api/src/routing/RouteRef.ts b/packages/core-api/src/routing/RouteRef.ts index 9f9980ed0c..8a03680791 100644 --- a/packages/core-api/src/routing/RouteRef.ts +++ b/packages/core-api/src/routing/RouteRef.ts @@ -14,30 +14,25 @@ * limitations under the License. */ -import type { RouteRefConfig, RouteRefOverrideConfig } from './types'; - -export class MutableRouteRef { - private effectiveConfig: RouteRefConfig = this.config; +import type { RouteRefConfig } from './types'; +export class AbsoluteRouteRef { constructor(private readonly config: RouteRefConfig) {} - override(overrideConfig: RouteRefOverrideConfig) { - this.effectiveConfig = { ...this.config, ...overrideConfig }; - } - get icon() { - return this.effectiveConfig.icon; + return this.config.icon; } + // TODO(Rugvip): Remove this, routes are looked up via the registry instead get path() { - return this.effectiveConfig.path; + return this.config.path; } get title() { - return this.effectiveConfig.title; + return this.config.title; } } -export function createRouteRef(config: RouteRefConfig): MutableRouteRef { - return new MutableRouteRef(config); +export function createRouteRef(config: RouteRefConfig): AbsoluteRouteRef { + return new AbsoluteRouteRef(config); } diff --git a/packages/core-api/src/routing/RouteRefRegistry.test.ts b/packages/core-api/src/routing/RouteRefRegistry.test.ts index 7547de1eb4..76272cb9f7 100644 --- a/packages/core-api/src/routing/RouteRefRegistry.test.ts +++ b/packages/core-api/src/routing/RouteRefRegistry.test.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { RouteRefRegistry, resolveRoute } from './RouteRefRegistry'; +import { RouteRefRegistry } from './RouteRefRegistry'; +import { resolveRoute } from './types'; const ref1 = { [resolveRoute]: (path: string) => path }; const ref11 = { [resolveRoute]: (path: string) => path }; diff --git a/packages/core-api/src/routing/RouteRefRegistry.ts b/packages/core-api/src/routing/RouteRefRegistry.ts index 7d6260f78a..3b9bf4a7a9 100644 --- a/packages/core-api/src/routing/RouteRefRegistry.ts +++ b/packages/core-api/src/routing/RouteRefRegistry.ts @@ -14,11 +14,7 @@ * limitations under the License. */ -export const resolveRoute = Symbol('resolve-route'); - -type ConcreteRoute = { - [resolveRoute](path: string): string; -}; +import { ConcreteRoute, resolveRoute } from './types'; const rootRoute: ConcreteRoute = { [resolveRoute]: () => '', diff --git a/packages/core-api/src/routing/index.ts b/packages/core-api/src/routing/index.ts index 67d4c82167..1b80c0838e 100644 --- a/packages/core-api/src/routing/index.ts +++ b/packages/core-api/src/routing/index.ts @@ -14,6 +14,5 @@ * limitations under the License. */ -export * from './types'; +export type { RouteRef, RouteRefConfig, ConcreteRoute } from './types'; export { createRouteRef } from './RouteRef'; -export type { MutableRouteRef } from './RouteRef'; diff --git a/packages/core-api/src/routing/types.ts b/packages/core-api/src/routing/types.ts index 515dc31de6..9e5e2bb3a7 100644 --- a/packages/core-api/src/routing/types.ts +++ b/packages/core-api/src/routing/types.ts @@ -16,7 +16,14 @@ import { IconComponent } from '../icons'; +export const resolveRoute = Symbol('resolve-route'); + +export type ConcreteRoute = { + [resolveRoute](path: string): string; +}; + export type RouteRef = { + // TODO(Rugvip): Remove path, look up via registry instead path: string; icon?: IconComponent; title: string; @@ -27,9 +34,3 @@ export type RouteRefConfig = { icon?: IconComponent; title: string; }; - -export type RouteRefOverrideConfig = { - path?: string; - icon?: IconComponent; - title?: string; -}; From 9155138168be2014c2b17ed831f00c6345b65718 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 19 Sep 2020 15:21:43 +0200 Subject: [PATCH 41/85] core-api/routing: make RouteRef implement ConcreteRoute and use in registry test --- packages/core-api/src/routing/RouteRef.ts | 8 ++++++-- .../core-api/src/routing/RouteRefRegistry.test.ts | 13 +++++++------ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/core-api/src/routing/RouteRef.ts b/packages/core-api/src/routing/RouteRef.ts index 8a03680791..cc36446f8b 100644 --- a/packages/core-api/src/routing/RouteRef.ts +++ b/packages/core-api/src/routing/RouteRef.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import type { RouteRefConfig } from './types'; +import { ConcreteRoute, resolveRoute, RouteRefConfig } from './types'; -export class AbsoluteRouteRef { +export class AbsoluteRouteRef implements ConcreteRoute { constructor(private readonly config: RouteRefConfig) {} get icon() { @@ -31,6 +31,10 @@ export class AbsoluteRouteRef { get title() { return this.config.title; } + + [resolveRoute](path: string) { + return path; + } } export function createRouteRef(config: RouteRefConfig): AbsoluteRouteRef { diff --git a/packages/core-api/src/routing/RouteRefRegistry.test.ts b/packages/core-api/src/routing/RouteRefRegistry.test.ts index 76272cb9f7..91bbc70117 100644 --- a/packages/core-api/src/routing/RouteRefRegistry.test.ts +++ b/packages/core-api/src/routing/RouteRefRegistry.test.ts @@ -15,13 +15,14 @@ */ import { RouteRefRegistry } from './RouteRefRegistry'; -import { resolveRoute } from './types'; +import { createRouteRef } from './RouteRef'; -const ref1 = { [resolveRoute]: (path: string) => path }; -const ref11 = { [resolveRoute]: (path: string) => path }; -const ref12 = { [resolveRoute]: (path: string) => path }; -const ref121 = { [resolveRoute]: (path: string) => path }; -const ref2 = { [resolveRoute]: (path: string) => path }; +const dummyConfig = { path: '/', icon: null, title: 'my-title' }; +const ref1 = createRouteRef(dummyConfig); +const ref11 = createRouteRef(dummyConfig); +const ref12 = createRouteRef(dummyConfig); +const ref121 = createRouteRef(dummyConfig); +const ref2 = createRouteRef(dummyConfig); describe('RouteRefRegistry', () => { it('should be constructed with a root route', () => { From 0f3e9b29d44e1add0693a79abdcdbed11051be96 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 19 Sep 2020 16:32:41 +0200 Subject: [PATCH 42/85] core-api/routing: initial subroute implementation --- packages/core-api/src/routing/RouteRef.ts | 41 ++++++++++++++++++- .../src/routing/RouteRefRegistry.test.ts | 39 +++++++++++++++++- .../core-api/src/routing/RouteRefRegistry.ts | 20 +++++---- packages/core-api/src/routing/types.ts | 7 +++- 4 files changed, 95 insertions(+), 12 deletions(-) diff --git a/packages/core-api/src/routing/RouteRef.ts b/packages/core-api/src/routing/RouteRef.ts index cc36446f8b..51bb9ee1a4 100644 --- a/packages/core-api/src/routing/RouteRef.ts +++ b/packages/core-api/src/routing/RouteRef.ts @@ -14,7 +14,36 @@ * limitations under the License. */ -import { ConcreteRoute, resolveRoute, RouteRefConfig } from './types'; +import { ConcreteRoute, ref, resolveRoute, RouteRefConfig } from './types'; +import { generatePath } from 'react-router-dom'; + +type SubRouteConfig = { + path: string; +}; + +export class SubRouteRef { + constructor( + private readonly parent: ConcreteRoute, + private readonly config: SubRouteConfig, + ) {} + + [ref]() { + return this; + } + + link(params: T): ConcreteRoute { + const selfRef = this as unknown; + + return { + [ref]: () => selfRef, + [resolveRoute]: (path: string) => { + const ownPart = generatePath(this.config.path, params); + const parentPart = this.parent[resolveRoute](path); + return parentPart + ownPart; + }, + }; + } +} export class AbsoluteRouteRef implements ConcreteRoute { constructor(private readonly config: RouteRefConfig) {} @@ -32,6 +61,16 @@ export class AbsoluteRouteRef implements ConcreteRoute { return this.config.title; } + createSubRoute( + config: SubRouteConfig, + ) { + return new SubRouteRef(this, config); + } + + [ref]() { + return this; + } + [resolveRoute](path: string) { return path; } diff --git a/packages/core-api/src/routing/RouteRefRegistry.test.ts b/packages/core-api/src/routing/RouteRefRegistry.test.ts index 91bbc70117..2849bcf32b 100644 --- a/packages/core-api/src/routing/RouteRefRegistry.test.ts +++ b/packages/core-api/src/routing/RouteRefRegistry.test.ts @@ -17,12 +17,14 @@ import { RouteRefRegistry } from './RouteRefRegistry'; import { createRouteRef } from './RouteRef'; -const dummyConfig = { path: '/', icon: null, title: 'my-title' }; +const dummyConfig = { path: '/', icon: () => null, title: 'my-title' }; const ref1 = createRouteRef(dummyConfig); const ref11 = createRouteRef(dummyConfig); const ref12 = createRouteRef(dummyConfig); const ref121 = createRouteRef(dummyConfig); const ref2 = createRouteRef(dummyConfig); +const ref2a = ref2.createSubRoute({ path: '/a' }); +const ref2b = ref2.createSubRoute<{ id: string }>({ path: '/b/:id' }); describe('RouteRefRegistry', () => { it('should be constructed with a root route', () => { @@ -30,7 +32,7 @@ describe('RouteRefRegistry', () => { expect(registry.resolveRoute([], [])).toBe(''); }); - it('should register and resolve some routes', () => { + it('should register and resolve some absolute routes', () => { const registry = new RouteRefRegistry(); expect(registry.registerRoute([ref1], '1')).toBe(true); expect(registry.registerRoute([ref1, ref11], '11')).toBe(true); @@ -59,6 +61,39 @@ describe('RouteRefRegistry', () => { expect(registry.resolveRoute([ref1, ref12, ref121], [ref1])).toBe('/1'); }); + it('should register and resolve with sub routes', () => { + const registry = new RouteRefRegistry(); + expect(registry.registerRoute([ref1], '1')).toBe(true); + expect(registry.registerRoute([ref2], '2')).toBe(true); + expect(registry.registerRoute([ref2a], '2')).toBe(true); + expect(registry.registerRoute([ref2a, ref1], '1')).toBe(true); + expect(registry.registerRoute([ref2a, ref2], '2')).toBe(true); + expect(registry.registerRoute([ref2b], '2')).toBe(true); + expect(registry.registerRoute([ref2b, ref1], '1')).toBe(true); + expect(registry.registerRoute([ref2b, ref2], '2')).toBe(true); + + expect(registry.resolveRoute([], [ref1])).toBe('/1'); + expect(registry.resolveRoute([], [ref2])).toBe('/2'); + expect(registry.resolveRoute([], [ref2a.link({}), ref1])).toBe('/2/a/1'); + expect(registry.resolveRoute([], [ref2a.link({}), ref2])).toBe('/2/a/2'); + expect(registry.resolveRoute([ref2a.link({})], [ref2])).toBe('/2/a/2'); + expect(registry.resolveRoute([ref2a.link({}), ref1], [ref2])).toBe( + '/2/a/2', + ); + expect(registry.resolveRoute([], [ref2b.link({ id: 'abc' }), ref1])).toBe( + '/2/b/abc/1', + ); + expect(registry.resolveRoute([], [ref2b.link({ id: 'xyz' }), ref2])).toBe( + '/2/b/xyz/2', + ); + expect(registry.resolveRoute([ref2b.link({ id: 'abc' })], [ref2])).toBe( + '/2/b/abc/2', + ); + expect( + registry.resolveRoute([ref2b.link({ id: 'abc' }), ref1], [ref2]), + ).toBe('/2/b/abc/2'); + }); + it('should throw when registering routes incorrectly', () => { const registry = new RouteRefRegistry(); expect(() => { diff --git a/packages/core-api/src/routing/RouteRefRegistry.ts b/packages/core-api/src/routing/RouteRefRegistry.ts index 3b9bf4a7a9..7377a90672 100644 --- a/packages/core-api/src/routing/RouteRefRegistry.ts +++ b/packages/core-api/src/routing/RouteRefRegistry.ts @@ -14,9 +14,12 @@ * limitations under the License. */ -import { ConcreteRoute, resolveRoute } from './types'; +import { ConcreteRoute, ref, resolveRoute, ReferencedRoute } from './types'; const rootRoute: ConcreteRoute = { + [ref]() { + return this; + }, [resolveRoute]: () => '', }; @@ -25,18 +28,18 @@ export type RouteRefResolver = { }; class Node { - readonly children = new Map(); + readonly children = new Map(); constructor(readonly path: string, readonly parent: Node | undefined) {} /** * Look up a node in the tree given a path. */ - findNode(routes: ConcreteRoute[]): Node | undefined { + findNode(routes: ReferencedRoute[]): Node | undefined { let node = this as Node | undefined; for (let i = 0; i < routes.length; i++) { - node = node?.children.get(routes[i]); + node = node?.children.get(routes[i][ref]()); } return node; @@ -48,7 +51,7 @@ class Node { * * Returns true if the node was added, or false if the node already existed. */ - addNode(routes: ConcreteRoute[], path: string): boolean { + addNode(routes: ReferencedRoute[], path: string): boolean { if (routes.length === 0) { throw new Error('Must provide at least 1 route to add routing node'); } @@ -59,11 +62,12 @@ class Node { } const lastRoute = routes[routes.length - 1]; - if (parentNode.children.has(lastRoute)) { + const lastRouteRef = lastRoute[ref](); + if (parentNode.children.has(lastRouteRef)) { return false; } - parentNode.children.set(lastRoute, new Node(path, parentNode)); + parentNode.children.set(lastRouteRef, new Node(path, parentNode)); return true; } @@ -107,7 +111,7 @@ export class RouteRefRegistry { * Register a new leaf path for a sequence of routes. All ancestor * routes must already exist. */ - registerRoute(routes: ConcreteRoute[], path: string): boolean { + registerRoute(routes: ReferencedRoute[], path: string): boolean { return this.root.addNode(routes, path); } diff --git a/packages/core-api/src/routing/types.ts b/packages/core-api/src/routing/types.ts index 9e5e2bb3a7..291029c3f2 100644 --- a/packages/core-api/src/routing/types.ts +++ b/packages/core-api/src/routing/types.ts @@ -17,8 +17,13 @@ import { IconComponent } from '../icons'; export const resolveRoute = Symbol('resolve-route'); +export const ref = Symbol('route-ref'); -export type ConcreteRoute = { +export type ReferencedRoute = { + [ref](): unknown; +}; + +export type ConcreteRoute = ReferencedRoute & { [resolveRoute](path: string): string; }; From 735c1277d1c327b299c5c4c90599e734e5be64c8 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 20 Sep 2020 13:21:55 +0200 Subject: [PATCH 43/85] core-api/routing: switch ref to prop and rename to routeReference --- packages/core-api/src/routing/RouteRef.ts | 19 ++++++++++++------- .../core-api/src/routing/RouteRefRegistry.ts | 13 +++++++++---- packages/core-api/src/routing/types.ts | 4 ++-- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/packages/core-api/src/routing/RouteRef.ts b/packages/core-api/src/routing/RouteRef.ts index 51bb9ee1a4..5e7a4294d9 100644 --- a/packages/core-api/src/routing/RouteRef.ts +++ b/packages/core-api/src/routing/RouteRef.ts @@ -14,28 +14,33 @@ * limitations under the License. */ -import { ConcreteRoute, ref, resolveRoute, RouteRefConfig } from './types'; +import { + ConcreteRoute, + routeReference, + ReferencedRoute, + resolveRoute, + RouteRefConfig, +} from './types'; import { generatePath } from 'react-router-dom'; type SubRouteConfig = { path: string; }; -export class SubRouteRef { +export class SubRouteRef + implements ReferencedRoute { constructor( private readonly parent: ConcreteRoute, private readonly config: SubRouteConfig, ) {} - [ref]() { + get [routeReference]() { return this; } link(params: T): ConcreteRoute { - const selfRef = this as unknown; - return { - [ref]: () => selfRef, + [routeReference]: this, [resolveRoute]: (path: string) => { const ownPart = generatePath(this.config.path, params); const parentPart = this.parent[resolveRoute](path); @@ -67,7 +72,7 @@ export class AbsoluteRouteRef implements ConcreteRoute { return new SubRouteRef(this, config); } - [ref]() { + get [routeReference]() { return this; } diff --git a/packages/core-api/src/routing/RouteRefRegistry.ts b/packages/core-api/src/routing/RouteRefRegistry.ts index 7377a90672..4bc824ac3f 100644 --- a/packages/core-api/src/routing/RouteRefRegistry.ts +++ b/packages/core-api/src/routing/RouteRefRegistry.ts @@ -14,10 +14,15 @@ * limitations under the License. */ -import { ConcreteRoute, ref, resolveRoute, ReferencedRoute } from './types'; +import { + ConcreteRoute, + routeReference, + resolveRoute, + ReferencedRoute, +} from './types'; const rootRoute: ConcreteRoute = { - [ref]() { + get [routeReference]() { return this; }, [resolveRoute]: () => '', @@ -39,7 +44,7 @@ class Node { let node = this as Node | undefined; for (let i = 0; i < routes.length; i++) { - node = node?.children.get(routes[i][ref]()); + node = node?.children.get(routes[i][routeReference]); } return node; @@ -62,7 +67,7 @@ class Node { } const lastRoute = routes[routes.length - 1]; - const lastRouteRef = lastRoute[ref](); + const lastRouteRef = lastRoute[routeReference]; if (parentNode.children.has(lastRouteRef)) { return false; } diff --git a/packages/core-api/src/routing/types.ts b/packages/core-api/src/routing/types.ts index 291029c3f2..162ac74bde 100644 --- a/packages/core-api/src/routing/types.ts +++ b/packages/core-api/src/routing/types.ts @@ -17,10 +17,10 @@ import { IconComponent } from '../icons'; export const resolveRoute = Symbol('resolve-route'); -export const ref = Symbol('route-ref'); +export const routeReference = Symbol('route-ref'); export type ReferencedRoute = { - [ref](): unknown; + [routeReference]: unknown; }; export type ConcreteRoute = ReferencedRoute & { From 240366572ca5d4c37e9da05f5b964c66a831872c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 20 Sep 2020 14:41:42 +0200 Subject: [PATCH 44/85] core-api/routing: allow duplicate registration if paths match --- packages/core-api/src/routing/RouteRefRegistry.test.ts | 1 + packages/core-api/src/routing/RouteRefRegistry.ts | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/core-api/src/routing/RouteRefRegistry.test.ts b/packages/core-api/src/routing/RouteRefRegistry.test.ts index 2849bcf32b..91d730de40 100644 --- a/packages/core-api/src/routing/RouteRefRegistry.test.ts +++ b/packages/core-api/src/routing/RouteRefRegistry.test.ts @@ -44,6 +44,7 @@ describe('RouteRefRegistry', () => { expect(registry.registerRoute([ref1, ref12], 'duplicate')).toBe(false); expect(registry.registerRoute([ref2], '2')).toBe(true); expect(registry.registerRoute([ref2], 'duplicate')).toBe(false); + expect(registry.registerRoute([ref2], '2')).toBe(true); expect(registry.resolveRoute([], [ref1])).toBe('/1'); expect(registry.resolveRoute([], [ref11])).toBe(undefined); diff --git a/packages/core-api/src/routing/RouteRefRegistry.ts b/packages/core-api/src/routing/RouteRefRegistry.ts index 4bc824ac3f..7e55cbe8f7 100644 --- a/packages/core-api/src/routing/RouteRefRegistry.ts +++ b/packages/core-api/src/routing/RouteRefRegistry.ts @@ -68,8 +68,10 @@ class Node { const lastRoute = routes[routes.length - 1]; const lastRouteRef = lastRoute[routeReference]; - if (parentNode.children.has(lastRouteRef)) { - return false; + + const existingNode = parentNode.children.get(lastRouteRef); + if (existingNode) { + return existingNode.path === path; } parentNode.children.set(lastRouteRef, new Node(path, parentNode)); From 8804c5e338b19ef27eeea37d47cf567415a62353 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 20 Sep 2020 14:49:46 +0200 Subject: [PATCH 45/85] core-api/routing: only require args in link call if routeRef has params --- packages/core-api/src/routing/RouteRef.ts | 8 ++++---- packages/core-api/src/routing/RouteRefRegistry.test.ts | 10 ++++------ 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/core-api/src/routing/RouteRef.ts b/packages/core-api/src/routing/RouteRef.ts index 5e7a4294d9..c8b0855de7 100644 --- a/packages/core-api/src/routing/RouteRef.ts +++ b/packages/core-api/src/routing/RouteRef.ts @@ -27,7 +27,7 @@ type SubRouteConfig = { path: string; }; -export class SubRouteRef +export class SubRouteRef implements ReferencedRoute { constructor( private readonly parent: ConcreteRoute, @@ -38,11 +38,11 @@ export class SubRouteRef return this; } - link(params: T): ConcreteRoute { + link(...args: Args): ConcreteRoute { return { [routeReference]: this, [resolveRoute]: (path: string) => { - const ownPart = generatePath(this.config.path, params); + const ownPart = generatePath(this.config.path, args[0] ?? {}); const parentPart = this.parent[resolveRoute](path); return parentPart + ownPart; }, @@ -66,7 +66,7 @@ export class AbsoluteRouteRef implements ConcreteRoute { return this.config.title; } - createSubRoute( + createSubRoute( config: SubRouteConfig, ) { return new SubRouteRef(this, config); diff --git a/packages/core-api/src/routing/RouteRefRegistry.test.ts b/packages/core-api/src/routing/RouteRefRegistry.test.ts index 91d730de40..fa1ef584f1 100644 --- a/packages/core-api/src/routing/RouteRefRegistry.test.ts +++ b/packages/core-api/src/routing/RouteRefRegistry.test.ts @@ -75,12 +75,10 @@ describe('RouteRefRegistry', () => { expect(registry.resolveRoute([], [ref1])).toBe('/1'); expect(registry.resolveRoute([], [ref2])).toBe('/2'); - expect(registry.resolveRoute([], [ref2a.link({}), ref1])).toBe('/2/a/1'); - expect(registry.resolveRoute([], [ref2a.link({}), ref2])).toBe('/2/a/2'); - expect(registry.resolveRoute([ref2a.link({})], [ref2])).toBe('/2/a/2'); - expect(registry.resolveRoute([ref2a.link({}), ref1], [ref2])).toBe( - '/2/a/2', - ); + expect(registry.resolveRoute([], [ref2a.link(), ref1])).toBe('/2/a/1'); + expect(registry.resolveRoute([], [ref2a.link(), ref2])).toBe('/2/a/2'); + expect(registry.resolveRoute([ref2a.link()], [ref2])).toBe('/2/a/2'); + expect(registry.resolveRoute([ref2a.link(), ref1], [ref2])).toBe('/2/a/2'); expect(registry.resolveRoute([], [ref2b.link({ id: 'abc' }), ref1])).toBe( '/2/b/abc/1', ); From d7cd977ce7ed451934ba6dd4f6e9fc634163974b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 22 Sep 2020 18:01:04 +0200 Subject: [PATCH 46/85] core-api: add back MutableRouteRef for backwards compatibility and export AbsoluteRouteRef --- packages/core-api/src/routing/RouteRef.ts | 5 +++++ packages/core-api/src/routing/index.ts | 1 + 2 files changed, 6 insertions(+) diff --git a/packages/core-api/src/routing/RouteRef.ts b/packages/core-api/src/routing/RouteRef.ts index c8b0855de7..c33335cb38 100644 --- a/packages/core-api/src/routing/RouteRef.ts +++ b/packages/core-api/src/routing/RouteRef.ts @@ -84,3 +84,8 @@ export class AbsoluteRouteRef implements ConcreteRoute { export function createRouteRef(config: RouteRefConfig): AbsoluteRouteRef { return new AbsoluteRouteRef(config); } + +// TODO(Rugvip): Added for backwards compatibility, remove once old usage is gone +// We may want to avoid exporting the AbsoluteRouteRef itself though, and consider +// a different model for how to create sub routes, just avoid this +export type MutableRouteRef = AbsoluteRouteRef; diff --git a/packages/core-api/src/routing/index.ts b/packages/core-api/src/routing/index.ts index 1b80c0838e..29de34ec42 100644 --- a/packages/core-api/src/routing/index.ts +++ b/packages/core-api/src/routing/index.ts @@ -15,4 +15,5 @@ */ export type { RouteRef, RouteRefConfig, ConcreteRoute } from './types'; +export type { MutableRouteRef, AbsoluteRouteRef } from './RouteRef'; export { createRouteRef } from './RouteRef'; From 521728348d0939f4695d64f7793d409dd598e6ca Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 22 Sep 2020 14:56:52 +0200 Subject: [PATCH 47/85] backend: change the default backend plugin mount point to /api --- CHANGELOG.md | 4 +++ app-config.yaml | 4 +-- docs/auth/add-auth-provider.md | 7 ++--- .../software-templates/adding-templates.md | 4 +-- docs/features/techdocs/getting-started.md | 4 +-- docs/openapi/definitions/auth.yaml | 6 ++--- docs/tutorials/quickstart-app-auth.md | 2 +- packages/app/src/App.test.tsx | 2 +- packages/app/src/apis.ts | 12 --------- packages/backend/package.json | 1 + packages/backend/src/index.ts | 26 ++++++++++++------- packages/backend/src/plugins/auth.ts | 11 ++++---- .../templates/default-app/app-config.yaml.hbs | 4 +-- .../default-app/packages/app/src/App.test.tsx | 2 +- .../packages/backend/package.json.hbs | 1 + .../default-app/packages/backend/src/index.ts | 18 ++++++++----- packages/e2e-test/src/e2e-test.ts | 2 +- plugins/auth-backend/README.md | 6 ++--- .../auth-backend/scripts/start-saml-idp.sh | 2 +- plugins/auth-backend/src/service/router.ts | 4 ++- plugins/techdocs-backend/README.md | 4 +-- .../src/techdocs/stages/publish/local.ts | 2 +- 22 files changed, 68 insertions(+), 60 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25360f3e1e..d360dd995d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ If you encounter issues while upgrading to a newer version, don't hesitate to re - Renamed `SessionStateApi` to `SessionApi` and `logout` to `signOut`. Custom implementations of the `SingInPage` app-component will need to rename their `logout` function. The different auth provider items for the `UserSettingsMenu` have been consolidated into a single `ProviderSettingsItem`, meaning you need to replace existing usages of `OAuthProviderSettings` and `OIDCProviderSettings`. [#2555](https://github.com/spotify/backstage/pull/2555). +### @backstage/auth-backend + +- The default mount path of backend plugins was changed to `/api/:pluginId`, and as part of that it was needed to enable configuration of the base path of the auth backend, so that it can construct redirect URLs correctly. The default base path is `/api/auth`, but you need to set this to `/auth` if you want to keep using the old path. Note that you will also need to reconfigure any allowed redirect URLs to include `/api` if you switch to the new recommended pattern. [#2562](https://github.com/spotify/backstage/pull/2562) + ## v0.1.1-alpha.22 ### @backstage/core diff --git a/app-config.yaml b/app-config.yaml index 60ecb813f7..fca0a39496 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -33,8 +33,8 @@ organization: name: Spotify techdocs: - storageUrl: http://localhost:7000/techdocs/static/docs - requestUrl: http://localhost:7000/techdocs/docs + storageUrl: http://localhost:7000/api/techdocs/static/docs + requestUrl: http://localhost:7000/api/techdocs/docs generators: techdocs: 'docker' diff --git a/docs/auth/add-auth-provider.md b/docs/auth/add-auth-provider.md index c5f7645a49..56586cbfc6 100644 --- a/docs/auth/add-auth-provider.md +++ b/docs/auth/add-auth-provider.md @@ -229,9 +229,10 @@ name. ### Test the new provider -You can `curl -i localhost:7000/auth/providerA/start` and which should provide a -`302` redirect with a `Location` header. Paste the url from that header into a -web browser and you should be able to trigger the authorization flow. +You can `curl -i localhost:7000/api/auth/providerA/start` and which should +provide a `302` redirect with a `Location` header. Paste the url from that +header into a web browser and you should be able to trigger the authorization +flow. --- diff --git a/docs/features/software-templates/adding-templates.md b/docs/features/software-templates/adding-templates.md index fad5c68e41..59019e754e 100644 --- a/docs/features/software-templates/adding-templates.md +++ b/docs/features/software-templates/adding-templates.md @@ -88,7 +88,7 @@ running: ```sh curl \ --location \ - --request POST 'localhost:7000/catalog/locations' \ + --request POST 'localhost:7000/api/catalog/locations' \ --header 'Content-Type: application/json' \ --data-raw "{\"type\": \"file\", \"target\": \"${YOUR PATH HERE}/template.yaml\"}" ``` @@ -98,7 +98,7 @@ If loading from a Git location, you can run the following ```sh curl \ --location \ - --request POST 'localhost:7000/catalog/locations' \ + --request POST 'localhost:7000/api/catalog/locations' \ --header 'Content-Type: application/json' \ --data-raw "{\"type\": \"github\", \"target\": \"https://${YOUR GITHUB REPO}blob/master/${PATH TO FOLDER}/template.yaml\"}" ``` diff --git a/docs/features/techdocs/getting-started.md b/docs/features/techdocs/getting-started.md index b9d215fd8a..03494564c3 100644 --- a/docs/features/techdocs/getting-started.md +++ b/docs/features/techdocs/getting-started.md @@ -57,8 +57,8 @@ The default storage and request URLs: ```yaml techdocs: - storageUrl: http://localhost:7000/techdocs/static/docs - requestUrl: http://localhost:7000/techdocs/docs + storageUrl: http://localhost:7000/api/techdocs/static/docs + requestUrl: http://localhost:7000/api/techdocs/docs ``` If you want `techdocs-backend` to manage building and publishing, you want diff --git a/docs/openapi/definitions/auth.yaml b/docs/openapi/definitions/auth.yaml index eb96d434c2..cb736fd2a9 100644 --- a/docs/openapi/definitions/auth.yaml +++ b/docs/openapi/definitions/auth.yaml @@ -6,10 +6,10 @@ info: **Provided by `@backstage/auth-backend`.** - The purpose of the Auth APIs in Backstage are to identify the user, and to provide a way for plugins + The purpose of the Auth APIs in Backstage are to identify the user, and to provide a way for plugins to request access to 3rd party services on behalf of that user. - The API is supplied with a list of providers - such as `Google` or `Github` - and will add the endpoints + The API is supplied with a list of providers - such as `Google` or `Github` - and will add the endpoints described below to each of those providers. Read more about [User Authentication and Authorization in Backstage](https://github.com/spotify/backstage/blob/master/docs/auth/overview.md). @@ -21,7 +21,7 @@ externalDocs: description: Backstage official documentation url: https://github.com/spotify/backstage/blob/master/docs/README.md servers: - - url: http://localhost:7000/auth/ + - url: http://localhost:7000/api/auth/ tags: - name: provider description: List of endpoints per provider diff --git a/docs/tutorials/quickstart-app-auth.md b/docs/tutorials/quickstart-app-auth.md index 2615e37633..ad2bf1af88 100644 --- a/docs/tutorials/quickstart-app-auth.md +++ b/docs/tutorials/quickstart-app-auth.md @@ -115,7 +115,7 @@ export AUTH_GITHUB_CLIENT_SECRET=xxx > Log into http://github.com > Navigate to (Settings > Developer Settings > OAuth Apps > New OAuth App)[https://github.com/settings/applications/new] > Set Homepage URL = http://localhost:3000 -> Set Callback URL = http://localhost:7000/auth/github +> Set Callback URL = http://localhost:7000/api/auth/github > Click [Register application] > On the next page, copy and paste your new Client ID and Client Secret to the environment variables above, `AUTH_GITHUB_CLIENT_ID` & `AUTH_GITHUB_CLIENT_SECRET` > Don't forget to `source` that profile file again if necessary. diff --git a/packages/app/src/App.test.tsx b/packages/app/src/App.test.tsx index cddd093855..40c49416bf 100644 --- a/packages/app/src/App.test.tsx +++ b/packages/app/src/App.test.tsx @@ -31,7 +31,7 @@ describe('App', () => { baseUrl: 'http://localhost:3003', }, techdocs: { - storageUrl: 'http://localhost:7000/techdocs/static/docs', + storageUrl: 'http://localhost:7000/api/techdocs/static/docs', }, }, context: 'test', diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index 7f57f6f970..581a591152 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -16,11 +16,8 @@ import { errorApiRef, - discoveryApiRef, - UrlPatternDiscovery, githubAuthApiRef, createApiFactory, - configApiRef, } from '@backstage/core'; import { @@ -38,15 +35,6 @@ import { } from '@roadiehq/backstage-plugin-github-pull-requests'; export const apis = [ - // TODO(Rugvip): migrate to use /api - createApiFactory({ - api: discoveryApiRef, - deps: { configApi: configApiRef }, - factory: ({ configApi }) => - UrlPatternDiscovery.compile( - `${configApi.getString('backend.baseUrl')}/{{ pluginId }}`, - ), - }), createApiFactory({ api: graphQlBrowseApiRef, deps: { errorApi: errorApiRef, githubAuthApi: githubAuthApiRef }, diff --git a/packages/backend/package.json b/packages/backend/package.json index 147ea630c2..9e14062992 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -37,6 +37,7 @@ "dockerode": "^3.2.0", "example-app": "^0.1.1-alpha.22", "express": "^4.17.1", + "express-promise-router": "^3.0.3", "knex": "^0.21.1", "pg": "^8.3.0", "pg-connection-string": "^2.3.0", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 7c52175d76..9d4a738ecf 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -22,12 +22,14 @@ * Happy hacking! */ +import Router from 'express-promise-router'; import { createDatabaseClient, createServiceBuilder, loadBackendConfig, getRootLogger, useHotMemoize, + notFoundHandler, } from '@backstage/backend-common'; import { ConfigReader, AppConfig } from '@backstage/config'; import healthcheck from './plugins/healthcheck'; @@ -78,19 +80,23 @@ async function main() { const graphqlEnv = useHotMemoize(module, () => createEnv('graphql')); const appEnv = useHotMemoize(module, () => createEnv('app')); + const apiRouter = Router(); + apiRouter.use('/catalog', await catalog(catalogEnv)); + apiRouter.use('/rollbar', await rollbar(rollbarEnv)); + apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv)); + apiRouter.use('/sentry', await sentry(sentryEnv)); + apiRouter.use('/auth', await auth(authEnv, '/api/auth')); + apiRouter.use('/identity', await identity(identityEnv)); + apiRouter.use('/techdocs', await techdocs(techdocsEnv)); + apiRouter.use('/kubernetes', await kubernetes(kubernetesEnv)); + apiRouter.use('/proxy', await proxy(proxyEnv, '/api/proxy')); + apiRouter.use('/graphql', await graphql(graphqlEnv)); + apiRouter.use(notFoundHandler()); + const service = createServiceBuilder(module) .loadConfig(configReader) .addRouter('', await healthcheck(healthcheckEnv)) - .addRouter('/catalog', await catalog(catalogEnv)) - .addRouter('/rollbar', await rollbar(rollbarEnv)) - .addRouter('/scaffolder', await scaffolder(scaffolderEnv)) - .addRouter('/sentry', await sentry(sentryEnv)) - .addRouter('/auth', await auth(authEnv)) - .addRouter('/identity', await identity(identityEnv)) - .addRouter('/techdocs', await techdocs(techdocsEnv)) - .addRouter('/kubernetes', await kubernetes(kubernetesEnv)) - .addRouter('/proxy', await proxy(proxyEnv, '/proxy')) - .addRouter('/graphql', await graphql(graphqlEnv)) + .addRouter('/api', apiRouter) .addRouter('', await app(appEnv)); await service.start().catch(err => { diff --git a/packages/backend/src/plugins/auth.ts b/packages/backend/src/plugins/auth.ts index b24dd6ca6b..60baaff6b7 100644 --- a/packages/backend/src/plugins/auth.ts +++ b/packages/backend/src/plugins/auth.ts @@ -17,10 +17,9 @@ import { createRouter } from '@backstage/plugin-auth-backend'; import { PluginEnvironment } from '../types'; -export default async function createPlugin({ - logger, - database, - config, -}: PluginEnvironment) { - return await createRouter({ logger, config, database }); +export default async function createPlugin( + { logger, database, config }: PluginEnvironment, + basePath: string, +) { + return await createRouter({ logger, config, database, basePath }); } diff --git a/packages/create-app/templates/default-app/app-config.yaml.hbs b/packages/create-app/templates/default-app/app-config.yaml.hbs index a7d7aa8010..92c517bf93 100644 --- a/packages/create-app/templates/default-app/app-config.yaml.hbs +++ b/packages/create-app/templates/default-app/app-config.yaml.hbs @@ -44,8 +44,8 @@ proxy: changeOrigin: true techdocs: - storageUrl: http://localhost:7000/techdocs/static/docs - requestUrl: http://localhost:7000/techdocs/docs + storageUrl: http://localhost:7000/api/techdocs/static/docs + requestUrl: http://localhost:7000/api/techdocs/docs generators: techdocs: 'docker' diff --git a/packages/create-app/templates/default-app/packages/app/src/App.test.tsx b/packages/create-app/templates/default-app/packages/app/src/App.test.tsx index e88c3b47dd..21a2eaa703 100644 --- a/packages/create-app/templates/default-app/packages/app/src/App.test.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/App.test.tsx @@ -12,7 +12,7 @@ describe('App', () => { app: { title: 'Test' }, backend: { baseUrl: 'http://localhost:7000' }, techdocs: { - storageUrl: 'http://localhost:7000/techdocs/static/docs', + storageUrl: 'http://localhost:7000/api/techdocs/static/docs', }, }, context: 'test', diff --git a/packages/create-app/templates/default-app/packages/backend/package.json.hbs b/packages/create-app/templates/default-app/packages/backend/package.json.hbs index adde81e830..b70f36650e 100644 --- a/packages/create-app/templates/default-app/packages/backend/package.json.hbs +++ b/packages/create-app/templates/default-app/packages/backend/package.json.hbs @@ -31,6 +31,7 @@ "@gitbeaker/node": "^23.5.0", "dockerode": "^3.2.0", "express": "^4.17.1", + "express-promise-router": "^3.0.3", "knex": "^0.21.1", {{#if dbTypePG}} "pg": "^8.3.0", diff --git a/packages/create-app/templates/default-app/packages/backend/src/index.ts b/packages/create-app/templates/default-app/packages/backend/src/index.ts index 6a014727e9..457b95a7a9 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/index.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/index.ts @@ -6,12 +6,14 @@ * Happy hacking! */ +import Router from 'express-promise-router'; import { createDatabaseClient, createServiceBuilder, loadBackendConfig, getRootLogger, useHotMemoize, + notFoundHandler, } from '@backstage/backend-common'; import { ConfigReader, AppConfig } from '@backstage/config'; import auth from './plugins/auth'; @@ -51,14 +53,18 @@ async function main() { const proxyEnv = useHotMemoize(module, () => createEnv('proxy')); const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs')); + const apiRouter = Router(); + apiRouter.use('/catalog', await catalog(catalogEnv)) + apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv)) + apiRouter.use('/auth', await auth(authEnv)) + apiRouter.use('/identity', await identity(identityEnv)) + apiRouter.use('/techdocs', await techdocs(techdocsEnv)) + apiRouter.use('/proxy', await proxy(proxyEnv, '/api/proxy')) + apiRouter.use(notFoundHandler()); + const service = createServiceBuilder(module) .loadConfig(configReader) - .addRouter('/catalog', await catalog(catalogEnv)) - .addRouter('/scaffolder', await scaffolder(scaffolderEnv)) - .addRouter('/auth', await auth(authEnv)) - .addRouter('/identity', await identity(identityEnv)) - .addRouter('/techdocs', await techdocs(techdocsEnv)) - .addRouter('/proxy', await proxy(proxyEnv, '/proxy')); + .addRouter('/api', apiRouter) await service.start().catch(err => { console.log(err); diff --git a/packages/e2e-test/src/e2e-test.ts b/packages/e2e-test/src/e2e-test.ts index 969a77cb2a..ec34f48959 100644 --- a/packages/e2e-test/src/e2e-test.ts +++ b/packages/e2e-test/src/e2e-test.ts @@ -384,7 +384,7 @@ async function testBackendStart(appDir: string, isPostgres: boolean) { print('Try to fetch entities from the backend'); // Try fetch entities, should be ok - await fetch('http://localhost:7000/catalog/entities').then(res => + await fetch('http://localhost:7000/api/catalog/entities').then(res => res.json(), ); print('Entities fetched successfully'); diff --git a/plugins/auth-backend/README.md b/plugins/auth-backend/README.md index 9af90df3f2..868d88b3d0 100644 --- a/plugins/auth-backend/README.md +++ b/plugins/auth-backend/README.md @@ -34,7 +34,7 @@ Follow this link, [Create new OAuth App](https://github.com/settings/application 1. Set Application Name to `backstage-dev` or something along those lines. 1. You can set the Homepage URL to whatever you want to. 1. The Authorization Callback URL should match the redirect URI set in Backstage. - 1. Set this to `http://localhost:7000/auth/github` for local development. + 1. Set this to `http://localhost:7000/api/auth/github` for local development. 1. Set this to `http://{APP_FQDN}:{APP_BACKEND_PORT}/auth/github` for non-local deployments. ```bash @@ -78,14 +78,14 @@ export AUTH_AUTH0_CLIENT_SECRET=x #### Creating an Azure AD App Registration -An Azure AD App Registration is required to be able to sign in using Azure AD and the Microsoft Graph API. +An Azure AD App Registration is required to be able to sign in using Azure AD and the Microsoft Graph API. Click [here](https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredApps) to create a new one. - Click on the `New Registration` button. - Give the app a name. e.g. `backstage-dev` - Select `Accounts in this organizational directory only` under supported account types. - Enter the callback URL for your backstage backend instance: - - For local development, this is likely `http://localhost:7000/auth/microsoft/handler/frame` + - For local development, this is likely `http://localhost:7000/api/auth/microsoft/handler/frame` - For non-local deployments, this will be `https://{APP_FQDN}:{APP_BACKEND_PORT}/auth/microsoft/handler/frame` - Click `Register`. diff --git a/plugins/auth-backend/scripts/start-saml-idp.sh b/plugins/auth-backend/scripts/start-saml-idp.sh index 33217f7978..b312a4b85e 100755 --- a/plugins/auth-backend/scripts/start-saml-idp.sh +++ b/plugins/auth-backend/scripts/start-saml-idp.sh @@ -18,4 +18,4 @@ fi echo "Downloading and starting SAML-IdP" export NPM_CONFIG_REGISTRY=https://registry.npmjs.org -exec npx saml-idp --acsUrl "http://localhost:7000/auth/saml/handler/frame" --audience "http://localhost:7000" --port 7001 +exec npx saml-idp --acsUrl "http://localhost:7000/api/auth/saml/handler/frame" --audience "http://localhost:7000" --port 7001 diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 19a74d47c5..e70e687879 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -28,6 +28,7 @@ export interface RouterOptions { logger: Logger; database: Knex; config: Config; + basePath?: string; } export async function createRouter( @@ -38,7 +39,8 @@ export async function createRouter( const appUrl = options.config.getString('app.baseUrl'); const backendUrl = options.config.getString('backend.baseUrl'); - const authUrl = `${backendUrl}/auth`; + // TODO(Rugvip): Replace with service discovery of external URL + const authUrl = backendUrl + (options.basePath ?? '/api/auth'); const keyDurationSeconds = 3600; diff --git a/plugins/techdocs-backend/README.md b/plugins/techdocs-backend/README.md index 2185c3b3e4..b7eca2c5cb 100644 --- a/plugins/techdocs-backend/README.md +++ b/plugins/techdocs-backend/README.md @@ -18,11 +18,11 @@ yarn start ## What techdocs-backend does -This plugin is the backend part of the techdocs plugin. It provides building and serving of your docs without having to use another service and hosting provider. To use it set your techdocs storageUrl in your `app-config.yml` to `http://localhost:7000/techdocs/static/docs`. +This plugin is the backend part of the techdocs plugin. It provides building and serving of your docs without having to use another service and hosting provider. To use it set your techdocs storageUrl in your `app-config.yml` to `http://localhost:7000/api/techdocs/static/docs`. ```yaml techdocs: - storageUrl: http://localhost:7000/techdocs/static/docs + storageUrl: http://localhost:7000/api/techdocs/static/docs ``` ## Extending techdocs-backend diff --git a/plugins/techdocs-backend/src/techdocs/stages/publish/local.ts b/plugins/techdocs-backend/src/techdocs/stages/publish/local.ts index e33c6fa17d..f328443965 100644 --- a/plugins/techdocs-backend/src/techdocs/stages/publish/local.ts +++ b/plugins/techdocs-backend/src/techdocs/stages/publish/local.ts @@ -64,7 +64,7 @@ export class LocalPublish implements PublisherBase { } resolve({ - remoteUrl: `http://localhost:7000/techdocs/static/docs/${entity.metadata.name}`, + remoteUrl: `http://localhost:7000/api/techdocs/static/docs/${entity.metadata.name}`, }); }); }); From 9959558d2f41c9a470d3cbd9007530f1c3601a59 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Wed, 23 Sep 2020 14:06:23 +0200 Subject: [PATCH 48/85] fix dense in metadata table --- .../StructuredMetadataTable/MetadataTable.tsx | 2 +- .../StructuredMetadataTable.stories.tsx | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/core/src/components/StructuredMetadataTable/MetadataTable.tsx b/packages/core/src/components/StructuredMetadataTable/MetadataTable.tsx index 1d863db764..ad468f623c 100644 --- a/packages/core/src/components/StructuredMetadataTable/MetadataTable.tsx +++ b/packages/core/src/components/StructuredMetadataTable/MetadataTable.tsx @@ -71,7 +71,7 @@ export const MetadataTable = ({ dense?: boolean; children: React.ReactNode; }) => ( -
+
{!dense && ( diff --git a/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.stories.tsx b/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.stories.tsx index bdf0f65375..9a9a4b42b6 100644 --- a/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.stories.tsx +++ b/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.stories.tsx @@ -56,3 +56,16 @@ export const Default = () => ( ); + +export const DenseTable = () => ( + + +
+ +
+
+
+); From 091ab63d19cb743fdbe56e6a873d4792f43534d9 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Wed, 23 Sep 2020 14:56:46 +0200 Subject: [PATCH 49/85] set dense as default value --- .../components/StructuredMetadataTable/MetadataTable.tsx | 6 ------ .../StructuredMetadataTable.stories.tsx | 6 +++--- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/core/src/components/StructuredMetadataTable/MetadataTable.tsx b/packages/core/src/components/StructuredMetadataTable/MetadataTable.tsx index ad468f623c..b34e535a00 100644 --- a/packages/core/src/components/StructuredMetadataTable/MetadataTable.tsx +++ b/packages/core/src/components/StructuredMetadataTable/MetadataTable.tsx @@ -72,12 +72,6 @@ export const MetadataTable = ({ children: React.ReactNode; }) => (
- {!dense && ( - - - - - )} {children}
); diff --git a/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.stories.tsx b/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.stories.tsx index 9a9a4b42b6..9db7385872 100644 --- a/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.stories.tsx +++ b/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.stories.tsx @@ -57,14 +57,14 @@ export const Default = () => ( ); -export const DenseTable = () => ( +export const NotDenseTable = () => (
- +
From 54e937ee90cd5c70fd2d8544f39bfd0b5528520b Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Wed, 23 Sep 2020 14:57:55 +0200 Subject: [PATCH 50/85] change class component to functional component --- .../StructuredMetadataTable.tsx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.tsx b/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.tsx index f966bd570f..52ad9a320a 100644 --- a/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.tsx +++ b/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { Component, Fragment, ReactElement } from 'react'; +import React, { Fragment, ReactElement } from 'react'; import { withStyles, createStyles, WithStyles, Theme } from '@material-ui/core'; import startCase from 'lodash/startCase'; @@ -142,17 +142,17 @@ const TableItem = ({ ); }; -interface ComponentProps { +type Props = { metadata: { [key: string]: any }; dense?: boolean; options?: any; -} +}; -export class StructuredMetadataTable extends Component { - render() { - const { metadata, dense, options } = this.props; - const metadataItems = mapToItems(metadata, options || {}); - - return {metadataItems}; - } -} +export const StructuredMetadataTable = ({ + metadata, + dense = true, + options, +}: Props) => { + const metadataItems = mapToItems(metadata, options || {}); + return {metadataItems}; +}; From 7258144cf9c083e582a36f01fa67630e7a46cce5 Mon Sep 17 00:00:00 2001 From: "J Shamsul Bahri (jibone))" Date: Wed, 23 Sep 2020 23:25:46 +0800 Subject: [PATCH 51/85] refactor to use sessionApi --- packages/core-api/src/apis/definitions/auth.ts | 18 +++--------------- .../apis/implementations/auth/saml/SamlAuth.ts | 17 +++++++++++------ 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/packages/core-api/src/apis/definitions/auth.ts b/packages/core-api/src/apis/definitions/auth.ts index 076a7479d6..109f15b26e 100644 --- a/packages/core-api/src/apis/definitions/auth.ts +++ b/packages/core-api/src/apis/definitions/auth.ts @@ -92,20 +92,6 @@ export type OAuthApi = { ): Promise; }; -/** - * This API provides access to SAML 2 credentials. Verify user access with identity provider. - */ -export type SamlApi = { - getBackstageIdentity( - options?: AuthRequestOptions, - ): Promise; - - getProfile(options?: AuthRequestOptions): Promise; - - // Not sure if this is needed. - logout(): Promise; -}; - /** * This API provides access to OpenID Connect credentials. It lets you request ID tokens, * which can be passed to backend services to prove the user's identity. @@ -328,7 +314,9 @@ export const oauth2ApiRef = createApiRef< /** * Provides authentication for saml based identity providers */ -export const samlAuthApiRef = createApiRef({ +export const samlAuthApiRef = createApiRef< + ProfileInfoApi & BackstageIdentityApi & SessionApi +>({ id: 'core.auth.saml', description: 'Example of how to use SAML custom provider', }); diff --git a/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts b/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts index 2c8c98ecf6..433d3befe8 100644 --- a/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts @@ -23,7 +23,9 @@ import { BackstageIdentity, SessionState, AuthRequestOptions, - SamlApi, + ProfileInfoApi, + BackstageIdentityApi, + SessionApi, } from '../../../definitions/auth'; import { AuthProvider, DiscoveryApi } from '../../../definitions'; import { SamlSession } from './types'; @@ -50,7 +52,7 @@ const DEFAULT_PROVIDER = { icon: SamlIcon, }; -class SamlAuth implements SamlApi { +class SamlAuth implements ProfileInfoApi, BackstageIdentityApi, SessionApi { static create({ discoveryApi, environment = 'development', @@ -80,6 +82,13 @@ class SamlAuth implements SamlApi { constructor(private readonly sessionManager: SessionManager) {} + async signIn() { + await this.getBackstageIdentity({}); + } + async signOut() { + await this.sessionManager.removeSession(); + } + async getBackstageIdentity( options: AuthRequestOptions, ): Promise { @@ -96,10 +105,6 @@ class SamlAuth implements SamlApi { const session = await this.sessionManager.getSession(options); return session?.profile; } - - async logout() { - await this.sessionManager.removeSession(); - } } export default SamlAuth; From 1ef015d68cec783e33358b91843a46674c8b12fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 23 Sep 2020 20:13:30 +0200 Subject: [PATCH 52/85] fix: address comments --- packages/catalog-model/src/entity/index.ts | 3 +- packages/catalog-model/src/entity/ref.test.ts | 11 +++- packages/catalog-model/src/entity/ref.ts | 62 ++++++++++++------- packages/catalog-model/src/index.ts | 8 +-- packages/catalog-model/src/types.ts | 24 ++++--- 5 files changed, 62 insertions(+), 46 deletions(-) diff --git a/packages/catalog-model/src/entity/index.ts b/packages/catalog-model/src/entity/index.ts index 12ec19667b..a6e90e6975 100644 --- a/packages/catalog-model/src/entity/index.ts +++ b/packages/catalog-model/src/entity/index.ts @@ -17,8 +17,7 @@ export { entityMetaGeneratedFields } from './Entity'; export type { Entity, EntityMeta } from './Entity'; export * from './policies'; -export { parseEntityName, parseEntityRef, serializeEntityRef } from './ref'; -export type { EntityRefContext } from './ref'; +export { parseEntityName, serializeEntityRef } from './ref'; export { entityHasChanges, generateEntityEtag, diff --git a/packages/catalog-model/src/entity/ref.test.ts b/packages/catalog-model/src/entity/ref.test.ts index 8a8b7d3f86..b76f01f1f9 100644 --- a/packages/catalog-model/src/entity/ref.test.ts +++ b/packages/catalog-model/src/entity/ref.test.ts @@ -68,33 +68,40 @@ describe('ref', () => { expect(() => parseEntityName({ kind: '', namespace: 'b', name: 'c' }), ).toThrow(); + expect(() => parseEntityName({ namespace: 'b', name: 'c' })).toThrow(); expect(() => parseEntityName({ kind: 'a', namespace: '', name: 'c' }), ).toThrow(); + expect(() => parseEntityName({ kind: 'a', name: 'c' })).not.toThrow(); expect(() => parseEntityName({ kind: 'a', namespace: 'b', name: '' }), ).toThrow(); + expect(() => + parseEntityName({ kind: 'a', namespace: 'b' } as any), + ).toThrow(); // two are empty expect(() => parseEntityName({ kind: '', namespace: '', name: 'c' }), ).toThrow(); + expect(() => parseEntityName({ name: 'c' })).toThrow(); expect(() => parseEntityName({ kind: '', namespace: 'b', name: '' }), ).toThrow(); + expect(() => parseEntityName({ namespace: 'b' } as any)).toThrow(); expect(() => parseEntityName({ kind: 'a', namespace: '', name: '' }), ).toThrow(); + expect(() => parseEntityName({ kind: 'a' } as any)).toThrow(); // three are empty expect(() => parseEntityName({ kind: '', namespace: '', name: '' }), ).toThrow(); + expect(() => parseEntityName({} as any)).toThrow(); // one is left out, one empty expect(() => parseEntityName({ namespace: '', name: 'c' })).toThrow(); expect(() => parseEntityName({ namespace: 'b', name: '' })).toThrow(); expect(() => parseEntityName({ kind: '', name: 'c' })).toThrow(); expect(() => parseEntityName({ kind: 'a', name: '' })).toThrow(); - // nothing at all - expect(() => parseEntityName({} as any)).toThrow(); }); it('adds defaults where necessary to strings', () => { diff --git a/packages/catalog-model/src/entity/ref.ts b/packages/catalog-model/src/entity/ref.ts index 1a579eeb7e..b9320a5ac7 100644 --- a/packages/catalog-model/src/entity/ref.ts +++ b/packages/catalog-model/src/entity/ref.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import { CompoundEntityRef, EntityName, EntityRef } from '../types'; +import { EntityName, EntityRef } from '../types'; /** * The context of defaults that entity reference parsing happens within. */ -export type EntityRefContext = { +type EntityRefContext = { /** The default kind, if none is given in the reference */ defaultKind?: string; /** The default namespace, if none is given in the reference */ @@ -53,19 +53,7 @@ export function parseEntityName( ); } - // This is a corner case - when the ref contained a namespace but it was the - // empty string (so the default did not kick in) - if (!namespace) { - throw new Error( - `Entity reference ${kind}:${name} did not contain a namespace`, - ); - } - - return { - kind, - namespace: namespace!, - name, - }; + return { kind, namespace, name }; } /** @@ -79,16 +67,44 @@ export function parseEntityName( * @param context The context of defaults that the parsing happens within * @returns The compound form of the reference */ +export function parseEntityRef( + ref: EntityRef, + context?: { defaultKind: string }, +): { + kind: string; + namespace?: string; + name: string; +}; +export function parseEntityRef( + ref: EntityRef, + context?: { defaultNamespace: string }, +): { + kind?: string; + namespace: string; + name: string; +}; +export function parseEntityRef( + ref: EntityRef, + context?: { defaultKind: string; defaultNamespace: string }, +): { + kind: string; + namespace: string; + name: string; +}; export function parseEntityRef( ref: EntityRef, context: EntityRefContext = {}, -): CompoundEntityRef { +): { + kind?: string; + namespace?: string; + name: string; +} { if (!ref) { throw new Error(`Entity reference must not be empty`); } if (typeof ref === 'string') { - const match = /^(?:([^:/]+):)?(?:([^:/]+)\/)?([^:/]+)$/.exec(ref.trim()); + const match = /^([^:/]+:)?([^:/]+\/)?([^:/]+)$/.exec(ref.trim()); if (!match) { throw new Error( `Entity reference "${ref}" was not on the form [:][/]`, @@ -96,8 +112,8 @@ export function parseEntityRef( } return { - kind: match[1] ?? context.defaultKind, - namespace: match[2] ?? context.defaultNamespace, + kind: match[1]?.slice(0, -1) ?? context.defaultKind, + namespace: match[2]?.slice(0, -1) ?? context.defaultNamespace, name: match[3], }; } @@ -127,9 +143,11 @@ export function parseEntityRef( * @param ref The reference to serialize * @returns The same reference on either string or compound form */ -export function serializeEntityRef( - ref: CompoundEntityRef | EntityName, -): EntityRef { +export function serializeEntityRef(ref: { + kind?: string; + namespace?: string; + name: string; +}): EntityRef { const { kind, namespace, name } = ref; if ( kind?.includes(':') || diff --git a/packages/catalog-model/src/index.ts b/packages/catalog-model/src/index.ts index 16e7e3477b..f93a4001a5 100644 --- a/packages/catalog-model/src/index.ts +++ b/packages/catalog-model/src/index.ts @@ -18,11 +18,5 @@ export * from './entity'; export { EntityPolicies } from './EntityPolicies'; export * from './kinds'; export * from './location'; -export type { - CompoundEntityRef, - EntityName, - EntityPolicy, - EntityRef, - JSONSchema, -} from './types'; +export type { EntityName, EntityPolicy, EntityRef, JSONSchema } from './types'; export * from './validation'; diff --git a/packages/catalog-model/src/types.ts b/packages/catalog-model/src/types.ts index 1e0a778827..3aafdc241a 100644 --- a/packages/catalog-model/src/types.ts +++ b/packages/catalog-model/src/types.ts @@ -45,20 +45,18 @@ export type EntityName = { }; /** - * A reference by name to an entity, where the kind and/or the namespace can be - * left out. + * A reference by name to an entity, either as a compact string representation, + * or as a compound reference structure. + * + * The string representation is on the form [:][/]. * * Left-out parts of the reference need to be handled by the application, * either by rejecting the reference or by falling back to default values. */ -export type CompoundEntityRef = { - kind?: string; - namespace?: string; - name: string; -}; - -/** - * A reference by name to an entity, either as a compact string representation, - * or as a compound reference structure. - */ -export type EntityRef = string | CompoundEntityRef; +export type EntityRef = + | string + | { + kind?: string; + namespace?: string; + name: string; + }; From 32def99071323d4dd681af37cbe965349f84845b Mon Sep 17 00:00:00 2001 From: "J Shamsul Bahri (jibone))" Date: Thu, 24 Sep 2020 10:45:11 +0800 Subject: [PATCH 53/85] Remove getTokenId and userId --- .../core-api/src/apis/implementations/auth/saml/SamlAuth.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts b/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts index 433d3befe8..dbd9368842 100644 --- a/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts @@ -41,7 +41,6 @@ type CreateOptions = { }; export type SamlAuthResponse = { - userId: string; profile: ProfileInfo; backstageIdentity: BackstageIdentity; }; @@ -96,11 +95,6 @@ class SamlAuth implements ProfileInfoApi, BackstageIdentityApi, SessionApi { return session?.backstageIdentity; } - async getIdToken(options: AuthRequestOptions = {}) { - const session = await this.sessionManager.getSession(options); - return session?.backstageIdentity ?? ''; - } - async getProfile(options: AuthRequestOptions = {}) { const session = await this.sessionManager.getSession(options); return session?.profile; From e3b504798af83337c3b1c88d6f2fc26f0f3be540 Mon Sep 17 00:00:00 2001 From: "J Shamsul Bahri (jibone))" Date: Thu, 24 Sep 2020 11:28:43 +0800 Subject: [PATCH 54/85] revert identifyEnv implementation --- plugins/auth-backend/src/providers/saml/provider.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index 82e0acbdcc..a90f3e3053 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -105,13 +105,8 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { res.send('noop'); } - identifyEnv(req: express.Request): string | undefined { - const reqEnv = req.query.env?.toString(); - if (reqEnv) { - return reqEnv; - } - - return 'development'; + identifyEnv(): string | undefined { + return undefined; } } From 5f71c826b91b306d05ed97973057fd0f97fbf16d Mon Sep 17 00:00:00 2001 From: Roberto Devesa Date: Thu, 24 Sep 2020 06:46:08 +0200 Subject: [PATCH 55/85] Fix typo (#2584) --- docs/overview/what-is-backstage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/overview/what-is-backstage.md b/docs/overview/what-is-backstage.md index 0d16943c9b..1a5e80a302 100644 --- a/docs/overview/what-is-backstage.md +++ b/docs/overview/what-is-backstage.md @@ -1,7 +1,7 @@ --- id: what-is-backstage title: What is Backstage? -description: Backsatge is an open platform for building developer portals. +description: Backstage is an open platform for building developer portals. Powered by a centralized service catalog, Backstage restores order to your microservices and infrastructure --- From 56b82f523d26723ec55c3c2327dfa7425c50a60d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 24 Sep 2020 07:55:54 +0200 Subject: [PATCH 56/85] chore: add contrib to CONTRIBUTING.md (#2585) --- CONTRIBUTING.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a6f7e5e5e1..d3c2780967 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,6 +32,10 @@ If you start developing a plugin that you aim to release as open source, we sugg You can also use this process if you have an idea for a good plugin but you hope that someone else will pick up the work. +## Adding Non-code Contributions + +Since there is such a large landscape of possible development, build, and deployment environments, we welcome community contributions in these areas in the [`/contrib`](https://github.com/spotify/backstage/tree/master/contrib) folder of the project. This is an excellent place to put things that help out the community at large, but which may not fit within the scope of the core product to support natively. Here, you will find Helm charts, alternative Docker images, and much more. + ## Write Documentation The current documentation is very limited. Help us make the `/docs` folder come alive. From 4ee2c6a16d2f8111769a0600b73c0f8343e81633 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Thu, 24 Sep 2020 09:03:33 +0200 Subject: [PATCH 57/85] fix(techdocs-core): use the content of the readme as long description for the pypi package (#2578) --- packages/techdocs-container/techdocs-core/setup.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/techdocs-container/techdocs-core/setup.py b/packages/techdocs-container/techdocs-core/setup.py index e5a8206150..00e49dd366 100644 --- a/packages/techdocs-container/techdocs-core/setup.py +++ b/packages/techdocs-container/techdocs-core/setup.py @@ -14,13 +14,19 @@ See the License for the specific language governing permissions and limitations under the License. """ from setuptools import setup, find_packages +from os import path +# read the contents of the README file in the current directory +this_dir = path.abspath(path.dirname(__file__)) +with open(path.join(this_dir, "README.md"), encoding="utf-8") as file: + long_description = file.read() setup( name="mkdocs-techdocs-core", version="0.0.8", description="A Mkdocs package that contains TechDocs defaults", - long_description="", + long_description=long_description, + long_description_content_type="text/markdown", keywords="mkdocs", url="https://github.com/spotify/backstage", author="TechDocs Core", From 5be5d6bca565f7a9c1ab0b4f2432f97da45c7b00 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 24 Sep 2020 10:48:44 +0200 Subject: [PATCH 58/85] chore(gcp): updating gcp projects npm visibility --- plugins/gcp-projects/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index b71cebf6e4..8c7f79fd96 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -4,7 +4,6 @@ "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", - "private": true, "publishConfig": { "access": "public", "main": "dist/index.esm.js", From 37a4a3afd0ed498ac35e983ffb1cb56b0662468e Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 24 Sep 2020 10:52:26 +0200 Subject: [PATCH 59/85] chore(deps): updating dependencies for plugin --- plugins/api-docs/package.json | 3 ++- plugins/catalog/package.json | 3 ++- plugins/circleci/package.json | 3 ++- plugins/explore/package.json | 3 ++- plugins/gcp-projects/package.json | 3 ++- plugins/github-actions/package.json | 3 ++- plugins/gitops-profiles/package.json | 3 ++- plugins/graphiql/package.json | 3 ++- plugins/jenkins/package.json | 3 ++- plugins/lighthouse/package.json | 3 ++- plugins/newrelic/package.json | 3 ++- plugins/register-component/package.json | 3 ++- plugins/rollbar/package.json | 3 ++- plugins/scaffolder/package.json | 3 ++- plugins/sentry/package.json | 3 ++- plugins/tech-radar/package.json | 3 ++- plugins/techdocs/package.json | 3 ++- plugins/welcome/package.json | 3 ++- 18 files changed, 36 insertions(+), 18 deletions(-) diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 254dc9aba0..ee86e29183 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -50,7 +50,8 @@ "@types/react": "^16.9", "@types/swagger-ui-react": "^3.23.3", "jest-fetch-mock": "^3.0.3", - "msw": "^0.20.5" + "msw": "^0.20.5", + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index f5520f39f9..9d777c9bcd 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -53,7 +53,8 @@ "jest-fetch-mock": "^3.0.3", "msw": "^0.20.5", "react-test-renderer": "^16.13.1", - "whatwg-fetch": "^3.4.0" + "whatwg-fetch": "^3.4.0", + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index 47600d5452..7dce54f8b3 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -47,7 +47,8 @@ "@types/node": "^12.0.0", "@types/react-lazylog": "^4.5.0", "jest-fetch-mock": "^3.0.3", - "msw": "^0.20.5" + "msw": "^0.20.5", + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/plugins/explore/package.json b/plugins/explore/package.json index f094c4f908..7a46a2954f 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -42,7 +42,8 @@ "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "jest-fetch-mock": "^3.0.3", - "msw": "^0.20.5" + "msw": "^0.20.5", + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index fa72f0e66d..c6424056c8 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -40,7 +40,8 @@ "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "jest-fetch-mock": "^3.0.3", - "msw": "^0.20.5" + "msw": "^0.20.5", + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index 1ee1e44074..8ee00bef06 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -48,7 +48,8 @@ "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "jest-fetch-mock": "^3.0.3", - "msw": "^0.20.5" + "msw": "^0.20.5", + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index 7805f35f85..79b9b3a708 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -40,7 +40,8 @@ "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "jest-fetch-mock": "^3.0.3", - "msw": "^0.20.5" + "msw": "^0.20.5", + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index 5747ff5726..ab80901fa0 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -54,7 +54,8 @@ "@types/node": "^12.0.0", "jest-fetch-mock": "^3.0.3", "react-router-dom": "6.0.0-beta.0", - "msw": "^0.20.5" + "msw": "^0.20.5", + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index 07ec728b34..091ef9356a 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -45,7 +45,8 @@ "@types/node": "^12.0.0", "@types/testing-library__jest-dom": "^5.9.1", "jest-fetch-mock": "^3.0.3", - "msw": "^0.20.5" + "msw": "^0.20.5", + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index ea73010491..a5e7ae7b62 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -43,7 +43,8 @@ "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "jest-fetch-mock": "^3.0.3", - "msw": "^0.20.5" + "msw": "^0.20.5", + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index 63a6d0a1b4..7ad697a254 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -39,7 +39,8 @@ "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "jest-fetch-mock": "^3.0.3", - "msw": "^0.20.5" + "msw": "^0.20.5", + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/plugins/register-component/package.json b/plugins/register-component/package.json index c2d9709643..58fe9855a2 100644 --- a/plugins/register-component/package.json +++ b/plugins/register-component/package.json @@ -44,7 +44,8 @@ "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "jest-fetch-mock": "^3.0.3", - "msw": "^0.20.5" + "msw": "^0.20.5", + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index c2154a2c85..3faa06d0d3 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -48,7 +48,8 @@ "@types/node": "^12.0.0", "@types/react": "^16.9", "jest-fetch-mock": "^3.0.3", - "msw": "^0.20.5" + "msw": "^0.20.5", + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 21b95a6aaf..db8f335156 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -50,7 +50,8 @@ "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "jest-fetch-mock": "^3.0.3", - "msw": "^0.20.5" + "msw": "^0.20.5", + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index 1fd836b83f..8ac1ab2838 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -44,7 +44,8 @@ "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "jest-fetch-mock": "^3.0.3", - "msw": "^0.20.5" + "msw": "^0.20.5", + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index 36318be766..a8eded052c 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -46,7 +46,8 @@ "@types/node": "^12.0.0", "@types/react": "^16.9", "jest-fetch-mock": "^3.0.3", - "msw": "^0.20.5" + "msw": "^0.20.5", + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index d467f8ef5f..ab2e0dd038 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -49,7 +49,8 @@ "@types/node": "^12.0.0", "canvas": "^2.6.1", "jest-fetch-mock": "^3.0.3", - "msw": "^0.20.5" + "msw": "^0.20.5", + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/plugins/welcome/package.json b/plugins/welcome/package.json index 7784d8ff97..61406087aa 100644 --- a/plugins/welcome/package.json +++ b/plugins/welcome/package.json @@ -40,7 +40,8 @@ "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "jest-fetch-mock": "^3.0.3", - "msw": "^0.20.5" + "msw": "^0.20.5", + "node-fetch": "^2.6.1" }, "files": [ "dist" From 95c4d68e511243755da2747b0cf0d541555f9e24 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 24 Sep 2020 11:20:07 +0200 Subject: [PATCH 60/85] chore(cli): added node-fetch as a setup dependeny --- packages/cli/templates/default-plugin/src/setupTests.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/cli/templates/default-plugin/src/setupTests.ts b/packages/cli/templates/default-plugin/src/setupTests.ts index 7b0828bfa8..cc559f672e 100644 --- a/packages/cli/templates/default-plugin/src/setupTests.ts +++ b/packages/cli/templates/default-plugin/src/setupTests.ts @@ -1 +1,2 @@ import '@testing-library/jest-dom'; +global.fetch = require('node-fetch'); From 6b8a6d8d52749dd7acbe749c5a24053a3577a8fd Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Thu, 24 Sep 2020 11:26:25 +0200 Subject: [PATCH 61/85] fix(techdocs-core): change development status of techdocs core package (#2589) --- packages/techdocs-container/techdocs-core/README.md | 4 ++++ packages/techdocs-container/techdocs-core/setup.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/techdocs-container/techdocs-core/README.md b/packages/techdocs-container/techdocs-core/README.md index 49b9fce4e4..7f34722e81 100644 --- a/packages/techdocs-container/techdocs-core/README.md +++ b/packages/techdocs-container/techdocs-core/README.md @@ -82,6 +82,10 @@ Extensions: ## Changelog +### 0.0.9 + +- Change development status to 3 - Alpha + ### 0.0.8 - Superfences and Codehilite doesn't work very well together (squidfunk/mkdocs-material#1604) so therefore the codehilite extension is replaced by pymdownx.highlight diff --git a/packages/techdocs-container/techdocs-core/setup.py b/packages/techdocs-container/techdocs-core/setup.py index 00e49dd366..4385bc2e38 100644 --- a/packages/techdocs-container/techdocs-core/setup.py +++ b/packages/techdocs-container/techdocs-core/setup.py @@ -23,7 +23,7 @@ with open(path.join(this_dir, "README.md"), encoding="utf-8") as file: setup( name="mkdocs-techdocs-core", - version="0.0.8", + version="0.0.9", description="A Mkdocs package that contains TechDocs defaults", long_description=long_description, long_description_content_type="text/markdown", @@ -43,7 +43,7 @@ setup( "pymdown-extensions==7.1", ], classifiers=[ - "Development Status :: 1 - Planning", + "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "License :: OSI Approved :: Apache Software License", From 63d3a4d3dac92878918d8bc7282aaeb8ed8ae4ea Mon Sep 17 00:00:00 2001 From: Iain Billett Date: Mon, 21 Sep 2020 16:06:57 +0100 Subject: [PATCH 62/85] Add widget to show recent git workflow runs Adds a widget to show recent git workflow runs to the github actions plugin. The default setting is the last 5 runs across all branches but both branch and the number of runs are configurable. --- .../app/src/components/catalog/EntityPage.tsx | 20 +-- .../Cards/RecentWorkflowRunsCard.test.tsx | 127 ++++++++++++++++++ .../Cards/RecentWorkflowRunsCard.tsx | 88 ++++++++++++ .../src/components/Cards/index.ts | 1 + .../src/components/useWorkflowRuns.ts | 4 +- 5 files changed, 230 insertions(+), 10 deletions(-) create mode 100644 plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx create mode 100644 plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index d96fe9841f..28fb012c12 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -14,17 +14,18 @@ * limitations under the License. */ import { - Router as GitHubActionsRouter, isPluginApplicableToEntity as isGitHubActionsAvailable, + RecentWorkflowRunsCard, + Router as GitHubActionsRouter, } from '@backstage/plugin-github-actions'; import { - Router as JenkinsRouter, isPluginApplicableToEntity as isJenkinsAvailable, LatestRunCard as JenkinsLatestRunCard, + Router as JenkinsRouter, } from '@backstage/plugin-jenkins'; import { - Router as CircleCIRouter, isPluginApplicableToEntity as isCircleCIAvailable, + Router as CircleCIRouter, } from '@backstage/plugin-circleci'; import { Router as ApiDocsRouter } from '@backstage/plugin-api-docs'; import { Router as SentryRouter } from '@backstage/plugin-sentry'; @@ -62,14 +63,15 @@ const CICDSwitcher = ({ entity }: { entity: Entity }) => { const OverviewContent = ({ entity }: { entity: Entity }) => ( - + + {isJenkinsAvailable(entity) && } + {isGitHubActionsAvailable(entity) && ( + + )} + + - {isJenkinsAvailable(entity) && ( - - - - )} ); diff --git a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx new file mode 100644 index 0000000000..e5cb931ec1 --- /dev/null +++ b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx @@ -0,0 +1,127 @@ +/* + * 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 type { Props as RecentWorkflowRunsCardProps } from './RecentWorkflowRunsCard'; +import React from 'react'; +import { render } from '@testing-library/react'; +import { RecentWorkflowRunsCard } from './RecentWorkflowRunsCard'; +import { useApi } from '@backstage/core-api'; +import { useWorkflowRuns } from '../useWorkflowRuns'; +import { ThemeProvider } from '@material-ui/core'; +import { lightTheme } from '@backstage/theme'; +import { MemoryRouter } from 'react-router'; + +jest.mock('../useWorkflowRuns', () => ({ + useWorkflowRuns: jest.fn(), +})); +jest.mock('@backstage/core-api'); + +describe('', () => { + const entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'software', + annotations: { + 'github.com/project-slug': 'theorg/the-service', + }, + }, + spec: { + owner: 'guest', + type: 'service', + lifecycle: 'production', + }, + }; + + const workflowRuns = [1, 2, 3, 4, 5].map(n => ({ + id: `run-id-${n}`, + message: `Commit message for workflow ${n}`, + source: { branchName: `branch-${n}` }, + status: 'completed', + })); + const mockErrorApi = { post: jest.fn() }; + + beforeEach(() => { + (useWorkflowRuns as jest.Mock).mockReturnValue([{ runs: workflowRuns }]); + (useApi as jest.Mock).mockReturnValue(mockErrorApi); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + const renderSubject = (props: RecentWorkflowRunsCardProps = { entity }) => + render( + + + + + , + ); + + it('renders a table with a row for each workflow', async () => { + const subject = renderSubject(); + + workflowRuns.forEach(run => { + expect(subject.getByText(run.message)).toBeInTheDocument(); + }); + }); + + it('renders a workflow row correctly', async () => { + const subject = renderSubject(); + const [run] = workflowRuns; + expect(subject.getByText(run.message).closest('a')).toHaveAttribute( + 'href', + `/ci-cd/${run.id}`, + ); + expect(subject.getByText(run.source.branchName)).toBeInTheDocument(); + }); + + it('requests only the required number of workflow runs', async () => { + const limit = 3; + renderSubject({ entity, limit }); + expect(useWorkflowRuns).toHaveBeenCalledWith( + expect.objectContaining({ initialPageSize: limit }), + ); + }); + + it('uses the github repo and owner from the entity annotation', async () => { + renderSubject(); + expect(useWorkflowRuns).toHaveBeenCalledWith( + expect.objectContaining({ owner: 'theorg', repo: 'the-service' }), + ); + }); + + it('filters workflows by branch if one is specified', async () => { + const branch = 'master'; + renderSubject({ entity, branch }); + expect(useWorkflowRuns).toHaveBeenCalledWith( + expect.objectContaining({ branch }), + ); + }); + + describe('where there is an error fetching workflows', () => { + const error = 'error getting workflows'; + beforeEach(() => { + (useWorkflowRuns as jest.Mock).mockReturnValue([{ runs: [], error }]); + }); + + it('sends the error to the errorApi', async () => { + renderSubject(); + expect(mockErrorApi.post).toHaveBeenCalledWith(error); + }); + }); +}); diff --git a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx new file mode 100644 index 0000000000..014aed1633 --- /dev/null +++ b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx @@ -0,0 +1,88 @@ +/* + * 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 { Entity } from '@backstage/catalog-model'; +import { errorApiRef, useApi } from '@backstage/core-api'; +import { GITHUB_ACTIONS_ANNOTATION } from '../useProjectName'; +import { useWorkflowRuns } from '../useWorkflowRuns'; +import React, { useEffect } from 'react'; +import { Table } from '@backstage/core'; +import { WorkflowRunStatus } from '../WorkflowRunStatus'; +import { Card, Link, TableContainer } from '@material-ui/core'; +import { generatePath, Link as RouterLink } from 'react-router-dom'; + +const firstLine = (message: string): string => message.split('\n')[0]; + +export type Props = { + entity: Entity; + branch?: string; + dense?: boolean; + limit?: number; +}; + +export const RecentWorkflowRunsCard = ({ + entity, + branch, + dense = false, + limit = 5, +}: Props) => { + const errorApi = useApi(errorApiRef); + const [owner, repo] = ( + entity?.metadata.annotations?.[GITHUB_ACTIONS_ANNOTATION] ?? '/' + ).split('/'); + const [{ runs = [], loading, error }] = useWorkflowRuns({ + owner, + repo, + branch, + initialPageSize: limit, + }); + useEffect(() => { + if (error) { + errorApi.post(error); + } + }, [error, errorApi]); + + return ( + + ( + + {firstLine(data.message)} + + ), + }, + { title: 'Branch', field: 'source.branchName' }, + { title: 'Status', field: 'status', render: WorkflowRunStatus }, + ]} + data={runs} + /> + + ); +}; diff --git a/plugins/github-actions/src/components/Cards/index.ts b/plugins/github-actions/src/components/Cards/index.ts index 8c987ea1d5..ab918d3b77 100644 --- a/plugins/github-actions/src/components/Cards/index.ts +++ b/plugins/github-actions/src/components/Cards/index.ts @@ -14,3 +14,4 @@ * limitations under the License. */ export { LatestWorkflowRunCard, LatestWorkflowsForBranchCard } from './Cards'; +export { RecentWorkflowRunsCard } from './RecentWorkflowRunsCard'; diff --git a/plugins/github-actions/src/components/useWorkflowRuns.ts b/plugins/github-actions/src/components/useWorkflowRuns.ts index f935d57810..25ede3f997 100644 --- a/plugins/github-actions/src/components/useWorkflowRuns.ts +++ b/plugins/github-actions/src/components/useWorkflowRuns.ts @@ -24,10 +24,12 @@ export function useWorkflowRuns({ owner, repo, branch, + initialPageSize = 5, }: { owner: string; repo: string; branch?: string; + initialPageSize?: number; }) { const api = useApi(githubActionsApiRef); const auth = useApi(githubAuthApiRef); @@ -36,7 +38,7 @@ export function useWorkflowRuns({ const [total, setTotal] = useState(0); const [page, setPage] = useState(0); - const [pageSize, setPageSize] = useState(5); + const [pageSize, setPageSize] = useState(initialPageSize); const { loading, value: runs, retry, error } = useAsyncRetry< WorkflowRun[] From f910491c58e5c8db2f63338c0ef9df67537fb148 Mon Sep 17 00:00:00 2001 From: Iain Billett Date: Wed, 23 Sep 2020 09:43:13 +0100 Subject: [PATCH 63/85] Add widget to show recent git workflow runs * Fix Entity Page layout * Fix mock api injection --- .../app/src/components/catalog/EntityPage.tsx | 16 ++++++++++------ .../Cards/RecentWorkflowRunsCard.test.tsx | 14 +++++++++----- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 28fb012c12..931979d2d6 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -63,15 +63,19 @@ const CICDSwitcher = ({ entity }: { entity: Entity }) => { const OverviewContent = ({ entity }: { entity: Entity }) => ( - - {isJenkinsAvailable(entity) && } - {isGitHubActionsAvailable(entity) && ( - - )} - + {isJenkinsAvailable(entity) && ( + + + + )} + {isGitHubActionsAvailable(entity) && ( + + + + )} ); diff --git a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx index e5cb931ec1..1d6001e038 100644 --- a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx +++ b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx @@ -18,7 +18,7 @@ import type { Props as RecentWorkflowRunsCardProps } from './RecentWorkflowRunsC import React from 'react'; import { render } from '@testing-library/react'; import { RecentWorkflowRunsCard } from './RecentWorkflowRunsCard'; -import { useApi } from '@backstage/core-api'; +import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core-api'; import { useWorkflowRuns } from '../useWorkflowRuns'; import { ThemeProvider } from '@material-ui/core'; import { lightTheme } from '@backstage/theme'; @@ -27,7 +27,11 @@ import { MemoryRouter } from 'react-router'; jest.mock('../useWorkflowRuns', () => ({ useWorkflowRuns: jest.fn(), })); -jest.mock('@backstage/core-api'); + +const mockErrorApi: jest.Mocked = { + post: jest.fn(), + error$: jest.fn(), +}; describe('', () => { const entity = { @@ -52,11 +56,9 @@ describe('', () => { source: { branchName: `branch-${n}` }, status: 'completed', })); - const mockErrorApi = { post: jest.fn() }; beforeEach(() => { (useWorkflowRuns as jest.Mock).mockReturnValue([{ runs: workflowRuns }]); - (useApi as jest.Mock).mockReturnValue(mockErrorApi); }); afterEach(() => { @@ -67,7 +69,9 @@ describe('', () => { render( - + + + , ); From f77e21ea8db430ba1f34c6bc1f899bd862b016e3 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 24 Sep 2020 11:46:52 +0200 Subject: [PATCH 64/85] chore(diff): updating plugin diff for new k8s plugin --- plugins/kubernetes/package.json | 4 +++- yarn.lock | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index d9311deb85..ffe6207680 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -39,7 +39,9 @@ "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5", + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/yarn.lock b/yarn.lock index 24050ef3d4..0b6a8c83f4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16110,7 +16110,7 @@ node-fetch@^1.0.1: encoding "^0.1.11" is-stream "^1.0.1" -node-fetch@^2.1.2, node-fetch@^2.2.0, node-fetch@^2.3.0, node-fetch@^2.5.0, node-fetch@^2.6.0: +node-fetch@^2.1.2, node-fetch@^2.2.0, node-fetch@^2.3.0, node-fetch@^2.5.0, node-fetch@^2.6.0, node-fetch@^2.6.1: version "2.6.1" resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== From 3b2e301c52258447ec58f688a4f93a6c68777823 Mon Sep 17 00:00:00 2001 From: Iain Billett Date: Wed, 23 Sep 2020 16:38:52 +0100 Subject: [PATCH 65/85] Add widget to show recent git workflow runs * Add switcher for recent CI/CD widgets --- .../app/src/components/catalog/EntityPage.tsx | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 931979d2d6..79ca074c35 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -31,7 +31,7 @@ import { Router as ApiDocsRouter } from '@backstage/plugin-api-docs'; import { Router as SentryRouter } from '@backstage/plugin-sentry'; import { EmbeddedDocsRouter as DocsRouter } from '@backstage/plugin-techdocs'; import { Router as KubernetesRouter } from '@backstage/plugin-kubernetes'; -import React from 'react'; +import React, { ReactNode } from 'react'; import { AboutCard, EntityPageLayout, @@ -61,21 +61,34 @@ const CICDSwitcher = ({ entity }: { entity: Entity }) => { } }; +const RecentCICDRunsSwitcher = ({ entity }: { entity: Entity }) => { + let content: ReactNode; + switch (true) { + case isJenkinsAvailable(entity): + content = ; + break; + case isGitHubActionsAvailable(entity): + content = ; + break; + default: + content = null; + } + if (!content) { + return null; + } + return ( + + {content} + + ); +}; + const OverviewContent = ({ entity }: { entity: Entity }) => ( - {isJenkinsAvailable(entity) && ( - - - - )} - {isGitHubActionsAvailable(entity) && ( - - - - )} + ); From d432037459c195060ce2ffb673df1abad155148c Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 24 Sep 2020 12:37:48 +0200 Subject: [PATCH 66/85] chore(cli): addding backstage backend plugin creation --- packages/e2e-test/src/e2e-test.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/e2e-test/src/e2e-test.ts b/packages/e2e-test/src/e2e-test.ts index 969a77cb2a..7efcce567d 100644 --- a/packages/e2e-test/src/e2e-test.ts +++ b/packages/e2e-test/src/e2e-test.ts @@ -55,6 +55,9 @@ async function main() { print('Creating a Backstage Plugin'); const pluginName = await createPlugin('test-plugin', appDir); + print('Creating a Backstage Backend Plugin'); + await createPlugin('test-backend-plugin', appDir, ['--backend']); + print('Starting the app'); await testAppServe(pluginName, appDir); @@ -238,8 +241,12 @@ async function overrideModuleResolutions(appDir: string, workspaceDir: string) { /** * Uses create-plugin command to create a new plugin in the app */ -async function createPlugin(pluginName: string, appDir: string) { - const child = spawnPiped(['yarn', 'create-plugin'], { +async function createPlugin( + pluginName: string, + appDir: string, + options: string[] = [], +) { + const child = spawnPiped(['yarn', 'create-plugin', ...options], { cwd: appDir, }); From 06ae3734e7f2e9533b64ac60b855b45af0a96843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20=C3=85lund?= Date: Thu, 24 Sep 2020 14:33:30 +0200 Subject: [PATCH 67/85] [docs] Add links to CNCF information (#2570) * [docs] restructure * Add CNCF links * Update background.md * Update Footer.js * Update README.md --- README.md | 6 +++--- docs/overview/architecture-overview.md | 17 +++++++++++++++++ docs/overview/architecture-terminology.md | 20 -------------------- docs/overview/background.md | 9 ++++++++- docs/overview/support.md | 7 ++----- docs/overview/what-is-backstage.md | 9 ++++++++- microsite/core/Footer.js | 1 + microsite/sidebars.json | 1 - mkdocs.yml | 1 - 9 files changed, 39 insertions(+), 32 deletions(-) delete mode 100644 docs/overview/architecture-terminology.md diff --git a/README.md b/README.md index 9ef09aee06..e05c1eb16e 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,8 @@ Out of the box, Backstage includes: For more information go to [backstage.io](https://backstage.io) or join our [Discord chatroom](https://discord.gg/EBHEGzX). +🎉 Backstage is a CNCF Sandbox project. Read the announcement [here](https://backstage.io/blog/2020/09/23/backstage-cncf-sandbox). + ## Project roadmap A detailed project roadmap, including already delivered milestones, is available [here](https://backstage.io/docs/overview/roadmap). @@ -51,11 +53,9 @@ Check out [the documentation](https://backstage.io/docs/getting-started) on how - [Code of Conduct](CODE_OF_CONDUCT.md) - This is how we roll - [Adopters](ADOPTERS.md) - Companies already using Backstage - [Blog](https://backstage.io/blog/) - Announcements and updates -- [Newsletter](https://mailchi.mp/spotify/backstage-community) +- [Newsletter](https://mailchi.mp/spotify/backstage-community) - Subscribe to our email newsletter - Give us a star ⭐️ - If you are using Backstage or think it is an interesting project, we would love a star ❤️ -Or, if you are an open source developer and are interested in joining our team, please reach out to [foss-opportunities@spotify.com ](mailto:foss-opportunities@spotify.com) - ## License Copyright 2020 Spotify AB. diff --git a/docs/overview/architecture-overview.md b/docs/overview/architecture-overview.md index f2c6dad61f..8edf3e9e4c 100644 --- a/docs/overview/architecture-overview.md +++ b/docs/overview/architecture-overview.md @@ -4,6 +4,23 @@ title: Architecture overview description: Documentation on Architecture overview --- +## Terminology + +Backstage is constructed out of three parts. We separate Backstage in this way +because we see three groups of contributors that work with Backstage in three +different ways. + +- Core - Base functionality built by core developers in the open source project. +- App - The app is an instance of a Backstage app that is deployed and tweaked. + The app ties together core functionality with additional plugins. The app is + built and maintained by app developers, usually a productivity team within a + company. +- Plugins - Additional functionality to make your Backstage app useful for your + company. Plugins can be specific to a company or open sourced and reusable. At + Spotify we have over 100 plugins built by over 50 different teams. It has been + very powerful to get contributions from various infrastructure teams added + into a single unified developer experience. + ## Overview The following diagram shows how Backstage might look when deployed inside a diff --git a/docs/overview/architecture-terminology.md b/docs/overview/architecture-terminology.md deleted file mode 100644 index 092ca7fb01..0000000000 --- a/docs/overview/architecture-terminology.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -id: architecture-terminology -title: Architecture terminology -description: Documentation on Architecture terminology ---- - -Backstage is constructed out of three parts. We separate Backstage in this way -because we see three groups of contributors that work with Backstage in three -different ways. - -- Core - Base functionality built by core devs in the open source project. -- App - The app is an instance of a Backstage app that is deployed and tweaked. - The app ties together core functionality with additional plugins. The app is - built and maintained by app developers, usually a productivity team within a - company. -- Plugins - Additional functionality to make your Backstage app useful for your - company. Plugins can be specific to a company or open sourced and reusable. At - Spotify we have over 100 plugins built by over 50 different teams. It has been - very powerful to get contributions from various infrastructure teams added - into a single unified developer experience. diff --git a/docs/overview/background.md b/docs/overview/background.md index 486260bac3..aca46614c2 100644 --- a/docs/overview/background.md +++ b/docs/overview/background.md @@ -1,7 +1,8 @@ --- id: background title: The Spotify Story -description: Documentation on Background and Story behind making of Backstage +description: Backstage was born out of necessity at Spotify. We found that as we grew, our +infrastructure was becoming more fragmented, our engineers less productive. --- Backstage was born out of necessity at Spotify. We found that as we grew, our @@ -29,3 +30,9 @@ building a new microservice using an automated template in Backstage. Create, maintain, and find the documentation for all that software in Backstage. One place for everything. Accessible to everyone. + +Backstage was originally built by Spotify and then donated to the CNCF. +Backstage is currently in the Sandbox phase. Read the announcement +[here](https://backstage.io/blog/2020/09/23/backstage-cncf-sandbox). + + diff --git a/docs/overview/support.md b/docs/overview/support.md index 6e00c17fab..12bc7cfaf9 100644 --- a/docs/overview/support.md +++ b/docs/overview/support.md @@ -13,10 +13,7 @@ description: Support and Community Details and Links - [FAQ](../FAQ.md) - Frequently Asked Questions - [Code of Conduct](../../CODE_OF_CONDUCT.md) - This is how we roll - [Blog](https://backstage.io/blog/) - Announcements and updates -- [Newsletter](https://mailchi.mp/spotify/backstage-community) +- [Newsletter](https://mailchi.mp/spotify/backstage-community) - Subscribe to + our email newsletter - Give us a star ⭐️ - If you are using Backstage or think it is an interesting project, we would love a star ❤️ - -Or, if you are an open source developer and are interested in joining our team, -please reach out to -[foss-opportunities@spotify.com ](mailto:foss-opportunities@spotify.com) diff --git a/docs/overview/what-is-backstage.md b/docs/overview/what-is-backstage.md index 1a5e80a302..3d41c714ee 100644 --- a/docs/overview/what-is-backstage.md +++ b/docs/overview/what-is-backstage.md @@ -33,7 +33,14 @@ Out of the box, Backstage includes: [open source plugins](https://github.com/spotify/backstage/tree/master/plugins) that further expand Backstage’s customizability and functionality -### Benefits +## Backstage and the CNCF + +Backstage is a CNCF Sandbox project. Read the announcement +[here](https://backstage.io/blog/2020/09/23/backstage-cncf-sandbox). + + + +## Benefits - For _engineering managers_, it allows you to maintain standards and best practices across the organization, and can help you manage your whole tech diff --git a/microsite/core/Footer.js b/microsite/core/Footer.js index 761862c1fc..8e3c4c086d 100644 --- a/microsite/core/Footer.js +++ b/microsite/core/Footer.js @@ -51,6 +51,7 @@ class Footer extends React.Component { Subscribe to our newsletter + CNCF Sandbox
More
diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 8fa4935a38..e9993b2e8b 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -3,7 +3,6 @@ "Overview": [ "overview/what-is-backstage", "overview/architecture-overview", - "overview/architecture-terminology", "overview/roadmap", "overview/vision", "overview/background", diff --git a/mkdocs.yml b/mkdocs.yml index ba607f8a6e..f85044822e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -5,7 +5,6 @@ nav: - Overview: - What is Backstage?: 'overview/what-is-backstage.md' - Backstage architecture: 'overview/architecture-overview.md' - - Architecture and terminology: 'overview/architecture-terminology.md' - Roadmap: 'overview/roadmap.md' - Vision: 'overview/vision.md' - The Spotify story: 'overview/background.md' From ab4de802318307f787c7b3876a30c5bac80bf7c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 23 Sep 2020 14:01:09 +0200 Subject: [PATCH 68/85] feat(catalog): entirely case insensitive read path of entities --- packages/catalog-model/src/entity/Entity.ts | 7 +- .../catalog-model/src/entity/constants.ts | 29 +++++++ packages/catalog-model/src/entity/index.ts | 12 ++- .../DefaultNamespaceEntityPolicy.test.ts | 6 +- .../policies/DefaultNamespaceEntityPolicy.ts | 3 +- packages/catalog-model/src/entity/ref.test.ts | 11 +-- packages/catalog-model/src/entity/ref.ts | 19 ++++- .../catalog-model/src/location/annotation.ts | 1 + .../20200923104503_case_insensitivity.js | 32 +++++++ .../catalog/CoalescedEntitiesCatalog.test.ts | 28 ++++-- .../src/catalog/CoalescedEntitiesCatalog.ts | 10 +-- .../catalog/DatabaseEntitiesCatalog.test.ts | 28 +++--- .../src/catalog/DatabaseEntitiesCatalog.ts | 45 ++-------- .../src/catalog/StaticEntitiesCatalog.ts | 22 +++-- plugins/catalog-backend/src/catalog/types.ts | 8 +- .../src/database/CommonDatabase.test.ts | 85 +++++++++++++++++++ .../src/database/CommonDatabase.ts | 62 ++++++++------ .../src/database/search.test.ts | 20 ++++- .../catalog-backend/src/database/search.ts | 23 ++++- plugins/catalog-backend/src/database/types.ts | 8 +- .../ingestion/HigherOrderOperations.test.ts | 18 ++-- .../src/ingestion/HigherOrderOperations.ts | 11 ++- .../src/service/router.test.ts | 12 ++- plugins/catalog-backend/src/service/router.ts | 4 +- 24 files changed, 351 insertions(+), 153 deletions(-) create mode 100644 packages/catalog-model/src/entity/constants.ts create mode 100644 plugins/catalog-backend/migrations/20200923104503_case_insensitivity.js diff --git a/packages/catalog-model/src/entity/Entity.ts b/packages/catalog-model/src/entity/Entity.ts index 7f323043b1..9e0aec7de6 100644 --- a/packages/catalog-model/src/entity/Entity.ts +++ b/packages/catalog-model/src/entity/Entity.ts @@ -87,7 +87,7 @@ export type EntityMeta = JsonObject & { /** * The name of the entity. * - * Must be uniqe within the catalog at any given point in time, for any + * Must be unique within the catalog at any given point in time, for any * given namespace + kind pair. */ name: string; @@ -120,8 +120,3 @@ export type EntityMeta = JsonObject & { */ tags?: string[]; }; - -/** - * The keys of EntityMeta that are auto-generated. - */ -export const entityMetaGeneratedFields = ['uid', 'etag', 'generation'] as const; diff --git a/packages/catalog-model/src/entity/constants.ts b/packages/catalog-model/src/entity/constants.ts new file mode 100644 index 0000000000..42ea2ae8ba --- /dev/null +++ b/packages/catalog-model/src/entity/constants.ts @@ -0,0 +1,29 @@ +/* + * 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. + */ + +/** + * The namespace that entities without an explicit namespace fall into. + */ +export const ENTITY_DEFAULT_NAMESPACE = 'default'; + +/** + * The keys of EntityMeta that are auto-generated. + */ +export const ENTITY_META_GENERATED_FIELDS = [ + 'uid', + 'etag', + 'generation', +] as const; diff --git a/packages/catalog-model/src/entity/index.ts b/packages/catalog-model/src/entity/index.ts index a6e90e6975..759a94ca67 100644 --- a/packages/catalog-model/src/entity/index.ts +++ b/packages/catalog-model/src/entity/index.ts @@ -14,10 +14,18 @@ * limitations under the License. */ -export { entityMetaGeneratedFields } from './Entity'; +export { + ENTITY_DEFAULT_NAMESPACE, + ENTITY_META_GENERATED_FIELDS, +} from './constants'; export type { Entity, EntityMeta } from './Entity'; export * from './policies'; -export { parseEntityName, serializeEntityRef } from './ref'; +export { + getEntityName, + parseEntityName, + parseEntityRef, + serializeEntityRef, +} from './ref'; export { entityHasChanges, generateEntityEtag, diff --git a/packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.test.ts index 68f1cec649..c6bda864cb 100644 --- a/packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.test.ts +++ b/packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.test.ts @@ -15,6 +15,7 @@ */ import yaml from 'yaml'; +import { ENTITY_DEFAULT_NAMESPACE } from '../constants'; import { DefaultNamespaceEntityPolicy } from './DefaultNamespaceEntityPolicy'; describe('DefaultNamespaceEntityPolicy', () => { @@ -54,7 +55,10 @@ describe('DefaultNamespaceEntityPolicy', () => { await expect(result).resolves.not.toBe(withoutNamespace); await expect(result).resolves.toEqual( expect.objectContaining({ - metadata: { name: 'my-component-yay', namespace: 'default' }, + metadata: { + name: 'my-component-yay', + namespace: ENTITY_DEFAULT_NAMESPACE, + }, }), ); }); diff --git a/packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.ts b/packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.ts index e5aba745c7..3119164454 100644 --- a/packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.ts @@ -16,6 +16,7 @@ import lodash from 'lodash'; import { EntityPolicy } from '../../types'; +import { ENTITY_DEFAULT_NAMESPACE } from '../constants'; import { Entity } from '../Entity'; /** @@ -24,7 +25,7 @@ import { Entity } from '../Entity'; export class DefaultNamespaceEntityPolicy implements EntityPolicy { private readonly namespace: string; - constructor(namespace: string = 'default') { + constructor(namespace: string = ENTITY_DEFAULT_NAMESPACE) { this.namespace = namespace; } diff --git a/packages/catalog-model/src/entity/ref.test.ts b/packages/catalog-model/src/entity/ref.test.ts index b76f01f1f9..d9787fe675 100644 --- a/packages/catalog-model/src/entity/ref.test.ts +++ b/packages/catalog-model/src/entity/ref.test.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { ENTITY_DEFAULT_NAMESPACE } from './constants'; import { parseEntityName, parseEntityRef, serializeEntityRef } from './ref'; describe('ref', () => { @@ -27,7 +28,7 @@ describe('ref', () => { expect(() => parseEntityName('b/c')).toThrow(/kind/); expect(parseEntityName('a:c')).toEqual({ kind: 'a', - namespace: 'default', + namespace: ENTITY_DEFAULT_NAMESPACE, name: 'c', }); expect(() => parseEntityName('c')).toThrow(/kind/); @@ -116,7 +117,7 @@ describe('ref', () => { ).toEqual({ kind: 'a', namespace: 'y', name: 'c' }); expect(parseEntityName('a:c', { defaultKind: 'x' })).toEqual({ kind: 'a', - namespace: 'default', + namespace: ENTITY_DEFAULT_NAMESPACE, name: 'c', }); expect( @@ -124,7 +125,7 @@ describe('ref', () => { ).toEqual({ kind: 'x', namespace: 'y', name: 'c' }); expect(parseEntityName('c', { defaultKind: 'x' })).toEqual({ kind: 'x', - namespace: 'default', + namespace: ENTITY_DEFAULT_NAMESPACE, name: 'c', }); }); @@ -150,7 +151,7 @@ describe('ref', () => { ).toEqual({ kind: 'a', namespace: 'y', name: 'c' }); expect( parseEntityName({ kind: 'a', name: 'c' }, { defaultKind: 'x' }), - ).toEqual({ kind: 'a', namespace: 'default', name: 'c' }); + ).toEqual({ kind: 'a', namespace: ENTITY_DEFAULT_NAMESPACE, name: 'c' }); expect( parseEntityName( { name: 'c' }, @@ -159,7 +160,7 @@ describe('ref', () => { ).toEqual({ kind: 'x', namespace: 'y', name: 'c' }); expect(parseEntityName({ name: 'c' }, { defaultKind: 'x' })).toEqual({ kind: 'x', - namespace: 'default', + namespace: ENTITY_DEFAULT_NAMESPACE, name: 'c', }); // empty strings are errors, not defaults diff --git a/packages/catalog-model/src/entity/ref.ts b/packages/catalog-model/src/entity/ref.ts index b9320a5ac7..c4c86b176c 100644 --- a/packages/catalog-model/src/entity/ref.ts +++ b/packages/catalog-model/src/entity/ref.ts @@ -15,6 +15,23 @@ */ import { EntityName, EntityRef } from '../types'; +import { ENTITY_DEFAULT_NAMESPACE } from './constants'; +import { Entity } from './Entity'; + +/** + * Extracts the kind, namespace and name that form the name triplet of the + * given entity. + * + * @param entity An entity + * @returns The complete entity name + */ +export function getEntityName(entity: Entity): EntityName { + return { + kind: entity.kind, + namespace: entity.metadata.namespace || ENTITY_DEFAULT_NAMESPACE, + name: entity.metadata.name, + }; +} /** * The context of defaults that entity reference parsing happens within. @@ -43,7 +60,7 @@ export function parseEntityName( context: EntityRefContext = {}, ): EntityName { const { kind, namespace, name } = parseEntityRef(ref, { - defaultNamespace: 'default', + defaultNamespace: ENTITY_DEFAULT_NAMESPACE, ...context, }); diff --git a/packages/catalog-model/src/location/annotation.ts b/packages/catalog-model/src/location/annotation.ts index ab7a90249a..371d095685 100644 --- a/packages/catalog-model/src/location/annotation.ts +++ b/packages/catalog-model/src/location/annotation.ts @@ -13,4 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export const LOCATION_ANNOTATION = 'backstage.io/managed-by-location'; diff --git a/plugins/catalog-backend/migrations/20200923104503_case_insensitivity.js b/plugins/catalog-backend/migrations/20200923104503_case_insensitivity.js new file mode 100644 index 0000000000..eeec950d7f --- /dev/null +++ b/plugins/catalog-backend/migrations/20200923104503_case_insensitivity.js @@ -0,0 +1,32 @@ +/* + * 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. + */ + +// @ts-check + +/** + * @param {import('knex')} knex + */ +exports.up = async function up(knex) { + await knex('entities') + .where({ namespace: null }) + .update({ namespace: 'default' }); + await knex('entities_search').update({ + key: knex.raw('LOWER(key)'), + value: knex.raw('LOWER(value)'), + }); +}; + +exports.down = async function down() {}; diff --git a/plugins/catalog-backend/src/catalog/CoalescedEntitiesCatalog.test.ts b/plugins/catalog-backend/src/catalog/CoalescedEntitiesCatalog.test.ts index bcd1248a66..1cfea23e43 100644 --- a/plugins/catalog-backend/src/catalog/CoalescedEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/catalog/CoalescedEntitiesCatalog.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; +import { Entity, ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model'; import { Logger } from 'winston'; import { CoalescedEntitiesCatalog } from './CoalescedEntitiesCatalog'; import { EntitiesCatalog } from './types'; @@ -115,9 +115,13 @@ describe('CoalescedEntitiesCatalog', () => { c1.entityByName.mockResolvedValueOnce(undefined); c2.entityByName.mockResolvedValueOnce(e2); const catalog = new CoalescedEntitiesCatalog([c1, c2], logger); - await expect(catalog.entityByName('k', undefined, 'n2')).resolves.toBe( - e2, - ); + await expect( + catalog.entityByName({ + kind: 'k', + namespace: ENTITY_DEFAULT_NAMESPACE, + name: 'n2', + }), + ).resolves.toBe(e2); expect(c1.entityByName).toBeCalledTimes(1); expect(c2.entityByName).toBeCalledTimes(1); }); @@ -127,7 +131,11 @@ describe('CoalescedEntitiesCatalog', () => { c2.entityByName.mockResolvedValueOnce(undefined); const catalog = new CoalescedEntitiesCatalog([c1, c2], logger); await expect( - catalog.entityByName('k', undefined, 'n2'), + catalog.entityByName({ + kind: 'k', + namespace: ENTITY_DEFAULT_NAMESPACE, + name: 'n2', + }), ).resolves.toBeUndefined(); expect(c1.entityByName).toBeCalledTimes(1); expect(c2.entityByName).toBeCalledTimes(1); @@ -137,9 +145,13 @@ describe('CoalescedEntitiesCatalog', () => { c1.entityByName.mockResolvedValueOnce(e1); c2.entityByName.mockRejectedValueOnce(new Error('boo')); const catalog = new CoalescedEntitiesCatalog([c1, c2], logger); - await expect(catalog.entityByName('k', undefined, 'n2')).resolves.toBe( - e1, - ); + await expect( + catalog.entityByName({ + kind: 'k', + namespace: ENTITY_DEFAULT_NAMESPACE, + name: 'n2', + }), + ).resolves.toBe(e1); expect(c1.entityByName).toBeCalledTimes(1); expect(c2.entityByName).toBeCalledTimes(1); expect(mockLogger.warn).toBeCalledWith(expect.stringMatching(/boo/)); diff --git a/plugins/catalog-backend/src/catalog/CoalescedEntitiesCatalog.ts b/plugins/catalog-backend/src/catalog/CoalescedEntitiesCatalog.ts index 5a0922495e..7eaf1868cb 100644 --- a/plugins/catalog-backend/src/catalog/CoalescedEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/catalog/CoalescedEntitiesCatalog.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; +import { Entity, EntityName } from '@backstage/catalog-model'; import { Logger } from 'winston'; import { EntityFilters } from '../database'; import { EntitiesCatalog } from './types'; @@ -72,14 +72,10 @@ export class CoalescedEntitiesCatalog implements EntitiesCatalog { return results.find(Boolean); } - async entityByName( - kind: string, - namespace: string | undefined, - name: string, - ): Promise { + async entityByName(name: EntityName): Promise { const ops = this.inner.map(async catalog => { try { - return await catalog.entityByName(kind, namespace, name); + return await catalog.entityByName(name); } catch (e) { this.logger.warn(`Inner entityByName call failed, ${e}`); return undefined; diff --git a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts index 5d35fe814e..b94365a9ba 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts @@ -27,7 +27,7 @@ describe('DatabaseEntitiesCatalog', () => { addEntity: jest.fn(), updateEntity: jest.fn(), entities: jest.fn(), - entity: jest.fn(), + entityByName: jest.fn(), entityByUid: jest.fn(), removeEntity: jest.fn(), addLocation: jest.fn(), @@ -61,12 +61,12 @@ describe('DatabaseEntitiesCatalog', () => { const catalog = new DatabaseEntitiesCatalog(db); const result = await catalog.addOrUpdateEntity(entity); - expect(db.entities).toHaveBeenCalledTimes(1); - expect(db.entities).toHaveBeenCalledWith(expect.anything(), [ - { key: 'kind', values: ['b'] }, - { key: 'name', values: ['c'] }, - { key: 'namespace', values: ['d'] }, - ]); + expect(db.entityByName).toHaveBeenCalledTimes(1); + expect(db.entityByName).toHaveBeenCalledWith(expect.anything(), { + kind: 'b', + namespace: 'd', + name: 'c', + }); expect(db.addEntity).toHaveBeenCalledTimes(1); expect(result).toBe(entity); }); @@ -146,18 +146,18 @@ describe('DatabaseEntitiesCatalog', () => { }, }; - db.entities.mockResolvedValue([{ entity: existing }]); + db.entityByName.mockResolvedValue({ entity: existing }); db.updateEntity.mockResolvedValue({ entity: existing }); const catalog = new DatabaseEntitiesCatalog(db); const result = await catalog.addOrUpdateEntity(added); - expect(db.entities).toHaveBeenCalledTimes(1); - expect(db.entities).toHaveBeenCalledWith(expect.anything(), [ - { key: 'kind', values: ['b'] }, - { key: 'name', values: ['c'] }, - { key: 'namespace', values: ['d'] }, - ]); + expect(db.entityByName).toHaveBeenCalledTimes(1); + expect(db.entityByName).toHaveBeenCalledWith(expect.anything(), { + kind: 'b', + namespace: 'd', + name: 'c', + }); expect(db.updateEntity).toHaveBeenCalledTimes(1); expect(db.updateEntity).toHaveBeenCalledWith( expect.anything(), diff --git a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts index d6809e468f..f6aec0c268 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts @@ -16,7 +16,9 @@ import { NotFoundError } from '@backstage/backend-common'; import { + EntityName, generateUpdatedEntity, + getEntityName, LOCATION_ANNOTATION, } from '@backstage/catalog-model'; import type { Entity } from '@backstage/catalog-model'; @@ -34,20 +36,15 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog { } async entityByUid(uid: string): Promise { - const matches = await this.database.transaction(tx => - this.database.entities(tx, [{ key: 'uid', values: [uid] }]), + const response = await this.database.transaction(tx => + this.database.entityByUid(tx, uid), ); - - return matches.length ? matches[0].entity : undefined; + return response?.entity; } - async entityByName( - kind: string, - namespace: string | undefined, - name: string, - ): Promise { + async entityByName(name: EntityName): Promise { const response = await this.database.transaction(tx => - this.entityByNameInternal(tx, kind, name, namespace), + this.database.entityByName(tx, name), ); return response?.entity; } @@ -61,12 +58,7 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog { // entity) existing entity, to know whether to update or add const existing = entity.metadata.uid ? await this.database.entityByUid(tx, entity.metadata.uid) - : await this.entityByNameInternal( - tx, - entity.kind, - entity.metadata.name, - entity.metadata.namespace, - ); + : await this.database.entityByName(tx, getEntityName(entity)); // If it's an update, run the algorithm for annotation merging, updating // etag/generation, etc. @@ -113,25 +105,4 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog { return undefined; }); } - - private async entityByNameInternal( - tx: unknown, - kind: string, - name: string, - namespace: string | undefined, - ): Promise { - const matches = await this.database.entities(tx, [ - { key: 'kind', values: [kind] }, - { key: 'name', values: [name] }, - { - key: 'namespace', - values: - !namespace || namespace === 'default' - ? [null, 'default'] - : [namespace], - }, - ]); - - return matches.length ? matches[0] : undefined; - } } diff --git a/plugins/catalog-backend/src/catalog/StaticEntitiesCatalog.ts b/plugins/catalog-backend/src/catalog/StaticEntitiesCatalog.ts index 22bbd2e1a3..65fbfb9071 100644 --- a/plugins/catalog-backend/src/catalog/StaticEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/catalog/StaticEntitiesCatalog.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Entity } from '@backstage/catalog-model'; +import { Entity, EntityName, getEntityName } from '@backstage/catalog-model'; import lodash from 'lodash'; import type { EntitiesCatalog } from './types'; @@ -34,17 +34,15 @@ export class StaticEntitiesCatalog implements EntitiesCatalog { return item ? lodash.cloneDeep(item) : undefined; } - async entityByName( - kind: string, - name: string, - namespace: string | undefined, - ): Promise { - const item = this._entities.find( - e => - kind === e.kind && - name === e.metadata.name && - namespace === e.metadata.namespace, - ); + async entityByName(name: EntityName): Promise { + const item = this._entities.find(e => { + const candidate = getEntityName(e); + return ( + name.kind.toLowerCase() === candidate.kind.toLowerCase() && + name.namespace.toLowerCase() === candidate.namespace.toLowerCase() && + name.name.toLowerCase() === candidate.name.toLowerCase() + ); + }); return item ? lodash.cloneDeep(item) : undefined; } diff --git a/plugins/catalog-backend/src/catalog/types.ts b/plugins/catalog-backend/src/catalog/types.ts index b0d3dde0fb..37618a2440 100644 --- a/plugins/catalog-backend/src/catalog/types.ts +++ b/plugins/catalog-backend/src/catalog/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Entity, Location } from '@backstage/catalog-model'; +import { Entity, EntityName, Location } from '@backstage/catalog-model'; import type { EntityFilters } from '../database'; // @@ -24,11 +24,7 @@ import type { EntityFilters } from '../database'; export type EntitiesCatalog = { entities(filters?: EntityFilters): Promise; entityByUid(uid: string): Promise; - entityByName( - kind: string, - namespace: string | undefined, - name: string, - ): Promise; + entityByName(name: EntityName): Promise; addOrUpdateEntity(entity: Entity, locationId?: string): Promise; removeEntityByUid(uid: string): Promise; }; diff --git a/plugins/catalog-backend/src/database/CommonDatabase.test.ts b/plugins/catalog-backend/src/database/CommonDatabase.test.ts index b86b4ef959..8b8067f641 100644 --- a/plugins/catalog-backend/src/database/CommonDatabase.test.ts +++ b/plugins/catalog-backend/src/database/CommonDatabase.test.ts @@ -395,5 +395,90 @@ describe('CommonDatabase', () => { ]), ); }); + + it('can get all specific entities for matching filters case insensitively)', async () => { + const entities: Entity[] = [ + { apiVersion: 'A', kind: 'K1', metadata: { name: 'N' } }, + { + apiVersion: 'a', + kind: 'k2', + metadata: { name: 'n' }, + spec: { c: 'Some' }, + }, + { + apiVersion: 'a', + kind: 'k3', + metadata: { name: 'n' }, + spec: { c: null }, + }, + ]; + + await db.transaction(async tx => { + for (const entity of entities) { + await db.addEntity(tx, { entity }); + } + }); + + const rows = await db.transaction(async tx => + db.entities(tx, [ + { key: 'ApiVersioN', values: ['A'] }, + { key: 'spEc.C', values: [null, 'some'] }, + ]), + ); + + expect(rows.length).toEqual(3); + expect(rows).toEqual( + expect.arrayContaining([ + { + locationId: undefined, + entity: expect.objectContaining({ kind: 'K1' }), + }, + { + locationId: undefined, + entity: expect.objectContaining({ kind: 'k2' }), + }, + { + locationId: undefined, + entity: expect.objectContaining({ kind: 'k3' }), + }, + ]), + ); + }); + }); + + describe('entityByName', () => { + it('can get entities case insensitively', async () => { + const entities: Entity[] = [ + { + apiVersion: 'a', + kind: 'k1', + metadata: { name: 'n' }, + }, + { + apiVersion: 'B', + kind: 'K2', + metadata: { name: 'N', namespace: 'NS' }, + }, + ]; + + await db.transaction(async tx => { + for (const entity of entities) { + await db.addEntity(tx, { entity }); + } + }); + + const e1 = await db.transaction(async tx => + db.entityByName(tx, { kind: 'k1', namespace: 'default', name: 'n' }), + ); + expect(e1!.entity.metadata.name).toEqual('n'); + const e2 = await db.transaction(async tx => + db.entityByName(tx, { kind: 'k2', namespace: 'nS', name: 'n' }), + ); + expect(e2!.entity.metadata.name).toEqual('N'); + const e3 = await db.transaction(async tx => + db.entityByName(tx, { kind: 'unknown', namespace: 'nS', name: 'n' }), + ); + expect(e3).toBeUndefined(); + }); }); }); diff --git a/plugins/catalog-backend/src/database/CommonDatabase.ts b/plugins/catalog-backend/src/database/CommonDatabase.ts index 4ad5362fb6..9ac1fff951 100644 --- a/plugins/catalog-backend/src/database/CommonDatabase.ts +++ b/plugins/catalog-backend/src/database/CommonDatabase.ts @@ -22,9 +22,12 @@ import { import { Entity, EntityMeta, - entityMetaGeneratedFields, + EntityName, + ENTITY_DEFAULT_NAMESPACE, + ENTITY_META_GENERATED_FIELDS, generateEntityEtag, generateEntityUid, + getEntityName, Location, } from '@backstage/catalog-model'; import Knex from 'knex'; @@ -172,7 +175,7 @@ export class CommonDatabase implements Database { let builder = tx('entities'); for (const [indexU, filter] of (filters ?? []).entries()) { const index = Number(indexU); - const key = filter.key.replace('*', '%'); + const key = filter.key.toLowerCase().replace('*', '%'); const keyOp = filter.key.includes('*') ? 'like' : '='; let matchNulls = false; @@ -183,9 +186,9 @@ export class CommonDatabase implements Database { if (!value) { matchNulls = true; } else if (value.includes('*')) { - matchLike.push(value.replace('*', '%')); + matchLike.push(value.toLowerCase().replace('*', '%')); } else { - matchIn.push(value); + matchIn.push(value.toLowerCase()); } } @@ -219,16 +222,19 @@ export class CommonDatabase implements Database { return rows.map(row => this.toEntityResponse(row)); } - async entity( + async entityByName( txOpaque: unknown, - kind: string, - name: string, - namespace?: string, + name: EntityName, ): Promise { const tx = txOpaque as Knex.Transaction; const rows = await tx('entities') - .where({ kind, name, namespace: namespace || null }) + .whereRaw( + tx.raw( + 'LOWER(kind) = LOWER(?) AND LOWER(namespace) = LOWER(?) AND LOWER(name) = LOWER(?)', + [name.kind, name.namespace, name.name], + ), + ) .select(); if (rows.length !== 1) { @@ -240,11 +246,13 @@ export class CommonDatabase implements Database { async entityByUid( txOpaque: unknown, - id: string, + uid: string, ): Promise { const tx = txOpaque as Knex.Transaction; - const rows = await tx('entities').where({ id }).select(); + const rows = await tx('entities') + .where({ id: uid }) + .select(); if (rows.length !== 1) { return undefined; @@ -381,36 +389,40 @@ export class CommonDatabase implements Database { tx: Knex.Transaction, data: Entity, ): Promise { - const newKind = data.kind; - const newName = data.metadata.name; - const newNamespace = data.metadata.namespace; + const { + kind: newKind, + namespace: newNamespace, + name: newName, + } = getEntityName(data); const newKindNorm = this.normalize(newKind); + const newNamespaceNorm = this.normalize(newNamespace); 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 { + kind: oldKind, + namespace: oldNamespace, + name: oldName, + } = getEntityName(item.entity); const oldKindNorm = this.normalize(oldKind); + const oldNamespaceNorm = this.normalize(oldNamespace); const oldNameNorm = this.normalize(oldName); - const oldNamespaceNorm = this.normalize(oldNamespace || ''); if ( oldKindNorm === newKindNorm && - oldNameNorm === newNameNorm && - oldNamespaceNorm === newNamespaceNorm + oldNamespaceNorm === newNamespaceNorm && + oldNameNorm === newNameNorm ) { // 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 + oldNamespace !== newNamespace || + oldName !== newName ) { const message = `Kind, namespace, name are too similar to an existing entity`; throw new ConflictError(message); @@ -431,9 +443,9 @@ export class CommonDatabase implements Database { api_version: entity.apiVersion, kind: entity.kind, name: entity.metadata.name, - namespace: entity.metadata.namespace || null, + namespace: entity.metadata.namespace || ENTITY_DEFAULT_NAMESPACE, metadata: JSON.stringify( - lodash.omit(entity.metadata, ...entityMetaGeneratedFields), + lodash.omit(entity.metadata, ...ENTITY_META_GENERATED_FIELDS), ), spec: entity.spec ? JSON.stringify(entity.spec) : null, }; diff --git a/plugins/catalog-backend/src/database/search.test.ts b/plugins/catalog-backend/src/database/search.test.ts index 38a2d40e74..6da87f9150 100644 --- a/plugins/catalog-backend/src/database/search.test.ts +++ b/plugins/catalog-backend/src/database/search.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Entity } from '@backstage/catalog-model'; +import { ENTITY_DEFAULT_NAMESPACE, Entity } from '@backstage/catalog-model'; import { buildEntitySearch, visitEntityPart } from './search'; import type { DbEntitiesSearchRow } from './types'; @@ -95,6 +95,16 @@ describe('search', () => { { entity_id: 'eid', key: 'root.list.a', value: '2' }, ]); }); + + it('emits lowercase version of keys and values', () => { + const input = { theRoot: { listItems: [{ a: 'One' }, { a: 2 }] } }; + const output: DbEntitiesSearchRow[] = []; + visitEntityPart('eid', '', input, output); + expect(output).toEqual([ + { entity_id: 'eid', key: 'theroot.listitems.a', value: 'one' }, + { entity_id: 'eid', key: 'theroot.listitems.a', value: '2' }, + ]); + }); }); describe('buildEntitySearch', () => { @@ -108,11 +118,17 @@ describe('search', () => { { entity_id: 'eid', key: 'metadata.name', value: 'n' }, { entity_id: 'eid', key: 'metadata.namespace', value: null }, { entity_id: 'eid', key: 'metadata.uid', value: null }, - { entity_id: 'eid', key: 'apiVersion', value: 'a' }, + { + entity_id: 'eid', + key: 'metadata.namespace', + value: ENTITY_DEFAULT_NAMESPACE, + }, + { entity_id: 'eid', key: 'apiversion', value: 'a' }, { entity_id: 'eid', key: 'kind', value: 'b' }, { entity_id: 'eid', key: 'name', value: 'n' }, { entity_id: 'eid', key: 'namespace', value: null }, { entity_id: 'eid', key: 'uid', value: null }, + { entity_id: 'eid', key: 'namespace', value: ENTITY_DEFAULT_NAMESPACE }, ]); }); diff --git a/plugins/catalog-backend/src/database/search.ts b/plugins/catalog-backend/src/database/search.ts index c14acb6661..cbcd01a5d0 100644 --- a/plugins/catalog-backend/src/database/search.ts +++ b/plugins/catalog-backend/src/database/search.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Entity } from '@backstage/catalog-model'; +import { Entity, ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model'; import type { DbEntitiesSearchRow } from './types'; // Search entries that start with these prefixes, also get a shorthand without @@ -26,7 +26,7 @@ const SHORTHAND_KEY_PREFIXES = [ 'spec.', ]; -// These are exluded in the generic loop, either because they do not make sense +// These are excluded in the generic loop, either because they do not make sense // to index, or because they are special-case always inserted whether they are // null or not const SPECIAL_KEYS = [ @@ -42,7 +42,7 @@ function toValue(current: any): string | null { return null; } - return String(current); + return String(current).toLowerCase(); } // Helper for iterating through a nested structure and outputting a list of @@ -106,7 +106,12 @@ export function visitEntityPart( // object for (const [key, value] of Object.entries(current)) { - visitEntityPart(entityId, path ? `${path}.${key}` : key, value, output); + visitEntityPart( + entityId, + (path ? `${path}.${key}` : key).toLowerCase(), + value, + output, + ); } } @@ -141,6 +146,16 @@ export function buildEntitySearch( }, ]; + // Namespace not specified has the default value "default", so we want to + // match on that as well + if (!entity.metadata.namespace) { + result.push({ + entity_id: entityId, + key: 'metadata.namespace', + value: toValue(ENTITY_DEFAULT_NAMESPACE), + }); + } + // Visit the entire structure recursively visitEntityPart(entityId, '', entity, result); diff --git a/plugins/catalog-backend/src/database/types.ts b/plugins/catalog-backend/src/database/types.ts index 0537b87800..8aaf212583 100644 --- a/plugins/catalog-backend/src/database/types.ts +++ b/plugins/catalog-backend/src/database/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Entity, Location } from '@backstage/catalog-model'; +import type { Entity, EntityName, Location } from '@backstage/catalog-model'; export type DbEntitiesRow = { id: string; @@ -129,11 +129,9 @@ export type Database = { entities(tx: unknown, filters?: EntityFilters): Promise; - entity( + entityByName( tx: unknown, - kind: string, - name: string, - namespace?: string, + name: EntityName, ): Promise; entityByUid(tx: unknown, uid: string): Promise; diff --git a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts index de68a16e9c..32fcc27909 100644 --- a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts +++ b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts @@ -15,7 +15,12 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import { Entity, Location, LocationSpec } from '@backstage/catalog-model'; +import { + Entity, + ENTITY_DEFAULT_NAMESPACE, + Location, + LocationSpec, +} from '@backstage/catalog-model'; import { EntitiesCatalog, LocationsCatalog } from '../catalog'; import { LocationUpdateStatus } from '../catalog/types'; import { DatabaseLocationUpdateLogStatus } from '../database/types'; @@ -200,12 +205,11 @@ describe('HigherOrderOperations', () => { target: 'thing', }); expect(entitiesCatalog.entityByName).toHaveBeenCalledTimes(1); - expect(entitiesCatalog.entityByName).toHaveBeenNthCalledWith( - 1, - 'Component', - undefined, - 'c1', - ); + expect(entitiesCatalog.entityByName).toHaveBeenNthCalledWith(1, { + kind: 'Component', + namespace: ENTITY_DEFAULT_NAMESPACE, + name: 'c1', + }); expect(entitiesCatalog.addOrUpdateEntity).toHaveBeenCalledTimes(1); expect(entitiesCatalog.addOrUpdateEntity).toHaveBeenNthCalledWith( 1, diff --git a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts index 0224dc7f1d..1b60666c8e 100644 --- a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts +++ b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts @@ -18,6 +18,7 @@ import { InputError } from '@backstage/backend-common'; import { Entity, entityHasChanges, + getEntityName, Location, LocationSpec, } from '@backstage/catalog-model'; @@ -162,16 +163,14 @@ export class HigherOrderOperations implements HigherOrderOperation { const { entity } = item; this.logger.debug( - `Read entity kind="${entity.kind}" name="${ - entity.metadata.name - }" namespace="${entity.metadata.namespace || ''}"`, + `Read entity kind="${entity.kind}" namespace="${ + entity.metadata.namespace || '' + }" name="${entity.metadata.name}"`, ); try { const previous = await this.entitiesCatalog.entityByName( - entity.kind, - entity.metadata.namespace, - entity.metadata.name, + getEntityName(entity), ); if (!previous) { diff --git a/plugins/catalog-backend/src/service/router.test.ts b/plugins/catalog-backend/src/service/router.test.ts index 29b3f7ce04..32d7b6aa63 100644 --- a/plugins/catalog-backend/src/service/router.test.ts +++ b/plugins/catalog-backend/src/service/router.test.ts @@ -136,7 +136,11 @@ describe('createRouter', () => { const response = await request(app).get('/entities/by-name/k/ns/n'); expect(entitiesCatalog.entityByName).toHaveBeenCalledTimes(1); - expect(entitiesCatalog.entityByName).toHaveBeenCalledWith('k', 'ns', 'n'); + expect(entitiesCatalog.entityByName).toHaveBeenCalledWith({ + kind: 'k', + namespace: 'ns', + name: 'n', + }); expect(response.status).toEqual(200); expect(response.body).toEqual(expect.objectContaining(entity)); }); @@ -147,7 +151,11 @@ describe('createRouter', () => { const response = await request(app).get('/entities/by-name/b/d/c'); expect(entitiesCatalog.entityByName).toHaveBeenCalledTimes(1); - expect(entitiesCatalog.entityByName).toHaveBeenCalledWith('b', 'd', 'c'); + expect(entitiesCatalog.entityByName).toHaveBeenCalledWith({ + kind: 'b', + namespace: 'd', + name: 'c', + }); expect(response.status).toEqual(404); expect(response.text).toMatch(/name/); }); diff --git a/plugins/catalog-backend/src/service/router.ts b/plugins/catalog-backend/src/service/router.ts index 2919e73396..8da589c721 100644 --- a/plugins/catalog-backend/src/service/router.ts +++ b/plugins/catalog-backend/src/service/router.ts @@ -67,11 +67,11 @@ export async function createRouter( }) .get('/entities/by-name/:kind/:namespace/:name', async (req, res) => { const { kind, namespace, name } = req.params; - const entity = await entitiesCatalog.entityByName( + const entity = await entitiesCatalog.entityByName({ kind, namespace, name, - ); + }); if (!entity) { res .status(404) From 94aa8068af390e595a01f16c4d22cbcc3345b97f Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Thu, 24 Sep 2020 16:20:16 +0200 Subject: [PATCH 69/85] TechDocs: Add troubleshooting docs --- docs/features/techdocs/troubleshooting.md | 10 ++++++++++ microsite/sidebars.json | 1 + mkdocs.yml | 1 + 3 files changed, 12 insertions(+) create mode 100644 docs/features/techdocs/troubleshooting.md diff --git a/docs/features/techdocs/troubleshooting.md b/docs/features/techdocs/troubleshooting.md new file mode 100644 index 0000000000..3a9b2fcfd0 --- /dev/null +++ b/docs/features/techdocs/troubleshooting.md @@ -0,0 +1,10 @@ +--- +id: troubleshooting +title: Troubleshooting TechDocs +sidebar_label: Troubleshooting +description: Troubleshooting for TechDocs +--- + +- TechDocs will fail to clone your docs if you have a git config which overrides + the `https` protocol with `ssh` or something else. Make sure to remove your + git config locally when you try TechDocs. diff --git a/microsite/sidebars.json b/microsite/sidebars.json index e9993b2e8b..76709e5726 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -70,6 +70,7 @@ "features/techdocs/concepts", "features/techdocs/architecture", "features/techdocs/creating-and-publishing", + "features/techdocs/troubleshooting", "features/techdocs/faqs" ] } diff --git a/mkdocs.yml b/mkdocs.yml index f85044822e..58dc418925 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -50,6 +50,7 @@ nav: - Concepts: 'features/techdocs/concepts.md' - TechDocs Architecture: 'features/techdocs/architecture.md' - Creating and Publishing Documentation: 'features/techdocs/creating-and-publishing.md' + - Troubleshooting: 'features/techdocs/troubleshooting.md' - FAQ: 'features/techdocs/FAQ.md' - Plugins: - Overview: 'plugins/index.md' From 7cae75590962e2c36c85bced303b5b788bcd6b92 Mon Sep 17 00:00:00 2001 From: Julien Lind Date: Thu, 24 Sep 2020 16:26:30 +0200 Subject: [PATCH 70/85] Update README.md (#2591) API Path missing on non-local deployment --- plugins/auth-backend/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/auth-backend/README.md b/plugins/auth-backend/README.md index 868d88b3d0..355fcc1536 100644 --- a/plugins/auth-backend/README.md +++ b/plugins/auth-backend/README.md @@ -35,7 +35,7 @@ Follow this link, [Create new OAuth App](https://github.com/settings/application 1. You can set the Homepage URL to whatever you want to. 1. The Authorization Callback URL should match the redirect URI set in Backstage. 1. Set this to `http://localhost:7000/api/auth/github` for local development. - 1. Set this to `http://{APP_FQDN}:{APP_BACKEND_PORT}/auth/github` for non-local deployments. + 1. Set this to `http://{APP_FQDN}:{APP_BACKEND_PORT}/api/auth/github` for non-local deployments. ```bash export AUTH_GITHUB_CLIENT_ID=x From 54ad1c5f31e2b0afdbf1fb40df936a51c5acced7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 23 Sep 2020 22:07:29 +0200 Subject: [PATCH 71/85] feat(catalog-model): add the User entity --- .../software-catalog/descriptor-format.md | 166 ++++++++++++++++++ packages/catalog-model/src/EntityPolicies.ts | 2 + .../src/kinds/UserEntityV1alpha1.test.ts | 150 ++++++++++++++++ .../src/kinds/UserEntityV1alpha1.ts | 62 +++++++ packages/catalog-model/src/kinds/index.ts | 13 +- 5 files changed, 389 insertions(+), 4 deletions(-) create mode 100644 packages/catalog-model/src/kinds/UserEntityV1alpha1.test.ts create mode 100644 packages/catalog-model/src/kinds/UserEntityV1alpha1.ts diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index 301d80630c..0c50e3669a 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -22,6 +22,8 @@ humans. However, the structure and semantics is the same in both cases. - [Kind: Component](#kind-component) - [Kind: Template](#kind-template) - [Kind: API](#kind-api) +- [Kind: Group](#kind-group) +- [Kind: User](#kind-user) ## Overall Shape Of An Entity @@ -571,3 +573,167 @@ group of people in an organizational structure. The definition of the API, based on the format defined by `spec.type`. This field is required. + +## Kind: Group + +Describes the following entity kind: + +| Field | Value | +| ------------ | ----------------------- | +| `apiVersion` | `backstage.io/v1alpha1` | +| `kind` | `Group` | + +A group describes an organizational entity, such as for example a team, a +business unit, or a loose collection of people in an interest group. Members of +these groups are modeled in the catalog as kind [`User`](#kind-user). + +Descriptor files for this kind may look as follows. + +```yaml +apiVersion: backstage.io/v1alpha1 +kind: Group +metadata: + name: infrastructure + description: The infra business unit +spec: + type: business-unit + parent: ops + ancestors: [ops, global-synergies, acme-corp] + children: [backstage, other] + descendants: [backstage, other, team-a, team-b, team-c, team-d] +``` + +In addition to the [common envelope metadata](#common-to-all-kinds-the-metadata) +shape, this kind has the following structure. + +### `apiVersion` and `kind` [required] + +Exactly equal to `backstage.io/v1alpha1` and `Group`, respectively. + +### `spec.type` [required] + +The type of group as a string, e.g. `team`. There is currently no enforced set +of values for this field, so it is left up to the adopting organization to +choose a nomenclature that matches their org hierarchy. + +Some common values for this field could be: + +- `team` +- `business-unit` +- `product-area` +- `root` - as a common virtual root of the hierarchy, if desired + +### `spec.parent` [optional] + +The immediate parent group in the hierarchy, if any. Not all groups must have a +parent; the catalog supports multi-root hierarchies. Groups may however not have +more than one parent. + +This field is an +[entity reference](https://backstage.io/docs/features/software-catalog/references), +with the default kind `Group` and the default namespace equal to the same +namespace as the user. Only `Group` entities may be referenced. Most commonly, +this field points to a group in the same namespace, so in those cases it is +sufficient to enter only the `metadata.name` field of that group. + +### `spec.ancestors` [required] + +The recursive list of parents up the hierarchy, by stepping through parents one +by one. The list must be present, but may be empty if `parent` is not present. +The first entry in the list is equal to `parent`, and then the following ones +are progressively farther up the hierarchy. + +The entries of this array are +[entity references](https://backstage.io/docs/features/software-catalog/references), +with the default kind `Group` and the default namespace equal to the same +namespace as the user. Only `Group` entities may be referenced. Most commonly, +these entries point to groups in the same namespace, so in those cases it is +sufficient to enter only the `metadata.name` field of those groups. + +### `spec.children` [required] + +The immediate child groups of this group in the hierarchy (whose `parent` field +points to this group). The list must be present, but may be empty if there are +no child groups. The items are not guaranteed to be ordered in any particular +way. + +The entries of this array are +[entity references](https://backstage.io/docs/features/software-catalog/references), +with the default kind `Group` and the default namespace equal to the same +namespace as the user. Only `Group` entities may be referenced. Most commonly, +these entries point to groups in the same namespace, so in those cases it is +sufficient to enter only the `metadata.name` field of those groups. + +### `spec.descendants` [required] + +The immediate and recursive child groups of this group in the hierarchy +(children, and children's children, etc.). The list must be present, but may be +empty if there are no child groups. The items are not guaranteed to be ordered +in any particular way. + +The entries of this array are +[entity references](https://backstage.io/docs/features/software-catalog/references), +with the default kind `Group` and the default namespace equal to the same +namespace as the user. Only `Group` entities may be referenced. Most commonly, +these entries point to groups in the same namespace, so in those cases it is +sufficient to enter only the `metadata.name` field of those groups. + +## Kind: User + +Describes the following entity kind: + +| Field | Value | +| ------------ | ----------------------- | +| `apiVersion` | `backstage.io/v1alpha1` | +| `kind` | `User` | + +A user describes a person, such as an employee, a contractor, or similar. Users +belong to [`Group`](#kind-group) entities in the catalog. + +These catalog user entries are connected to the way that authentication within +the Backstage ecosystem works. See the [auth](https://backstage.io/docs/auth) +section of the docs for a discussion of these concepts. + +Descriptor files for this kind may look as follows. + +```yaml +apiVersion: backstage.io/v1alpha1 +kind: User +metadata: + name: jdoe +spec: + profile: + displayName: Jenny Doe + email: jenny-doe@example.com + picture: https://example.com/staff/jenny-with-party-hat.jpeg + memberOf: [team-b, employees] +``` + +In addition to the [common envelope metadata](#common-to-all-kinds-the-metadata) +shape, this kind has the following structure. + +### `apiVersion` and `kind` [required] + +Exactly equal to `backstage.io/v1alpha1` and `User`, respectively. + +### `spec.profile` [optional] + +Optional profile information about the user, mainly for display purposes. All +fields of this structure are also optional. The email would be a primary email +of some form, that the user may wish to be used for contacting them. The picture +is expected to be a URL pointing to an image that's representative of the user, +and that a browser could fetch and render on a profile page or similar. + +### `spec.memberOf` [required] + +The list of groups that the user is a direct member of (i.e., no transitive +memberships are listed here). The list must be present, but may be empty if the +user is not member of any groups. The items are not guaranteed to be ordered in +any particular way. + +The entries of this array are +[entity references](https://backstage.io/docs/features/software-catalog/references), +with the default kind `Group` and the default namespace equal to the same +namespace as the user. Only `Group` entities may be referenced. Most commonly, +these entries point to groups in the same namespace, so in those cases it is +sufficient to enter only the `metadata.name` field of those groups. diff --git a/packages/catalog-model/src/EntityPolicies.ts b/packages/catalog-model/src/EntityPolicies.ts index 8cc51e5aa8..3e2f51b40a 100644 --- a/packages/catalog-model/src/EntityPolicies.ts +++ b/packages/catalog-model/src/EntityPolicies.ts @@ -28,6 +28,7 @@ import { GroupEntityV1alpha1Policy, LocationEntityV1alpha1Policy, TemplateEntityV1alpha1Policy, + UserEntityV1alpha1Policy, } from './kinds'; import { EntityPolicy } from './types'; @@ -77,6 +78,7 @@ export class EntityPolicies implements EntityPolicy { EntityPolicies.anyOf([ new ComponentEntityV1alpha1Policy(), new GroupEntityV1alpha1Policy(), + new UserEntityV1alpha1Policy(), new LocationEntityV1alpha1Policy(), new TemplateEntityV1alpha1Policy(), new ApiEntityV1alpha1Policy(), diff --git a/packages/catalog-model/src/kinds/UserEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/UserEntityV1alpha1.test.ts new file mode 100644 index 0000000000..b641c6cad3 --- /dev/null +++ b/packages/catalog-model/src/kinds/UserEntityV1alpha1.test.ts @@ -0,0 +1,150 @@ +/* + * 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 { EntityPolicy } from '../types'; +import { + UserEntityV1alpha1, + UserEntityV1alpha1Policy, +} from './UserEntityV1alpha1'; + +describe('UserV1alpha1Policy', () => { + let entity: UserEntityV1alpha1; + let policy: EntityPolicy; + + beforeEach(() => { + entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name: 'doe', + }, + spec: { + profile: { + displayName: 'John Doe', + email: 'john@doe.org', + picture: 'https://doe.org/john.jpeg', + }, + memberOf: ['team-a', 'developers'], + }, + }; + policy = new UserEntityV1alpha1Policy(); + }); + + it('happy path: accepts valid data', async () => { + await expect(policy.enforce(entity)).resolves.toBe(entity); + }); + + // root + + it('silently accepts v1beta1 as well', async () => { + (entity as any).apiVersion = 'backstage.io/v1beta1'; + await expect(policy.enforce(entity)).resolves.toBe(entity); + }); + + it('rejects unknown apiVersion', async () => { + (entity as any).apiVersion = 'backstage.io/v1beta0'; + await expect(policy.enforce(entity)).rejects.toThrow(/apiVersion/); + }); + + it('rejects unknown kind', async () => { + (entity as any).kind = 'Wizard'; + await expect(policy.enforce(entity)).rejects.toThrow(/kind/); + }); + + it('spec accepts unknown additional fields', async () => { + (entity as any).spec.foo = 'data'; + await expect(policy.enforce(entity)).resolves.toBe(entity); + }); + + // profile + + it('accepts missing profile', async () => { + delete (entity as any).spec.profile; + await expect(policy.enforce(entity)).resolves.toBe(entity); + }); + + it('rejects wrong profile', async () => { + (entity as any).spec.profile = 7; + await expect(policy.enforce(entity)).rejects.toThrow(/profile/); + }); + + it('profile accepts missing displayName', async () => { + delete (entity as any).spec.profile.displayName; + await expect(policy.enforce(entity)).resolves.toBe(entity); + }); + + it('profile rejects wrong displayName', async () => { + (entity as any).spec.profile.displayName = 7; + await expect(policy.enforce(entity)).rejects.toThrow(/displayName/); + }); + + it('profile rejects empty displayName', async () => { + (entity as any).spec.profile.displayName = ''; + await expect(policy.enforce(entity)).rejects.toThrow(/displayName/); + }); + + it('profile accepts missing email', async () => { + delete (entity as any).spec.profile.email; + await expect(policy.enforce(entity)).resolves.toBe(entity); + }); + + it('profile rejects wrong email', async () => { + (entity as any).spec.profile.email = 7; + await expect(policy.enforce(entity)).rejects.toThrow(/email/); + }); + + it('profile rejects empty email', async () => { + (entity as any).spec.profile.email = ''; + await expect(policy.enforce(entity)).rejects.toThrow(/email/); + }); + + it('profile accepts missing picture', async () => { + delete (entity as any).spec.profile.picture; + await expect(policy.enforce(entity)).resolves.toBe(entity); + }); + + it('profile rejects wrong picture', async () => { + (entity as any).spec.profile.picture = 7; + await expect(policy.enforce(entity)).rejects.toThrow(/picture/); + }); + + it('profile rejects empty picture', async () => { + (entity as any).spec.profile.picture = ''; + await expect(policy.enforce(entity)).rejects.toThrow(/picture/); + }); + + it('profile accepts unknown additional fields', async () => { + (entity as any).spec.profile.foo = 'data'; + await expect(policy.enforce(entity)).resolves.toBe(entity); + }); + + // memberOf + + it('rejects missing memberOf', async () => { + delete (entity as any).spec.memberOf; + await expect(policy.enforce(entity)).rejects.toThrow(/memberOf/); + }); + + it('rejects wrong memberOf', async () => { + (entity as any).spec.memberOf = 7; + await expect(policy.enforce(entity)).rejects.toThrow(/memberOf/); + }); + + it('rejects wrong memberOf item', async () => { + (entity as any).spec.memberOf[0] = 7; + await expect(policy.enforce(entity)).rejects.toThrow(/memberOf/); + }); +}); diff --git a/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts b/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts new file mode 100644 index 0000000000..a6a304f509 --- /dev/null +++ b/packages/catalog-model/src/kinds/UserEntityV1alpha1.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 * as yup from 'yup'; +import type { Entity } from '../entity/Entity'; +import type { EntityPolicy } from '../types'; + +const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const; +const KIND = 'User' as const; + +export interface UserEntityV1alpha1 extends Entity { + apiVersion: typeof API_VERSION[number]; + kind: typeof KIND; + spec: { + profile?: { + displayName?: string; + email?: string; + picture?: string; + }; + memberOf: string[]; + }; +} + +export class UserEntityV1alpha1Policy implements EntityPolicy { + private schema: yup.Schema; + + constructor() { + this.schema = yup.object>({ + apiVersion: yup.string().required().oneOf(API_VERSION), + kind: yup.string().required().equals([KIND]), + spec: yup + .object({ + profile: yup + .object({ + displayName: yup.string().min(1).notRequired(), + email: yup.string().min(1).notRequired(), + picture: yup.string().min(1).notRequired(), + }) + .notRequired(), + memberOf: yup.array(yup.string()).required(), + }) + .required(), + }); + } + + async enforce(envelope: Entity): Promise { + return await this.schema.validate(envelope, { strict: true }); + } +} diff --git a/packages/catalog-model/src/kinds/index.ts b/packages/catalog-model/src/kinds/index.ts index 6e8d27d204..1ac93b426d 100644 --- a/packages/catalog-model/src/kinds/index.ts +++ b/packages/catalog-model/src/kinds/index.ts @@ -14,6 +14,11 @@ * limitations under the License. */ +export { ApiEntityV1alpha1Policy } from './ApiEntityV1alpha1'; +export type { + ApiEntityV1alpha1 as ApiEntity, + ApiEntityV1alpha1, +} from './ApiEntityV1alpha1'; export { ComponentEntityV1alpha1Policy } from './ComponentEntityV1alpha1'; export type { ComponentEntityV1alpha1 as ComponentEntity, @@ -34,8 +39,8 @@ export type { TemplateEntityV1alpha1 as TemplateEntity, TemplateEntityV1alpha1, } from './TemplateEntityV1alpha1'; -export { ApiEntityV1alpha1Policy } from './ApiEntityV1alpha1'; +export { UserEntityV1alpha1Policy } from './UserEntityV1alpha1'; export type { - ApiEntityV1alpha1 as ApiEntity, - ApiEntityV1alpha1, -} from './ApiEntityV1alpha1'; + UserEntityV1alpha1 as UserEntity, + UserEntityV1alpha1, +} from './UserEntityV1alpha1'; From b41ffc8c0fb3b3430700cb48a05e99a8f96a60c1 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Thu, 24 Sep 2020 17:26:34 +0200 Subject: [PATCH 72/85] add /api to catalog endpoint (#2595) --- plugins/techdocs-backend/src/service/router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/techdocs-backend/src/service/router.ts b/plugins/techdocs-backend/src/service/router.ts index e0a066c168..14820da4e9 100644 --- a/plugins/techdocs-backend/src/service/router.ts +++ b/plugins/techdocs-backend/src/service/router.ts @@ -63,7 +63,7 @@ export async function createRouter({ const entity = (await ( await fetch( - `${baseUrl}/catalog/entities/by-name/${kind}/${namespace}/${name}`, + `${baseUrl}/api/catalog/entities/by-name/${kind}/${namespace}/${name}`, ) ).json()) as Entity; From b16e164815087f18c550a85ff333e7e3f3b492e9 Mon Sep 17 00:00:00 2001 From: Matthew Clarke Date: Thu, 24 Sep 2020 16:30:48 +0100 Subject: [PATCH 73/85] K8s add cluster lookup (#2571) * Kubernetes plugins boilerplate * pr feedback; check for k8s annotation * temp * fix config * add example local setup * dice rolelr * rever yarn lock update * forgotten prettier * prettier again * master lock * update lock with k8s deps * prettier * router tests * internal error * fix package.json * PR feedback * more pr feedback * rename component file * kubernetes fetcher tests * fix test * option constructor --- app-config.yaml | 4 + packages/backend/src/plugins/kubernetes.ts | 7 +- .../examples/dice-roller/README.md | 42 +++ .../examples/dice-roller/catalog-info.yaml | 13 + .../dice-roller/dice-roller-manifests.yaml | 79 ++++++ plugins/kubernetes-backend/package.json | 8 +- .../MultiTenantConfigClusterLocator.test.ts | 105 ++++++++ .../MultiTenantConfigClusterLocator.ts | 47 ++++ .../src/cluster-locator/types.ts | 17 ++ plugins/kubernetes-backend/src/index.ts | 1 + .../service/KubernetesClientProvider.test.ts | 136 ++++++++++ .../src/service/KubernetesClientProvider.ts | 96 +++++++ .../src/service/KubernetesFetcher.test.ts | 108 ++++++++ .../src/service/KubernetesFetcher.ts | 212 +++++++++++++++ ...ubernetesObjectsByServiceIdHandler.test.ts | 242 ++++++++++++++++++ .../getKubernetesObjectsByServiceIdHandler.ts | 69 +++++ .../src/service/router.test.ts | 87 +++++++ .../kubernetes-backend/src/service/router.ts | 67 ++++- .../src/service/standaloneApplication.ts | 4 +- plugins/kubernetes-backend/src/types/types.ts | 103 ++++++++ plugins/kubernetes/package.json | 1 + plugins/kubernetes/src/Router.tsx | 2 +- .../src/api/KubernetesBackendClient.ts | 5 +- plugins/kubernetes/src/api/types.ts | 3 +- .../KubernetesContent/KubernetesContent.tsx | 53 ++-- yarn.lock | 216 ++++++++++++++-- 26 files changed, 1671 insertions(+), 56 deletions(-) create mode 100644 plugins/kubernetes-backend/examples/dice-roller/README.md create mode 100644 plugins/kubernetes-backend/examples/dice-roller/catalog-info.yaml create mode 100644 plugins/kubernetes-backend/examples/dice-roller/dice-roller-manifests.yaml create mode 100644 plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.test.ts create mode 100644 plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.ts create mode 100644 plugins/kubernetes-backend/src/cluster-locator/types.ts create mode 100644 plugins/kubernetes-backend/src/service/KubernetesClientProvider.test.ts create mode 100644 plugins/kubernetes-backend/src/service/KubernetesClientProvider.ts create mode 100644 plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts create mode 100644 plugins/kubernetes-backend/src/service/KubernetesFetcher.ts create mode 100644 plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts create mode 100644 plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.ts create mode 100644 plugins/kubernetes-backend/src/service/router.test.ts create mode 100644 plugins/kubernetes-backend/src/types/types.ts diff --git a/app-config.yaml b/app-config.yaml index fca0a39496..01925c21dd 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -55,6 +55,10 @@ newrelic: lighthouse: baseUrl: http://localhost:3003 +kubernetes: + clusterLocatorMethod: 'configMultiTenant' + clusters: [] + catalog: rules: - allow: [Component, API, Group, Template, Location] diff --git a/packages/backend/src/plugins/kubernetes.ts b/packages/backend/src/plugins/kubernetes.ts index 42b7722ff6..7906765533 100644 --- a/packages/backend/src/plugins/kubernetes.ts +++ b/packages/backend/src/plugins/kubernetes.ts @@ -17,6 +17,9 @@ import { createRouter } from '@backstage/plugin-kubernetes-backend'; import { PluginEnvironment } from '../types'; -export default async function createPlugin({ logger }: PluginEnvironment) { - return await createRouter({ logger }); +export default async function createPlugin({ + logger, + config, +}: PluginEnvironment) { + return await createRouter({ logger, config }); } diff --git a/plugins/kubernetes-backend/examples/dice-roller/README.md b/plugins/kubernetes-backend/examples/dice-roller/README.md new file mode 100644 index 0000000000..b64e2df37f --- /dev/null +++ b/plugins/kubernetes-backend/examples/dice-roller/README.md @@ -0,0 +1,42 @@ +# Dice roller + +An app to roll dice (it doesn't actually do that). + +# Viewing in local Minikube running Backstage locally + +## Prerequisites + +- kubectl installed +- Minikube installed +- jq installed +- Backstage locally built and ready to run + +## Steps + +1. Start minikube +2. Get the Kubernetes master base url `kubectl cluster-info` +3. Apply manifests `kubectl apply -f dice-roller-manifests.yaml` +4. Get service account token (see below) +5. Start Backstage UI and backend +6. Register existing component in Backstage + - https://github.com/mclarke47/dice-roller/blob/master/catalog-info.yaml + +Update `app-config.yaml` as follows. + +```yaml +--- +kubernetes: + clusterLocatorMethod: 'configMultiTenant' + clusters: + - url: + name: minikube + serviceAccountToken: +``` + +### Getting the service account token + +``` +kubectl get secret DICE_ROLLER_TOKEN_NAME -o=json | jq -r '.data["token"]' | base64 --decode | pbcopy +``` + +Paste into `app-config.yaml` `kubernetes.clusters[].serviceAccountToken` diff --git a/plugins/kubernetes-backend/examples/dice-roller/catalog-info.yaml b/plugins/kubernetes-backend/examples/dice-roller/catalog-info.yaml new file mode 100644 index 0000000000..a944232224 --- /dev/null +++ b/plugins/kubernetes-backend/examples/dice-roller/catalog-info.yaml @@ -0,0 +1,13 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: dice-roller + description: It rolls dice + tags: + - go + annotations: + 'backstage.io/kubernetes-id': dice-roller +spec: + type: service + lifecycle: production + owner: guest diff --git a/plugins/kubernetes-backend/examples/dice-roller/dice-roller-manifests.yaml b/plugins/kubernetes-backend/examples/dice-roller/dice-roller-manifests.yaml new file mode 100644 index 0000000000..ef448c1688 --- /dev/null +++ b/plugins/kubernetes-backend/examples/dice-roller/dice-roller-manifests.yaml @@ -0,0 +1,79 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dice-roller + labels: + 'backstage.io/kubernetes-id': dice-roller +spec: + selector: + matchLabels: + app: dice-roller + replicas: 2 + template: + metadata: + labels: + app: dice-roller + 'backstage.io/kubernetes-id': dice-roller + spec: + containers: + - name: nginx + image: nginx:1.14.2 + ports: + - containerPort: 80 + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: dice-roller + namespace: default + labels: + 'backstage.io/kubernetes-id': dice-roller +data: + foo: bar + +--- +apiVersion: v1 +kind: Secret +metadata: + name: dice-roller + labels: + 'backstage.io/kubernetes-id': dice-roller +type: Opaque +data: + username: YWRtaW4= +--- +apiVersion: v1 +kind: Service +metadata: + name: dice-roller + labels: + 'backstage.io/kubernetes-id': dice-roller +spec: + selector: + app: dice-roller + ports: + - protocol: TCP + port: 80 + targetPort: 9376 + +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: dice-roller +automountServiceAccountToken: false + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: sa-admin +roleRef: + kind: ClusterRole + name: cluster-admin + apiGroup: rbac.authorization.k8s.io +subjects: + - kind: ServiceAccount + name: dice-roller + namespace: default diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index 406d9c8d7f..e1f7a39592 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -21,7 +21,9 @@ }, "dependencies": { "@backstage/backend-common": "^0.1.1-alpha.23", + "@backstage/config": "^0.1.1-alpha.23", "@types/express": "^4.17.6", + "@kubernetes/client-node": "^0.12.1", "compression": "^1.7.4", "cors": "^2.8.5", "express": "^4.17.1", @@ -29,12 +31,14 @@ "fs-extra": "^9.0.0", "helmet": "^4.0.0", "morgan": "^1.10.0", + "stream-buffers": "^3.0.2", "winston": "^3.2.1", "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.23", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "supertest": "^4.0.2", + "@backstage/cli": "^0.1.1-alpha.23" }, "files": [ "dist" diff --git a/plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.test.ts new file mode 100644 index 0000000000..34788d16cb --- /dev/null +++ b/plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.test.ts @@ -0,0 +1,105 @@ +/* + * 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 '@backstage/backend-common'; +import { MultiTenantConfigClusterLocator } from './MultiTenantConfigClusterLocator'; +import { ConfigReader, Config } from '@backstage/config'; + +describe('MultiTenantConfigClusterLocator', () => { + it('empty clusters returns empty cluster details', async () => { + const config: Config = new ConfigReader( + { + clusters: [], + }, + 'ctx', + ); + + const sut = MultiTenantConfigClusterLocator.fromConfig( + config.getConfigArray('clusters'), + ); + + const result = await sut.getClusterByServiceId('ignored'); + + expect(result).toStrictEqual([]); + }); + + it('one clusters returns one cluster details', async () => { + const config: Config = new ConfigReader( + { + clusters: [ + { + name: 'cluster1', + url: 'http://localhost:8080', + }, + ], + }, + 'ctx', + ); + + const sut = MultiTenantConfigClusterLocator.fromConfig( + config.getConfigArray('clusters'), + ); + + const result = await sut.getClusterByServiceId('ignored'); + + expect(result).toStrictEqual([ + { + name: 'cluster1', + serviceAccountToken: undefined, + url: 'http://localhost:8080', + }, + ]); + }); + + it('two clusters returns two cluster details', async () => { + const config: Config = new ConfigReader( + { + clusters: [ + { + name: 'cluster1', + serviceAccountToken: undefined, + url: 'http://localhost:8080', + }, + { + name: 'cluster2', + serviceAccountToken: undefined, + url: 'http://localhost:8081', + }, + ], + }, + 'ctx', + ); + + const sut = MultiTenantConfigClusterLocator.fromConfig( + config.getConfigArray('clusters'), + ); + + const result = await sut.getClusterByServiceId('ignored'); + + expect(result).toStrictEqual([ + { + name: 'cluster1', + serviceAccountToken: undefined, + url: 'http://localhost:8080', + }, + { + name: 'cluster2', + serviceAccountToken: undefined, + url: 'http://localhost:8081', + }, + ]); + }); +}); diff --git a/plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.ts new file mode 100644 index 0000000000..3d9e2dd8b4 --- /dev/null +++ b/plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.ts @@ -0,0 +1,47 @@ +/* + * 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 { Config } from '@backstage/config'; +import { ClusterDetails, KubernetesClusterLocator } from '..'; + +// This cluster locator assumes that every service is located on every cluster +// Therefore it will always return all clusters in an app configuration file +export class MultiTenantConfigClusterLocator + implements KubernetesClusterLocator { + private readonly clusterDetails: ClusterDetails[]; + + constructor(clusterDetails: ClusterDetails[]) { + this.clusterDetails = clusterDetails; + } + + static fromConfig(config: Config[]): MultiTenantConfigClusterLocator { + return new MultiTenantConfigClusterLocator( + config.map(c => { + return { + name: c.getString('name'), + url: c.getString('url'), + serviceAccountToken: c.getOptionalString('serviceAccountToken'), + }; + }), + ); + } + + // As this implementation always returns all clusters serviceId is ignored here + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async getClusterByServiceId(_serviceId: string): Promise { + return this.clusterDetails; + } +} diff --git a/plugins/kubernetes-backend/src/cluster-locator/types.ts b/plugins/kubernetes-backend/src/cluster-locator/types.ts new file mode 100644 index 0000000000..9b793da558 --- /dev/null +++ b/plugins/kubernetes-backend/src/cluster-locator/types.ts @@ -0,0 +1,17 @@ +/* + * 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 type ClusterLocatorMethod = 'configMultiTenant' | 'http'; diff --git a/plugins/kubernetes-backend/src/index.ts b/plugins/kubernetes-backend/src/index.ts index 7612c392a2..96f070ff1e 100644 --- a/plugins/kubernetes-backend/src/index.ts +++ b/plugins/kubernetes-backend/src/index.ts @@ -15,3 +15,4 @@ */ export * from './service/router'; +export * from './types/types'; diff --git a/plugins/kubernetes-backend/src/service/KubernetesClientProvider.test.ts b/plugins/kubernetes-backend/src/service/KubernetesClientProvider.test.ts new file mode 100644 index 0000000000..a3dc67716a --- /dev/null +++ b/plugins/kubernetes-backend/src/service/KubernetesClientProvider.test.ts @@ -0,0 +1,136 @@ +/* + * 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 '@backstage/backend-common'; +import { KubernetesClientProvider } from './KubernetesClientProvider'; + +describe('KubernetesClientProvider', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('can get core client by cluster details', async () => { + const sut = new KubernetesClientProvider(); + + const mockGetKubeConfig = jest.fn(sut.getKubeConfig.bind({})); + + sut.getKubeConfig = mockGetKubeConfig; + + const result = sut.getCoreClientByClusterDetails({ + name: 'cluster-name', + url: 'http://localhost:9999', + serviceAccountToken: 'TOKEN', + }); + + expect(result.basePath).toBe('http://localhost:9999'); + // These fields aren't on the type but are there + const auth = (result as any).authentications.default; + expect(auth.users[0].token).toBe('TOKEN'); + expect(auth.clusters[0].name).toBe('cluster-name'); + + expect(mockGetKubeConfig.mock.calls.length).toBe(1); + }); + + it('can get cached core client by cluster details', async () => { + const sut = new KubernetesClientProvider(); + + const mockGetKubeConfig = jest.fn(sut.getKubeConfig.bind({})); + + sut.getKubeConfig = mockGetKubeConfig; + + const result1 = sut.getCoreClientByClusterDetails({ + name: 'cluster-name', + url: 'http://localhost:9999', + serviceAccountToken: 'TOKEN', + }); + + const result2 = sut.getCoreClientByClusterDetails({ + name: 'cluster-name', + url: 'http://localhost:9999', + serviceAccountToken: 'TOKEN', + }); + + expect(result1.basePath).toBe('http://localhost:9999'); + // These fields aren't on the type but are there + const auth1 = (result1 as any).authentications.default; + expect(auth1.users[0].token).toBe('TOKEN'); + expect(auth1.clusters[0].name).toBe('cluster-name'); + + expect(result2.basePath).toBe('http://localhost:9999'); + // These fields aren't on the type but are there + const auth2 = (result2 as any).authentications.default; + expect(auth2.users[0].token).toBe('TOKEN'); + expect(auth2.clusters[0].name).toBe('cluster-name'); + + expect(mockGetKubeConfig.mock.calls.length).toBe(1); + }); + + it('can get apps client by cluster details', async () => { + const sut = new KubernetesClientProvider(); + + const mockGetKubeConfig = jest.fn(sut.getKubeConfig.bind({})); + + sut.getKubeConfig = mockGetKubeConfig; + + const result = sut.getAppsClientByClusterDetails({ + name: 'cluster-name', + url: 'http://localhost:9999', + serviceAccountToken: 'TOKEN', + }); + + expect(result.basePath).toBe('http://localhost:9999'); + // These fields aren't on the type but are there + const auth = (result as any).authentications.default; + expect(auth.users[0].token).toBe('TOKEN'); + expect(auth.clusters[0].name).toBe('cluster-name'); + + expect(mockGetKubeConfig.mock.calls.length).toBe(1); + }); + + it('can get cached apps client by cluster details', async () => { + const sut = new KubernetesClientProvider(); + + const mockGetKubeConfig = jest.fn(sut.getKubeConfig.bind({})); + + sut.getKubeConfig = mockGetKubeConfig; + + const result1 = sut.getAppsClientByClusterDetails({ + name: 'cluster-name', + url: 'http://localhost:9999', + serviceAccountToken: 'TOKEN', + }); + + const result2 = sut.getAppsClientByClusterDetails({ + name: 'cluster-name', + url: 'http://localhost:9999', + serviceAccountToken: 'TOKEN', + }); + + expect(result1.basePath).toBe('http://localhost:9999'); + // These fields aren't on the type but are there + const auth1 = (result1 as any).authentications.default; + expect(auth1.users[0].token).toBe('TOKEN'); + expect(auth1.clusters[0].name).toBe('cluster-name'); + + expect(result2.basePath).toBe('http://localhost:9999'); + // These fields aren't on the type but are there + const auth2 = (result2 as any).authentications.default; + expect(auth2.users[0].token).toBe('TOKEN'); + expect(auth2.clusters[0].name).toBe('cluster-name'); + + expect(mockGetKubeConfig.mock.calls.length).toBe(1); + }); +}); diff --git a/plugins/kubernetes-backend/src/service/KubernetesClientProvider.ts b/plugins/kubernetes-backend/src/service/KubernetesClientProvider.ts new file mode 100644 index 0000000000..c3986e3ff7 --- /dev/null +++ b/plugins/kubernetes-backend/src/service/KubernetesClientProvider.ts @@ -0,0 +1,96 @@ +/* + * 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 { ClusterDetails } from '..'; +import { AppsV1Api, CoreV1Api, KubeConfig } from '@kubernetes/client-node'; + +export class KubernetesClientProvider { + private readonly coreClientMap: { + [key: string]: CoreV1Api; + }; + + private readonly appsClientMap: { + [key: string]: AppsV1Api; + }; + + constructor() { + this.coreClientMap = {}; + this.appsClientMap = {}; + } + + // visible for testing + getKubeConfig(clusterDetails: ClusterDetails) { + const cluster = { + name: clusterDetails.name, + server: clusterDetails.url, + // TODO configure this + skipTLSVerify: true, + }; + + // TODO configure + const user = { + name: 'service-account', + token: clusterDetails.serviceAccountToken, + }; + + const context = { + name: `${clusterDetails.name}`, + user: user.name, + cluster: cluster.name, + }; + + const kc = new KubeConfig(); + kc.loadFromOptions({ + clusters: [cluster], + users: [user], + contexts: [context], + currentContext: context.name, + }); + return kc; + } + + getCoreClientByClusterDetails(clusterDetails: ClusterDetails) { + const clientMapKey = clusterDetails.name; + + if (this.coreClientMap.hasOwnProperty(clientMapKey)) { + return this.coreClientMap[clientMapKey]; + } + + const kc = this.getKubeConfig(clusterDetails); + + const client = kc.makeApiClient(CoreV1Api); + + this.coreClientMap[clientMapKey] = client; + + return client; + } + + getAppsClientByClusterDetails(clusterDetails: ClusterDetails) { + const clientMapKey = clusterDetails.name; + + if (this.appsClientMap.hasOwnProperty(clientMapKey)) { + return this.appsClientMap[clientMapKey]; + } + + const kc = this.getKubeConfig(clusterDetails); + + const client = kc.makeApiClient(AppsV1Api); + + this.appsClientMap[clientMapKey] = client; + + return client; + } +} diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts new file mode 100644 index 0000000000..359e7c5806 --- /dev/null +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts @@ -0,0 +1,108 @@ +/* + * 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 { getVoidLogger } from '@backstage/backend-common'; +import { KubernetesClientBasedFetcher } from './KubernetesFetcher'; + +describe('KubernetesClientProvider', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should return pods, services', async () => { + const clientMock: any = { + listPodForAllNamespaces: jest.fn(), + listServiceForAllNamespaces: jest.fn(), + }; + + const kubernetesClientProvider: any = { + getCoreClientByClusterDetails: jest.fn(() => clientMock), + getAppsClientByClusterDetails: jest.fn(() => clientMock), + }; + + const sut = new KubernetesClientBasedFetcher({ + kubernetesClientProvider, + logger: getVoidLogger(), + }); + + clientMock.listPodForAllNamespaces.mockResolvedValueOnce({ + body: { + items: [ + { + metadata: { + name: 'pod-name', + }, + }, + ], + }, + }); + + clientMock.listServiceForAllNamespaces.mockResolvedValueOnce({ + body: { + items: [ + { + metadata: { + name: 'service-name', + }, + }, + ], + }, + }); + + const result = await sut.fetchObjectsByServiceId( + 'some-service', + { + name: 'cluster1', + url: 'http://localhost:9999', + serviceAccountToken: undefined, + }, + new Set(['pods', 'services']), + ); + + expect(result).toStrictEqual([ + { + type: 'pods', + resources: [ + { + metadata: { + name: 'pod-name', + }, + }, + ], + }, + { + type: 'services', + resources: [ + { + metadata: { + name: 'service-name', + }, + }, + ], + }, + ]); + + expect(clientMock.listPodForAllNamespaces.mock.calls.length).toBe(1); + expect(clientMock.listServiceForAllNamespaces.mock.calls.length).toBe(1); + + expect( + kubernetesClientProvider.getAppsClientByClusterDetails.mock.calls.length, + ).toBe(2); + expect( + kubernetesClientProvider.getCoreClientByClusterDetails.mock.calls.length, + ).toBe(2); + }); +}); diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts new file mode 100644 index 0000000000..f6d3edb5e9 --- /dev/null +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts @@ -0,0 +1,212 @@ +/* + * 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 { + AppsV1Api, + CoreV1Api, + V1ConfigMap, + V1Deployment, + V1Pod, + V1ReplicaSet, + V1Secret, +} from '@kubernetes/client-node'; +import { KubernetesClientProvider } from './KubernetesClientProvider'; +import { V1Service } from '@kubernetes/client-node/dist/gen/model/v1Service'; +import { Logger } from 'winston'; +import { + KubernetesFetcher, + ClusterDetails, + KubernetesObjectTypes, + FetchResponse, +} from '..'; + +export interface Clients { + core: CoreV1Api; + apps: AppsV1Api; +} + +export interface KubernetesClientBasedFetcherOptions { + kubernetesClientProvider: KubernetesClientProvider; + logger: Logger; +} + +export class KubernetesClientBasedFetcher implements KubernetesFetcher { + private readonly kubernetesClientProvider: KubernetesClientProvider; + private readonly logger: Logger; + + constructor({ + kubernetesClientProvider, + logger, + }: KubernetesClientBasedFetcherOptions) { + this.kubernetesClientProvider = kubernetesClientProvider; + this.logger = logger; + } + + fetchObjectsByServiceId( + serviceId: string, + clusterDetails: ClusterDetails, + objectTypesToFetch: Set, + ): Promise { + return Promise.all( + Array.from(objectTypesToFetch).map(type => { + return this.fetchByObjectType(serviceId, clusterDetails, type); + }), + ); + } + + private fetchByObjectType( + serviceId: string, + clusterDetails: ClusterDetails, + type: KubernetesObjectTypes, + ): Promise { + switch (type) { + case 'pods': + return this.fetchPodsByServiceId(serviceId, clusterDetails).then(r => ({ + type: type, + resources: r, + })); + case 'configmaps': + return this.fetchConfigMapsByServiceId( + serviceId, + clusterDetails, + ).then(r => ({ type: type, resources: r })); + case 'deployments': + return this.fetchDeploymentsByServiceId( + serviceId, + clusterDetails, + ).then(r => ({ type: type, resources: r })); + case 'replicasets': + return this.fetchReplicaSetsByServiceId( + serviceId, + clusterDetails, + ).then(r => ({ type: type, resources: r })); + case 'secrets': + return this.fetchSecretsByServiceId( + serviceId, + clusterDetails, + ).then(r => ({ type: type, resources: r })); + case 'services': + return this.fetchServicesByServiceId( + serviceId, + clusterDetails, + ).then(r => ({ type: type, resources: r })); + default: + // unrecognised type + throw new Error(`unrecognised type=${type}`); + } + } + + private singleClusterFetch( + clusterDetails: ClusterDetails, + fn: (client: Clients) => Promise<{ body: { items: Array } }>, + ): Promise> { + const core = this.kubernetesClientProvider.getCoreClientByClusterDetails( + clusterDetails, + ); + const apps = this.kubernetesClientProvider.getAppsClientByClusterDetails( + clusterDetails, + ); + + this.logger.debug(`calling cluster=${clusterDetails.name}`); + return fn({ core, apps }).then(result => { + return result.body.items; + }); + } + + private fetchServicesByServiceId( + serviceId: string, + clusterDetails: ClusterDetails, + ): Promise> { + return this.singleClusterFetch(clusterDetails, ({ core }) => + core.listServiceForAllNamespaces( + false, + '', + '', + `backstage.io/kubernetes-id=${serviceId}`, + ), + ); + } + + private fetchPodsByServiceId( + serviceId: string, + clusterDetails: ClusterDetails, + ): Promise> { + return this.singleClusterFetch(clusterDetails, ({ core }) => + core.listPodForAllNamespaces( + false, + '', + '', + `backstage.io/kubernetes-id=${serviceId}`, + ), + ); + } + + private fetchConfigMapsByServiceId( + serviceId: string, + clusterDetails: ClusterDetails, + ): Promise> { + return this.singleClusterFetch(clusterDetails, ({ core }) => + core.listConfigMapForAllNamespaces( + false, + '', + '', + `backstage.io/kubernetes-id=${serviceId}`, + ), + ); + } + + private fetchSecretsByServiceId( + serviceId: string, + clusterDetails: ClusterDetails, + ): Promise> { + return this.singleClusterFetch(clusterDetails, ({ core }) => + core.listSecretForAllNamespaces( + false, + '', + '', + `backstage.io/kubernetes-id=${serviceId}`, + ), + ); + } + + private fetchDeploymentsByServiceId( + serviceId: string, + clusterDetails: ClusterDetails, + ): Promise> { + return this.singleClusterFetch(clusterDetails, ({ apps }) => + apps.listDeploymentForAllNamespaces( + false, + '', + '', + `backstage.io/kubernetes-id=${serviceId}`, + ), + ); + } + + private fetchReplicaSetsByServiceId( + serviceId: string, + clusterDetails: ClusterDetails, + ): Promise> { + return this.singleClusterFetch(clusterDetails, ({ apps }) => + apps.listReplicaSetForAllNamespaces( + false, + '', + '', + `backstage.io/kubernetes-id=${serviceId}`, + ), + ); + } +} diff --git a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts new file mode 100644 index 0000000000..3f8081571a --- /dev/null +++ b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts @@ -0,0 +1,242 @@ +/* + * 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 { handleGetKubernetesObjectsByServiceId } from './getKubernetesObjectsByServiceIdHandler'; +import { getVoidLogger } from '@backstage/backend-common'; +import { ClusterDetails } from '..'; + +const TEST_SERVICE_ID = 'my-service'; + +const fetchObjectsByServiceId = jest.fn(); + +const getClusterByServiceId = jest.fn(); + +const mockFetch = (mock: jest.Mock) => { + mock.mockImplementation((serviceId: string, clusterDetails: ClusterDetails) => + Promise.resolve([ + { + type: 'pods', + resources: [ + { + metadata: { + name: `my-pods-${serviceId}-${clusterDetails.name}`, + }, + }, + ], + }, + { + type: 'configmaps', + resources: [ + { + metadata: { + name: `my-configmaps-${serviceId}-${clusterDetails.name}`, + }, + }, + ], + }, + { + type: 'services', + resources: [ + { + metadata: { + name: `my-services-${serviceId}-${clusterDetails.name}`, + }, + }, + ], + }, + ]), + ); +}; + +describe('handleGetKubernetesObjectsByServiceId', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('retrieve objects for one cluster', async () => { + getClusterByServiceId.mockImplementation(() => + Promise.resolve([ + { + name: 'test-cluster', + }, + ]), + ); + + mockFetch(fetchObjectsByServiceId); + + const result = await handleGetKubernetesObjectsByServiceId( + TEST_SERVICE_ID, + { + fetchObjectsByServiceId, + }, + { + getClusterByServiceId, + }, + getVoidLogger(), + ); + + expect(getClusterByServiceId.mock.calls.length).toBe(1); + expect(fetchObjectsByServiceId.mock.calls.length).toBe(1); + expect(result).toStrictEqual({ + items: [ + { + cluster: { + name: 'test-cluster', + }, + resources: [ + { + resources: [ + { + metadata: { + name: 'my-pods-my-service-test-cluster', + }, + }, + ], + type: 'pods', + }, + { + resources: [ + { + metadata: { + name: 'my-configmaps-my-service-test-cluster', + }, + }, + ], + type: 'configmaps', + }, + { + resources: [ + { + metadata: { + name: 'my-services-my-service-test-cluster', + }, + }, + ], + type: 'services', + }, + ], + }, + ], + }); + }); + + it('retrieve objects for two clusters', async () => { + getClusterByServiceId.mockImplementation(() => + Promise.resolve([ + { + name: 'test-cluster', + }, + { + name: 'other-cluster', + }, + ]), + ); + + mockFetch(fetchObjectsByServiceId); + + const result = await handleGetKubernetesObjectsByServiceId( + TEST_SERVICE_ID, + { + fetchObjectsByServiceId, + }, + { + getClusterByServiceId, + }, + getVoidLogger(), + ); + + expect(getClusterByServiceId.mock.calls.length).toBe(1); + expect(fetchObjectsByServiceId.mock.calls.length).toBe(2); + expect(result).toStrictEqual({ + items: [ + { + cluster: { + name: 'test-cluster', + }, + resources: [ + { + resources: [ + { + metadata: { + name: 'my-pods-my-service-test-cluster', + }, + }, + ], + type: 'pods', + }, + { + resources: [ + { + metadata: { + name: 'my-configmaps-my-service-test-cluster', + }, + }, + ], + type: 'configmaps', + }, + { + resources: [ + { + metadata: { + name: 'my-services-my-service-test-cluster', + }, + }, + ], + type: 'services', + }, + ], + }, + { + cluster: { + name: 'other-cluster', + }, + resources: [ + { + resources: [ + { + metadata: { + name: 'my-pods-my-service-other-cluster', + }, + }, + ], + type: 'pods', + }, + { + resources: [ + { + metadata: { + name: 'my-configmaps-my-service-other-cluster', + }, + }, + ], + type: 'configmaps', + }, + { + resources: [ + { + metadata: { + name: 'my-services-my-service-other-cluster', + }, + }, + ], + type: 'services', + }, + ], + }, + ], + }); + }); +}); diff --git a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.ts b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.ts new file mode 100644 index 0000000000..52968d4c20 --- /dev/null +++ b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.ts @@ -0,0 +1,69 @@ +/* + * 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 { Logger } from 'winston'; +import { + KubernetesClusterLocator, + KubernetesFetcher, + KubernetesObjectTypes, + ObjectsByServiceIdResponse, +} from '..'; + +export type GetKubernetesObjectsByServiceIdHandler = ( + serviceId: string, + fetcher: KubernetesFetcher, + clusterLocator: KubernetesClusterLocator, + logger: Logger, + objectsToFetch?: Set, +) => Promise; + +const DEFAULT_OBJECTS = new Set([ + 'pods', + 'services', + 'configmaps', + 'secrets', + 'deployments', + 'replicasets', +]); + +export const handleGetKubernetesObjectsByServiceId: GetKubernetesObjectsByServiceIdHandler = async ( + serviceId, + fetcher, + clusterLocator, + logger, + objectsToFetch = DEFAULT_OBJECTS, +) => { + const clusterDetails = await clusterLocator.getClusterByServiceId(serviceId); + + logger.info( + `serviceId=${serviceId} clusterDetails=${clusterDetails.map(c => c.name)}`, + ); + + return Promise.all( + clusterDetails.map(cd => { + return fetcher + .fetchObjectsByServiceId(serviceId, cd, objectsToFetch) + .then(result => { + return { + cluster: { + name: cd.name, + }, + resources: result, + }; + }); + }), + ).then(r => ({ items: r })); +}; diff --git a/plugins/kubernetes-backend/src/service/router.test.ts b/plugins/kubernetes-backend/src/service/router.test.ts new file mode 100644 index 0000000000..7731fe9abb --- /dev/null +++ b/plugins/kubernetes-backend/src/service/router.test.ts @@ -0,0 +1,87 @@ +/* + * 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 { getVoidLogger } from '@backstage/backend-common'; +import express from 'express'; +import request from 'supertest'; +import { makeRouter } from './router'; +import { + KubernetesClusterLocator, + KubernetesFetcher, + ObjectsByServiceIdResponse, +} from '..'; + +describe('router', () => { + let app: express.Express; + let kubernetesFetcher: jest.Mocked; + let kubernetesClusterLocator: jest.Mocked; + let handleGetByServiceId: jest.Mock>; + + beforeAll(async () => { + kubernetesFetcher = { + fetchObjectsByServiceId: jest.fn(), + }; + + kubernetesClusterLocator = { + getClusterByServiceId: jest.fn(), + }; + + handleGetByServiceId = jest.fn(); + + const router = makeRouter( + getVoidLogger(), + kubernetesFetcher, + kubernetesClusterLocator, + handleGetByServiceId as any, + ); + app = express().use(router); + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('GET /services/:serviceId', () => { + it('happy path: lists kubernetes objects', async () => { + const result = { + clusterOne: { + pods: [ + { + metadata: { + name: 'pod1', + }, + }, + ], + }, + } as any; + handleGetByServiceId.mockReturnValueOnce(Promise.resolve(result)); + + const response = await request(app).get('/services/test-service'); + + expect(response.status).toEqual(200); + expect(response.body).toEqual(result); + }); + + it('internal error: lists kubernetes objects', async () => { + handleGetByServiceId.mockRejectedValue(Error('some internal error')); + + const response = await request(app).get('/services/test-service'); + + expect(response.status).toEqual(500); + expect(response.body).toEqual({ error: 'some internal error' }); + }); + }); +}); diff --git a/plugins/kubernetes-backend/src/service/router.ts b/plugins/kubernetes-backend/src/service/router.ts index b200896887..d038c1ed7d 100644 --- a/plugins/kubernetes-backend/src/service/router.ts +++ b/plugins/kubernetes-backend/src/service/router.ts @@ -17,19 +17,65 @@ import express from 'express'; import Router from 'express-promise-router'; import { Logger } from 'winston'; +import { Config } from '@backstage/config'; +import { ClusterLocatorMethod } from '../cluster-locator/types'; +import { MultiTenantConfigClusterLocator } from '../cluster-locator/MultiTenantConfigClusterLocator'; +import { KubernetesClientBasedFetcher } from './KubernetesFetcher'; +import { KubernetesClientProvider } from './KubernetesClientProvider'; +import { + GetKubernetesObjectsByServiceIdHandler, + handleGetKubernetesObjectsByServiceId, +} from './getKubernetesObjectsByServiceIdHandler'; +import { KubernetesClusterLocator, KubernetesFetcher } from '..'; export interface RouterOptions { logger: Logger; + config: Config; } -const makeRouter = (logger: Logger): express.Router => { +const getClusterLocator = (config: Config): KubernetesClusterLocator => { + const clusterLocatorMethod = config.getString( + 'kubernetes.clusterLocatorMethod', + ) as ClusterLocatorMethod; + + switch (clusterLocatorMethod) { + case 'configMultiTenant': + return MultiTenantConfigClusterLocator.fromConfig( + config.getConfigArray('kubernetes.clusters'), + ); + case 'http': + throw new Error('not implemented'); + default: + throw new Error( + `Unsupported kubernetes.clusterLocatorMethod "${clusterLocatorMethod}"`, + ); + } +}; + +export const makeRouter = ( + logger: Logger, + fetcher: KubernetesFetcher, + clusterLocator: KubernetesClusterLocator, + handleGetByServiceId: GetKubernetesObjectsByServiceIdHandler, +): express.Router => { const router = Router(); router.use(express.json()); + // TODO error handling router.get('/services/:serviceId', async (req, res) => { const serviceId = req.params.serviceId; - logger.info(`HERE ${serviceId}`); - res.send({ serviceId }); + + try { + const response = await handleGetByServiceId( + serviceId, + fetcher, + clusterLocator, + logger, + ); + res.send(response); + } catch (e) { + res.status(500).send({ error: e.message }); + } }); return router; @@ -41,5 +87,18 @@ export async function createRouter( const logger = options.logger; logger.info('Initializing Kubernetes backend'); - return makeRouter(logger); + + const clusterLocator = getClusterLocator(options.config); + + const fetcher = new KubernetesClientBasedFetcher({ + kubernetesClientProvider: new KubernetesClientProvider(), + logger, + }); + + return makeRouter( + logger, + fetcher, + clusterLocator, + handleGetKubernetesObjectsByServiceId, + ); } diff --git a/plugins/kubernetes-backend/src/service/standaloneApplication.ts b/plugins/kubernetes-backend/src/service/standaloneApplication.ts index 5eea4fb8f5..e23e87a7a2 100644 --- a/plugins/kubernetes-backend/src/service/standaloneApplication.ts +++ b/plugins/kubernetes-backend/src/service/standaloneApplication.ts @@ -25,6 +25,7 @@ import express from 'express'; import helmet from 'helmet'; import { Logger } from 'winston'; import { createRouter } from './router'; +import { ConfigReader } from '@backstage/config'; export interface ApplicationOptions { enableCors: boolean; @@ -35,6 +36,7 @@ export async function createStandaloneApplication( options: ApplicationOptions, ): Promise { const { enableCors, logger } = options; + const config = ConfigReader.fromConfigs([]); const app = express(); app.use(helmet()); @@ -44,7 +46,7 @@ export async function createStandaloneApplication( app.use(compression()); app.use(express.json()); app.use(requestLoggingHandler()); - app.use('/', await createRouter({ logger })); + app.use('/', await createRouter({ logger, config })); app.use(notFoundHandler()); app.use(errorHandler()); diff --git a/plugins/kubernetes-backend/src/types/types.ts b/plugins/kubernetes-backend/src/types/types.ts new file mode 100644 index 0000000000..2c69fd67a0 --- /dev/null +++ b/plugins/kubernetes-backend/src/types/types.ts @@ -0,0 +1,103 @@ +/* + * 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 { + V1ConfigMap, + V1Deployment, + V1Pod, + V1ReplicaSet, + V1Secret, + V1Service, +} from '@kubernetes/client-node'; + +export interface ClusterDetails { + name: string; + url: string; + // TODO this will eventually be configured by the auth translation work + serviceAccountToken: string | undefined; +} + +export interface ClusterObjects { + cluster: { name: string }; + resources: FetchResponse[]; +} + +export interface ObjectsByServiceIdResponse { + items: ClusterObjects[]; +} + +export type FetchResponse = + | PodFetchResponse + | ServiceFetchResponse + | ConfigMapFetchResponse + | SecretFetchResponse + | DeploymentFetchResponse + | ReplicaSetsFetchResponse; + +// TODO fairly sure there's a easier way to do this + +export type KubernetesObjectTypes = + | 'pods' + | 'services' + | 'configmaps' + | 'secrets' + | 'deployments' + | 'replicasets'; + +export interface PodFetchResponse { + type: 'pods'; + resources: Array; +} + +export interface ServiceFetchResponse { + type: 'services'; + resources: Array; +} + +export interface ConfigMapFetchResponse { + type: 'configmaps'; + resources: Array; +} + +export interface SecretFetchResponse { + type: 'secrets'; + resources: Array; +} + +export interface DeploymentFetchResponse { + type: 'deployments'; + resources: Array; +} + +export interface ReplicaSetsFetchResponse { + type: 'replicasets'; + resources: Array; +} + +// Fetches information from a kubernetes cluster using the cluster details object +// to target a specific cluster +export interface KubernetesFetcher { + fetchObjectsByServiceId( + serviceId: string, + clusterDetails: ClusterDetails, + objectTypesToFetch: Set, + ): Promise; +} + +// Used to locate which cluster(s) a service is running on +export interface KubernetesClusterLocator { + getClusterByServiceId(serviceId: string): Promise; +} diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index ffe6207680..d1f3d5e7ba 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -22,6 +22,7 @@ "dependencies": { "@backstage/catalog-model": "^0.1.1-alpha.23", "@backstage/core": "^0.1.1-alpha.23", + "@backstage/plugin-kubernetes-backend": "^0.1.1-alpha.23", "@backstage/theme": "^0.1.1-alpha.23", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", diff --git a/plugins/kubernetes/src/Router.tsx b/plugins/kubernetes/src/Router.tsx index 994788bf72..41f534fbef 100644 --- a/plugins/kubernetes/src/Router.tsx +++ b/plugins/kubernetes/src/Router.tsx @@ -22,7 +22,7 @@ import { rootCatalogKubernetesRouteRef } from './plugin'; import { KubernetesContent } from './components/KubernetesContent'; import { WarningPanel } from '@backstage/core'; -const KUBERNETES_ANNOTATION = 'backstage.io/kubernetes'; +const KUBERNETES_ANNOTATION = 'backstage.io/kubernetes-id'; export const Router = ({ entity }: { entity: Entity }) => { const kubernetesAnnotationValue = diff --git a/plugins/kubernetes/src/api/KubernetesBackendClient.ts b/plugins/kubernetes/src/api/KubernetesBackendClient.ts index fed3cd2ed0..cbb3b03a38 100644 --- a/plugins/kubernetes/src/api/KubernetesBackendClient.ts +++ b/plugins/kubernetes/src/api/KubernetesBackendClient.ts @@ -16,6 +16,7 @@ import { DiscoveryApi } from '@backstage/core'; import { KubernetesApi } from './types'; +import { ObjectsByServiceIdResponse } from '@backstage/plugin-kubernetes-backend'; export class KubernetesBackendClient implements KubernetesApi { private readonly discoveryApi: DiscoveryApi; @@ -37,7 +38,9 @@ export class KubernetesBackendClient implements KubernetesApi { return await response.json(); } - async getObjectsByServiceId(serviceId: String): Promise<{}> { + async getObjectsByServiceId( + serviceId: String, + ): Promise { return await this.getRequired(`/services/${serviceId}`); } } diff --git a/plugins/kubernetes/src/api/types.ts b/plugins/kubernetes/src/api/types.ts index 2ad6e567f9..5ec3cda6f8 100644 --- a/plugins/kubernetes/src/api/types.ts +++ b/plugins/kubernetes/src/api/types.ts @@ -15,6 +15,7 @@ */ import { createApiRef } from '@backstage/core'; +import { ObjectsByServiceIdResponse } from '@backstage/plugin-kubernetes-backend'; export const kubernetesApiRef = createApiRef({ id: 'plugin.kubernetes.service', @@ -23,5 +24,5 @@ export const kubernetesApiRef = createApiRef({ }); export interface KubernetesApi { - getObjectsByServiceId(serviceId: String): Promise<{}>; + getObjectsByServiceId(serviceId: String): Promise; } diff --git a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx index d79454a5d1..bbcef6a0d0 100644 --- a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx +++ b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx @@ -15,25 +15,19 @@ */ import React, { FC, useEffect, useState } from 'react'; -import { Typography, Grid } from '@material-ui/core'; -import { - InfoCard, - Page, - pageTheme, - Content, - ContentHeader, - useApi, -} from '@backstage/core'; +import { Grid } from '@material-ui/core'; +import { InfoCard, Page, pageTheme, Content, useApi } from '@backstage/core'; import { Entity } from '@backstage/catalog-model'; import { kubernetesApiRef } from '../../api/types'; +import { ObjectsByServiceIdResponse } from '@backstage/plugin-kubernetes-backend'; // TODO this is a temporary component used to construct the Kubernetes plugin boilerplate export const KubernetesContent: FC<{ entity: Entity }> = ({ entity }) => { const kubernetesApi = useApi(kubernetesApiRef); - const [kubernetesObjects, setKubernetesObjects] = useState<{} | undefined>( - undefined, - ); + const [kubernetesObjects, setKubernetesObjects] = useState< + ObjectsByServiceIdResponse | undefined + >(undefined); const [error, setError] = useState(undefined); useEffect(() => { @@ -50,21 +44,28 @@ export const KubernetesContent: FC<{ entity: Entity }> = ({ entity }) => { return ( - - - - - {kubernetesObjects === undefined &&
loading....
} - {error !== undefined &&
{error}
} - {kubernetesObjects !== undefined && ( -
- backend response: {JSON.stringify(kubernetesObjects)} -
- )} -
-
-
+ {kubernetesObjects === undefined &&
loading....
} + {error !== undefined &&
{error}
} + {kubernetesObjects !== undefined && ( +
+ {kubernetesObjects.items.map((item, i) => ( + + + {item.resources.map((fr, j) => ( +
+
+ {fr.type}:{' '} + {(fr.resources as any) + .map((v: any) => v.metadata.name) + .join(' ')} +
+ ))} +
+
+ ))} +
+ )}
diff --git a/yarn.lock b/yarn.lock index 0b6a8c83f4..46402a9815 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2441,6 +2441,34 @@ resolved "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== +"@kubernetes/client-node@^0.12.1": + version "0.12.2" + resolved "https://registry.npmjs.org/@kubernetes/client-node/-/client-node-0.12.2.tgz#8728684bd57d1cbcbe14fe742e79be7021403eea" + integrity sha512-J0UwyFl1Iv/IZ6WMP7LaizBEoKPnqwtc8tIO2q/X+EuDT7eGpPPAMHXSEOC/EI9JGIf0FaJEcDHhB/Dio/mKhw== + dependencies: + "@types/js-yaml" "^3.12.1" + "@types/node" "^10.12.0" + "@types/request" "^2.47.1" + "@types/stream-buffers" "^3.0.3" + "@types/tar" "^4.0.3" + "@types/underscore" "^1.8.9" + "@types/ws" "^6.0.1" + byline "^5.0.0" + execa "1.0.0" + isomorphic-ws "^4.0.1" + js-yaml "^3.13.1" + jsonpath-plus "^0.19.0" + openid-client "^4.1.1" + request "^2.88.0" + rfc4648 "^1.3.0" + shelljs "^0.8.2" + stream-buffers "^3.0.2" + tar "^6.0.2" + tmp-promise "^3.0.2" + tslib "^1.9.3" + underscore "^1.9.1" + ws "^7.3.1" + "@kyleshockey/object-assign-deep@^0.4.2": version "0.4.2" resolved "https://registry.npmjs.org/@kyleshockey/object-assign-deep/-/object-assign-deep-0.4.2.tgz#84900f0eefc372798f4751b5262830b8208922ec" @@ -4638,6 +4666,11 @@ "@types/node" "*" "@types/responselike" "*" +"@types/caseless@*": + version "0.12.2" + resolved "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8" + integrity sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w== + "@types/classnames@^2.2.9": version "2.2.10" resolved "https://registry.npmjs.org/@types/classnames/-/classnames-2.2.10.tgz#cc658ca319b6355399efc1f5b9e818f1a24bf999" @@ -4995,7 +5028,7 @@ resolved "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.6.tgz#f1a1cb35aff47bc5cfb05cb0c441ca91e914c26f" integrity sha512-+oY0FDTO2GYKEV0YPvSshGq9t7YozVkgvXLty7zogQNuCxBhT9/3INX9Q7H1aRZ4SUDRXAKlJuA4EA5nTt7SNw== -"@types/js-yaml@^3.12.5": +"@types/js-yaml@^3.12.1", "@types/js-yaml@^3.12.5": version "3.12.5" resolved "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-3.12.5.tgz#136d5e6a57a931e1cce6f9d8126aa98a9c92a6bb" integrity sha512-JCcp6J0GV66Y4ZMDAQCXot4xprYB+Zfd3meK9+INSJeVZwJmHAW30BBEEkPzXswMXuiyReUGOP3GxrADc9wPww== @@ -5139,6 +5172,11 @@ resolved "https://registry.npmjs.org/@types/node/-/node-10.17.28.tgz#0e36d718a29355ee51cec83b42d921299200f6d9" integrity sha512-dzjES1Egb4c1a89C7lKwQh8pwjYmlOAG9dW1pBgxEk57tMrLnssOfEthz8kdkNaBd7lIqQx7APm5+mZ619IiCQ== +"@types/node@^10.12.0": + version "10.17.35" + resolved "https://registry.npmjs.org/@types/node/-/node-10.17.35.tgz#58058f29b870e6ae57b20e4f6e928f02b7129f56" + integrity sha512-gXx7jAWpMddu0f7a+L+txMplp3FnHl53OhQIF9puXKq3hDGY/GjH+MF04oWnV/adPSCrbtHumDCFwzq2VhltWA== + "@types/node@^12.0.0": version "12.12.53" resolved "https://registry.npmjs.org/@types/node/-/node-12.12.53.tgz#be0d375933c3d15ef2380dafb3b0350ea7021129" @@ -5387,6 +5425,16 @@ resolved "https://registry.npmjs.org/@types/relateurl/-/relateurl-0.2.28.tgz#6bda7db8653fa62643f5ee69e9f69c11a392e3a6" integrity sha1-a9p9uGU/piZD9e5p6facEaOS46Y= +"@types/request@^2.47.1": + version "2.48.5" + resolved "https://registry.npmjs.org/@types/request/-/request-2.48.5.tgz#019b8536b402069f6d11bee1b2c03e7f232937a0" + integrity sha512-/LO7xRVnL3DxJ1WkPGDQrp4VTV1reX9RkC85mJ+Qzykj2Bdw+mG15aAfDahc76HtknjzE16SX/Yddn6MxVbmGQ== + dependencies: + "@types/caseless" "*" + "@types/node" "*" + "@types/tough-cookie" "*" + form-data "^2.5.0" + "@types/resolve@1.17.1": version "1.17.1" resolved "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" @@ -5472,6 +5520,13 @@ dependencies: "@types/node" "*" +"@types/stream-buffers@^3.0.3": + version "3.0.3" + resolved "https://registry.npmjs.org/@types/stream-buffers/-/stream-buffers-3.0.3.tgz#34e565bf64e3e4bdeee23fd4aa58d4636014a02b" + integrity sha512-NeFeX7YfFZDYsCfbuaOmFQ0OjSmHreKBpp7MQ4alWQBHeh2USLsj7qyMyn9t82kjqIX516CR/5SRHnARduRtbQ== + dependencies: + "@types/node" "*" + "@types/styled-jsx@^2.2.8": version "2.2.8" resolved "https://registry.npmjs.org/@types/styled-jsx/-/styled-jsx-2.2.8.tgz#b50d13d8a3c34036282d65194554cf186bab7234" @@ -5550,6 +5605,11 @@ dependencies: "@types/node" "*" +"@types/tough-cookie@*": + version "4.0.0" + resolved "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz#fef1904e4668b6e5ecee60c52cc6a078ffa6697d" + integrity sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A== + "@types/uglify-js@*": version "3.0.4" resolved "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.0.4.tgz#96beae23df6f561862a830b4288a49e86baac082" @@ -5557,6 +5617,11 @@ dependencies: source-map "^0.6.1" +"@types/underscore@^1.8.9": + version "1.10.23" + resolved "https://registry.npmjs.org/@types/underscore/-/underscore-1.10.23.tgz#cc672e8864000d288e1e39c609fd9cab84391ff3" + integrity sha512-vX1NPekXhrLquFWskH2thcvFAha187F/lM6xYOoEMZWwJ/6alSk0/ttmGP/YRqcqtCv0TMbZjYAdZyHAEcuU4g== + "@types/uuid@^8.0.0": version "8.0.0" resolved "https://registry.npmjs.org/@types/uuid/-/uuid-8.0.0.tgz#165aae4819ad2174a17476dbe66feebd549556c0" @@ -5618,6 +5683,13 @@ resolved "https://registry.npmjs.org/@types/whatwg-streams/-/whatwg-streams-0.0.7.tgz#28bfe73dc850562296367249c4b32a50db81e9d3" integrity sha512-6sDiSEP6DWcY2ZolsJ2s39ZmsoGQ7KVwBDI3sESQsEm9P2dHTcqnDIHRZFRNtLCzWp7hCFGqYbw5GyfpQnJ01A== +"@types/ws@^6.0.1": + version "6.0.4" + resolved "https://registry.npmjs.org/@types/ws/-/ws-6.0.4.tgz#7797707c8acce8f76d8c34b370d4645b70421ff1" + integrity sha512-PpPrX7SZW9re6+Ha8ojZG4Se8AZXgf0GK6zmfqEuCsY49LFDNXO3SByp44X3dFEqtB73lkCDAdUazhAjVPiNwg== + dependencies: + "@types/node" "*" + "@types/ws@^7.0.0": version "7.2.6" resolved "https://registry.npmjs.org/@types/ws/-/ws-7.2.6.tgz#516cbfb818310f87b43940460e065eb912a4178d" @@ -7156,7 +7228,7 @@ base64-js@^1.0.2, base64-js@^1.2.0: resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== -base64url@3.x.x: +base64url@3.x.x, base64url@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d" integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A== @@ -10630,6 +10702,19 @@ exec-sh@^0.3.2: resolved "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz#3a018ceb526cc6f6df2bb504b2bfe8e3a4934ec5" integrity sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A== +execa@1.0.0, execa@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== + dependencies: + cross-spawn "^6.0.0" + get-stream "^4.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + execa@3.4.0: version "3.4.0" resolved "https://registry.npmjs.org/execa/-/execa-3.4.0.tgz#c08ed4550ef65d858fac269ffc8572446f37eb89" @@ -10659,19 +10744,6 @@ execa@^0.7.0: signal-exit "^3.0.0" strip-eof "^1.0.0" -execa@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" - integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== - dependencies: - cross-spawn "^6.0.0" - get-stream "^4.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - execa@^4.0.0, execa@^4.0.1: version "4.0.2" resolved "https://registry.npmjs.org/execa/-/execa-4.0.2.tgz#ad87fb7b2d9d564f70d2b62d511bee41d5cbb240" @@ -11337,7 +11409,7 @@ fork-ts-checker-webpack-plugin@^4.0.5, fork-ts-checker-webpack-plugin@^4.1.4: tapable "^1.0.0" worker-rpc "^0.1.0" -form-data@^2.3.1, form-data@^2.3.2: +form-data@^2.3.1, form-data@^2.3.2, form-data@^2.5.0: version "2.5.1" resolved "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== @@ -11982,6 +12054,23 @@ got@^11.5.2: p-cancelable "^2.0.0" responselike "^2.0.0" +got@^11.6.2: + version "11.7.0" + resolved "https://registry.npmjs.org/got/-/got-11.7.0.tgz#a386360305571a74548872e674932b4ef70d3b24" + integrity sha512-7en2XwH2MEqOsrK0xaKhbWibBoZqy+f1RSUoIeF1BLcnf+pyQdDsljWMfmOh+QKJwuvDIiKx38GtPh5wFdGGjg== + dependencies: + "@sindresorhus/is" "^3.1.1" + "@szmarczak/http-timer" "^4.0.5" + "@types/cacheable-request" "^6.0.1" + "@types/responselike" "^1.0.0" + cacheable-lookup "^5.0.3" + cacheable-request "^7.0.1" + decompress-response "^6.0.0" + http2-wrapper "^1.0.0-beta.5.2" + lowercase-keys "^2.0.0" + p-cancelable "^2.0.0" + responselike "^2.0.0" + got@^9.6.0: version "9.6.0" resolved "https://registry.npmjs.org/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" @@ -13604,6 +13693,11 @@ isomorphic-form-data@~2.0.0: dependencies: form-data "^2.3.2" +isomorphic-ws@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" + integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== + isstream@~0.1.2: version "0.1.2" resolved "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -14110,6 +14204,13 @@ jose@^1.27.1: dependencies: "@panva/asn1.js" "^1.0.0" +jose@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/jose/-/jose-2.0.2.tgz#fb22385b80c658cc7a0cae05b7086c04c6be49f4" + integrity sha512-yD93lsiMA1go/qxSY/vXWBodmIZJIxeB7QhFi8z1yQ3KUwKENqI9UA8VCHlQ5h3x1zWuWZjoY87ByQzkQbIrQg== + dependencies: + "@panva/asn1.js" "^1.0.0" + js-cookie@^2.2.1: version "2.2.1" resolved "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8" @@ -14402,6 +14503,11 @@ jsonparse@^1.2.0: resolved "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= +jsonpath-plus@^0.19.0: + version "0.19.0" + resolved "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-0.19.0.tgz#b901e57607055933dc9a8bef0cc25160ee9dd64c" + integrity sha512-GSVwsrzW9LsA5lzsqe4CkuZ9wp+kxBb2GwNniaWzI2YFn5Ig42rSW8ZxVpWXaAfakXNrx5pgY5AbQq7kzX29kg== + jsonpointer@^4.0.1: version "4.0.1" resolved "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" @@ -15297,7 +15403,7 @@ make-dir@^3.0.0, make-dir@^3.0.2: dependencies: semver "^6.0.0" -make-error@1.x, make-error@^1.1.1: +make-error@1.x, make-error@^1.1.1, make-error@^1.3.6: version "1.3.6" resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== @@ -16500,6 +16606,11 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" +object-hash@^2.0.1: + version "2.0.3" + resolved "https://registry.npmjs.org/object-hash/-/object-hash-2.0.3.tgz#d12db044e03cd2ca3d77c0570d87225b02e1e6ea" + integrity sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg== + object-inspect@^1.7.0: version "1.7.0" resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67" @@ -16615,6 +16726,11 @@ octokit-pagination-methods@^1.1.0: resolved "https://registry.npmjs.org/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz#cf472edc9d551055f9ef73f6e42b4dbb4c80bea4" integrity sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ== +oidc-token-hash@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.0.tgz#acdfb1f4310f58e64d5d74a4e8671a426986e888" + integrity sha512-8Yr4CZSv+Tn8ZkN3iN2i2w2G92mUKClp4z7EGUfdsERiYSbj7P4i/NHm72ft+aUdsiFx9UdIPSTwbyzQ6C4URg== + omggif@1.0.7: version "1.0.7" resolved "https://registry.npmjs.org/omggif/-/omggif-1.0.7.tgz#59d2eecb0263de84635b3feb887c0c9973f1e49d" @@ -16683,6 +16799,20 @@ opencollective-postinstall@^2.0.2: resolved "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz#5657f1bede69b6e33a45939b061eb53d3c6c3a89" integrity sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw== +openid-client@^4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/openid-client/-/openid-client-4.1.1.tgz#3e8a25584c4292e9b9b03e60358f5549fb85197a" + integrity sha512-/qch3I3v8UtO0A7wVgyXJJjGX/knR8bv06DQpLuKQqLG5u4AHcgusGuVKPKAcneLZvHKbKovF2+3e2ngXyuudA== + dependencies: + base64url "^3.0.1" + got "^11.6.2" + jose "^2.0.2" + lru-cache "^6.0.0" + make-error "^1.3.6" + object-hash "^2.0.1" + oidc-token-hash "^5.0.0" + p-any "^3.0.0" + opn@^5.5.0: version "5.5.0" resolved "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc" @@ -16788,6 +16918,14 @@ overlayscrollbars@^1.10.2: resolved "https://registry.npmjs.org/overlayscrollbars/-/overlayscrollbars-1.13.0.tgz#1edb436328133b94877b558f77966d5497ca36a7" integrity sha512-p8oHrMeRAKxXDMPI/EBNITj/zTVHKNnAnM59Im+xnoZUlV07FyTg46wom2286jJlXGGfcPFG/ba5NUiCwWNd4w== +p-any@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/p-any/-/p-any-3.0.0.tgz#79847aeed70b5d3a10ea625296c0c3d2e90a87b9" + integrity sha512-5rqbqfsRWNb0sukt0awwgJMlaep+8jV45S15SKKB34z4UuzjcofIfnriCBhWjZP2jbVtjt9yRl7buB6RlKsu9w== + dependencies: + p-cancelable "^2.0.0" + p-some "^5.0.0" + p-cancelable@^1.0.0: version "1.1.0" resolved "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" @@ -16934,6 +17072,14 @@ p-retry@^3.0.1: dependencies: retry "^0.12.0" +p-some@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/p-some/-/p-some-5.0.0.tgz#8b730c74b4fe5169d7264a240ad010b6ebc686a4" + integrity sha512-Js5XZxo6vHjB9NOYAzWDYAIyyiPvva0DWESAIWIK7uhSpGsyg5FwUPxipU/SOQx5x9EqhOh545d1jo6cVkitig== + dependencies: + aggregate-error "^3.0.0" + p-cancelable "^2.0.0" + p-timeout@^3.1.0: version "3.2.0" resolved "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" @@ -19764,6 +19910,11 @@ reusify@^1.0.4: resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== +rfc4648@^1.3.0: + version "1.4.0" + resolved "https://registry.npmjs.org/rfc4648/-/rfc4648-1.4.0.tgz#c75b2856ad2e2d588b6ddb985d556f1f7f2a2abd" + integrity sha512-3qIzGhHlMHA6PoT6+cdPKZ+ZqtxkIvg8DZGKA5z6PQ33/uuhoJ+Ws/D/J9rXW6gXodgH8QYlz2UCl+sdUDmNIg== + rgb-regex@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1" @@ -20303,7 +20454,7 @@ shell-quote@1.7.2: resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg== -shelljs@^0.8.3: +shelljs@^0.8.2, shelljs@^0.8.3: version "0.8.4" resolved "https://registry.npmjs.org/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2" integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ== @@ -20867,6 +21018,11 @@ stream-browserify@^2.0.1: inherits "~2.0.1" readable-stream "^2.0.2" +stream-buffers@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/stream-buffers/-/stream-buffers-3.0.2.tgz#5249005a8d5c2d00b3a32e6e0a6ea209dc4f3521" + integrity sha512-DQi1h8VEBA/lURbSwFtEHnSTb9s2/pwLEaFuNhXwy1Dx3Sa0lOuYT2yNUr4/j2fs8oCAMANtrZ5OrPZtyVs3MQ== + stream-combiner@~0.0.4: version "0.0.4" resolved "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14" @@ -21731,6 +21887,13 @@ tinycolor2@^1.4.1: resolved "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz#f4fad333447bc0b07d4dc8e9209d8f39a8ac77e8" integrity sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g= +tmp-promise@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.2.tgz#6e933782abff8b00c3119d63589ca1fb9caaa62a" + integrity sha512-OyCLAKU1HzBjL6Ev3gxUeraJNlbNingmi8IrHHEsYH8LTmEuhvYfqvhn2F/je+mjf4N58UmZ96OMEy1JanSCpA== + dependencies: + tmp "^0.2.0" + tmp@^0.0.33: version "0.0.33" resolved "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -21738,6 +21901,13 @@ tmp@^0.0.33: dependencies: os-tmpdir "~1.0.2" +tmp@^0.2.0: + version "0.2.1" + resolved "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" + integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== + dependencies: + rimraf "^3.0.0" + tmp@~0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz#ee434a4e22543082e294ba6201dcc6eafefa2877" @@ -22190,6 +22360,11 @@ undefsafe@^2.0.2: dependencies: debug "^2.2.0" +underscore@^1.9.1: + version "1.11.0" + resolved "https://registry.npmjs.org/underscore/-/underscore-1.11.0.tgz#dd7c23a195db34267186044649870ff1bab5929e" + integrity sha512-xY96SsN3NA461qIRKZ/+qox37YXPtSBswMGfiNptr+wrt6ds4HaMw23TP612fEyGekRE6LNRiLYr/aqbHXNedw== + unfetch@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/unfetch/-/unfetch-4.1.0.tgz#6ec2dd0de887e58a4dee83a050ded80ffc4137db" @@ -23243,6 +23418,11 @@ ws@^7.2.3: resolved "https://registry.npmjs.org/ws/-/ws-7.3.0.tgz#4b2f7f219b3d3737bc1a2fbf145d825b94d38ffd" integrity sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w== +ws@^7.3.1: + version "7.3.1" + resolved "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8" + integrity sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA== + x-is-string@^0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/x-is-string/-/x-is-string-0.1.0.tgz#474b50865af3a49a9c4657f05acd145458f77d82" From 5e60184ea4dffa2a07a659feada0a62949ecf1e9 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 21 Sep 2020 10:22:33 -0600 Subject: [PATCH 74/85] Tweak dark mode colors --- packages/theme/src/themes.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/theme/src/themes.ts b/packages/theme/src/themes.ts index 73d6c13859..e92f36d22a 100644 --- a/packages/theme/src/themes.ts +++ b/packages/theme/src/themes.ts @@ -80,12 +80,12 @@ export const darkTheme = createTheme({ default: '#333333', }, status: { - ok: '#1DB954', - warning: '#FF9800', - error: '#E22134', - running: '#2E77D0', - pending: '#FFED51', - aborted: '#757575', + ok: '#9cdcaa', + warning: '#ffcd80', + error: '#f07278', + running: '#6cb6f5', + pending: '#fff9c2', + aborted: '#eeeeee', }, bursts: { fontColor: '#FEFEFE', @@ -103,7 +103,7 @@ export const darkTheme = createTheme({ }, border: '#E6E6E6', textContrast: '#FFFFFF', - textVerySubtle: '#DDD', + textVerySubtle: '#727272', textSubtle: '#EEEEEE', highlight: '#FFFBCC', errorBackground: '#FFEBEE', From 3947c92c2c35d87beb355c13bf544a1220fe75bc Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Thu, 24 Sep 2020 12:54:56 -0400 Subject: [PATCH 75/85] tweak contrast --- packages/theme/src/themes.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/theme/src/themes.ts b/packages/theme/src/themes.ts index e92f36d22a..72a1df716f 100644 --- a/packages/theme/src/themes.ts +++ b/packages/theme/src/themes.ts @@ -80,12 +80,12 @@ export const darkTheme = createTheme({ default: '#333333', }, status: { - ok: '#9cdcaa', - warning: '#ffcd80', - error: '#f07278', - running: '#6cb6f5', - pending: '#fff9c2', - aborted: '#eeeeee', + ok: '#71CF88', + warning: '#FFB84D', + error: '#F84C55', + running: '#3488E3', + pending: '#FEF071', + aborted: '#9E9E9E', }, bursts: { fontColor: '#FEFEFE', @@ -103,7 +103,7 @@ export const darkTheme = createTheme({ }, border: '#E6E6E6', textContrast: '#FFFFFF', - textVerySubtle: '#727272', + textVerySubtle: '#ececec', textSubtle: '#EEEEEE', highlight: '#FFFBCC', errorBackground: '#FFEBEE', From b097af87a4477115d65f6933d863d1c8d3548ba5 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Thu, 24 Sep 2020 23:01:28 +0200 Subject: [PATCH 76/85] Enable carryforward for codecov flags Our builds for Pull Requests do not upload coverage report for flags. This overrides the coverage data for flags. Setting carryforward flag to true prevents the data for flags to be overridden. --- .codecov.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.codecov.yml b/.codecov.yml index d03ad99836..1b8e770b59 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -13,6 +13,8 @@ flags: core: paths: - packages/core/ + carryforward: true core-api: paths: - packages/core-api/ + carryforward: true From b0565777e98d2864bf1566c0000c4d63a559043d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 24 Sep 2020 23:37:52 +0200 Subject: [PATCH 77/85] backend-common: add initial discovery API and simple single host implementation --- .../src/discovery/SingleHostDiscovery.ts | 80 +++++++++++++++++++ .../backend-common/src/discovery/index.ts | 18 +++++ .../backend-common/src/discovery/types.ts | 61 ++++++++++++++ packages/backend-common/src/index.ts | 1 + .../src/service/lib/ServiceBuilderImpl.ts | 2 +- .../backend-common/src/service/lib/config.ts | 12 ++- 6 files changed, 166 insertions(+), 8 deletions(-) create mode 100644 packages/backend-common/src/discovery/SingleHostDiscovery.ts create mode 100644 packages/backend-common/src/discovery/index.ts create mode 100644 packages/backend-common/src/discovery/types.ts diff --git a/packages/backend-common/src/discovery/SingleHostDiscovery.ts b/packages/backend-common/src/discovery/SingleHostDiscovery.ts new file mode 100644 index 0000000000..1970583e5f --- /dev/null +++ b/packages/backend-common/src/discovery/SingleHostDiscovery.ts @@ -0,0 +1,80 @@ +/* + * 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 { Config } from '@backstage/config'; +import { PluginEndpointDiscovery } from './types'; +import { readBaseOptions } from '../service/lib/config'; +import { DEFAULT_PORT } from '../service/lib/ServiceBuilderImpl'; + +/** + * SingleHostDiscovery is a basic PluginEndpointDiscovery implementation + * that assumes that all plugins are hosted in a single deployment. + * + * The deployment may be scaled horizontally, as long as the external URL + * is the same for all instances. However, internal URLs will always be + * resolved to the same host, so there won't be any balancing of internal traffic. + */ +export class SingleHostDiscovery implements PluginEndpointDiscovery { + /** + * Creates a new SingleHostDiscovery discovery instance by reading + * from the `backend` config section, specifically the `.baseUrl` for + * discovering the external URL, and the `.listen` and `.https` config + * for the internal one. + * + * The basePath defaults to `/api`, meaning the default full internal + * path for the `catalog` plugin will be `http://localhost:7000/api/catalog`. + */ + static fromConfig(config: Config, options?: { basePath?: string }) { + const basePath = options?.basePath ?? '/api'; + const externalBaseUrl = config.getString('backend.baseUrl'); + + const { listenHost = '::', listenPort = DEFAULT_PORT } = readBaseOptions( + config.getConfig('backend'), + ); + const protocol = config.has('backend.https') ? 'https' : 'http'; + + // Translate bind-all to localhost, and support IPv6 + let host = listenHost; + if (host === '::') { + host = '::1'; + } else if (host === '0.0.0.0') { + host = '127.0.0.1'; + } + if (host.includes(':')) { + host = `[${host}]`; + } + + const internalBaseUrl = `${protocol}://${host}:${listenPort}`; + + return new SingleHostDiscovery( + internalBaseUrl + basePath, + externalBaseUrl + basePath, + ); + } + + private constructor( + private readonly internalBaseUrl: string, + private readonly externalBaseUrl: string, + ) {} + + async getBaseUrl(pluginId: string): Promise { + return `${this.internalBaseUrl}/${pluginId}`; + } + + async getExternalBaseUrl(pluginId: string): Promise { + return `${this.externalBaseUrl}/${pluginId}`; + } +} diff --git a/packages/backend-common/src/discovery/index.ts b/packages/backend-common/src/discovery/index.ts new file mode 100644 index 0000000000..7fe320c1e5 --- /dev/null +++ b/packages/backend-common/src/discovery/index.ts @@ -0,0 +1,18 @@ +/* + * 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 { SingleHostDiscovery } from './SingleHostDiscovery'; +export type { PluginEndpointDiscovery } from './types'; diff --git a/packages/backend-common/src/discovery/types.ts b/packages/backend-common/src/discovery/types.ts new file mode 100644 index 0000000000..22eb4b23d4 --- /dev/null +++ b/packages/backend-common/src/discovery/types.ts @@ -0,0 +1,61 @@ +/* + * 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. + */ + +/** + * The PluginEndpointDiscovery is used to provide a mechanism for backend + * plugins to discover the endpoints for itself or other backend plugins. + * + * The purpose of the discovery API is to allow for many different deployment + * setups and routing methods through a central configuration, instead + * of letting each individual plugin manage that configuration. + * + * Implementations of the discovery API can be as simple as a URL pattern + * using the pluginId, but could also have overrides for individual plugins, + * or query a separate discovery service. + */ +export type PluginEndpointDiscovery = { + /** + * Returns the internal HTTP base URL for a given plugin, without a trailing slash. + * + * The returned URL should point to an internal endpoint for the plugin, with + * the shortest route possible. The URL should be used for service-to-service + * communication within a Backstage backend deployment. + * + * This method must always be called just before making a request, as opposed to + * fetching the URL when constructing an API client. That is to ensure that more + * flexible routing patterns can be supported. + * + * For example, asking for the URL for `catalog` may return something + * like `http://10.1.2.3/api/catalog` + */ + getBaseUrl(pluginId: string): Promise; + + /** + * Returns the external HTTP base backend URL for a given plugin, without a trailing slash. + * + * The returned URL should point to an external endpoint for the plugin, such that + * it is reachable from the Backstage frontend and other external services. The returned + * URL should be usable for example as a callback / webhook URL. + * + * The returned URL should be stable and in general not change unless other static + * or external configuration is changed. Changes should not come as a surprise + * to an operator of the Backstage backend. + * + * For example, asking for the URL for `catalog` may return something + * like `https://backstage.example.com/api/catalog` + */ + getExternalBaseUrl(pluginId: string): Promise; +}; diff --git a/packages/backend-common/src/index.ts b/packages/backend-common/src/index.ts index c527f5fea1..7074040655 100644 --- a/packages/backend-common/src/index.ts +++ b/packages/backend-common/src/index.ts @@ -16,6 +16,7 @@ export * from './config'; export * from './database'; +export * from './discovery'; export * from './errors'; export * from './logging'; export * from './middleware'; diff --git a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts index 10f9ca4d9b..f3bc59335b 100644 --- a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts +++ b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts @@ -39,7 +39,7 @@ import { import { createHttpServer, createHttpsServer } from './hostFactory'; import { metricsHandler } from './metrics'; -const DEFAULT_PORT = 7000; +export const DEFAULT_PORT = 7000; // '' is express default, which listens to all interfaces const DEFAULT_HOST = ''; diff --git a/packages/backend-common/src/service/lib/config.ts b/packages/backend-common/src/service/lib/config.ts index e257bd4111..3b053c7555 100644 --- a/packages/backend-common/src/service/lib/config.ts +++ b/packages/backend-common/src/service/lib/config.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ConfigReader } from '@backstage/config'; +import { Config } from '@backstage/config'; import { CorsOptions } from 'cors'; export type BaseOptions = { @@ -71,7 +71,7 @@ export type CertificateAttributes = { * } * ``` */ -export function readBaseOptions(config: ConfigReader): BaseOptions { +export function readBaseOptions(config: Config): BaseOptions { if (typeof config.get('listen') === 'string') { // TODO(freben): Expand this to support more addresses and perhaps optional const { host, port } = parseListenAddress(config.getString('listen')); @@ -105,7 +105,7 @@ export function readBaseOptions(config: ConfigReader): BaseOptions { * } * ``` */ -export function readCorsOptions(config: ConfigReader): CorsOptions | undefined { +export function readCorsOptions(config: Config): CorsOptions | undefined { const cc = config.getOptionalConfig('cors'); if (!cc) { return undefined; @@ -138,9 +138,7 @@ export function readCorsOptions(config: ConfigReader): CorsOptions | undefined { * } * ``` */ -export function readHttpsSettings( - config: ConfigReader, -): HttpsSettings | undefined { +export function readHttpsSettings(config: Config): HttpsSettings | undefined { const cc = config.getOptionalConfig('https'); if (!cc) { @@ -157,7 +155,7 @@ export function readHttpsSettings( } function getOptionalStringOrStrings( - config: ConfigReader, + config: Config, key: string, ): string | string[] | undefined { const value = config.getOptional(key); From 9433565fe7734b17c8577c2740f83e15f3ea0a77 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Thu, 24 Sep 2020 18:25:16 +0200 Subject: [PATCH 78/85] feat: add ApiDefinitionAtLocationProcessor that allows to load a API definition from another location Resolves #2572 --- .../well-known-annotations.md | 21 +++ packages/catalog-model/examples/all-apis.yaml | 2 + .../catalog-model/examples/spotify-api.yaml | 14 ++ .../src/ingestion/LocationReaders.ts | 30 +++- .../ApiDefinitionAtLocationProcessor.test.ts | 132 ++++++++++++++++++ .../ApiDefinitionAtLocationProcessor.ts | 70 ++++++++++ .../src/ingestion/processors/types.ts | 6 + 7 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 packages/catalog-model/examples/spotify-api.yaml create mode 100644 plugins/catalog-backend/src/ingestion/processors/ApiDefinitionAtLocationProcessor.test.ts create mode 100644 plugins/catalog-backend/src/ingestion/processors/ApiDefinitionAtLocationProcessor.ts diff --git a/docs/features/software-catalog/well-known-annotations.md b/docs/features/software-catalog/well-known-annotations.md index ba5b491e07..e5b66bb3b2 100644 --- a/docs/features/software-catalog/well-known-annotations.md +++ b/docs/features/software-catalog/well-known-annotations.md @@ -41,6 +41,27 @@ expecting a two-item array out of it. The format of the target part is type-dependent and could conceivably even be an empty string, but the separator colon is always present. +### backstage.io/definition-at-location + +```yaml +# Example +apiVersion: backstage.io/v1alpha1 +kind: API +metadata: + name: petstore + annotations: + backstage.io/definition-at-location: 'url:https://petstore.swagger.io/v2/swagger.json' +spec: + type: openapi +``` + +This annotation allows to fetch an API definition from another location, instead +of wrapping the API definition inside the definition field. This allows to +easitly consume existing API definition. The definition is fetched during +ingestion by a processor and included in the entity. It is updated on every +refresh. The annotation contains a location reference string that contains the +location processor type and the target. + ### backstage.io/techdocs-ref ```yaml diff --git a/packages/catalog-model/examples/all-apis.yaml b/packages/catalog-model/examples/all-apis.yaml index 33b000d1ae..3c4f7804bf 100644 --- a/packages/catalog-model/examples/all-apis.yaml +++ b/packages/catalog-model/examples/all-apis.yaml @@ -8,3 +8,5 @@ spec: targets: - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/hello-world-api.yaml - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/streetlights-api.yaml + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/spotify-api.yaml + - https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/swapi-graphql.yaml diff --git a/packages/catalog-model/examples/spotify-api.yaml b/packages/catalog-model/examples/spotify-api.yaml new file mode 100644 index 0000000000..30524dfdd4 --- /dev/null +++ b/packages/catalog-model/examples/spotify-api.yaml @@ -0,0 +1,14 @@ +apiVersion: backstage.io/v1alpha1 +kind: API +metadata: + name: spotify + description: The Spotify web API + tags: + - spotify + - rest + annotations: + backstage.io/definition-at-location: 'url:https://raw.githubusercontent.com/APIs-guru/openapi-directory/master/APIs/spotify.com/v1/swagger.yaml' +spec: + type: openapi + lifecycle: production + owner: spotify@example.com diff --git a/plugins/catalog-backend/src/ingestion/LocationReaders.ts b/plugins/catalog-backend/src/ingestion/LocationReaders.ts index 8da6e20ab3..11558feae0 100644 --- a/plugins/catalog-backend/src/ingestion/LocationReaders.ts +++ b/plugins/catalog-backend/src/ingestion/LocationReaders.ts @@ -47,6 +47,7 @@ import { import { YamlProcessor } from './processors/YamlProcessor'; import { LocationReader, ReadLocationResult } from './types'; import { CatalogRulesEnforcer } from './CatalogRules'; +import { ApiDefinitionAtLocationProcessor } from './processors/ApiDefinitionAtLocationProcessor'; // The max amount of nesting depth of generated work items const MAX_DEPTH = 10; @@ -85,6 +86,7 @@ export class LocationReaders implements LocationReader { new AzureApiReaderProcessor(config), new UrlReaderProcessor(), new YamlProcessor(), + new ApiDefinitionAtLocationProcessor(), new EntityPolicyProcessor(entityPolicy), new LocationRefProcessor(), new AnnotateLocationEntityProcessor(), @@ -218,7 +220,12 @@ export class LocationReaders implements LocationReader { for (const processor of this.processors) { if (processor.processEntity) { try { - current = await processor.processEntity(current, item.location, emit); + current = await processor.processEntity( + current, + item.location, + emit, + this.readLocation.bind(this), + ); } catch (e) { const message = `Processor ${processor.constructor.name} threw an error while processing entity at ${item.location.type} ${item.location.target}, ${e}`; emit(result.generalError(item.location, message)); @@ -248,4 +255,25 @@ export class LocationReaders implements LocationReader { } } } + + private async readLocation( + location: LocationSpec, + ): Promise { + let locationResult: LocationProcessorResult | undefined; + + await this.handleLocation( + { + type: 'location', + location, + optional: false, + }, + r => (locationResult = r), + ); + + if (!locationResult) { + throw new Error('No location loaded'); + } + + return locationResult; + } } diff --git a/plugins/catalog-backend/src/ingestion/processors/ApiDefinitionAtLocationProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/ApiDefinitionAtLocationProcessor.test.ts new file mode 100644 index 0000000000..ef6d348ad5 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/ApiDefinitionAtLocationProcessor.test.ts @@ -0,0 +1,132 @@ +import { ApiEntity, Entity, LocationSpec } from '@backstage/catalog-model'; +/* + * 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 { ApiDefinitionAtLocationProcessor } from './ApiDefinitionAtLocationProcessor'; +import { LocationProcessorResult } from './types'; + +describe('ApiDefinitionAtLocationProcessor', () => { + let processor: ApiDefinitionAtLocationProcessor; + let entity: Entity; + let location: LocationSpec; + + beforeEach(() => { + processor = new ApiDefinitionAtLocationProcessor(); + entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'API', + metadata: { + name: 'test', + }, + spec: { + lifecycle: 'production', + owner: 'info@example.com', + type: 'openapi', + definition: 'Hello', + }, + }; + location = { + type: 'url', + target: `http://example.com/api.yaml`, + }; + }); + + it('should skip entities without annotation', async () => { + const read = jest.fn( + (): Promise => { + throw new Error(); + }, + ); + + const generated = (await processor.processEntity( + entity, + location, + () => {}, + read, + )) as ApiEntity; + + expect(generated.spec.definition).toBe('Hello'); + }); + + it('should load from location', async () => { + entity.metadata.annotations = { + 'backstage.io/definition-at-location': + 'url:http://example.com/openapi.yaml', + }; + + const read = jest.fn( + (l: LocationSpec): Promise => + Promise.resolve({ + type: 'data', + data: Buffer.from('Hello'), + location: l, + }), + ); + + const generated = (await processor.processEntity( + entity, + location, + () => {}, + read, + )) as ApiEntity; + + expect(generated.spec.definition).toBe('Hello'); + expect(read.mock.calls[0][0]).toStrictEqual({ + type: 'url', + target: 'http://example.com/openapi.yaml', + }); + }); + + it('should throw errors while loading', async () => { + entity.metadata.annotations = { + 'backstage.io/definition-at-location': 'missing', + }; + + const read = jest.fn( + (l: LocationSpec): Promise => + Promise.resolve({ + type: 'error', + error: new Error('Failed to load location'), + location: l, + }), + ); + + await expect( + processor.processEntity(entity, location, () => {}, read), + ).rejects.toThrow('Failed to read location: Failed to load location'); + }); + + it('should throw errors if location read has wrong type', async () => { + entity.metadata.annotations = { + 'backstage.io/definition-at-location': 'wrong', + }; + + const read = jest.fn( + (l: LocationSpec): Promise => + Promise.resolve({ + type: 'location', + optional: false, + location: l, + }), + ); + + await expect( + processor.processEntity(entity, location, () => {}, read), + ).rejects.toThrow( + `Only supports location processor results of type 'data', but got 'location'`, + ); + }); +}); diff --git a/plugins/catalog-backend/src/ingestion/processors/ApiDefinitionAtLocationProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/ApiDefinitionAtLocationProcessor.ts new file mode 100644 index 0000000000..c359e78292 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/ApiDefinitionAtLocationProcessor.ts @@ -0,0 +1,70 @@ +/* + * 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 { ApiEntity, Entity, LocationSpec } from '@backstage/catalog-model'; +import { + LocationProcessor, + LocationProcessorEmit, + LocationProcessorRead, +} from './types'; + +const DEFINITION_AT_LOCATION_ANNOTATION = 'backstage.io/definition-at-location'; + +export class ApiDefinitionAtLocationProcessor implements LocationProcessor { + async processEntity( + entity: Entity, + _location: LocationSpec, + _emit: LocationProcessorEmit, + read: LocationProcessorRead, + ): Promise { + if ( + entity.kind !== 'API' || + !entity.metadata.annotations || + !entity.metadata.annotations[DEFINITION_AT_LOCATION_ANNOTATION] + ) { + return entity; + } + + const reference = + entity.metadata.annotations[DEFINITION_AT_LOCATION_ANNOTATION]; + const { type, target } = extractReference(reference); + const result = await read({ type, target }); + + if (result.type === 'error') { + throw new Error(`Failed to read location: ${result.error.message}`); + } + + if (result.type !== 'data') { + throw new Error( + `Only supports location processor results of type 'data', but got '${result.type}'`, + ); + } + + const definition = result.data.toString(); + const apiEntity = entity as ApiEntity; + apiEntity.spec.definition = definition; + + return entity; + } +} + +function extractReference(reference: string): { type: string; target: string } { + const delimiterIndex = reference.indexOf(':'); + const type = reference.slice(0, delimiterIndex); + const target = reference.slice(delimiterIndex + 1); + + return { type, target }; +} diff --git a/plugins/catalog-backend/src/ingestion/processors/types.ts b/plugins/catalog-backend/src/ingestion/processors/types.ts index c8f3f6f482..3aaa2d21f9 100644 --- a/plugins/catalog-backend/src/ingestion/processors/types.ts +++ b/plugins/catalog-backend/src/ingestion/processors/types.ts @@ -50,6 +50,7 @@ export type LocationProcessor = { * * @param entity The entity to process * @param location The location that the entity came from + * @param read Reads the contents of a location * @param emit A sink for auxiliary items resulting from the processing * @returns The same entity or a modifid version of it */ @@ -57,6 +58,7 @@ export type LocationProcessor = { entity: Entity, location: LocationSpec, emit: LocationProcessorEmit, + read: LocationProcessorRead, ): Promise; /** @@ -107,3 +109,7 @@ export type LocationProcessorResult = | LocationProcessorDataResult | LocationProcessorEntityResult | LocationProcessorErrorResult; + +export type LocationProcessorRead = ( + location: LocationSpec, +) => Promise; From 0e76ab787334e50ca924a3f6b1681a24215edd42 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 24 Sep 2020 23:43:08 +0200 Subject: [PATCH 79/85] backend: add and use for external URL discovery --- packages/backend/src/index.ts | 8 +++++--- packages/backend/src/plugins/auth.ts | 12 +++++++----- packages/backend/src/plugins/proxy.ts | 11 ++++++----- packages/backend/src/types.ts | 2 ++ plugins/auth-backend/src/service/router.ts | 11 ++++++----- .../auth-backend/src/service/standaloneServer.ts | 3 +++ plugins/proxy-backend/src/service/router.test.ts | 8 ++++++-- plugins/proxy-backend/src/service/router.ts | 14 ++++++-------- .../proxy-backend/src/service/standaloneServer.ts | 4 +++- 9 files changed, 44 insertions(+), 29 deletions(-) diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 9d4a738ecf..16cb35cade 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -30,6 +30,7 @@ import { getRootLogger, useHotMemoize, notFoundHandler, + SingleHostDiscovery, } from '@backstage/backend-common'; import { ConfigReader, AppConfig } from '@backstage/config'; import healthcheck from './plugins/healthcheck'; @@ -59,7 +60,8 @@ function makeCreateEnv(loadedConfigs: AppConfig[]) { }, }, ); - return { logger, database, config }; + const discovery = SingleHostDiscovery.fromConfig(config); + return { logger, database, config, discovery }; }; } @@ -85,11 +87,11 @@ async function main() { apiRouter.use('/rollbar', await rollbar(rollbarEnv)); apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv)); apiRouter.use('/sentry', await sentry(sentryEnv)); - apiRouter.use('/auth', await auth(authEnv, '/api/auth')); + apiRouter.use('/auth', await auth(authEnv)); apiRouter.use('/identity', await identity(identityEnv)); apiRouter.use('/techdocs', await techdocs(techdocsEnv)); apiRouter.use('/kubernetes', await kubernetes(kubernetesEnv)); - apiRouter.use('/proxy', await proxy(proxyEnv, '/api/proxy')); + apiRouter.use('/proxy', await proxy(proxyEnv)); apiRouter.use('/graphql', await graphql(graphqlEnv)); apiRouter.use(notFoundHandler()); diff --git a/packages/backend/src/plugins/auth.ts b/packages/backend/src/plugins/auth.ts index 60baaff6b7..913c8b783d 100644 --- a/packages/backend/src/plugins/auth.ts +++ b/packages/backend/src/plugins/auth.ts @@ -17,9 +17,11 @@ import { createRouter } from '@backstage/plugin-auth-backend'; import { PluginEnvironment } from '../types'; -export default async function createPlugin( - { logger, database, config }: PluginEnvironment, - basePath: string, -) { - return await createRouter({ logger, config, database, basePath }); +export default async function createPlugin({ + logger, + database, + config, + discovery, +}: PluginEnvironment) { + return await createRouter({ logger, config, database, discovery }); } diff --git a/packages/backend/src/plugins/proxy.ts b/packages/backend/src/plugins/proxy.ts index e96acf69d3..867e742dc0 100644 --- a/packages/backend/src/plugins/proxy.ts +++ b/packages/backend/src/plugins/proxy.ts @@ -18,9 +18,10 @@ import { createRouter } from '@backstage/plugin-proxy-backend'; import { PluginEnvironment } from '../types'; -export default async function createPlugin( - { logger, config }: PluginEnvironment, - pathPrefix: string, -) { - return await createRouter({ logger, config, pathPrefix }); +export default async function createPlugin({ + logger, + config, + discovery, +}: PluginEnvironment) { + return await createRouter({ logger, config, discovery }); } diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index f7df3d05c6..3709fc8d9a 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -17,9 +17,11 @@ import Knex from 'knex'; import { Logger } from 'winston'; import { Config } from '@backstage/config'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; export type PluginEnvironment = { logger: Logger; database: Knex; config: Config; + discovery: PluginEndpointDiscovery; }; diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index e70e687879..56ac587126 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -22,13 +22,16 @@ import { Logger } from 'winston'; import { createAuthProviderRouter } from '../providers'; import { Config } from '@backstage/config'; import { DatabaseKeyStore, TokenFactory, createOidcRouter } from '../identity'; -import { NotFoundError } from '@backstage/backend-common'; +import { + NotFoundError, + PluginEndpointDiscovery, +} from '@backstage/backend-common'; export interface RouterOptions { logger: Logger; database: Knex; config: Config; - basePath?: string; + discovery: PluginEndpointDiscovery; } export async function createRouter( @@ -38,9 +41,7 @@ export async function createRouter( const logger = options.logger.child({ plugin: 'auth' }); const appUrl = options.config.getString('app.baseUrl'); - const backendUrl = options.config.getString('backend.baseUrl'); - // TODO(Rugvip): Replace with service discovery of external URL - const authUrl = backendUrl + (options.basePath ?? '/api/auth'); + const authUrl = await options.discovery.getExternalBaseUrl('auth'); const keyDurationSeconds = 3600; diff --git a/plugins/auth-backend/src/service/standaloneServer.ts b/plugins/auth-backend/src/service/standaloneServer.ts index dc91c92631..71771340a5 100644 --- a/plugins/auth-backend/src/service/standaloneServer.ts +++ b/plugins/auth-backend/src/service/standaloneServer.ts @@ -23,6 +23,7 @@ import { createServiceBuilder, useHotMemoize, loadBackendConfig, + SingleHostDiscovery, } from '@backstage/backend-common'; export interface ServerOptions { @@ -34,6 +35,7 @@ export async function startStandaloneServer( ): Promise { const logger = options.logger.child({ service: 'auth-backend' }); const config = ConfigReader.fromConfigs(await loadBackendConfig()); + const discovery = SingleHostDiscovery.fromConfig(config); const database = useHotMemoize(module, () => { const knex = Knex({ @@ -52,6 +54,7 @@ export async function startStandaloneServer( logger, config, database, + discovery, }); const service = createServiceBuilder(module) diff --git a/plugins/proxy-backend/src/service/router.test.ts b/plugins/proxy-backend/src/service/router.test.ts index c7d7fff1d4..07737e9ea8 100644 --- a/plugins/proxy-backend/src/service/router.test.ts +++ b/plugins/proxy-backend/src/service/router.test.ts @@ -17,16 +17,20 @@ import { createRouter } from './router'; import * as winston from 'winston'; import { ConfigReader } from '@backstage/config'; -import { loadBackendConfig } from '@backstage/backend-common'; +import { + loadBackendConfig, + SingleHostDiscovery, +} from '@backstage/backend-common'; describe('createRouter', () => { it('works', async () => { const logger = winston.createLogger(); const config = ConfigReader.fromConfigs(await loadBackendConfig()); + const discovery = SingleHostDiscovery.fromConfig(config); const router = await createRouter({ config, logger, - pathPrefix: '/proxy', + discovery, }); expect(router).toBeDefined(); }); diff --git a/plugins/proxy-backend/src/service/router.ts b/plugins/proxy-backend/src/service/router.ts index 7cd5f37598..8cec31719c 100644 --- a/plugins/proxy-backend/src/service/router.ts +++ b/plugins/proxy-backend/src/service/router.ts @@ -23,12 +23,12 @@ import createProxyMiddleware, { } from 'http-proxy-middleware'; import { Logger } from 'winston'; import http from 'http'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; export interface RouterOptions { logger: Logger; config: Config; - // The URL path prefix that the router itself is mounted as, commonly "/proxy" - pathPrefix: string; + discovery: PluginEndpointDiscovery; } export interface ProxyConfig extends ProxyMiddlewareConfig { @@ -76,16 +76,14 @@ export async function createRouter( ): Promise { const router = Router(); + const externalUrl = await options.discovery.getExternalBaseUrl('proxy'); + const { pathname: pathPrefix } = new URL(externalUrl); + const proxyConfig = options.config.getOptional('proxy') ?? {}; Object.entries(proxyConfig).forEach(([route, proxyRouteConfig]) => { router.use( route, - buildMiddleware( - options.pathPrefix, - options.logger, - route, - proxyRouteConfig, - ), + buildMiddleware(pathPrefix, options.logger, route, proxyRouteConfig), ); }); diff --git a/plugins/proxy-backend/src/service/standaloneServer.ts b/plugins/proxy-backend/src/service/standaloneServer.ts index 68e9ade183..83980729d8 100644 --- a/plugins/proxy-backend/src/service/standaloneServer.ts +++ b/plugins/proxy-backend/src/service/standaloneServer.ts @@ -17,6 +17,7 @@ import { createServiceBuilder, loadBackendConfig, + SingleHostDiscovery, } from '@backstage/backend-common'; import { Server } from 'http'; import { Logger } from 'winston'; @@ -37,10 +38,11 @@ export async function startStandaloneServer( logger.debug('Creating application...'); const config = ConfigReader.fromConfigs(await loadBackendConfig()); + const discovery = SingleHostDiscovery.fromConfig(config); const router = await createRouter({ config, logger, - pathPrefix: '/proxy', + discovery, }); const service = createServiceBuilder(module) .enableCors({ origin: 'http://localhost:3000' }) From 2a443ce68de13c92c5eaff347cb44f403e9e38f1 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 24 Sep 2020 23:59:38 +0200 Subject: [PATCH 80/85] techdocs-backend: use endpoint discovery to look up catalog url --- packages/backend/src/plugins/techdocs.ts | 2 ++ .../techdocs-backend/src/service/router.ts | 25 +++++++++++++------ .../src/service/standaloneServer.ts | 7 +++++- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/packages/backend/src/plugins/techdocs.ts b/packages/backend/src/plugins/techdocs.ts index 58dca83b43..9f66cbe2ff 100644 --- a/packages/backend/src/plugins/techdocs.ts +++ b/packages/backend/src/plugins/techdocs.ts @@ -29,6 +29,7 @@ import Docker from 'dockerode'; export default async function createPlugin({ logger, config, + discovery, }: PluginEnvironment) { const generators = new Generators(); const techdocsGenerator = new TechdocsGenerator(logger, config); @@ -51,5 +52,6 @@ export default async function createPlugin({ dockerClient, logger, config, + discovery, }); } diff --git a/plugins/techdocs-backend/src/service/router.ts b/plugins/techdocs-backend/src/service/router.ts index 14820da4e9..bd9e71cb8b 100644 --- a/plugins/techdocs-backend/src/service/router.ts +++ b/plugins/techdocs-backend/src/service/router.ts @@ -26,7 +26,10 @@ import { PublisherBase, LocalPublish, } from '../techdocs'; -import { resolvePackagePath } from '@backstage/backend-common'; +import { + PluginEndpointDiscovery, + resolvePackagePath, +} from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; import { DocsBuilder } from './helpers'; @@ -35,6 +38,7 @@ type RouterOptions = { generators: GeneratorBuilder; publisher: PublisherBase; logger: Logger; + discovery: PluginEndpointDiscovery; database?: Knex; // TODO: Make database required when we're implementing database stuff. config: Config; dockerClient: Docker; @@ -52,20 +56,25 @@ export async function createRouter({ config, dockerClient, logger, + discovery, }: RouterOptions): Promise { const router = Router(); router.get('/docs/:kind/:namespace/:name/*', async (req, res) => { - const baseUrl = config.getString('backend.baseUrl'); const storageUrl = config.getString('techdocs.storageUrl'); const { kind, namespace, name } = req.params; - const entity = (await ( - await fetch( - `${baseUrl}/api/catalog/entities/by-name/${kind}/${namespace}/${name}`, - ) - ).json()) as Entity; + const catalogUrl = await discovery.getBaseUrl('catalog'); + const triple = [kind, namespace, name].map(encodeURIComponent).join('/'); + + const catalogRes = await fetch(`${catalogUrl}/entities/by-name/${triple}`); + if (!catalogRes.ok) { + catalogRes.body.pipe(res.status(catalogRes.status)); + return; + } + + const entity: Entity = await catalogRes.json(); const docsBuilder = new DocsBuilder({ preparers, @@ -80,7 +89,7 @@ export async function createRouter({ await docsBuilder.build(); } - return res.redirect(`${storageUrl}${req.path.replace('/docs', '')}`); + res.redirect(`${storageUrl}${req.path.replace('/docs', '')}`); }); if (publisher instanceof LocalPublish) { diff --git a/plugins/techdocs-backend/src/service/standaloneServer.ts b/plugins/techdocs-backend/src/service/standaloneServer.ts index 7a39742b80..79e8b0b945 100644 --- a/plugins/techdocs-backend/src/service/standaloneServer.ts +++ b/plugins/techdocs-backend/src/service/standaloneServer.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { createServiceBuilder } from '@backstage/backend-common'; +import { + createServiceBuilder, + SingleHostDiscovery, +} from '@backstage/backend-common'; import { Server } from 'http'; import { Logger } from 'winston'; import { createRouter } from './router'; @@ -39,6 +42,7 @@ export async function startStandaloneServer( ): Promise { const logger = options.logger.child({ service: 'techdocs-backend' }); const config = ConfigReader.fromConfigs([]); + const discovery = SingleHostDiscovery.fromConfig(config); logger.debug('Creating application...'); const preparers = new Preparers(); @@ -61,6 +65,7 @@ export async function startStandaloneServer( publisher, dockerClient, config, + discovery, }); const service = createServiceBuilder(module) .enableCors({ origin: 'http://localhost:3000' }) From aa970ab49b136509f197df921ad245be80a6ca50 Mon Sep 17 00:00:00 2001 From: "J Shamsul Bahri (jibone))" Date: Fri, 25 Sep 2020 17:02:17 +0800 Subject: [PATCH 81/85] rename SamlAuthConnector to DirectAuthConnector --- .../core-api/src/apis/implementations/auth/saml/SamlAuth.ts | 4 ++-- .../{SamlAuthConnector.ts => DirectAuthConnector.ts} | 6 +++--- packages/core-api/src/lib/AuthConnector/index.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) rename packages/core-api/src/lib/AuthConnector/{SamlAuthConnector.ts => DirectAuthConnector.ts} (94%) diff --git a/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts b/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts index dbd9368842..973b402756 100644 --- a/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts @@ -15,7 +15,7 @@ */ import SamlIcon from '@material-ui/icons/AcUnit'; -import { SamlAuthConnector } from '../../../../lib/AuthConnector'; +import { DirectAuthConnector } from '../../../../lib/AuthConnector'; import { SessionManager } from '../../../../lib/AuthSessionManager/types'; import { Observable } from '../../../../types'; import { @@ -57,7 +57,7 @@ class SamlAuth implements ProfileInfoApi, BackstageIdentityApi, SessionApi { environment = 'development', provider = DEFAULT_PROVIDER, }: CreateOptions) { - const connector = new SamlAuthConnector({ + const connector = new DirectAuthConnector({ discoveryApi, environment, provider, diff --git a/packages/core-api/src/lib/AuthConnector/SamlAuthConnector.ts b/packages/core-api/src/lib/AuthConnector/DirectAuthConnector.ts similarity index 94% rename from packages/core-api/src/lib/AuthConnector/SamlAuthConnector.ts rename to packages/core-api/src/lib/AuthConnector/DirectAuthConnector.ts index e8ab412756..517bf82ae7 100644 --- a/packages/core-api/src/lib/AuthConnector/SamlAuthConnector.ts +++ b/packages/core-api/src/lib/AuthConnector/DirectAuthConnector.ts @@ -27,13 +27,13 @@ type Options = { provider: AuthProvider & { id: string }; }; -export type SamlResponse = { +export type DirectAuthResponse = { userId: string; profile: ProfileInfo; backstageIdentity: BackstageIdentity; }; -export class SamlAuthConnector { +export class DirectAuthConnector { private readonly discoveryApi: DiscoveryApi; private readonly environment: string | undefined; private readonly provider: AuthProvider & { id: string }; @@ -46,7 +46,7 @@ export class SamlAuthConnector { this.provider = provider; } - async createSession(): Promise { + async createSession(): Promise { const popupUrl = await this.buildUrl('/start'); const payload = await showLoginPopup({ url: popupUrl, diff --git a/packages/core-api/src/lib/AuthConnector/index.ts b/packages/core-api/src/lib/AuthConnector/index.ts index 84f5067443..388619e2c1 100644 --- a/packages/core-api/src/lib/AuthConnector/index.ts +++ b/packages/core-api/src/lib/AuthConnector/index.ts @@ -15,5 +15,5 @@ */ export { DefaultAuthConnector } from './DefaultAuthConnector'; -export { SamlAuthConnector } from './SamlAuthConnector'; +export { DirectAuthConnector } from './DirectAuthConnector'; export * from './types'; From 4e5dd5f506ddbca42914c15862452b389a12138d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 25 Sep 2020 11:03:20 +0200 Subject: [PATCH 82/85] CHANGELOG: add entry for backend service discovery --- CHANGELOG.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d360dd995d..0d6ac3a063 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,13 +8,27 @@ If you encounter issues while upgrading to a newer version, don't hesitate to re > Collect changes for the next release below +### Backend (example-backend, or backends created with @backstage/create-app) + +- The default mount point for backend plugins have been changed to `/api`. These changes are done in the backend package itself, so it is recommended that you sync up existing backend packages with this new pattern. [#2562](https://github.com/spotify/backstage/pull/2562) +- A service discovery mechanism for backend plugins has been added, and is now a requirement for several backend plugins. See [packages/backend/src/index.ts](./packages/backend/src/index.ts) for how to set it up using `SingleHostDiscovery` from `@backstage/backend-common`. Note that the default base path for plugins is set to `/api` to that change, but it can be set to use the old behavior via the `basePath` option. [#2600](https://github.com/spotify/backstage/pull/2600) + ### @backstage/core - Renamed `SessionStateApi` to `SessionApi` and `logout` to `signOut`. Custom implementations of the `SingInPage` app-component will need to rename their `logout` function. The different auth provider items for the `UserSettingsMenu` have been consolidated into a single `ProviderSettingsItem`, meaning you need to replace existing usages of `OAuthProviderSettings` and `OIDCProviderSettings`. [#2555](https://github.com/spotify/backstage/pull/2555). ### @backstage/auth-backend -- The default mount path of backend plugins was changed to `/api/:pluginId`, and as part of that it was needed to enable configuration of the base path of the auth backend, so that it can construct redirect URLs correctly. The default base path is `/api/auth`, but you need to set this to `/auth` if you want to keep using the old path. Note that you will also need to reconfigure any allowed redirect URLs to include `/api` if you switch to the new recommended pattern. [#2562](https://github.com/spotify/backstage/pull/2562) +- The default mount path of backend plugins was changed to `/api/:pluginId`, and as part of that it was needed to enable configuration of the base path of the auth backend, so that it can construct redirect URLs correctly. Note that you will also need to reconfigure any allowed redirect URLs to include `/api` if you switch to the new recommended pattern. [#2562](https://github.com/spotify/backstage/pull/2562) +- The auth backend now requires an implementation of `PluginEndpointDiscovery` from `@backstage/backend-common` to be passed in as `discovery`. See the changes to `@backstage/backend`. + +### @backstage/proxy-backend + +- The proxy backend now requires an implementation of `PluginEndpointDiscovery` from `@backstage/backend-common` to be passed in as `discovery`. See the changes to `@backstage/backend`. + +### @backstage/techdocs-backend + +- The TechDocs backend now requires an implementation of `PluginEndpointDiscovery` from `@backstage/backend-common` to be passed in as `discovery`. See the changes to `@backstage/backend`. ## v0.1.1-alpha.22 From 95fb7b5e4e59e05861c1deb4052132bc75ed9899 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20=C3=85lund?= Date: Fri, 25 Sep 2020 11:50:37 +0200 Subject: [PATCH 83/85] docs: update roadmap (#2602) --- docs/overview/roadmap.md | 44 ++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/docs/overview/roadmap.md b/docs/overview/roadmap.md index d6c1d2d641..c6b822ad62 100644 --- a/docs/overview/roadmap.md +++ b/docs/overview/roadmap.md @@ -27,10 +27,11 @@ We have divided the project into three high-level _phases_: With a single catalog, Backstage makes it easy for a team to manage ten services — and makes it possible for your company to manage thousands of them. -- 🐇 **Phase 3:** Ecosystem (later) - Everyone's infrastructure stack is - different. By fostering a vibrant community of contributors we hope to provide - an ecosystem of Open Source plugins/integrations that allows you to pick the - tools that match your stack. +- 🐇 **Phase 3:** Ecosystem (ongoing, see + [Plugin Marketplace](https://backstage.io/plugins)) - Everyone's + infrastructure stack is different. By fostering a vibrant community of + contributors we hope to provide an ecosystem of Open Source + plugins/integrations that allows you to pick the tools that match your stack. ## Detailed roadmap @@ -53,24 +54,24 @@ guidelines to get started. it much easier to see how a plugin can be built that integrates with the Backstage Service Catalog. +- **[Kubernetes support](https://github.com/spotify/backstage/milestone/20)** - + Native support for Kubernetes, making it easier for developers to see and + manage their services running in k8s. + +- **[Helm charts](https://github.com/spotify/backstage/issues/2540)** - Provide + Helm charts for easy deployments of Backstage and its subsystems on + Kubernetes. + +- **[Backstage platform is stable](https://github.com/spotify/backstage/milestone/19)** - + The platform APIs and features are stable and can be depended on for + production use. After this plugins will require little to no maintenance. + - **Backstage Design System** - By providing design guidelines for common plugin layouts together, rich set of reusable UI components ([Storybook](https://backstage.io/storybook)) and Figma design resources. The Design System will make it easy to design and build plugins that are consistent across the platform -- supporting both developers and designers. -- **[TechDocs v1](https://github.com/spotify/backstage/milestone/16)** - Our - docs-like-code feature TechDocs working end to end. - -- **[Initial GraphQL API](https://github.com/spotify/backstage/milestone/13)** - - A GraphQL API will open up the rich metadata provided by Backstage in a single - query. Plugins can easily query this API as well as extend the model where - needed. - -- **Production deployments** - Provide instructions and default configurations - (e.g. through Helm charts) for easy deployments of Backstage and its - subsystems on Kubernetes. - - **Cloud Cost Insights plugin (from Spotify)** - Spotify teams are fully responsible for their own software, including the cost of the cloud resources they use. By making our internal cost insights plugin available as open source @@ -96,10 +97,6 @@ Chances are that someone will jump in and help build it. ### Future work 🔮 -- **[Backstage platform is stable](https://github.com/spotify/backstage/milestone/19)** - - The platform APIs and features are stable and can be depended on for - production use. After this plugins will require little to no maintenance. - - **Deploy a product demo at `demo.backstage.io`** - Deploy a typical Backstage deployment available publicly so that people can click around and get a feel for the product without having to install anything. @@ -118,8 +115,15 @@ Chances are that someone will jump in and help build it. [AWS](https://github.com/spotify/backstage/issues/290), [Azure](https://github.com/spotify/backstage/issues/348) and others. +- **[Initial GraphQL API](https://github.com/spotify/backstage/milestone/13)** - + A GraphQL API will open up the rich metadata provided by Backstage in a single + query. Plugins can easily query this API as well as extend the model where + needed. + ### Completed milestones ✅ +- [Donate Backstage to the CNCF 🎉](https://backstage.io/blog/2020/09/23/backstage-cncf-sandbox) +- [TechDocs v1](https://backstage.io/blog/2020/09/08/announcing-tech-docs) - [Plugin marketplace](https://backstage.io/plugins) - [Improved and move documentation to backstage.io](https://backstage.io/docs/overview/what-is-backstage) - [Backstage Service Catalog (alpha)](https://backstage.io/blog/2020/06/22/backstage-service-catalog-alpha) From 1ae59f084b25dd95136e365e8dce2beaff8ff93a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 25 Sep 2020 11:11:33 +0200 Subject: [PATCH 84/85] create-app: update to use backend discovery --- .../default-app/packages/backend/src/index.ts | 6 ++++-- .../default-app/packages/backend/src/plugins/auth.ts | 3 ++- .../default-app/packages/backend/src/plugins/proxy.ts | 11 ++++++----- .../packages/backend/src/plugins/techdocs.ts | 2 ++ .../default-app/packages/backend/src/types.ts | 2 ++ 5 files changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/create-app/templates/default-app/packages/backend/src/index.ts b/packages/create-app/templates/default-app/packages/backend/src/index.ts index 457b95a7a9..fa058b6f71 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/index.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/index.ts @@ -14,6 +14,7 @@ import { getRootLogger, useHotMemoize, notFoundHandler, + SingleHostDiscovery, } from '@backstage/backend-common'; import { ConfigReader, AppConfig } from '@backstage/config'; import auth from './plugins/auth'; @@ -37,7 +38,8 @@ function makeCreateEnv(loadedConfigs: AppConfig[]) { }, }, ); - return { logger, database, config }; + const discovery = SingleHostDiscovery.fromConfig(config); + return { logger, database, config, discovery }; }; } @@ -59,7 +61,7 @@ async function main() { apiRouter.use('/auth', await auth(authEnv)) apiRouter.use('/identity', await identity(identityEnv)) apiRouter.use('/techdocs', await techdocs(techdocsEnv)) - apiRouter.use('/proxy', await proxy(proxyEnv, '/api/proxy')) + apiRouter.use('/proxy', await proxy(proxyEnv)) apiRouter.use(notFoundHandler()); const service = createServiceBuilder(module) diff --git a/packages/create-app/templates/default-app/packages/backend/src/plugins/auth.ts b/packages/create-app/templates/default-app/packages/backend/src/plugins/auth.ts index 61af87cce0..fe19855d5d 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/plugins/auth.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/plugins/auth.ts @@ -5,6 +5,7 @@ export default async function createPlugin({ logger, database, config, + discovery, }: PluginEnvironment) { - return await createRouter({ logger, config, database }); + return await createRouter({ logger, config, database, discovery }); } diff --git a/packages/create-app/templates/default-app/packages/backend/src/plugins/proxy.ts b/packages/create-app/templates/default-app/packages/backend/src/plugins/proxy.ts index b20265c2cd..388d3fc446 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/plugins/proxy.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/plugins/proxy.ts @@ -2,9 +2,10 @@ import { createRouter } from '@backstage/plugin-proxy-backend'; import { PluginEnvironment } from '../types'; -export default async function createPlugin( - { logger, config }: PluginEnvironment, - pathPrefix: string, -) { - return await createRouter({ logger, config, pathPrefix }); +export default async function createPlugin({ + logger, + config, + discovery, +}: PluginEnvironment) { + return await createRouter({ logger, config, discovery }); } diff --git a/packages/create-app/templates/default-app/packages/backend/src/plugins/techdocs.ts b/packages/create-app/templates/default-app/packages/backend/src/plugins/techdocs.ts index 9c7de3512b..b522992a75 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/plugins/techdocs.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/plugins/techdocs.ts @@ -13,6 +13,7 @@ import Docker from 'dockerode'; export default async function createPlugin({ logger, config, + discovery, }: PluginEnvironment) { const generators = new Generators(); const techdocsGenerator = new TechdocsGenerator(logger, config); @@ -37,5 +38,6 @@ export default async function createPlugin({ dockerClient, logger, config, + discovery, }); } diff --git a/packages/create-app/templates/default-app/packages/backend/src/types.ts b/packages/create-app/templates/default-app/packages/backend/src/types.ts index 570a5fcdb1..d145255390 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/types.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/types.ts @@ -1,9 +1,11 @@ import Knex from 'knex'; import { Logger } from 'winston'; import { Config } from '@backstage/config'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; export type PluginEnvironment = { logger: Logger; database: Knex; config: Config; + discovery: PluginEndpointDiscovery; }; From ad90193d2ac45bf1e0bb16efd2f147ae11af8911 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 25 Sep 2020 12:14:52 +0200 Subject: [PATCH 85/85] fix(backend): make csp configurable to unbreak app-backend served apps not being able to fetch --- app-config.development.yaml | 2 + app-config.yaml | 2 + .../src/service/lib/ServiceBuilderImpl.ts | 43 ++++++++++++++++- .../backend-common/src/service/lib/config.ts | 46 ++++++++++++++++--- .../default-app/app-config.development.yaml | 2 + .../templates/default-app/app-config.yaml.hbs | 2 + 6 files changed, 88 insertions(+), 9 deletions(-) diff --git a/app-config.development.yaml b/app-config.development.yaml index da274ba1a8..817847c6d6 100644 --- a/app-config.development.yaml +++ b/app-config.development.yaml @@ -9,3 +9,5 @@ backend: origin: http://localhost:3000 methods: [GET, POST, PUT, DELETE] credentials: true + csp: + connect-src: ["'self'", 'http:', 'https:'] diff --git a/app-config.yaml b/app-config.yaml index 01925c21dd..a29240fd9c 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -9,6 +9,8 @@ backend: database: client: sqlite3 connection: ':memory:' + csp: + connect-src: ["'self'", 'https:'] # See README.md in the proxy-backend plugin for information on the configuration format proxy: diff --git a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts index 10f9ca4d9b..35298dba6e 100644 --- a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts +++ b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts @@ -31,10 +31,12 @@ import { } from '../../middleware'; import { ServiceBuilder } from '../types'; import { + CspOptions, + HttpsSettings, readBaseOptions, readCorsOptions, + readCspOptions, readHttpsSettings, - HttpsSettings, } from './config'; import { createHttpServer, createHttpsServer } from './hostFactory'; import { metricsHandler } from './metrics'; @@ -42,12 +44,27 @@ import { metricsHandler } from './metrics'; const DEFAULT_PORT = 7000; // '' is express default, which listens to all interfaces const DEFAULT_HOST = ''; +// taken from the helmet source code - don't seem to be exported +const DEFAULT_CSP = { + 'default-src': ["'self'"], + 'base-uri': ["'self'"], + 'block-all-mixed-content': [], + 'font-src': ["'self'", 'https:', 'data:'], + 'frame-ancestors': ["'self'"], + 'img-src': ["'self'", 'data:'], + 'object-src': ["'none'"], + 'script-src': ["'self'"], + 'script-src-attr': ["'none'"], + 'style-src': ["'self'", 'https:', "'unsafe-inline'"], + 'upgrade-insecure-requests': [], +}; export class ServiceBuilderImpl implements ServiceBuilder { private port: number | undefined; private host: string | undefined; private logger: Logger | undefined; private corsOptions: cors.CorsOptions | undefined; + private cspOptions: CspOptions | undefined; private httpsSettings: HttpsSettings | undefined; private enableMetrics: boolean = true; private routers: [string, Router][]; @@ -79,6 +96,11 @@ export class ServiceBuilderImpl implements ServiceBuilder { this.corsOptions = corsOptions; } + const cspOptions = readCspOptions(backendConfig); + if (cspOptions) { + this.cspOptions = cspOptions; + } + const httpsSettings = readHttpsSettings(backendConfig); if (httpsSettings) { this.httpsSettings = httpsSettings; @@ -115,6 +137,11 @@ export class ServiceBuilderImpl implements ServiceBuilder { return this; } + setCsp(options: CspOptions): ServiceBuilder { + this.cspOptions = options; + return this; + } + addRouter(root: string, router: Router): ServiceBuilder { this.routers.push([root, router]); return this; @@ -127,10 +154,20 @@ export class ServiceBuilderImpl implements ServiceBuilder { host, logger, corsOptions, + cspOptions, httpsSettings, } = this.getOptions(); - app.use(helmet()); + app.use( + helmet({ + contentSecurityPolicy: { + directives: { + ...DEFAULT_CSP, + ...cspOptions, + }, + }, + }), + ); if (corsOptions) { app.use(cors(corsOptions)); } @@ -177,6 +214,7 @@ export class ServiceBuilderImpl implements ServiceBuilder { host: string; logger: Logger; corsOptions?: cors.CorsOptions; + cspOptions?: CspOptions; httpsSettings?: HttpsSettings; } { return { @@ -184,6 +222,7 @@ export class ServiceBuilderImpl implements ServiceBuilder { host: this.host ?? DEFAULT_HOST, logger: this.logger ?? getRootLogger(), corsOptions: this.corsOptions, + cspOptions: this.cspOptions, httpsSettings: this.httpsSettings, }; } diff --git a/packages/backend-common/src/service/lib/config.ts b/packages/backend-common/src/service/lib/config.ts index e257bd4111..bfd966b499 100644 --- a/packages/backend-common/src/service/lib/config.ts +++ b/packages/backend-common/src/service/lib/config.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ConfigReader } from '@backstage/config'; +import { Config } from '@backstage/config'; import { CorsOptions } from 'cors'; export type BaseOptions = { @@ -57,6 +57,13 @@ export type CertificateAttributes = { commonName?: string; }; +/** + * A map from CSP directive names to their values. + * + * Added here since helmet doesn't export this type publicly. + */ +export type CspOptions = Record; + /** * Reads some base options out of a config object. * @@ -71,7 +78,7 @@ export type CertificateAttributes = { * } * ``` */ -export function readBaseOptions(config: ConfigReader): BaseOptions { +export function readBaseOptions(config: Config): BaseOptions { if (typeof config.get('listen') === 'string') { // TODO(freben): Expand this to support more addresses and perhaps optional const { host, port } = parseListenAddress(config.getString('listen')); @@ -105,7 +112,7 @@ export function readBaseOptions(config: ConfigReader): BaseOptions { * } * ``` */ -export function readCorsOptions(config: ConfigReader): CorsOptions | undefined { +export function readCorsOptions(config: Config): CorsOptions | undefined { const cc = config.getOptionalConfig('cors'); if (!cc) { return undefined; @@ -123,6 +130,33 @@ export function readCorsOptions(config: ConfigReader): CorsOptions | undefined { }); } +/** + * Attempts to read a CSP options object from the root of a config object. + * + * @param config The root of a backend config object + * @returns A CSP options object, or undefined if not specified + * + * @example + * ```yaml + * backend: + * csp: + * connect-src: ["'self'", 'http:', 'https:'] + * ``` + */ +export function readCspOptions(config: Config): CspOptions | undefined { + const cc = config.getOptionalConfig('csp'); + if (!cc) { + return undefined; + } + + const result: CspOptions = {}; + for (const key of cc.keys()) { + result[key] = cc.getStringArray(key); + } + + return result; +} + /** * Attempts to read a https settings object from the root of a config object. * @@ -138,9 +172,7 @@ export function readCorsOptions(config: ConfigReader): CorsOptions | undefined { * } * ``` */ -export function readHttpsSettings( - config: ConfigReader, -): HttpsSettings | undefined { +export function readHttpsSettings(config: Config): HttpsSettings | undefined { const cc = config.getOptionalConfig('https'); if (!cc) { @@ -157,7 +189,7 @@ export function readHttpsSettings( } function getOptionalStringOrStrings( - config: ConfigReader, + config: Config, key: string, ): string | string[] | undefined { const value = config.getOptional(key); diff --git a/packages/create-app/templates/default-app/app-config.development.yaml b/packages/create-app/templates/default-app/app-config.development.yaml index da274ba1a8..817847c6d6 100644 --- a/packages/create-app/templates/default-app/app-config.development.yaml +++ b/packages/create-app/templates/default-app/app-config.development.yaml @@ -9,3 +9,5 @@ backend: origin: http://localhost:3000 methods: [GET, POST, PUT, DELETE] credentials: true + csp: + connect-src: ["'self'", 'http:', 'https:'] diff --git a/packages/create-app/templates/default-app/app-config.yaml.hbs b/packages/create-app/templates/default-app/app-config.yaml.hbs index 92c517bf93..17e9fa0728 100644 --- a/packages/create-app/templates/default-app/app-config.yaml.hbs +++ b/packages/create-app/templates/default-app/app-config.yaml.hbs @@ -9,6 +9,8 @@ backend: baseUrl: http://localhost:7000 listen: port: 7000 + csp: + connect-src: ["'self'", 'https:'] {{#if dbTypeSqlite}} database: client: sqlite3