From ca61ed0e9d9b7848b73d1041fd301ba78e5c7ef4 Mon Sep 17 00:00:00 2001 From: Taras Date: Tue, 4 Oct 2022 09:52:09 -0400 Subject: [PATCH 01/37] Added platform plugin --- packages/app/package.json | 3 +- packages/app/src/App.tsx | 2 + plugins/platform/.eslintrc.js | 1 + plugins/platform/README.md | 13 +++ plugins/platform/dev/index.tsx | 12 +++ plugins/platform/package.json | 53 +++++++++++ .../ExampleComponent.test.tsx | 32 +++++++ .../ExampleComponent/ExampleComponent.tsx | 38 ++++++++ .../src/components/ExampleComponent/index.ts | 1 + .../ExampleFetchComponent.test.tsx | 25 ++++++ .../ExampleFetchComponent.tsx | 90 +++++++++++++++++++ .../components/ExampleFetchComponent/index.ts | 1 + plugins/platform/src/index.ts | 1 + plugins/platform/src/plugin.test.ts | 7 ++ plugins/platform/src/plugin.ts | 19 ++++ plugins/platform/src/routes.ts | 5 ++ plugins/platform/src/setupTests.ts | 2 + 17 files changed, 304 insertions(+), 1 deletion(-) create mode 100644 plugins/platform/.eslintrc.js create mode 100644 plugins/platform/README.md create mode 100644 plugins/platform/dev/index.tsx create mode 100644 plugins/platform/package.json create mode 100644 plugins/platform/src/components/ExampleComponent/ExampleComponent.test.tsx create mode 100644 plugins/platform/src/components/ExampleComponent/ExampleComponent.tsx create mode 100644 plugins/platform/src/components/ExampleComponent/index.ts create mode 100644 plugins/platform/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx create mode 100644 plugins/platform/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx create mode 100644 plugins/platform/src/components/ExampleFetchComponent/index.ts create mode 100644 plugins/platform/src/index.ts create mode 100644 plugins/platform/src/plugin.test.ts create mode 100644 plugins/platform/src/plugin.ts create mode 100644 plugins/platform/src/routes.ts create mode 100644 plugins/platform/src/setupTests.ts diff --git a/packages/app/package.json b/packages/app/package.json index 905b82b8af..d9d495db3f 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -27,12 +27,13 @@ "@backstage/plugin-search-react": "^0.2.1", "@backstage/plugin-tech-radar": "^0.5.11", "@backstage/plugin-techdocs": "^1.1.0", - "@backstage/plugin-techdocs-react": "^1.0.1", "@backstage/plugin-techdocs-module-addons-contrib": "^1.0.1", + "@backstage/plugin-techdocs-react": "^1.0.1", "@backstage/plugin-user-settings": "^0.4.3", "@backstage/theme": "^0.2.15", "@frontside/backstage-plugin-effection-inspector": "^0.1.0", "@frontside/backstage-plugin-humanitec": "^0.3.0", + "@frontside/plugin-platform": "^0.1.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "history": "^5.0.0", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 01754379e1..c19384a545 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -33,6 +33,7 @@ import { FlatRoutes } from '@backstage/core-app-api'; import { orgPlugin } from '@backstage/plugin-org'; import { InspectorPage } from '@frontside/backstage-plugin-effection-inspector'; import { GraphiQLPage } from '@backstage/plugin-graphiql'; +import { PlatformPage } from '@frontside/plugin-platform'; const app = createApp({ apis, @@ -90,6 +91,7 @@ const routes = ( } /> } /> } /> + }/> ); diff --git a/plugins/platform/.eslintrc.js b/plugins/platform/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/platform/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/platform/README.md b/plugins/platform/README.md new file mode 100644 index 0000000000..319126d862 --- /dev/null +++ b/plugins/platform/README.md @@ -0,0 +1,13 @@ +# platform + +Welcome to the platform 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 [/platform](http://localhost:3000/platform). + +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/plugins/platform/dev/index.tsx b/plugins/platform/dev/index.tsx new file mode 100644 index 0000000000..d708d5f6e6 --- /dev/null +++ b/plugins/platform/dev/index.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { createDevApp } from '@backstage/dev-utils'; +import { platformPlugin, PlatformPage } from '../src/plugin'; + +createDevApp() + .registerPlugin(platformPlugin) + .addPage({ + element: , + title: 'Root Page', + path: '/platform' + }) + .render(); diff --git a/plugins/platform/package.json b/plugins/platform/package.json new file mode 100644 index 0000000000..0206736d99 --- /dev/null +++ b/plugins/platform/package.json @@ -0,0 +1,53 @@ +{ + "name": "@frontside/plugin-platform", + "version": "0.1.0", + "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" + }, + "backstage": { + "role": "frontend-plugin" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/core-components": "^0.9.5", + "@backstage/core-plugin-api": "^1.0.3", + "@backstage/theme": "^0.2.15", + "@material-ui/core": "^4.9.13", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.57", + "react-use": "^17.2.4" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, + "devDependencies": { + "@backstage/cli": "^0.17.2", + "@backstage/core-app-api": "^1.0.3", + "@backstage/dev-utils": "^1.0.1", + "@backstage/test-utils": "^1.1.1", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^12.1.3", + "@testing-library/user-event": "^14.0.0", + "@types/jest": "*", + "@types/node": "*", + "msw": "^0.42.0", + "cross-fetch": "^3.1.5" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/platform/src/components/ExampleComponent/ExampleComponent.test.tsx b/plugins/platform/src/components/ExampleComponent/ExampleComponent.test.tsx new file mode 100644 index 0000000000..95e3fddcaa --- /dev/null +++ b/plugins/platform/src/components/ExampleComponent/ExampleComponent.test.tsx @@ -0,0 +1,32 @@ +import React from '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'; +import { + setupRequestMockHandlers, + renderInTestApp, +} from "@backstage/test-utils"; + +describe('ExampleComponent', () => { + const server = setupServer(); + // Enable sane handlers for network requests + setupRequestMockHandlers(server); + + // setup mock response + beforeEach(() => { + server.use( + rest.get('/*', (_, res, ctx) => res(ctx.status(200), ctx.json({}))), + ); + }); + + it('should render', async () => { + const rendered = await renderInTestApp( + + + , + ); + expect(rendered.getByText('Welcome to platform!')).toBeInTheDocument(); + }); +}); diff --git a/plugins/platform/src/components/ExampleComponent/ExampleComponent.tsx b/plugins/platform/src/components/ExampleComponent/ExampleComponent.tsx new file mode 100644 index 0000000000..7904107ca3 --- /dev/null +++ b/plugins/platform/src/components/ExampleComponent/ExampleComponent.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { Typography, Grid } from '@material-ui/core'; +import { + InfoCard, + Header, + Page, + Content, + ContentHeader, + HeaderLabel, + SupportButton, +} from '@backstage/core-components'; +import { ExampleFetchComponent } from '../ExampleFetchComponent'; + +export const ExampleComponent = () => ( + +
+ + +
+ + + A description of your plugin goes here. + + + + + + All content should be wrapped in a card like this. + + + + + + + + +
+); diff --git a/plugins/platform/src/components/ExampleComponent/index.ts b/plugins/platform/src/components/ExampleComponent/index.ts new file mode 100644 index 0000000000..8b8437521b --- /dev/null +++ b/plugins/platform/src/components/ExampleComponent/index.ts @@ -0,0 +1 @@ +export { ExampleComponent } from './ExampleComponent'; diff --git a/plugins/platform/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx b/plugins/platform/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx new file mode 100644 index 0000000000..95533c899d --- /dev/null +++ b/plugins/platform/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { ExampleFetchComponent } from './ExampleFetchComponent'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; + +describe('ExampleFetchComponent', () => { + const server = setupServer(); + // Enable sane handlers for network requests + setupRequestMockHandlers(server); + + // setup mock response + beforeEach(() => { + 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(); + expect(await rendered.findByTestId('progress')).toBeInTheDocument(); + }); +}); diff --git a/plugins/platform/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx b/plugins/platform/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx new file mode 100644 index 0000000000..8790e3572d --- /dev/null +++ b/plugins/platform/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import { Table, TableColumn, Progress } from '@backstage/core-components'; +import Alert from '@material-ui/lab/Alert'; +import useAsync from 'react-use/lib/useAsync'; + +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 = ({ users }: DenseTableProps) => { + 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 ( + + ); +}; + +export const ExampleFetchComponent = () => { + 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 ; +}; diff --git a/plugins/platform/src/components/ExampleFetchComponent/index.ts b/plugins/platform/src/components/ExampleFetchComponent/index.ts new file mode 100644 index 0000000000..41a43e84f1 --- /dev/null +++ b/plugins/platform/src/components/ExampleFetchComponent/index.ts @@ -0,0 +1 @@ +export { ExampleFetchComponent } from './ExampleFetchComponent'; diff --git a/plugins/platform/src/index.ts b/plugins/platform/src/index.ts new file mode 100644 index 0000000000..ead53f1314 --- /dev/null +++ b/plugins/platform/src/index.ts @@ -0,0 +1 @@ +export { platformPlugin, PlatformPage } from './plugin'; diff --git a/plugins/platform/src/plugin.test.ts b/plugins/platform/src/plugin.test.ts new file mode 100644 index 0000000000..ba829a47f1 --- /dev/null +++ b/plugins/platform/src/plugin.test.ts @@ -0,0 +1,7 @@ +import { platformPlugin } from './plugin'; + +describe('platform', () => { + it('should export plugin', () => { + expect(platformPlugin).toBeDefined(); + }); +}); diff --git a/plugins/platform/src/plugin.ts b/plugins/platform/src/plugin.ts new file mode 100644 index 0000000000..c8fc67de45 --- /dev/null +++ b/plugins/platform/src/plugin.ts @@ -0,0 +1,19 @@ +import { createPlugin, createRoutableExtension } from '@backstage/core-plugin-api'; + +import { rootRouteRef } from './routes'; + +export const platformPlugin = createPlugin({ + id: 'platform', + routes: { + root: rootRouteRef, + }, +}); + +export const PlatformPage = platformPlugin.provide( + createRoutableExtension({ + name: 'PlatformPage', + component: () => + import('./components/ExampleComponent').then(m => m.ExampleComponent), + mountPoint: rootRouteRef, + }), +); diff --git a/plugins/platform/src/routes.ts b/plugins/platform/src/routes.ts new file mode 100644 index 0000000000..d6b3157ddd --- /dev/null +++ b/plugins/platform/src/routes.ts @@ -0,0 +1,5 @@ +import { createRouteRef } from '@backstage/core-plugin-api'; + +export const rootRouteRef = createRouteRef({ + id: 'platform', +}); diff --git a/plugins/platform/src/setupTests.ts b/plugins/platform/src/setupTests.ts new file mode 100644 index 0000000000..48c09b5346 --- /dev/null +++ b/plugins/platform/src/setupTests.ts @@ -0,0 +1,2 @@ +import '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; From 24c5116638b242e850fd5f3393dc9784ab3da84b Mon Sep 17 00:00:00 2001 From: Taras Date: Tue, 4 Oct 2022 09:58:05 -0400 Subject: [PATCH 02/37] Added IDP plugins --- packages/app/package.json | 2 +- packages/app/src/App.tsx | 2 +- plugins/platform-backend/.eslintrc.js | 1 + plugins/platform-backend/README.md | 14 ++++++ plugins/platform-backend/package.json | 44 ++++++++++++++++ plugins/platform-backend/src/index.ts | 17 +++++++ plugins/platform-backend/src/run.ts | 33 ++++++++++++ .../src/service/router.test.ts | 45 +++++++++++++++++ .../platform-backend/src/service/router.ts | 40 +++++++++++++++ .../src/service/standaloneServer.ts | 50 +++++++++++++++++++ plugins/platform-backend/src/setupTests.ts | 17 +++++++ plugins/platform/package.json | 2 +- 12 files changed, 264 insertions(+), 3 deletions(-) create mode 100644 plugins/platform-backend/.eslintrc.js create mode 100644 plugins/platform-backend/README.md create mode 100644 plugins/platform-backend/package.json create mode 100644 plugins/platform-backend/src/index.ts create mode 100644 plugins/platform-backend/src/run.ts create mode 100644 plugins/platform-backend/src/service/router.test.ts create mode 100644 plugins/platform-backend/src/service/router.ts create mode 100644 plugins/platform-backend/src/service/standaloneServer.ts create mode 100644 plugins/platform-backend/src/setupTests.ts diff --git a/packages/app/package.json b/packages/app/package.json index d9d495db3f..4e54fc7d66 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -33,7 +33,7 @@ "@backstage/theme": "^0.2.15", "@frontside/backstage-plugin-effection-inspector": "^0.1.0", "@frontside/backstage-plugin-humanitec": "^0.3.0", - "@frontside/plugin-platform": "^0.1.0", + "@frontside/backstage-plugin-platform": "^0.1.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "history": "^5.0.0", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index c19384a545..7d9067b8ec 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -33,7 +33,7 @@ import { FlatRoutes } from '@backstage/core-app-api'; import { orgPlugin } from '@backstage/plugin-org'; import { InspectorPage } from '@frontside/backstage-plugin-effection-inspector'; import { GraphiQLPage } from '@backstage/plugin-graphiql'; -import { PlatformPage } from '@frontside/plugin-platform'; +import { PlatformPage } from '@frontside/backstage-plugin-platform'; const app = createApp({ apis, diff --git a/plugins/platform-backend/.eslintrc.js b/plugins/platform-backend/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/platform-backend/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/platform-backend/README.md b/plugins/platform-backend/README.md new file mode 100644 index 0000000000..feba2941e5 --- /dev/null +++ b/plugins/platform-backend/README.md @@ -0,0 +1,14 @@ +# platform-backend + +Welcome to the platform-backend 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 [/platform-backend](http://localhost:3000/platform-backend). + +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/plugins/platform-backend/package.json b/plugins/platform-backend/package.json new file mode 100644 index 0000000000..b6076ec81c --- /dev/null +++ b/plugins/platform-backend/package.json @@ -0,0 +1,44 @@ +{ + "name": "@frontside/backstage-plugin-platform-backend", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": true, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/backend-common": "^0.14.0", + "@backstage/config": "^1.0.1", + "@types/express": "*", + "express": "^4.17.1", + "express-promise-router": "^4.1.0", + "winston": "^3.2.1", + "node-fetch": "^2.6.7", + "yn": "^4.0.0" + }, + "devDependencies": { + "@backstage/cli": "^0.17.2", + "@types/supertest": "^2.0.8", + "supertest": "^4.0.2", + "msw": "^0.42.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/platform-backend/src/index.ts b/plugins/platform-backend/src/index.ts new file mode 100644 index 0000000000..ca73cb27ba --- /dev/null +++ b/plugins/platform-backend/src/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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/plugins/platform-backend/src/run.ts b/plugins/platform-backend/src/run.ts new file mode 100644 index 0000000000..0a3ed2b7f0 --- /dev/null +++ b/plugins/platform-backend/src/run.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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) : 7007; +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/plugins/platform-backend/src/service/router.test.ts b/plugins/platform-backend/src/service/router.test.ts new file mode 100644 index 0000000000..8b77a04348 --- /dev/null +++ b/plugins/platform-backend/src/service/router.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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 /health', () => { + it('returns ok', async () => { + const response = await request(app).get('/health'); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ status: 'ok' }); + }); + }); +}); diff --git a/plugins/platform-backend/src/service/router.ts b/plugins/platform-backend/src/service/router.ts new file mode 100644 index 0000000000..9ceaa47627 --- /dev/null +++ b/plugins/platform-backend/src/service/router.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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/plugins/platform-backend/src/service/standaloneServer.ts b/plugins/platform-backend/src/service/standaloneServer.ts new file mode 100644 index 0000000000..e53a6f9160 --- /dev/null +++ b/plugins/platform-backend/src/service/standaloneServer.ts @@ -0,0 +1,50 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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: 'platform-backend-backend' }); + logger.debug('Starting application server...'); + const router = await createRouter({ + logger, + }); + + let service = createServiceBuilder(module) + .setPort(options.port) + .addRouter('/platform-backend', router); + if (options.enableCors) { + service = service.enableCors({ origin: 'http://localhost:3000' }); + } + + return await service.start().catch(err => { + logger.error(err); + process.exit(1); + }); +} + +module.hot?.accept(); diff --git a/plugins/platform-backend/src/setupTests.ts b/plugins/platform-backend/src/setupTests.ts new file mode 100644 index 0000000000..d3232290a7 --- /dev/null +++ b/plugins/platform-backend/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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 {}; diff --git a/plugins/platform/package.json b/plugins/platform/package.json index 0206736d99..8c3ac8a6e4 100644 --- a/plugins/platform/package.json +++ b/plugins/platform/package.json @@ -1,5 +1,5 @@ { - "name": "@frontside/plugin-platform", + "name": "@frontside/backstage-plugin-platform", "version": "0.1.0", "main": "src/index.ts", "types": "src/index.ts", From 7f2d1ad540b821aa522fdac3a32e5e93716b5edf Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Wed, 5 Oct 2022 11:23:40 -0500 Subject: [PATCH 03/37] compile executables if they don not exist and report their location --- packages/backend/.eslintrc.js | 6 +- packages/backend/package.json | 1 + packages/backend/src/index.ts | 3 + packages/backend/src/plugins/ldp.ts | 10 ++ plugins/platform-backend/.eslintrc.js | 6 +- plugins/platform-backend/cli/main.ts | 1 + plugins/platform-backend/package.json | 3 +- plugins/platform-backend/src/executables.ts | 110 ++++++++++++++++++ .../platform-backend/src/service/router.ts | 24 +++- yarn.lock | 64 ++++++++++ 10 files changed, 223 insertions(+), 5 deletions(-) create mode 100644 packages/backend/src/plugins/ldp.ts create mode 100644 plugins/platform-backend/cli/main.ts create mode 100644 plugins/platform-backend/src/executables.ts diff --git a/packages/backend/.eslintrc.js b/packages/backend/.eslintrc.js index e2a53a6ad2..e28be0ab46 100644 --- a/packages/backend/.eslintrc.js +++ b/packages/backend/.eslintrc.js @@ -1 +1,5 @@ -module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { + rules: { + 'prefer-const': 'off' + } +}); diff --git a/packages/backend/package.json b/packages/backend/package.json index e4d49e439b..3135f2478f 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -42,6 +42,7 @@ "@frontside/backstage-plugin-graphql": "^0.4.0", "@frontside/backstage-plugin-incremental-ingestion-backend": "*", "@frontside/backstage-plugin-incremental-ingestion-github": "*", + "@frontside/backstage-plugin-platform-backend": "*", "graphql-modules": "^2.1.0", "@gitbeaker/node": "^34.6.0", "@internal/plugin-healthcheck": "0.1.0", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 38b77e0935..3778d76656 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -33,6 +33,7 @@ import healthcheck from './plugins/healthcheck'; import effectionInspector from './plugins/effection-inspector'; import humanitec from './plugins/humanitec'; import graphql from './plugins/graphql'; +import ldp from './plugins/ldp'; import { PluginEnvironment } from './types'; import { CatalogClient } from '@backstage/catalog-client'; @@ -92,6 +93,7 @@ async function main() { const searchEnv = useHotMemoize(module, () => createEnv('search')); const appEnv = useHotMemoize(module, () => createEnv('app')); const humanitecEnv = useHotMemoize(module, () => createEnv('humanitec')); + const ldpEnv = useHotMemoize(module, () => createEnv('ldp')); const apiRouter = Router(); apiRouter.use('/catalog', await catalog(catalogEnv)); @@ -104,6 +106,7 @@ async function main() { apiRouter.use('/effection-inspector', await effectionInspector(effectionInspectorEnv)); apiRouter.use('/humanitec', await humanitec(humanitecEnv)); apiRouter.use('/graphql', await graphql(graphqlEnv)); + apiRouter.use('/ldp', await ldp(ldpEnv)); apiRouter.use(notFoundHandler()); const service = createServiceBuilder(module) diff --git a/packages/backend/src/plugins/ldp.ts b/packages/backend/src/plugins/ldp.ts new file mode 100644 index 0000000000..12f12e05f8 --- /dev/null +++ b/packages/backend/src/plugins/ldp.ts @@ -0,0 +1,10 @@ +import type { Router } from 'express'; +import { createRouter } from '@frontside/backstage-plugin-platform-backend'; +import { PluginEnvironment } from '../types'; + +export default async function createPlugin({ + logger, + discovery, +}: PluginEnvironment): Promise { + return await createRouter({ logger, discovery }); +} diff --git a/plugins/platform-backend/.eslintrc.js b/plugins/platform-backend/.eslintrc.js index e2a53a6ad2..e28be0ab46 100644 --- a/plugins/platform-backend/.eslintrc.js +++ b/plugins/platform-backend/.eslintrc.js @@ -1 +1,5 @@ -module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { + rules: { + 'prefer-const': 'off' + } +}); diff --git a/plugins/platform-backend/cli/main.ts b/plugins/platform-backend/cli/main.ts new file mode 100644 index 0000000000..464f545123 --- /dev/null +++ b/plugins/platform-backend/cli/main.ts @@ -0,0 +1 @@ +console.log('Hello Backstage!'); diff --git a/plugins/platform-backend/package.json b/plugins/platform-backend/package.json index b6076ec81c..24381fb617 100644 --- a/plugins/platform-backend/package.json +++ b/plugins/platform-backend/package.json @@ -30,7 +30,8 @@ "express-promise-router": "^4.1.0", "winston": "^3.2.1", "node-fetch": "^2.6.7", - "yn": "^4.0.0" + "yn": "^4.0.0", + "node-deno": "^0.0.2" }, "devDependencies": { "@backstage/cli": "^0.17.2", diff --git a/plugins/platform-backend/src/executables.ts b/plugins/platform-backend/src/executables.ts new file mode 100644 index 0000000000..25aa5ba3d8 --- /dev/null +++ b/plugins/platform-backend/src/executables.ts @@ -0,0 +1,110 @@ +import type { Logger } from 'winston'; +import type { CompilationTarget } from 'node-deno'; +import { CompilationTargets, compile } from 'node-deno'; +import { existsSync } from 'fs'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; + +export type Executables = Record; + +export type Executable = { + type: 'error'; + error: Error; +} | { + type: 'failure', + stderr: string; + stdout: string; +} | { + type: 'compiled'; + url: string; + stderr: string; + stdout: string; +} | { + type: 'compiling'; + stderr: string; + stdout: string; +} + +export interface FindOrCreateOptions { + baseURL: string; + logger: Logger; + distDir: string; + executableName: string; + entrypoint: string; +} + +export function findOrCreateExecutables(options: FindOrCreateOptions): Executables { + options.logger.info(`generating executables for ${options.executableName}`); + return CompilationTargets.reduce((executables, target) => { + return { + ...executables, + [target]: findOrCreateExecutable(target, options), + } + }, {}) as Executables; +} + +function findOrCreateExecutable(target: CompilationTarget, options: FindOrCreateOptions): Executable { + let { logger, distDir, executableName, entrypoint, baseURL } = options; + let output = `${distDir}/${executableName}-${target}`; + let url = `${baseURL}/${executableName}-${target}`; + + let executable: Executable = { + type: 'compiling', + stdout: '', + stderr: '', + } + + + if (existsSync(output)) { + logger.info(`found existing executable: ${output}`); + executable = { + type: 'compiled', + url, + stdout: '', + stderr: '', + } + } else { + logger.info(`compiling ${executableName} for ${target}`); + compile({ + target, + output, + entrypoint, + }).then(result => { + let stdio = { + stdout: result.stdout, + stderr: result.stderr, + }; + if (result.code !== 0) { + logger.error(`compilation for ${target} failed: ${stdio.stderr}`); + executable = { + type: 'failure', + ...stdio, + } + } else { + logger.info(`compilation complete: ${output}`); + executable = { + type: 'compiled', + url, + ...stdio, + } + } + }).catch(error => { + logger.error(`compilation error: ${error}`); + executable = { + type: 'error', + error + } + }); + } + + return new Proxy({} as Executable, { + get(_, prop: keyof Executable) { + return executable[prop]; + }, + ownKeys: () => Object.keys(executable), + getOwnPropertyDescriptor: (_, key) => ({ + value: executable[key as keyof Executable], + enumerable: true, + configurable: true, + }) + }); +} diff --git a/plugins/platform-backend/src/service/router.ts b/plugins/platform-backend/src/service/router.ts index 9ceaa47627..c3f13b48f4 100644 --- a/plugins/platform-backend/src/service/router.ts +++ b/plugins/platform-backend/src/service/router.ts @@ -14,19 +14,32 @@ * limitations under the License. */ -import { errorHandler } from '@backstage/backend-common'; + +import { errorHandler, PluginEndpointDiscovery, resolvePackagePath } from '@backstage/backend-common'; import express from 'express'; import Router from 'express-promise-router'; import { Logger } from 'winston'; +import { findOrCreateExecutables } from '../executables'; export interface RouterOptions { logger: Logger; + discovery: PluginEndpointDiscovery; } export async function createRouter( options: RouterOptions, ): Promise { - const { logger } = options; + const { logger, discovery } = options; + + let baseURL = await discovery.getBaseUrl('ldp'); + + let executables = findOrCreateExecutables({ + logger, + distDir: 'dist-bin', + baseURL, + executableName: 'my-ldp', + entrypoint: resolvePackagePath("@frontside/backstage-plugin-platform-backend", "cli", "main.ts"), + }) const router = Router(); router.use(express.json()); @@ -35,6 +48,13 @@ export async function createRouter( logger.info('PONG!'); response.send({ status: 'ok' }); }); + + router.get('/executables', (_, response)=> { + response.send(executables); + }); + + router.use('/executables/dist', express.static('dist-bin')); + router.use(errorHandler()); return router; } diff --git a/yarn.lock b/yarn.lock index 52af7babde..a2d18a3849 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3097,6 +3097,15 @@ chalk "^4.1.2" stacktrace-parser "^0.1.10" +"@effection/main@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@effection/main/-/main-2.1.0.tgz#90a691d1a78e17ec27ba7ff7e5a87894182c2876" + integrity sha512-jnAlVjsLy1feJeNBLHwmA/mDpnoJsYO3gGtAg2XALS4EiIc7nhNDeoj9D6bsBxqUHHEp2FYupYztFK0vU11UFA== + dependencies: + "@effection/core" "2.2.0" + chalk "^4.1.2" + stacktrace-parser "^0.1.10" + "@effection/process@^2.0.4": version "2.1.0" resolved "https://registry.yarnpkg.com/@effection/process/-/process-2.1.0.tgz#de44de2c7078d25bedfaf9d25a0c61c8bfa0a465" @@ -3107,6 +3116,16 @@ effection "2.0.4" shellwords "^0.1.1" +"@effection/process@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@effection/process/-/process-2.1.1.tgz#bf48a884faa06b8004c473065c385ab3939e6439" + integrity sha512-VNRbRCKwbP48iZcDB66pH4oe77dmffdeQVnLHFth2HIPfJisGH0vTE/3aAQ+1KDaJImWXtq4xOTaurjwqYPHEg== + dependencies: + cross-spawn "^7.0.3" + ctrlc-windows "^2.1.0" + effection "2.0.5" + shellwords "^0.1.1" + "@effection/react@^2.1.4": version "2.2.0" resolved "https://registry.yarnpkg.com/@effection/react/-/react-2.2.0.tgz#c64bf7ca42d8dfd68faedf74c0304fe1dd803908" @@ -7178,6 +7197,11 @@ address@^1.0.1, address@^1.1.2: resolved "https://registry.yarnpkg.com/address/-/address-1.2.0.tgz#d352a62c92fee90f89a693eccd2a8b2139ab02d9" integrity sha512-tNEZYz5G/zYunxFm7sfhAxkXEuLj3K6BKwv6ZURlsF6yiUQ65z0Q2wZW9L5cPUl9ocofGvXOdFYbFHp0+6MOig== +adm-zip@^0.5.4: + version "0.5.9" + resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.5.9.tgz#b33691028333821c0cf95c31374c5462f2905a83" + integrity sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg== + agent-base@6, agent-base@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" @@ -9725,6 +9749,11 @@ ctrlc-windows@^2.0.0: "@mapbox/node-pre-gyp" "^1.0.5" neon-cli "^0.8.1" +ctrlc-windows@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ctrlc-windows/-/ctrlc-windows-2.1.0.tgz#f2096a96ac1d03181e0ec808c2c8a67fdc20b300" + integrity sha512-OrX5KI+K+2NMN91QIhYZdW7VDO2YsSdTZW494pA7Nvw/wBdU2hz+MGP006bR978zOTrG6Q8EIeJvLJmLqc6MsQ== + cypress@^7.3.0: version "7.7.0" resolved "https://registry.yarnpkg.com/cypress/-/cypress-7.7.0.tgz#0839ae28e5520536f9667d6c9ae81496b3836e64" @@ -10085,6 +10114,14 @@ delegates@^1.0.0: resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== +deno-bin@^1.26.0: + version "1.26.0" + resolved "https://registry.yarnpkg.com/deno-bin/-/deno-bin-1.26.0.tgz#03158f4d75950866051e81f7a98d30740b51b958" + integrity sha512-4RIh4Igx2F4E4EGttAerFV35QIzyHB9fzw4tTBTEUKHyom/Kuj85WoHDggBvoPliAAd+xDk7fJT9dyLu1w4EpQ== + dependencies: + adm-zip "^0.5.4" + follow-redirects "^1.10.0" + denque@^2.0.1: version "2.1.0" resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" @@ -10456,6 +10493,19 @@ effection@2.0.4, effection@^2.0.0, effection@^2.0.4: "@effection/stream" "2.0.3" "@effection/subscription" "2.0.3" +effection@2.0.5, effection@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/effection/-/effection-2.0.5.tgz#caac782994f8f69644bac3eda32228d8799dd244" + integrity sha512-q+5iex8LMWP3kitokhkCQErDIN1awRHy7MVqsDIweGeTl0rgpX4Y1KDjE4onvnPF9JtyvhOEFpAAUMRnqi0wqg== + dependencies: + "@effection/channel" "2.0.3" + "@effection/core" "2.2.0" + "@effection/events" "2.0.3" + "@effection/fetch" "2.0.4" + "@effection/main" "2.1.0" + "@effection/stream" "2.0.3" + "@effection/subscription" "2.0.3" + electron-to-chromium@^1.4.188: version "1.4.192" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.192.tgz#fac050058b3e0713b401a1088cc579e14c2ab165" @@ -11825,6 +11875,11 @@ follow-redirects@^1.0.0, follow-redirects@^1.14.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5" integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA== +follow-redirects@^1.10.0: + version "1.15.2" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" + integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -16725,6 +16780,15 @@ node-cache@^5.1.2: dependencies: clone "2.x" +node-deno@^0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/node-deno/-/node-deno-0.0.2.tgz#9ece02a018bb50d0ed6b54c04e4c688fa1c8b314" + integrity sha512-uP+ATXzDyfqxzfPR/TPg47n+eCQGYNMJqQs+/mARh2UsQaAn+O7z9yNnkKXqS7rQ2z0Zcg1tzsXYsbCxJeyFxQ== + dependencies: + "@effection/process" "^2.1.1" + deno-bin "^1.26.0" + effection "^2.0.5" + node-domexception@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" From 301c1fd51a043a831088a08cee95c31cc6225810 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Wed, 5 Oct 2022 11:40:03 -0500 Subject: [PATCH 04/37] Make sure that dist url is used for executables --- plugins/platform-backend/src/service/router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/platform-backend/src/service/router.ts b/plugins/platform-backend/src/service/router.ts index c3f13b48f4..c1d99e3556 100644 --- a/plugins/platform-backend/src/service/router.ts +++ b/plugins/platform-backend/src/service/router.ts @@ -36,7 +36,7 @@ export async function createRouter( let executables = findOrCreateExecutables({ logger, distDir: 'dist-bin', - baseURL, + baseURL: `${baseURL}/executables/dist`, executableName: 'my-ldp', entrypoint: resolvePackagePath("@frontside/backstage-plugin-platform-backend", "cli", "main.ts"), }) From 52625c27cd8f566aee052414944de48db0d0bcf4 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Wed, 5 Oct 2022 13:06:58 -0500 Subject: [PATCH 05/37] Use windows .exe extension when checking existing executable --- plugins/platform-backend/src/executables.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/platform-backend/src/executables.ts b/plugins/platform-backend/src/executables.ts index 25aa5ba3d8..bf5aca8b0a 100644 --- a/plugins/platform-backend/src/executables.ts +++ b/plugins/platform-backend/src/executables.ts @@ -53,8 +53,9 @@ function findOrCreateExecutable(target: CompilationTarget, options: FindOrCreate stderr: '', } + let ext = target.includes('windows') ? '.exe' : '' - if (existsSync(output)) { + if (existsSync(`${output}${ext}`)) { logger.info(`found existing executable: ${output}`); executable = { type: 'compiled', From e01948ad6c00721acd19810f2b0dd69bf4b2bf43 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Wed, 5 Oct 2022 16:07:04 -0500 Subject: [PATCH 06/37] Hookup frontend --- packages/backend/src/index.ts | 6 +- .../backend/src/plugins/{ldp.ts => idp.ts} | 0 plugins/platform-backend/src/index.ts | 1 + .../platform-backend/src/service/router.ts | 2 +- plugins/platform/.eslintrc.js | 7 +- plugins/platform/src/api/executables-api.ts | 10 +++ .../ExampleFetchComponent.tsx | 74 ++++--------------- plugins/platform/src/plugin.ts | 19 ++++- 8 files changed, 54 insertions(+), 65 deletions(-) rename packages/backend/src/plugins/{ldp.ts => idp.ts} (100%) create mode 100644 plugins/platform/src/api/executables-api.ts diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 3778d76656..32dca91da2 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -33,7 +33,7 @@ import healthcheck from './plugins/healthcheck'; import effectionInspector from './plugins/effection-inspector'; import humanitec from './plugins/humanitec'; import graphql from './plugins/graphql'; -import ldp from './plugins/ldp'; +import idp from './plugins/idp'; import { PluginEnvironment } from './types'; import { CatalogClient } from '@backstage/catalog-client'; @@ -93,7 +93,7 @@ async function main() { const searchEnv = useHotMemoize(module, () => createEnv('search')); const appEnv = useHotMemoize(module, () => createEnv('app')); const humanitecEnv = useHotMemoize(module, () => createEnv('humanitec')); - const ldpEnv = useHotMemoize(module, () => createEnv('ldp')); + const idpEnv = useHotMemoize(module, () => createEnv('ldp')); const apiRouter = Router(); apiRouter.use('/catalog', await catalog(catalogEnv)); @@ -106,7 +106,7 @@ async function main() { apiRouter.use('/effection-inspector', await effectionInspector(effectionInspectorEnv)); apiRouter.use('/humanitec', await humanitec(humanitecEnv)); apiRouter.use('/graphql', await graphql(graphqlEnv)); - apiRouter.use('/ldp', await ldp(ldpEnv)); + apiRouter.use('/idp', await idp(idpEnv)); apiRouter.use(notFoundHandler()); const service = createServiceBuilder(module) diff --git a/packages/backend/src/plugins/ldp.ts b/packages/backend/src/plugins/idp.ts similarity index 100% rename from packages/backend/src/plugins/ldp.ts rename to packages/backend/src/plugins/idp.ts diff --git a/plugins/platform-backend/src/index.ts b/plugins/platform-backend/src/index.ts index ca73cb27ba..c444fa7a5d 100644 --- a/plugins/platform-backend/src/index.ts +++ b/plugins/platform-backend/src/index.ts @@ -15,3 +15,4 @@ */ export * from './service/router'; +export type { Executables } from './executables'; diff --git a/plugins/platform-backend/src/service/router.ts b/plugins/platform-backend/src/service/router.ts index c1d99e3556..8ff623d5ba 100644 --- a/plugins/platform-backend/src/service/router.ts +++ b/plugins/platform-backend/src/service/router.ts @@ -31,7 +31,7 @@ export async function createRouter( ): Promise { const { logger, discovery } = options; - let baseURL = await discovery.getBaseUrl('ldp'); + let baseURL = await discovery.getBaseUrl('idp'); let executables = findOrCreateExecutables({ logger, diff --git a/plugins/platform/.eslintrc.js b/plugins/platform/.eslintrc.js index e2a53a6ad2..86d582272c 100644 --- a/plugins/platform/.eslintrc.js +++ b/plugins/platform/.eslintrc.js @@ -1 +1,6 @@ -module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { + rules: { + 'no-else-return': 'off', + 'prefer-const': 'off', + } +}); diff --git a/plugins/platform/src/api/executables-api.ts b/plugins/platform/src/api/executables-api.ts new file mode 100644 index 0000000000..13dacbdfd2 --- /dev/null +++ b/plugins/platform/src/api/executables-api.ts @@ -0,0 +1,10 @@ +import type { Executables } from '@frontside/backstage-plugin-platform-backend'; +import { createApiRef } from '@backstage/core-plugin-api'; + +export const executablesApiRef = createApiRef({ + id: 'plugin.platform.executables', +}); + +export interface ExecutablesAPI { + fetchExecutables(): Promise; +} diff --git a/plugins/platform/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx b/plugins/platform/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx index 8790e3572d..e8f8196415 100644 --- a/plugins/platform/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx +++ b/plugins/platform/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx @@ -1,71 +1,31 @@ import React from 'react'; -import { makeStyles } from '@material-ui/core/styles'; import { Table, TableColumn, Progress } from '@backstage/core-components'; import Alert from '@material-ui/lab/Alert'; import useAsync from 'react-use/lib/useAsync'; +import { useApi } from '@backstage/core-plugin-api'; -const useStyles = makeStyles({ - avatar: { - height: 32, - width: 32, - borderRadius: '50%', - }, -}); +import type { Executables } from '@frontside/backstage-plugin-platform-backend'; +import { executablesApiRef } from '../../api/executables-api'; -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 = ({ users }: DenseTableProps) => { - const classes = useStyles(); +export const DenseTable = ({ executables }: { executables: Executables}) => { const columns: TableColumn[] = [ - { title: 'Avatar', field: 'avatar' }, - { title: 'Name', field: 'name' }, - { title: 'Email', field: 'email' }, - { title: 'Nationality', field: 'nationality' }, + { title: 'Architecture', field: 'target' }, + { title: 'Status', field: 'status' }, + { title: 'URL', field: 'url' }, ]; - const data = users.map(user => { + const data = Object.entries(executables).map(([target, executable]) => { return { - avatar: ( - {user.name.first} - ), - name: `${user.name.first} ${user.name.last}`, - email: user.email, - nationality: user.nat, + target, + url: executable.type === 'compiled' ? executable.url : 'N/A', + status: executable.type, }; }); return (
{ }; export const ExampleFetchComponent = () => { - 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; - }, []); + let api = useApi(executablesApiRef); + const { value, loading, error } = useAsync(api.fetchExecutables, []); if (loading) { return ; } else if (error) { return {error.message}; + } else { + return ; } - return ; }; diff --git a/plugins/platform/src/plugin.ts b/plugins/platform/src/plugin.ts index c8fc67de45..071a5b3a94 100644 --- a/plugins/platform/src/plugin.ts +++ b/plugins/platform/src/plugin.ts @@ -1,5 +1,5 @@ -import { createPlugin, createRoutableExtension } from '@backstage/core-plugin-api'; - +import { createApiFactory, createPlugin, discoveryApiRef, createRoutableExtension } from '@backstage/core-plugin-api'; +import { ExecutablesAPI, executablesApiRef } from './api/executables-api' import { rootRouteRef } from './routes'; export const platformPlugin = createPlugin({ @@ -7,6 +7,21 @@ export const platformPlugin = createPlugin({ routes: { root: rootRouteRef, }, + apis: [ + createApiFactory({ + api: executablesApiRef, + deps: { discoveryApi: discoveryApiRef }, + factory({ discoveryApi, }) { + return { + async fetchExecutables() { + let baseUrl = await discoveryApi.getBaseUrl('idp'); + let response = await fetch(`${baseUrl}/executables`); + return await response.json(); + } + } as ExecutablesAPI; + } + }) + ] }); export const PlatformPage = platformPlugin.provide( From f702703477290ea566d2f5cb92ff13864969ad2d Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Wed, 5 Oct 2022 16:26:31 -0500 Subject: [PATCH 07/37] fixup compile errors --- plugins/platform-backend/src/executables.ts | 1 - plugins/platform-backend/src/run.ts | 33 ------------ .../src/service/router.test.ts | 45 ----------------- .../src/service/standaloneServer.ts | 50 ------------------- 4 files changed, 129 deletions(-) delete mode 100644 plugins/platform-backend/src/run.ts delete mode 100644 plugins/platform-backend/src/service/router.test.ts delete mode 100644 plugins/platform-backend/src/service/standaloneServer.ts diff --git a/plugins/platform-backend/src/executables.ts b/plugins/platform-backend/src/executables.ts index bf5aca8b0a..0c5bf02140 100644 --- a/plugins/platform-backend/src/executables.ts +++ b/plugins/platform-backend/src/executables.ts @@ -2,7 +2,6 @@ import type { Logger } from 'winston'; import type { CompilationTarget } from 'node-deno'; import { CompilationTargets, compile } from 'node-deno'; import { existsSync } from 'fs'; -import { PluginEndpointDiscovery } from '@backstage/backend-common'; export type Executables = Record; diff --git a/plugins/platform-backend/src/run.ts b/plugins/platform-backend/src/run.ts deleted file mode 100644 index 0a3ed2b7f0..0000000000 --- a/plugins/platform-backend/src/run.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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) : 7007; -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/plugins/platform-backend/src/service/router.test.ts b/plugins/platform-backend/src/service/router.test.ts deleted file mode 100644 index 8b77a04348..0000000000 --- a/plugins/platform-backend/src/service/router.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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 /health', () => { - it('returns ok', async () => { - const response = await request(app).get('/health'); - - expect(response.status).toEqual(200); - expect(response.body).toEqual({ status: 'ok' }); - }); - }); -}); diff --git a/plugins/platform-backend/src/service/standaloneServer.ts b/plugins/platform-backend/src/service/standaloneServer.ts deleted file mode 100644 index e53a6f9160..0000000000 --- a/plugins/platform-backend/src/service/standaloneServer.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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: 'platform-backend-backend' }); - logger.debug('Starting application server...'); - const router = await createRouter({ - logger, - }); - - let service = createServiceBuilder(module) - .setPort(options.port) - .addRouter('/platform-backend', router); - if (options.enableCors) { - service = service.enableCors({ origin: 'http://localhost:3000' }); - } - - return await service.start().catch(err => { - logger.error(err); - process.exit(1); - }); -} - -module.hot?.accept(); From d4290d9adb2e4ade9b7f2a44685e3194b2c77313 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Fri, 7 Oct 2022 08:56:58 -0500 Subject: [PATCH 08/37] add Curl installer --- packages/backend/.gitignore | 1 + plugins/platform-backend/cli/install.sh | 214 ++++++++++++++++++ .../platform-backend/src/service/router.ts | 9 +- .../ExampleComponent/ExampleComponent.tsx | 2 +- 4 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 packages/backend/.gitignore create mode 100644 plugins/platform-backend/cli/install.sh diff --git a/packages/backend/.gitignore b/packages/backend/.gitignore new file mode 100644 index 0000000000..ff278ebc46 --- /dev/null +++ b/packages/backend/.gitignore @@ -0,0 +1 @@ +/dist-bin/ diff --git a/plugins/platform-backend/cli/install.sh b/plugins/platform-backend/cli/install.sh new file mode 100644 index 0000000000..cf68afa897 --- /dev/null +++ b/plugins/platform-backend/cli/install.sh @@ -0,0 +1,214 @@ +executables_url() { + echo "http://localhost:3000/api/idp/executables" +} +download_url() { + echo "http://localhost:7007/api/idp/executables/dist" +} + +info() { + local action="$1" + local details="$2" + command printf '\033[1;32m%12s\033[0m %s\n' "$action" "$details" 1>&2 +} + +error() { + command printf '\033[1;31mError\033[0m: %s\n\n' "$1" 1>&2 +} + +warning() { + command printf '\033[1;33mWarning\033[0m: %s\n\n' "$1" 1>&2 +} + +request() { + command printf '\033[1m%s\033[0m\n' "$1" 1>&2 +} + +eprintf() { + command printf '%s\n' "$1" 1>&2 +} + +bold() { + command printf '\033[1m%s\033[0m' "$1" +} + +usage() { + cat >&2 </dev/null + +# default to running setup after installing +should_run_setup="true" + +# install to IDP_HOME, defaulting to ~/.volta +install_dir="${IDP_HOME:-"$HOME/.idp"}" + +# parse command line options +while [ $# -gt 0 ] +do + arg="$1" + + case "$arg" in + -h|--help) + usage + exit 0 + ;; + --skip-setup) + shift # shift off the argument + should_run_setup="false" + ;; + *) + error "unknown option: '$arg'" + usage + exit 1 + ;; + esac +done + +install "$install_dir" "$should_run_setup" diff --git a/plugins/platform-backend/src/service/router.ts b/plugins/platform-backend/src/service/router.ts index 8ff623d5ba..16fbee23cb 100644 --- a/plugins/platform-backend/src/service/router.ts +++ b/plugins/platform-backend/src/service/router.ts @@ -20,6 +20,7 @@ import express from 'express'; import Router from 'express-promise-router'; import { Logger } from 'winston'; import { findOrCreateExecutables } from '../executables'; +import { readFile } from 'fs/promises'; export interface RouterOptions { logger: Logger; @@ -37,7 +38,7 @@ export async function createRouter( logger, distDir: 'dist-bin', baseURL: `${baseURL}/executables/dist`, - executableName: 'my-ldp', + executableName: 'my-idp', entrypoint: resolvePackagePath("@frontside/backstage-plugin-platform-backend", "cli", "main.ts"), }) @@ -49,6 +50,12 @@ export async function createRouter( response.send({ status: 'ok' }); }); + router.get('/install.sh', async (_, response) => { + response.setHeader('Content-Type', 'text/plain'); + let installerBytes = await readFile(resolvePackagePath("@frontside/backstage-plugin-platform-backend", "cli", "install.sh")); + response.send(String(installerBytes)); + }); + router.get('/executables', (_, response)=> { response.send(executables); }); diff --git a/plugins/platform/src/components/ExampleComponent/ExampleComponent.tsx b/plugins/platform/src/components/ExampleComponent/ExampleComponent.tsx index 7904107ca3..7981f34952 100644 --- a/plugins/platform/src/components/ExampleComponent/ExampleComponent.tsx +++ b/plugins/platform/src/components/ExampleComponent/ExampleComponent.tsx @@ -25,7 +25,7 @@ export const ExampleComponent = () => ( - All content should be wrapped in a card like this. + curl -sSL http://localhost:7007/api/idp/install.sh | sh From e1df94227a85429cf05afa38f8e43566b55c43e8 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Fri, 7 Oct 2022 09:14:02 -0500 Subject: [PATCH 09/37] remove silly tests --- .../ExampleComponent.test.tsx | 32 ------------------- .../ExampleFetchComponent.test.tsx | 25 --------------- 2 files changed, 57 deletions(-) delete mode 100644 plugins/platform/src/components/ExampleComponent/ExampleComponent.test.tsx delete mode 100644 plugins/platform/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx diff --git a/plugins/platform/src/components/ExampleComponent/ExampleComponent.test.tsx b/plugins/platform/src/components/ExampleComponent/ExampleComponent.test.tsx deleted file mode 100644 index 95e3fddcaa..0000000000 --- a/plugins/platform/src/components/ExampleComponent/ExampleComponent.test.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from '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'; -import { - setupRequestMockHandlers, - renderInTestApp, -} from "@backstage/test-utils"; - -describe('ExampleComponent', () => { - const server = setupServer(); - // Enable sane handlers for network requests - setupRequestMockHandlers(server); - - // setup mock response - beforeEach(() => { - server.use( - rest.get('/*', (_, res, ctx) => res(ctx.status(200), ctx.json({}))), - ); - }); - - it('should render', async () => { - const rendered = await renderInTestApp( - - - , - ); - expect(rendered.getByText('Welcome to platform!')).toBeInTheDocument(); - }); -}); diff --git a/plugins/platform/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx b/plugins/platform/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx deleted file mode 100644 index 95533c899d..0000000000 --- a/plugins/platform/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import { render } from '@testing-library/react'; -import { ExampleFetchComponent } from './ExampleFetchComponent'; -import { rest } from 'msw'; -import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; - -describe('ExampleFetchComponent', () => { - const server = setupServer(); - // Enable sane handlers for network requests - setupRequestMockHandlers(server); - - // setup mock response - beforeEach(() => { - 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(); - expect(await rendered.findByTestId('progress')).toBeInTheDocument(); - }); -}); From 2207f42f6d7c80da0070596e2d5e35df978ec153 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Fri, 7 Oct 2022 09:24:58 -0500 Subject: [PATCH 10/37] Remove last reference to `ldp` --- packages/backend/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 32dca91da2..db6ee815a2 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -93,7 +93,7 @@ async function main() { const searchEnv = useHotMemoize(module, () => createEnv('search')); const appEnv = useHotMemoize(module, () => createEnv('app')); const humanitecEnv = useHotMemoize(module, () => createEnv('humanitec')); - const idpEnv = useHotMemoize(module, () => createEnv('ldp')); + const idpEnv = useHotMemoize(module, () => createEnv('idp')); const apiRouter = Router(); apiRouter.use('/catalog', await catalog(catalogEnv)); From 7edda3aa851f1ae31fcde8c7fc82c09b49454821 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Mon, 10 Oct 2022 08:17:52 -0500 Subject: [PATCH 11/37] Don't call them Example components --- .../AllExecutables.tsx} | 2 +- plugins/platform/src/components/AllExecutables/index.ts | 1 + plugins/platform/src/components/ExampleComponent/index.ts | 1 - .../platform/src/components/ExampleFetchComponent/index.ts | 1 - .../ExampleComponent.tsx => Install/Install.tsx} | 6 +++--- plugins/platform/src/components/Install/index.ts | 1 + plugins/platform/src/plugin.ts | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) rename plugins/platform/src/components/{ExampleFetchComponent/ExampleFetchComponent.tsx => AllExecutables/AllExecutables.tsx} (96%) create mode 100644 plugins/platform/src/components/AllExecutables/index.ts delete mode 100644 plugins/platform/src/components/ExampleComponent/index.ts delete mode 100644 plugins/platform/src/components/ExampleFetchComponent/index.ts rename plugins/platform/src/components/{ExampleComponent/ExampleComponent.tsx => Install/Install.tsx} (87%) create mode 100644 plugins/platform/src/components/Install/index.ts diff --git a/plugins/platform/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx b/plugins/platform/src/components/AllExecutables/AllExecutables.tsx similarity index 96% rename from plugins/platform/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx rename to plugins/platform/src/components/AllExecutables/AllExecutables.tsx index e8f8196415..a85b53e9b6 100644 --- a/plugins/platform/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx +++ b/plugins/platform/src/components/AllExecutables/AllExecutables.tsx @@ -33,7 +33,7 @@ export const DenseTable = ({ executables }: { executables: Executables}) => { ); }; -export const ExampleFetchComponent = () => { +export const AllExecutables = () => { let api = useApi(executablesApiRef); const { value, loading, error } = useAsync(api.fetchExecutables, []); diff --git a/plugins/platform/src/components/AllExecutables/index.ts b/plugins/platform/src/components/AllExecutables/index.ts new file mode 100644 index 0000000000..f376585c64 --- /dev/null +++ b/plugins/platform/src/components/AllExecutables/index.ts @@ -0,0 +1 @@ +export { AllExecutables } from './AllExecutables'; diff --git a/plugins/platform/src/components/ExampleComponent/index.ts b/plugins/platform/src/components/ExampleComponent/index.ts deleted file mode 100644 index 8b8437521b..0000000000 --- a/plugins/platform/src/components/ExampleComponent/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ExampleComponent } from './ExampleComponent'; diff --git a/plugins/platform/src/components/ExampleFetchComponent/index.ts b/plugins/platform/src/components/ExampleFetchComponent/index.ts deleted file mode 100644 index 41a43e84f1..0000000000 --- a/plugins/platform/src/components/ExampleFetchComponent/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ExampleFetchComponent } from './ExampleFetchComponent'; diff --git a/plugins/platform/src/components/ExampleComponent/ExampleComponent.tsx b/plugins/platform/src/components/Install/Install.tsx similarity index 87% rename from plugins/platform/src/components/ExampleComponent/ExampleComponent.tsx rename to plugins/platform/src/components/Install/Install.tsx index 7981f34952..019f4465b6 100644 --- a/plugins/platform/src/components/ExampleComponent/ExampleComponent.tsx +++ b/plugins/platform/src/components/Install/Install.tsx @@ -9,9 +9,9 @@ import { HeaderLabel, SupportButton, } from '@backstage/core-components'; -import { ExampleFetchComponent } from '../ExampleFetchComponent'; +import { AllExecutables } from '../AllExecutables'; -export const ExampleComponent = () => ( +export const Install = () => (
@@ -30,7 +30,7 @@ export const ExampleComponent = () => ( - + diff --git a/plugins/platform/src/components/Install/index.ts b/plugins/platform/src/components/Install/index.ts new file mode 100644 index 0000000000..2c54307650 --- /dev/null +++ b/plugins/platform/src/components/Install/index.ts @@ -0,0 +1 @@ +export { Install } from './Install'; diff --git a/plugins/platform/src/plugin.ts b/plugins/platform/src/plugin.ts index 071a5b3a94..1b196b93dc 100644 --- a/plugins/platform/src/plugin.ts +++ b/plugins/platform/src/plugin.ts @@ -28,7 +28,7 @@ export const PlatformPage = platformPlugin.provide( createRoutableExtension({ name: 'PlatformPage', component: () => - import('./components/ExampleComponent').then(m => m.ExampleComponent), + import('./components/Install').then(m => m.Install), mountPoint: rootRouteRef, }), ); From 16ad87090dc36aac71545e12f46f3d390138c6e0 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Mon, 10 Oct 2022 09:52:44 -0500 Subject: [PATCH 12/37] customize the executable name and URL In order to standup the executable delivery mechanism, we just hard-coded everything to be local development values and a fixed version of the executable name. However, the entire point is that everyone gets their own internal developer platform that is unique to them. We want to be able to customize that experience and also have it work in production environments. This adds the `executableName`, and `appURL` parameter to `createRouter` where you can pass in the specific name that you want your binary package to have. It will then compile it as that executable, and adjust the install script using a nunjucks template. --- packages/backend/src/plugins/idp.ts | 8 ++++++- plugins/platform-backend/cli/install.sh | 23 +++++++++++-------- plugins/platform-backend/package.json | 17 +++++++++----- plugins/platform-backend/src/executables.ts | 12 ++++++---- .../platform-backend/src/service/router.ts | 16 +++++++++---- .../AllExecutables/AllExecutables.tsx | 6 +++-- yarn.lock | 5 ++++ 7 files changed, 60 insertions(+), 27 deletions(-) diff --git a/packages/backend/src/plugins/idp.ts b/packages/backend/src/plugins/idp.ts index 12f12e05f8..fb647729de 100644 --- a/packages/backend/src/plugins/idp.ts +++ b/packages/backend/src/plugins/idp.ts @@ -3,8 +3,14 @@ import { createRouter } from '@frontside/backstage-plugin-platform-backend'; import { PluginEnvironment } from '../types'; export default async function createPlugin({ + config, logger, discovery, }: PluginEnvironment): Promise { - return await createRouter({ logger, discovery }); + return await createRouter({ + executableName: 'idp', + logger, + discovery, + appURL: `${config.getString('app.baseUrl')}/platform`, + }); } diff --git a/plugins/platform-backend/cli/install.sh b/plugins/platform-backend/cli/install.sh index cf68afa897..64ee2d2d02 100644 --- a/plugins/platform-backend/cli/install.sh +++ b/plugins/platform-backend/cli/install.sh @@ -1,8 +1,13 @@ +executable_name() { + echo "{{executableName}}" +} + executables_url() { - echo "http://localhost:3000/api/idp/executables" + echo "{{appURL}}" } -download_url() { - echo "http://localhost:7007/api/idp/executables/dist" + +downloads_url() { + echo "{{downloadsURL}}" } info() { @@ -33,7 +38,7 @@ bold() { usage() { cat >&2 <; +export interface Executables extends Record { + executableName: string; +} export type Executable = { type: 'error'; @@ -24,7 +26,7 @@ export type Executable = { } export interface FindOrCreateOptions { - baseURL: string; + downloadsURL: string; logger: Logger; distDir: string; executableName: string; @@ -38,13 +40,13 @@ export function findOrCreateExecutables(options: FindOrCreateOptions): Executabl ...executables, [target]: findOrCreateExecutable(target, options), } - }, {}) as Executables; + }, { executableName: options.executableName }) as Executables; } function findOrCreateExecutable(target: CompilationTarget, options: FindOrCreateOptions): Executable { - let { logger, distDir, executableName, entrypoint, baseURL } = options; + let { logger, distDir, executableName, entrypoint, downloadsURL } = options; let output = `${distDir}/${executableName}-${target}`; - let url = `${baseURL}/${executableName}-${target}`; + let url = `${downloadsURL}/${executableName}-${target}`; let executable: Executable = { type: 'compiling', diff --git a/plugins/platform-backend/src/service/router.ts b/plugins/platform-backend/src/service/router.ts index 16fbee23cb..05d8dff909 100644 --- a/plugins/platform-backend/src/service/router.ts +++ b/plugins/platform-backend/src/service/router.ts @@ -21,24 +21,28 @@ import Router from 'express-promise-router'; import { Logger } from 'winston'; import { findOrCreateExecutables } from '../executables'; import { readFile } from 'fs/promises'; +import * as nunjucks from 'nunjucks'; export interface RouterOptions { logger: Logger; discovery: PluginEndpointDiscovery; + executableName: string; + appURL: string; } export async function createRouter( options: RouterOptions, ): Promise { - const { logger, discovery } = options; + const { logger, discovery, executableName, appURL } = options; let baseURL = await discovery.getBaseUrl('idp'); + let downloadsURL = `${baseURL}/executables/dist`; let executables = findOrCreateExecutables({ logger, distDir: 'dist-bin', - baseURL: `${baseURL}/executables/dist`, - executableName: 'my-idp', + downloadsURL, + executableName, entrypoint: resolvePackagePath("@frontside/backstage-plugin-platform-backend", "cli", "main.ts"), }) @@ -53,7 +57,11 @@ export async function createRouter( router.get('/install.sh', async (_, response) => { response.setHeader('Content-Type', 'text/plain'); let installerBytes = await readFile(resolvePackagePath("@frontside/backstage-plugin-platform-backend", "cli", "install.sh")); - response.send(String(installerBytes)); + response.send(nunjucks.renderString(String(installerBytes), { + appURL, + downloadsURL, + executableName, + })); }); router.get('/executables', (_, response)=> { diff --git a/plugins/platform/src/components/AllExecutables/AllExecutables.tsx b/plugins/platform/src/components/AllExecutables/AllExecutables.tsx index a85b53e9b6..b2375b9ec8 100644 --- a/plugins/platform/src/components/AllExecutables/AllExecutables.tsx +++ b/plugins/platform/src/components/AllExecutables/AllExecutables.tsx @@ -15,7 +15,9 @@ export const DenseTable = ({ executables }: { executables: Executables}) => { { title: 'URL', field: 'url' }, ]; - const data = Object.entries(executables).map(([target, executable]) => { + let { executableName, ...binaries } = executables; + + const data = Object.entries(binaries).map(([target, executable]) => { return { target, url: executable.type === 'compiled' ? executable.url : 'N/A', @@ -25,7 +27,7 @@ export const DenseTable = ({ executables }: { executables: Executables}) => { return (
Date: Mon, 10 Oct 2022 14:27:02 -0500 Subject: [PATCH 13/37] Add basic help and info commands One of the really nice things about having your own developer platform is that we can configure the compiled binary to talk back directly to the server it was downloaded from. To accomplish this, we "statically" pass the executable name, the backend url, and the platform description as arguments to the compilation. That way we can use them not only to generate help and binary info for diagnostic purposes, but we will obviously need the url to connect to the backstage server. --- plugins/platform-backend/cli/cli.ts | 31 +++++++++++++++++++ plugins/platform-backend/cli/deno.json | 7 +++++ plugins/platform-backend/cli/deps.ts | 2 ++ plugins/platform-backend/cli/main.ts | 17 +++++++++- plugins/platform-backend/package.json | 2 +- plugins/platform-backend/src/executables.ts | 12 +++++-- .../platform-backend/src/service/router.ts | 1 + yarn.lock | 8 ++--- 8 files changed, 72 insertions(+), 8 deletions(-) create mode 100644 plugins/platform-backend/cli/cli.ts create mode 100644 plugins/platform-backend/cli/deno.json create mode 100644 plugins/platform-backend/cli/deps.ts diff --git a/plugins/platform-backend/cli/cli.ts b/plugins/platform-backend/cli/cli.ts new file mode 100644 index 0000000000..9340e0e87d --- /dev/null +++ b/plugins/platform-backend/cli/cli.ts @@ -0,0 +1,31 @@ +import { parse } from "./deps.ts"; + +const usage = (name: string, description: string) => ` +${name}: ${description} + +USAGE: + ${name} COMMAND [OPTIONS] +`; + +export interface CLIOptions { + name: string; + description: string; + apiURL: string; + args: string[]; + target: string; +} + +export async function cli(options: CLIOptions) { + let { apiURL, description, args, name, target } = options; + let flags = parse(args); + let [command] = flags._; + + switch (command) { + case "help": + console.log(usage(name, description)) + break; + default: + console.log(`${name}\n${Array(name.length).fill("=").join('')}\narchitecture: ${target}\nbackstage: ${apiURL}`) + break; + } +} diff --git a/plugins/platform-backend/cli/deno.json b/plugins/platform-backend/cli/deno.json new file mode 100644 index 0000000000..777ab91408 --- /dev/null +++ b/plugins/platform-backend/cli/deno.json @@ -0,0 +1,7 @@ +{ + "lint": { + "rules": { + "exclude": ["prefer-const"] + } + } +} diff --git a/plugins/platform-backend/cli/deps.ts b/plugins/platform-backend/cli/deps.ts new file mode 100644 index 0000000000..2461b5f207 --- /dev/null +++ b/plugins/platform-backend/cli/deps.ts @@ -0,0 +1,2 @@ +export { parse } from "https://deno.land/std@0.159.0/flags/mod.ts"; +export { assert } from "https://deno.land/std@0.159.0/testing/asserts.ts"; diff --git a/plugins/platform-backend/cli/main.ts b/plugins/platform-backend/cli/main.ts index 464f545123..d225bb52ac 100644 --- a/plugins/platform-backend/cli/main.ts +++ b/plugins/platform-backend/cli/main.ts @@ -1 +1,16 @@ -console.log('Hello Backstage!'); +import { assert } from "./deps.ts"; +import { cli } from "./cli.ts"; + +let [name, apiURL, description, ...args] = Deno.args; + +assert(name, "compiled incorrectly - executable name is not defined"); +assert(apiURL, "compiled incorrectly - backstage platform url is not found"); +assert(description, "compiled incorrectly - no platform description defined"); + +await cli({ + name, + description, + apiURL, + args, + target: Deno.build.target, +}).catch((error) => console.error(error)); diff --git a/plugins/platform-backend/package.json b/plugins/platform-backend/package.json index 858207826e..58a7d65e9d 100644 --- a/plugins/platform-backend/package.json +++ b/plugins/platform-backend/package.json @@ -28,7 +28,7 @@ "@types/express": "*", "express": "^4.17.1", "express-promise-router": "^4.1.0", - "node-deno": "^0.0.2", + "node-deno": "^0.0.3", "node-fetch": "^2.6.7", "nunjucks": "^3.2.3", "winston": "^3.2.1", diff --git a/plugins/platform-backend/src/executables.ts b/plugins/platform-backend/src/executables.ts index b95bf2503c..7b4413fa55 100644 --- a/plugins/platform-backend/src/executables.ts +++ b/plugins/platform-backend/src/executables.ts @@ -26,6 +26,7 @@ export type Executable = { } export interface FindOrCreateOptions { + baseURL: string; downloadsURL: string; logger: Logger; distDir: string; @@ -44,7 +45,7 @@ export function findOrCreateExecutables(options: FindOrCreateOptions): Executabl } function findOrCreateExecutable(target: CompilationTarget, options: FindOrCreateOptions): Executable { - let { logger, distDir, executableName, entrypoint, downloadsURL } = options; + let { logger, baseURL, distDir, executableName, entrypoint, downloadsURL } = options; let output = `${distDir}/${executableName}-${target}`; let url = `${downloadsURL}/${executableName}-${target}`; @@ -69,7 +70,14 @@ function findOrCreateExecutable(target: CompilationTarget, options: FindOrCreate compile({ target, output, - entrypoint, + // pass metadata as the first three args of the script + entrypoint: [entrypoint, executableName, baseURL, '"internal developer platform"'], + + // only allow network access back to the backstage server + allowNet: [baseURL], + + // fail immediately if a permission is not present + noPrompt: true, }).then(result => { let stdio = { stdout: result.stdout, diff --git a/plugins/platform-backend/src/service/router.ts b/plugins/platform-backend/src/service/router.ts index 05d8dff909..e065152e55 100644 --- a/plugins/platform-backend/src/service/router.ts +++ b/plugins/platform-backend/src/service/router.ts @@ -41,6 +41,7 @@ export async function createRouter( let executables = findOrCreateExecutables({ logger, distDir: 'dist-bin', + baseURL, downloadsURL, executableName, entrypoint: resolvePackagePath("@frontside/backstage-plugin-platform-backend", "cli", "main.ts"), diff --git a/yarn.lock b/yarn.lock index 9128805dc5..9023420d98 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16785,10 +16785,10 @@ node-cache@^5.1.2: dependencies: clone "2.x" -node-deno@^0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/node-deno/-/node-deno-0.0.2.tgz#9ece02a018bb50d0ed6b54c04e4c688fa1c8b314" - integrity sha512-uP+ATXzDyfqxzfPR/TPg47n+eCQGYNMJqQs+/mARh2UsQaAn+O7z9yNnkKXqS7rQ2z0Zcg1tzsXYsbCxJeyFxQ== +node-deno@^0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/node-deno/-/node-deno-0.0.3.tgz#c2cea2c42ce23daa72bc6fcce7d068c6196febef" + integrity sha512-5mJGsLXEsuTgIHiqKjclPbpkUimqRiuQ1eaQ37oxeeh3bmi7O3jP1C4aU4lpnXDrVf3dfnqzqhyjOMSAZpU1nw== dependencies: "@effection/process" "^2.1.1" deno-bin "^1.26.0" From b8484a5d6767080218da8e8319ffeb3319ed30be Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Wed, 12 Oct 2022 09:41:03 -0500 Subject: [PATCH 14/37] Add info command --- packages/backend/src/plugins/idp.ts | 2 + plugins/platform-backend/cli/cli.ts | 85 +++++++++++-- plugins/platform-backend/cli/deps.ts | 112 ++++++++++++++++++ plugins/platform-backend/cli/dev.ts | 9 ++ .../platform-backend/src/service/router.ts | 17 ++- 5 files changed, 214 insertions(+), 11 deletions(-) create mode 100644 plugins/platform-backend/cli/dev.ts diff --git a/packages/backend/src/plugins/idp.ts b/packages/backend/src/plugins/idp.ts index fb647729de..39d0b43577 100644 --- a/packages/backend/src/plugins/idp.ts +++ b/packages/backend/src/plugins/idp.ts @@ -6,11 +6,13 @@ export default async function createPlugin({ config, logger, discovery, + catalog, }: PluginEnvironment): Promise { return await createRouter({ executableName: 'idp', logger, discovery, appURL: `${config.getString('app.baseUrl')}/platform`, + catalog }); } diff --git a/plugins/platform-backend/cli/cli.ts b/plugins/platform-backend/cli/cli.ts index 9340e0e87d..d0e372737a 100644 --- a/plugins/platform-backend/cli/cli.ts +++ b/plugins/platform-backend/cli/cli.ts @@ -1,10 +1,10 @@ -import { parse } from "./deps.ts"; +import { parse, path, yaml, Entity } from "./deps.ts"; const usage = (name: string, description: string) => ` ${name}: ${description} USAGE: - ${name} COMMAND [OPTIONS] +${name} COMMAND [OPTIONS] `; export interface CLIOptions { @@ -15,17 +15,84 @@ export interface CLIOptions { target: string; } +class MainError extends Error { + name = 'Mainerror'; +} + export async function cli(options: CLIOptions) { let { apiURL, description, args, name, target } = options; + let get = (endpoint: string) => fetch(`${apiURL}/${endpoint}`); let flags = parse(args); let [command] = flags._; - switch (command) { - case "help": - console.log(usage(name, description)) - break; - default: - console.log(`${name}\n${Array(name.length).fill("=").join('')}\narchitecture: ${target}\nbackstage: ${apiURL}`) - break; + try { + switch (command) { + case "info": { + let ref = await findEntityContext(flags); + let response = await get(`components/${ref}/info`); + if (response.ok) { + await Deno.stdout.write(new TextEncoder().encode(await response.text())); + } else { + if (response.status === 404) { + throw new MainError(`unknown component '${ref}'`); + } else { + throw new MainError(`communication error with backstage server: ${response.status} ${response.statusText}`); + } + } + break; + } + case "version": + console.log(`${name}\n${Array(name.length).fill("=").join('')}\narchitecture: ${target}\nbackstage: ${apiURL}`); + break; + case "help": + default: + console.log(usage(name, description)) + break; + } + } catch (error) { + if (error instanceof MainError) { + console.log(error.message); + } else { + throw error; + } + } +} + +async function findEntityContext(flags: ReturnType): Promise { + if (flags.component) { + return flags.component; + } else { + let catalogInfoYaml = await findAndRead("catalog-info.yml", "catalog-info.yaml"); + if (catalogInfoYaml.found) { + let [info] = yaml.parseAll(catalogInfoYaml.content) as Iterable; + if (info && info.metadata?.name) { + return info.metadata.name; + } + } else { + throw new MainError('unable to determine the component. You can set it explicitly by passing the `--component` flag'); + } + } + return ''; +} + +type Find = { + found: false, +} | { + found: true, + content: string; +} +async function findAndRead(...paths: string[]): Promise { + for (let cwd = Deno.cwd(); cwd !== '/'; cwd = path.join(cwd, '..')) { + for (let path of paths) { + try { + let content = new TextDecoder().decode(await Deno.readFile(`${cwd}/${path}`)); + return { found: true, content }; + } catch (error) { + if (!(error instanceof Deno.errors.NotFound)) { + throw error; + } + } + } } + return { found: false }; } diff --git a/plugins/platform-backend/cli/deps.ts b/plugins/platform-backend/cli/deps.ts index 2461b5f207..419af4b2f2 100644 --- a/plugins/platform-backend/cli/deps.ts +++ b/plugins/platform-backend/cli/deps.ts @@ -1,2 +1,114 @@ export { parse } from "https://deno.land/std@0.159.0/flags/mod.ts"; export { assert } from "https://deno.land/std@0.159.0/testing/asserts.ts"; +export * as path from "https://deno.land/std@0.159.0/path/mod.ts"; +export * as yaml from "https://deno.land/std@0.159.0/encoding/yaml.ts"; + + +//we tried to get this from backstage. We really did. +export interface Entity { + /** + * The version of specification format for this particular entity that + * this is written against. + */ + apiVersion: string; + + /** + * The high level entity type being described. + */ + kind: string; + + /** + * Metadata related to the entity. + */ + metadata: EntityMeta; + + /** + * The specification data describing the entity itself. + */ + spec?: Record; + +} + +export interface EntityMeta { + /** + * A globally unique ID for the entity. + * + * This field can not be set by the user at creation time, and the server + * will reject an attempt to do so. The field will be populated in read + * operations. The field can (optionally) be specified when performing + * update or delete operations, but the server is free to reject requests + * that do so in such a way that it breaks semantics. + */ + uid?: string; + + /** + * An opaque string that changes for each update operation to any part of + * the entity, including metadata. + * + * This field can not be set by the user at creation time, and the server + * will reject an attempt to do so. The field will be populated in read + * operations. The field can (optionally) be specified when performing + * update or delete operations, and the server will then reject the + * operation if it does not match the current stored value. + */ + etag?: string; + + /** + * The name of the entity. + * + * Must be unique within the catalog at any given point in time, for any + * given namespace + kind pair. This value is part of the technical + * identifier of the entity, and as such it will appear in URLs, database + * tables, entity references, and similar. It is subject to restrictions + * regarding what characters are allowed. + * + * If you want to use a different, more human readable string with fewer + * restrictions on it in user interfaces, see the `title` field below. + */ + name: string; + + /** + * The namespace that the entity belongs to. + */ + namespace?: string; + + /** + * A display name of the entity, to be presented in user interfaces instead + * of the `name` property above, when available. + * + * This field is sometimes useful when the `name` is cumbersome or ends up + * being perceived as overly technical. The title generally does not have + * as stringent format requirements on it, so it may contain special + * characters and be more explanatory. Do keep it very short though, and + * avoid situations where a title can be confused with the name of another + * entity, or where two entities share a title. + * + * Note that this is only for display purposes, and may be ignored by some + * parts of the code. Entity references still always make use of the `name` + * property, not the title. + */ + title?: string; + + /** + * A short (typically relatively few words, on one line) description of the + * entity. + */ + description?: string; + + /** + * Key/value pairs of identifying information attached to the entity. + */ + labels?: Record; + + /** + * Key/value pairs of non-identifying auxiliary information attached to the + * entity. + */ + annotations?: Record; + + /** + * A list of single-valued strings, to for example classify catalog entities in + * various ways. + */ + tags?: string[]; +} diff --git a/plugins/platform-backend/cli/dev.ts b/plugins/platform-backend/cli/dev.ts new file mode 100644 index 0000000000..4fc475453d --- /dev/null +++ b/plugins/platform-backend/cli/dev.ts @@ -0,0 +1,9 @@ +import { cli } from './cli.ts'; + +await cli({ + name: 'idp', + description: 'internal developer platform ', + apiURL: 'http://localhost:7007/api/idp', + args: Deno.args, + target: Deno.build.target, +}).catch((error) => console.error(error)); diff --git a/plugins/platform-backend/src/service/router.ts b/plugins/platform-backend/src/service/router.ts index e065152e55..962a0d6533 100644 --- a/plugins/platform-backend/src/service/router.ts +++ b/plugins/platform-backend/src/service/router.ts @@ -18,7 +18,8 @@ import { errorHandler, PluginEndpointDiscovery, resolvePackagePath } from '@backstage/backend-common'; import express from 'express'; import Router from 'express-promise-router'; -import { Logger } from 'winston'; +import type { Logger } from 'winston'; +import type { CatalogClient } from '@backstage/catalog-client'; import { findOrCreateExecutables } from '../executables'; import { readFile } from 'fs/promises'; import * as nunjucks from 'nunjucks'; @@ -28,12 +29,13 @@ export interface RouterOptions { discovery: PluginEndpointDiscovery; executableName: string; appURL: string; + catalog: CatalogClient; } export async function createRouter( options: RouterOptions, ): Promise { - const { logger, discovery, executableName, appURL } = options; + const { catalog, logger, discovery, executableName, appURL } = options; let baseURL = await discovery.getBaseUrl('idp'); let downloadsURL = `${baseURL}/executables/dist`; @@ -71,6 +73,17 @@ export async function createRouter( router.use('/executables/dist', express.static('dist-bin')); + router.get('/components/:name/info', async (req, res) => { + let name = req.params.name; + let component = await catalog.getEntityByRef(`component:default/${name}`); + if (component) { + res.send(`${JSON.stringify(component, null, 2)}\n`); + } else { + res.sendStatus(404); + res.send("Not Found"); + } + }) + router.use(errorHandler()); return router; } From dd0a0d1bea9f7d8aab11945eeea533fab989eae5 Mon Sep 17 00:00:00 2001 From: Paul Cowan Date: Wed, 12 Oct 2022 16:09:52 +0100 Subject: [PATCH 15/37] update executable compile permissions --- plugins/platform-backend/src/executables.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/platform-backend/src/executables.ts b/plugins/platform-backend/src/executables.ts index 7b4413fa55..42edc8a868 100644 --- a/plugins/platform-backend/src/executables.ts +++ b/plugins/platform-backend/src/executables.ts @@ -74,7 +74,9 @@ function findOrCreateExecutable(target: CompilationTarget, options: FindOrCreate entrypoint: [entrypoint, executableName, baseURL, '"internal developer platform"'], // only allow network access back to the backstage server - allowNet: [baseURL], + allowNet: [new URL(baseURL).host], + + allowRead: true, // fail immediately if a permission is not present noPrompt: true, From 37f3921aa8a8708742606949c8f370201bbf8e10 Mon Sep 17 00:00:00 2001 From: Paul Cowan Date: Wed, 12 Oct 2022 17:56:50 +0100 Subject: [PATCH 16/37] refactor from switch to cliffy --- plugins/platform-backend/cli/cli.ts | 78 +++++++++---------- plugins/platform-backend/cli/deno.json | 3 + plugins/platform-backend/cli/deps.ts | 4 +- .../platform-backend/cli/{ => tasks}/dev.ts | 2 +- 4 files changed, 44 insertions(+), 43 deletions(-) rename plugins/platform-backend/cli/{ => tasks}/dev.ts (86%) diff --git a/plugins/platform-backend/cli/cli.ts b/plugins/platform-backend/cli/cli.ts index d0e372737a..6247fd274d 100644 --- a/plugins/platform-backend/cli/cli.ts +++ b/plugins/platform-backend/cli/cli.ts @@ -1,4 +1,4 @@ -import { parse, path, yaml, Entity } from "./deps.ts"; +import { parse, path, yaml, Entity, Command } from "./deps.ts"; const usage = (name: string, description: string) => ` ${name}: ${description} @@ -25,53 +25,51 @@ export async function cli(options: CLIOptions) { let flags = parse(args); let [command] = flags._; - try { - switch (command) { - case "info": { - let ref = await findEntityContext(flags); - let response = await get(`components/${ref}/info`); - if (response.ok) { - await Deno.stdout.write(new TextEncoder().encode(await response.text())); + const cmd = new Command() + .name(name) + .version(() => `${name}\n${Array(name.length).fill("=").join('')}\narchitecture: ${target}\nbackstage: ${apiURL}`) + .description(description) + .command('info', 'display info about a backstage component entity.') + .option('-c --component ', 'The backstage component entity') + .action(async ({ component }) => { + let ref = await findEntityContext(component); + let response = await get(`components/${ref}/info`); + if (response.ok) { + await Deno.stdout.write(new TextEncoder().encode(await response.text())); + } else { + if (response.status === 404) { + throw new MainError(`unknown component '${ref}'`); } else { - if (response.status === 404) { - throw new MainError(`unknown component '${ref}'`); - } else { - throw new MainError(`communication error with backstage server: ${response.status} ${response.statusText}`); - } + throw new MainError(`communication error with backstage server: ${response.status} ${response.statusText}`); } - break; } - case "version": - console.log(`${name}\n${Array(name.length).fill("=").join('')}\narchitecture: ${target}\nbackstage: ${apiURL}`); - break; - case "help": - default: - console.log(usage(name, description)) - break; - } - } catch (error) { - if (error instanceof MainError) { - console.log(error.message); - } else { - throw error; + }) + + try { + await cmd.parse(args); + } catch (error) { + if (error instanceof MainError) { + console.log(error.message); + } else { + throw error; + } } - } } -async function findEntityContext(flags: ReturnType): Promise { - if (flags.component) { - return flags.component; - } else { - let catalogInfoYaml = await findAndRead("catalog-info.yml", "catalog-info.yaml"); - if (catalogInfoYaml.found) { - let [info] = yaml.parseAll(catalogInfoYaml.content) as Iterable; - if (info && info.metadata?.name) { - return info.metadata.name; - } - } else { - throw new MainError('unable to determine the component. You can set it explicitly by passing the `--component` flag'); +async function findEntityContext(component?: string): Promise { + if (component) { + return component; + } + let catalogInfoYaml = await findAndRead("catalog-info.yml", "catalog-info.yaml"); + if (catalogInfoYaml.found) { + let [info] = yaml.parseAll(catalogInfoYaml.content) as Iterable; + if (info && info.metadata?.name) { + return info.metadata.name; } + } else { + throw new MainError('unable to determine the component. You can set it explicitly by passing the `--component` flag'); } + return ''; } diff --git a/plugins/platform-backend/cli/deno.json b/plugins/platform-backend/cli/deno.json index 777ab91408..4d7f259c66 100644 --- a/plugins/platform-backend/cli/deno.json +++ b/plugins/platform-backend/cli/deno.json @@ -1,4 +1,7 @@ { + "tasks": { + "dev": "deno run --allow-read --allow-net=localhost:7007 tasks/dev.ts" + }, "lint": { "rules": { "exclude": ["prefer-const"] diff --git a/plugins/platform-backend/cli/deps.ts b/plugins/platform-backend/cli/deps.ts index 419af4b2f2..55c24fb046 100644 --- a/plugins/platform-backend/cli/deps.ts +++ b/plugins/platform-backend/cli/deps.ts @@ -2,9 +2,9 @@ export { parse } from "https://deno.land/std@0.159.0/flags/mod.ts"; export { assert } from "https://deno.land/std@0.159.0/testing/asserts.ts"; export * as path from "https://deno.land/std@0.159.0/path/mod.ts"; export * as yaml from "https://deno.land/std@0.159.0/encoding/yaml.ts"; +export { Command } from "https://deno.land/x/cliffy@v0.25.2/command/mod.ts" - -//we tried to get this from backstage. We really did. +// we tried to get this from backstage. We really did. export interface Entity { /** * The version of specification format for this particular entity that diff --git a/plugins/platform-backend/cli/dev.ts b/plugins/platform-backend/cli/tasks/dev.ts similarity index 86% rename from plugins/platform-backend/cli/dev.ts rename to plugins/platform-backend/cli/tasks/dev.ts index 4fc475453d..1ae450e5f5 100644 --- a/plugins/platform-backend/cli/dev.ts +++ b/plugins/platform-backend/cli/tasks/dev.ts @@ -1,4 +1,4 @@ -import { cli } from './cli.ts'; +import { cli } from '../cli.ts'; await cli({ name: 'idp', From 6ce8b4cf1b9e436d157ed7ef27dc7ce8e91a9bfa Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Wed, 12 Oct 2022 15:50:02 -0500 Subject: [PATCH 17/37] remove `flags` --- plugins/platform-backend/cli/cli.ts | 22 ++++++++++------------ plugins/platform-backend/cli/deps.ts | 1 - 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/plugins/platform-backend/cli/cli.ts b/plugins/platform-backend/cli/cli.ts index 6247fd274d..6c37bcf050 100644 --- a/plugins/platform-backend/cli/cli.ts +++ b/plugins/platform-backend/cli/cli.ts @@ -1,11 +1,4 @@ -import { parse, path, yaml, Entity, Command } from "./deps.ts"; - -const usage = (name: string, description: string) => ` -${name}: ${description} - -USAGE: -${name} COMMAND [OPTIONS] -`; +import { path, yaml, Entity, Command } from "./deps.ts"; export interface CLIOptions { name: string; @@ -22,8 +15,6 @@ class MainError extends Error { export async function cli(options: CLIOptions) { let { apiURL, description, args, name, target } = options; let get = (endpoint: string) => fetch(`${apiURL}/${endpoint}`); - let flags = parse(args); - let [command] = flags._; const cmd = new Command() .name(name) @@ -33,8 +24,15 @@ export async function cli(options: CLIOptions) { .option('-c --component ', 'The backstage component entity') .action(async ({ component }) => { let ref = await findEntityContext(component); - let response = await get(`components/${ref}/info`); - if (response.ok) { + let response: Response; + try { + response = await get(`components/${ref}/info`); + } catch (error) { + throw new MainError(error.message); + } + if (!response) { + throw new MainError(`no response from server`); + } else if (response.ok) { await Deno.stdout.write(new TextEncoder().encode(await response.text())); } else { if (response.status === 404) { diff --git a/plugins/platform-backend/cli/deps.ts b/plugins/platform-backend/cli/deps.ts index 55c24fb046..67dacc7ad7 100644 --- a/plugins/platform-backend/cli/deps.ts +++ b/plugins/platform-backend/cli/deps.ts @@ -1,4 +1,3 @@ -export { parse } from "https://deno.land/std@0.159.0/flags/mod.ts"; export { assert } from "https://deno.land/std@0.159.0/testing/asserts.ts"; export * as path from "https://deno.land/std@0.159.0/path/mod.ts"; export * as yaml from "https://deno.land/std@0.159.0/encoding/yaml.ts"; From 4e3d63f831e67d993aaee3efef95072e60d9c9ea Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Wed, 12 Oct 2022 16:57:20 -0500 Subject: [PATCH 18/37] Make version more legible --- plugins/platform-backend/cli/cli.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/platform-backend/cli/cli.ts b/plugins/platform-backend/cli/cli.ts index 6c37bcf050..8b9ed7b878 100644 --- a/plugins/platform-backend/cli/cli.ts +++ b/plugins/platform-backend/cli/cli.ts @@ -18,7 +18,7 @@ export async function cli(options: CLIOptions) { const cmd = new Command() .name(name) - .version(() => `${name}\n${Array(name.length).fill("=").join('')}\narchitecture: ${target}\nbackstage: ${apiURL}`) + .version(() => `\narchitecture: ${target}\nbackstage: ${apiURL}`) .description(description) .command('info', 'display info about a backstage component entity.') .option('-c --component ', 'The backstage component entity') From 93ae6dab2aab92012da064a6bf909ebada249e82 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Thu, 13 Oct 2022 17:05:35 -0500 Subject: [PATCH 19/37] Add environments call --- packages/backend/package.json | 1 + packages/backend/src/plugins/idp.ts | 6 +- packages/backend/src/types.ts | 2 +- plugins/humanitec-common/src/constants.ts | 2 + plugins/humanitec-common/src/index.ts | 4 +- plugins/humanitec-common/src/platform-api.ts | 45 ++++++++++ plugins/platform-backend/cli/cli.ts | 85 ++++++++++++++----- plugins/platform-backend/cli/deps.ts | 3 +- plugins/platform-backend/cli/tasks/dev.ts | 8 +- plugins/platform-backend/src/index.ts | 1 + .../platform-backend/src/service/router.ts | 29 ++++++- plugins/platform-backend/src/types.ts | 35 ++++++++ 12 files changed, 190 insertions(+), 31 deletions(-) create mode 100644 plugins/humanitec-common/src/constants.ts create mode 100644 plugins/humanitec-common/src/platform-api.ts create mode 100644 plugins/platform-backend/src/types.ts diff --git a/packages/backend/package.json b/packages/backend/package.json index 3135f2478f..0ed2fba03b 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -38,6 +38,7 @@ "@backstage/plugin-techdocs-backend": "^1.1.0", "@frontside/backstage-plugin-effection-inspector-backend": "0.1.1", "@frontside/backstage-plugin-batch-loader": "0.2.1", + "@frontside/backstage-plugin-humanitec-common": "*", "@frontside/backstage-plugin-humanitec-backend": "^0.3.0", "@frontside/backstage-plugin-graphql": "^0.4.0", "@frontside/backstage-plugin-incremental-ingestion-backend": "*", diff --git a/packages/backend/src/plugins/idp.ts b/packages/backend/src/plugins/idp.ts index 39d0b43577..c8690023da 100644 --- a/packages/backend/src/plugins/idp.ts +++ b/packages/backend/src/plugins/idp.ts @@ -1,5 +1,6 @@ import type { Router } from 'express'; import { createRouter } from '@frontside/backstage-plugin-platform-backend'; +import { createHumanitecPlatformApi } from '@frontside/backstage-plugin-humanitec-common'; import { PluginEnvironment } from '../types'; export default async function createPlugin({ @@ -13,6 +14,9 @@ export default async function createPlugin({ logger, discovery, appURL: `${config.getString('app.baseUrl')}/platform`, - catalog + catalog, + platform: createHumanitecPlatformApi({ + token: config.getString('humanitec.token') + }) }); } diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index ea77d252ed..a43f65e6f8 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -23,5 +23,5 @@ export type PluginEnvironment = { tokenManager: TokenManager; scheduler: PluginTaskScheduler; permissions: PermissionEvaluator; - catalog: CatalogClient + catalog: CatalogClient; }; diff --git a/plugins/humanitec-common/src/constants.ts b/plugins/humanitec-common/src/constants.ts new file mode 100644 index 0000000000..e94ede15d3 --- /dev/null +++ b/plugins/humanitec-common/src/constants.ts @@ -0,0 +1,2 @@ +export const HUMANITEC_ORG_ID_ANNOTATION = "humanitec.com/orgId"; +export const HUMANITEC_APP_ID_ANNOTATION = "humanitec.com/appId"; diff --git a/plugins/humanitec-common/src/index.ts b/plugins/humanitec-common/src/index.ts index 51075d7dec..6bae1a7cb4 100644 --- a/plugins/humanitec-common/src/index.ts +++ b/plugins/humanitec-common/src/index.ts @@ -4,4 +4,6 @@ export * from './types/environment'; export * from './types/resources'; export * from './types/runtime'; export * from './clients/fetch-app-info'; -export * from './clients/humanitec'; \ No newline at end of file +export * from './clients/humanitec'; +export * from './platform-api'; +export * from './constants'; diff --git a/plugins/humanitec-common/src/platform-api.ts b/plugins/humanitec-common/src/platform-api.ts new file mode 100644 index 0000000000..0c5653ca4d --- /dev/null +++ b/plugins/humanitec-common/src/platform-api.ts @@ -0,0 +1,45 @@ +import type { PlatformApi } from '@frontside/backstage-plugin-platform-backend'; +import type { Entity } from '@backstage/catalog-model'; + +import { HUMANITEC_APP_ID_ANNOTATION, HUMANITEC_ORG_ID_ANNOTATION } from './constants'; +import { createHumanitecClient } from './clients/humanitec'; + +export function createHumanitecPlatformApi({ token }: { token: string }): PlatformApi { + + return { + async getEnvironments(ref) { + let entity = await ref.load(); + let { appId, orgId } = getHumanitecMetadata(entity); + let client = createHumanitecClient({ token, orgId }); + let environments = await client.getEnvironments(appId); + return { + hasNextPage: false, + hasPreviousPage: false, + beginCursor: '', + endCursor: '', + items: environments.map(env => ({ + cursor: '', + value: { + id: env.id, + name: env.name, + } + })), + }; + } + } +} + +function getHumanitecMetadata(entity: Entity) { + let orgId = entity.metadata.annotations[HUMANITEC_ORG_ID_ANNOTATION]; + let appId = entity.metadata.annotations[HUMANITEC_APP_ID_ANNOTATION]; + + if (!orgId) { + throw new Error(`${entity.kind}:${entity.metadata.name} is not a humanitec entity`); + + } + if (!appId) { + throw new Error(`${entity.kind}:${entity.metadata.name} is not a humanitec entity`); + + } + return { orgId, appId }; +} diff --git a/plugins/platform-backend/cli/cli.ts b/plugins/platform-backend/cli/cli.ts index 8b9ed7b878..db9fa9847f 100644 --- a/plugins/platform-backend/cli/cli.ts +++ b/plugins/platform-backend/cli/cli.ts @@ -1,4 +1,4 @@ -import { path, yaml, Entity, Command } from "./deps.ts"; +import { Command, Entity, path, yaml } from "./deps.ts"; export interface CLIOptions { name: string; @@ -9,7 +9,7 @@ export interface CLIOptions { } class MainError extends Error { - name = 'Mainerror'; + name = "Mainerror"; } export async function cli(options: CLIOptions) { @@ -20,8 +20,11 @@ export async function cli(options: CLIOptions) { .name(name) .version(() => `\narchitecture: ${target}\nbackstage: ${apiURL}`) .description(description) - .command('info', 'display info about a backstage component entity.') - .option('-c --component ', 'The backstage component entity') + .command("info", "display info about a backstage component entity.") + .option( + "-c --component ", + "The backstage component entity", + ) .action(async ({ component }) => { let ref = await findEntityContext(component); let response: Response; @@ -33,55 +36,95 @@ export async function cli(options: CLIOptions) { if (!response) { throw new MainError(`no response from server`); } else if (response.ok) { - await Deno.stdout.write(new TextEncoder().encode(await response.text())); + await Deno.stdout.write( + new TextEncoder().encode(await response.text()), + ); } else { if (response.status === 404) { throw new MainError(`unknown component '${ref}'`); } else { - throw new MainError(`communication error with backstage server: ${response.status} ${response.statusText}`); + throw new MainError( + `communication error with backstage server: ${response.status} ${response.statusText}`, + ); } } }) - - try { - await cmd.parse(args); - } catch (error) { - if (error instanceof MainError) { - console.log(error.message); + .command( + "environments", + "list enviroments in which a component is deployed", + ) + .option("-c --component ", "the component to query") + .action(async ({ component }) => { + let ref = await findEntityContext(component); + let response: Response; + try { + response = await get(`components/${ref}/environments`); + } catch (error) { + throw new MainError(error.message); + } + if (!response) { + throw new MainError(`no response from server`); + } else if (response.ok) { + await Deno.stdout.write( + new TextEncoder().encode(await response.text()), + ); } else { - throw error; + if (response.status === 404) { + throw new MainError(`unknown component '${ref}'`); + } else { + throw new MainError( + `communication error with backstage server: ${response.status} ${response.statusText}`, + ); + } } + }); + + try { + await cmd.parse(args); + } catch (error) { + if (error instanceof MainError) { + console.log(error.message); + } else { + throw error; } + } } async function findEntityContext(component?: string): Promise { if (component) { return component; } - let catalogInfoYaml = await findAndRead("catalog-info.yml", "catalog-info.yaml"); + let catalogInfoYaml = await findAndRead( + "catalog-info.yml", + "catalog-info.yaml", + ); if (catalogInfoYaml.found) { let [info] = yaml.parseAll(catalogInfoYaml.content) as Iterable; if (info && info.metadata?.name) { return info.metadata.name; } } else { - throw new MainError('unable to determine the component. You can set it explicitly by passing the `--component` flag'); + throw new MainError( + "unable to determine the component. You can set it explicitly by passing the `--component` flag", + ); } - return ''; + return ""; } type Find = { - found: false, + found: false; } | { - found: true, + found: true; content: string; -} +}; async function findAndRead(...paths: string[]): Promise { - for (let cwd = Deno.cwd(); cwd !== '/'; cwd = path.join(cwd, '..')) { + for (let cwd = Deno.cwd(); cwd !== "/"; cwd = path.join(cwd, "..")) { for (let path of paths) { try { - let content = new TextDecoder().decode(await Deno.readFile(`${cwd}/${path}`)); + let content = new TextDecoder().decode( + await Deno.readFile(`${cwd}/${path}`), + ); return { found: true, content }; } catch (error) { if (!(error instanceof Deno.errors.NotFound)) { diff --git a/plugins/platform-backend/cli/deps.ts b/plugins/platform-backend/cli/deps.ts index 67dacc7ad7..bb6eb0d5d2 100644 --- a/plugins/platform-backend/cli/deps.ts +++ b/plugins/platform-backend/cli/deps.ts @@ -1,7 +1,7 @@ export { assert } from "https://deno.land/std@0.159.0/testing/asserts.ts"; export * as path from "https://deno.land/std@0.159.0/path/mod.ts"; export * as yaml from "https://deno.land/std@0.159.0/encoding/yaml.ts"; -export { Command } from "https://deno.land/x/cliffy@v0.25.2/command/mod.ts" +export { Command } from "https://deno.land/x/cliffy@v0.25.2/command/mod.ts"; // we tried to get this from backstage. We really did. export interface Entity { @@ -25,7 +25,6 @@ export interface Entity { * The specification data describing the entity itself. */ spec?: Record; - } export interface EntityMeta { diff --git a/plugins/platform-backend/cli/tasks/dev.ts b/plugins/platform-backend/cli/tasks/dev.ts index 1ae450e5f5..313e81051c 100644 --- a/plugins/platform-backend/cli/tasks/dev.ts +++ b/plugins/platform-backend/cli/tasks/dev.ts @@ -1,9 +1,9 @@ -import { cli } from '../cli.ts'; +import { cli } from "../cli.ts"; await cli({ - name: 'idp', - description: 'internal developer platform ', - apiURL: 'http://localhost:7007/api/idp', + name: "idp", + description: "internal developer platform ", + apiURL: "http://localhost:7007/api/idp", args: Deno.args, target: Deno.build.target, }).catch((error) => console.error(error)); diff --git a/plugins/platform-backend/src/index.ts b/plugins/platform-backend/src/index.ts index c444fa7a5d..4ac605f78d 100644 --- a/plugins/platform-backend/src/index.ts +++ b/plugins/platform-backend/src/index.ts @@ -15,4 +15,5 @@ */ export * from './service/router'; +export * from './types'; export type { Executables } from './executables'; diff --git a/plugins/platform-backend/src/service/router.ts b/plugins/platform-backend/src/service/router.ts index 962a0d6533..ea9242587d 100644 --- a/plugins/platform-backend/src/service/router.ts +++ b/plugins/platform-backend/src/service/router.ts @@ -23,6 +23,7 @@ import type { CatalogClient } from '@backstage/catalog-client'; import { findOrCreateExecutables } from '../executables'; import { readFile } from 'fs/promises'; import * as nunjucks from 'nunjucks'; +import { PlatformApi, EntityRef } from '../types'; export interface RouterOptions { logger: Logger; @@ -30,12 +31,13 @@ export interface RouterOptions { executableName: string; appURL: string; catalog: CatalogClient; + platform: PlatformApi; } export async function createRouter( options: RouterOptions, ): Promise { - const { catalog, logger, discovery, executableName, appURL } = options; + const { catalog, logger, discovery, executableName, appURL, platform } = options; let baseURL = await discovery.getBaseUrl('idp'); let downloadsURL = `${baseURL}/executables/dist`; @@ -84,6 +86,31 @@ export async function createRouter( } }) + router.get('/components/:name/environments', async (req, res) => { + let name = req.params.name; + let component = await catalog.getEntityByRef(`component:default/${name}`); + + + if (component) { + let ref: EntityRef = { + ref: `component:default/${name}`, + compound: { + kind: 'component', + name, + namespace: 'default' + }, + load: () => Promise.resolve(component), + }; + let environments = await platform.getEnvironments(ref); + let names = environments.items.map(({ value }) => value.name); + + res.send(`${names.join("\n")}\n`); + } else { + res.sendStatus(404); + res.send("Not Found"); + } + }) + router.use(errorHandler()); return router; } diff --git a/plugins/platform-backend/src/types.ts b/plugins/platform-backend/src/types.ts new file mode 100644 index 0000000000..1526826948 --- /dev/null +++ b/plugins/platform-backend/src/types.ts @@ -0,0 +1,35 @@ +import type { Entity, CompoundEntityRef } from '@backstage/catalog-model'; + +export interface Environment { + id: string; + name: string; +} + +export interface PlatformApi { + getEnvironments(ref: EntityRef, page?: PageSpec): Promise>; +} + +export interface EntityRef { + ref: string; + compound: CompoundEntityRef; + load(): Promise; +} + +export interface Page { + hasNextPage: boolean; + hasPreviousPage: boolean; + beginCursor: string; + endCursor: string; + items: { + cursor: string; + value: T; + }[]; +} + +export type PageSpec = { + count: number; + before: string; +} | { + count: number; + after: string; +} From b56a521005794e9dea4c2fe6ca9220a15cf65482 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 14 Oct 2022 20:49:07 +0100 Subject: [PATCH 20/37] add idp create action (#116) * add scaffold cli action * rename scaffold to create * allow yaml argument to create from file or stdin --- .../cli/.vscode/settings.json | 5 + plugins/platform-backend/cli/cli.ts | 109 +++++++++++++++++- plugins/platform-backend/cli/deno.json | 2 +- plugins/platform-backend/cli/deps.ts | 4 + plugins/platform-backend/package.json | 2 + .../platform-backend/src/service/router.ts | 55 ++++++++- yarn.lock | 5 + 7 files changed, 178 insertions(+), 4 deletions(-) create mode 100644 plugins/platform-backend/cli/.vscode/settings.json diff --git a/plugins/platform-backend/cli/.vscode/settings.json b/plugins/platform-backend/cli/.vscode/settings.json new file mode 100644 index 0000000000..5e27b51276 --- /dev/null +++ b/plugins/platform-backend/cli/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "deno.enable": true, + "deno.lint": false, + "deno.unstable": true +} \ No newline at end of file diff --git a/plugins/platform-backend/cli/cli.ts b/plugins/platform-backend/cli/cli.ts index db9fa9847f..9018c947a9 100644 --- a/plugins/platform-backend/cli/cli.ts +++ b/plugins/platform-backend/cli/cli.ts @@ -1,4 +1,4 @@ -import { Command, Entity, path, yaml } from "./deps.ts"; +import { path, yaml, Entity, Command, EventSource, red, blue, green, format, readAll, assert } from "./deps.ts"; export interface CLIOptions { name: string; @@ -8,14 +8,56 @@ export interface CLIOptions { target: string; } +interface SSEMessage { + type: 'log' | 'completion' | 'error'; + createdAt: string; + body: { + message: string; + error?: { + name: string; + message: string; + } + }; +} + class MainError extends Error { name = "Mainerror"; } +const logTextColors: Record string> = { + 'log': blue, + 'completion': green, + 'error': red +} + +function logSSEMessage(raw: string) { + const message: SSEMessage = JSON.parse(raw) + const color = logTextColors[message.type]; + const timestamp = format(new Date(message.createdAt), 'dd-MM-yyyy:hh:mm'); + const logType = color(`[${message.type.toLocaleUpperCase()} - ${timestamp}]`); + + console.log(`${logType} - ${message.body.message})`); + + if (message.body.error) { + logSSEMessage(JSON.stringify({ + type: "error", + body: { + message: message.body.error.message + }, + createdAt: message.createdAt + })) + } +} + export async function cli(options: CLIOptions) { let { apiURL, description, args, name, target } = options; let get = (endpoint: string) => fetch(`${apiURL}/${endpoint}`); + let post = (endpoint: string, init: Omit) => fetch(`${apiURL}/${endpoint}`, { + method: 'POST', + ...init + }); + const cmd = new Command() .name(name) .version(() => `\narchitecture: ${target}\nbackstage: ${apiURL}`) @@ -77,6 +119,71 @@ export async function cli(options: CLIOptions) { ); } } + }) + .command('create', `create something new from a template. +usage: + +# heredoc +<', 'the scaffolder template', { + default: 'standard-microservice' + }) + .option('-f --file ', `an optional file path to a file containing the template's fields`) + .arguments("[input]") + .action(async ({ template, file }, input) => { + let body: string | undefined; + + if (input === "-") { + const stdinContent = await readAll(Deno.stdin); + body = new TextDecoder().decode(stdinContent); + } else if (file) { + body = Deno.readTextFileSync(file); + } + + assert(body, `no body has been created.`); + + const response = await post(`create/${template}`, { + headers: { + 'Content-Type': 'text/plain', + }, + body + }); + + if (response.status !== 200) { + throw new MainError(`create failed with ${response.status} - ${response.statusText}`) + } + + // deno-lint-ignore no-explicit-any + function sseMessageHandler(event: any) { + if (event.data) { + try { + logSSEMessage(event.data); + } catch (ex) { + console.error(ex); + } + } + } + + const { taskId } = await response.json(); + + const eventSourceUrl = `${apiURL}/tasks/${taskId}/eventstream`; + + const eventSource = new EventSource(eventSourceUrl, { withCredentials: true }); + + eventSource.addEventListener('log', sseMessageHandler); + eventSource.addEventListener('completion', (event: any) => { + sseMessageHandler(event); + + eventSource.close(); + }); + eventSource.addEventListener('error', sseMessageHandler); }); try { diff --git a/plugins/platform-backend/cli/deno.json b/plugins/platform-backend/cli/deno.json index 4d7f259c66..2df05e1196 100644 --- a/plugins/platform-backend/cli/deno.json +++ b/plugins/platform-backend/cli/deno.json @@ -1,6 +1,6 @@ { "tasks": { - "dev": "deno run --allow-read --allow-net=localhost:7007 tasks/dev.ts" + "dev": "deno run --location=http://localhost:7007 --allow-env --allow-read --allow-net=localhost:7007 tasks/dev.ts" }, "lint": { "rules": { diff --git a/plugins/platform-backend/cli/deps.ts b/plugins/platform-backend/cli/deps.ts index bb6eb0d5d2..2f37b60597 100644 --- a/plugins/platform-backend/cli/deps.ts +++ b/plugins/platform-backend/cli/deps.ts @@ -1,7 +1,11 @@ export { assert } from "https://deno.land/std@0.159.0/testing/asserts.ts"; export * as path from "https://deno.land/std@0.159.0/path/mod.ts"; export * as yaml from "https://deno.land/std@0.159.0/encoding/yaml.ts"; +export { format } from "https://deno.land/std@0.159.0/datetime/mod.ts"; export { Command } from "https://deno.land/x/cliffy@v0.25.2/command/mod.ts"; +export { EventSource } from "https://deno.land/x/eventsource@v0.0.2/mod.ts"; +export { red, blue, green } from "https://deno.land/std@0.159.0/fmt/colors.ts" +export { readAll } from "https://deno.land/std@0.159.0/streams/conversion.ts?s=copy"; // we tried to get this from backstage. We really did. export interface Entity { diff --git a/plugins/platform-backend/package.json b/plugins/platform-backend/package.json index 58a7d65e9d..07bfe94814 100644 --- a/plugins/platform-backend/package.json +++ b/plugins/platform-backend/package.json @@ -28,9 +28,11 @@ "@types/express": "*", "express": "^4.17.1", "express-promise-router": "^4.1.0", + "node-fetch-native": "^0.1.7", "node-deno": "^0.0.3", "node-fetch": "^2.6.7", "nunjucks": "^3.2.3", + "request": "^2.88.2", "winston": "^3.2.1", "yn": "^4.0.0" }, diff --git a/plugins/platform-backend/src/service/router.ts b/plugins/platform-backend/src/service/router.ts index ea9242587d..1019f19132 100644 --- a/plugins/platform-backend/src/service/router.ts +++ b/plugins/platform-backend/src/service/router.ts @@ -17,6 +17,7 @@ import { errorHandler, PluginEndpointDiscovery, resolvePackagePath } from '@backstage/backend-common'; import express from 'express'; +import request from 'request'; import Router from 'express-promise-router'; import type { Logger } from 'winston'; import type { CatalogClient } from '@backstage/catalog-client'; @@ -24,6 +25,8 @@ import { findOrCreateExecutables } from '../executables'; import { readFile } from 'fs/promises'; import * as nunjucks from 'nunjucks'; import { PlatformApi, EntityRef } from '../types'; +import fetch from 'node-fetch-native'; +import { load } from 'js-yaml'; export interface RouterOptions { logger: Logger; @@ -34,13 +37,15 @@ export interface RouterOptions { platform: PlatformApi; } + export async function createRouter( options: RouterOptions, -): Promise { + ): Promise { const { catalog, logger, discovery, executableName, appURL, platform } = options; - + let baseURL = await discovery.getBaseUrl('idp'); let downloadsURL = `${baseURL}/executables/dist`; + let scaffolderUrl = `${await discovery.getBaseUrl('scaffolder')}/v2/tasks`; let executables = findOrCreateExecutables({ logger, @@ -52,6 +57,7 @@ export async function createRouter( }) const router = Router(); + router.use(express.text()); router.use(express.json()); router.get('/health', (_, response) => { @@ -111,6 +117,51 @@ export async function createRouter( } }) + router.post('/create/:template', async (req, res) => { + const template = req.params.template; + + logger.info(`creating template ${template}`); + + + try { + const values = load(req.body) as Record; + + const post = await fetch(scaffolderUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + templateRef: `template:default/${template}`, + values: { + ...values + }, + secrets: {} + }) + }); + + if(post.status !== 201) { + throw new Error(`resource not created, ${post.status} - ${post.statusText}`); + } + + const { id } = (await post.json()) as { id: string }; + + res.json({ taskId: id }); + } catch(err) { + logger.error(err); + res.status(500); + res.render('error', { error: err }) + } + }); + + + router.get('/tasks/:taskId/eventstream', (req, res) => { + const { taskId } = req.params; + + const eventStreamUrl = `${scaffolderUrl}/${encodeURIComponent(taskId)}/eventstream` + + req.pipe(request(eventStreamUrl)).pipe(res); + }) router.use(errorHandler()); return router; } diff --git a/yarn.lock b/yarn.lock index 9023420d98..5e31601b63 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16799,6 +16799,11 @@ node-domexception@1.0.0: resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== +node-fetch-native@^0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-0.1.7.tgz#8a8ed0d5d1d1b89d34c6731a9d69d407c09df067" + integrity sha512-hps7dFJM0IEF056JftDSSjWDAwW9v2clwHoUJiHyYgl+ojoqjKyWybljMlpTmlC1O+864qovNlRLyAIjRxu9Ag== + node-fetch@2.6.7, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" From 070e0403dd504aba8ad5a4dee1a04609083a573e Mon Sep 17 00:00:00 2001 From: Taras Date: Sat, 15 Oct 2022 20:38:04 -0400 Subject: [PATCH 21/37] Disable github entity provider --- packages/backend/src/plugins/catalog.ts | 16 ++++++++-------- plugins/platform-backend/src/service/router.ts | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/backend/src/plugins/catalog.ts b/packages/backend/src/plugins/catalog.ts index 61f287cba4..437c52f6e8 100644 --- a/packages/backend/src/plugins/catalog.ts +++ b/packages/backend/src/plugins/catalog.ts @@ -23,14 +23,14 @@ export default async function createPlugin( config: env.config }) - incrementalBuilder.addIncrementalEntityProvider( - githubRepositoryProvider, - { - burstInterval: Duration.fromObject({ seconds: 3 }), - burstLength: Duration.fromObject({ seconds: 3 }), - restLength: Duration.fromObject({ day: 1 }) - } - ) + // incrementalBuilder.addIncrementalEntityProvider( + // githubRepositoryProvider, + // { + // burstInterval: Duration.fromObject({ seconds: 3 }), + // burstLength: Duration.fromObject({ seconds: 3 }), + // restLength: Duration.fromObject({ day: 1 }) + // } + // ) builder.addProcessor(new ScaffolderEntitiesProcessor()); diff --git a/plugins/platform-backend/src/service/router.ts b/plugins/platform-backend/src/service/router.ts index 1019f19132..67cf7d36d3 100644 --- a/plugins/platform-backend/src/service/router.ts +++ b/plugins/platform-backend/src/service/router.ts @@ -115,7 +115,7 @@ export async function createRouter( res.sendStatus(404); res.send("Not Found"); } - }) + }); router.post('/create/:template', async (req, res) => { const template = req.params.template; @@ -154,14 +154,14 @@ export async function createRouter( } }); - router.get('/tasks/:taskId/eventstream', (req, res) => { const { taskId } = req.params; const eventStreamUrl = `${scaffolderUrl}/${encodeURIComponent(taskId)}/eventstream` req.pipe(request(eventStreamUrl)).pipe(res); - }) + }); + router.use(errorHandler()); return router; } From c207184514455dc052a9ffa124250c3d3507e111 Mon Sep 17 00:00:00 2001 From: Taras Date: Sat, 15 Oct 2022 21:55:23 -0400 Subject: [PATCH 22/37] Outputting repositories --- plugins/platform-backend/cli/cli.ts | 34 ++++++++++++- plugins/platform-backend/package.json | 4 ++ .../platform-backend/src/service/router.ts | 22 ++++---- .../src/service/routes/repositories.ts | 51 +++++++++++++++++++ tsconfig.json | 5 +- yarn.lock | 31 ++++++++++- 6 files changed, 133 insertions(+), 14 deletions(-) create mode 100644 plugins/platform-backend/src/service/routes/repositories.ts diff --git a/plugins/platform-backend/cli/cli.ts b/plugins/platform-backend/cli/cli.ts index 9018c947a9..76c534f02e 100644 --- a/plugins/platform-backend/cli/cli.ts +++ b/plugins/platform-backend/cli/cli.ts @@ -51,7 +51,7 @@ function logSSEMessage(raw: string) { export async function cli(options: CLIOptions) { let { apiURL, description, args, name, target } = options; - let get = (endpoint: string) => fetch(`${apiURL}/${endpoint}`); + let get: typeof fetch = (endpoint, init) => fetch(`${apiURL}/${endpoint}`, init); let post = (endpoint: string, init: Omit) => fetch(`${apiURL}/${endpoint}`, { method: 'POST', @@ -91,11 +91,41 @@ export async function cli(options: CLIOptions) { } } }) + .command( + "clone", + "clone a repository associated with a component" + ) + .option( + "-c --component ", + "The backstage component entity", + ) + .action(async ({ component }) => { + if (!component) { + let response: Response; + try { + response = await get(`repositories`, { + headers: { + Accept: 'text/plain' + } + }); + } catch (error) { + throw new MainError(error.message); + } + if (response.ok) { + await Deno.stdout.write( + new TextEncoder().encode(await response.text()) + ) + } + } + }) .command( "environments", "list enviroments in which a component is deployed", ) - .option("-c --component ", "the component to query") + .option( + "-c --component ", + "The backstage component entity", + ) .action(async ({ component }) => { let ref = await findEntityContext(component); let response: Response; diff --git a/plugins/platform-backend/package.json b/plugins/platform-backend/package.json index 07bfe94814..041758baf3 100644 --- a/plugins/platform-backend/package.json +++ b/plugins/platform-backend/package.json @@ -26,8 +26,11 @@ "@backstage/backend-common": "^0.14.0", "@backstage/config": "^1.0.1", "@types/express": "*", + "cli-table3": "^0.6.3", + "chalk": "^4.1.2", "express": "^4.17.1", "express-promise-router": "^4.1.0", + "js-yaml": "^4.1.0", "node-fetch-native": "^0.1.7", "node-deno": "^0.0.3", "node-fetch": "^2.6.7", @@ -40,6 +43,7 @@ "@backstage/cli": "^0.17.2", "@types/nunjucks": "^3.2.1", "@types/supertest": "^2.0.8", + "@types/request": "^2.48.8", "msw": "^0.42.0", "supertest": "^4.0.2" }, diff --git a/plugins/platform-backend/src/service/router.ts b/plugins/platform-backend/src/service/router.ts index 67cf7d36d3..a195a383d4 100644 --- a/plugins/platform-backend/src/service/router.ts +++ b/plugins/platform-backend/src/service/router.ts @@ -14,19 +14,19 @@ * limitations under the License. */ - import { errorHandler, PluginEndpointDiscovery, resolvePackagePath } from '@backstage/backend-common'; +import type { CatalogClient } from '@backstage/catalog-client'; import express from 'express'; -import request from 'request'; import Router from 'express-promise-router'; -import type { Logger } from 'winston'; -import type { CatalogClient } from '@backstage/catalog-client'; -import { findOrCreateExecutables } from '../executables'; import { readFile } from 'fs/promises'; -import * as nunjucks from 'nunjucks'; -import { PlatformApi, EntityRef } from '../types'; -import fetch from 'node-fetch-native'; import { load } from 'js-yaml'; +import fetch from 'node-fetch-native'; +import * as nunjucks from 'nunjucks'; +import request from 'request'; +import type { Logger } from 'winston'; +import { findOrCreateExecutables } from '../executables'; +import { EntityRef, PlatformApi } from '../types'; +import { RepositoriesRoute } from './routes/repositories'; export interface RouterOptions { logger: Logger; @@ -96,7 +96,6 @@ export async function createRouter( let name = req.params.name; let component = await catalog.getEntityByRef(`component:default/${name}`); - if (component) { let ref: EntityRef = { ref: `component:default/${name}`, @@ -122,7 +121,6 @@ export async function createRouter( logger.info(`creating template ${template}`); - try { const values = load(req.body) as Record; @@ -162,6 +160,10 @@ export async function createRouter( req.pipe(request(eventStreamUrl)).pipe(res); }); + router.get('/repositories', RepositoriesRoute({ + catalog + })); + router.use(errorHandler()); return router; } diff --git a/plugins/platform-backend/src/service/routes/repositories.ts b/plugins/platform-backend/src/service/routes/repositories.ts new file mode 100644 index 0000000000..72f39ab64b --- /dev/null +++ b/plugins/platform-backend/src/service/routes/repositories.ts @@ -0,0 +1,51 @@ +import type { CatalogClient } from '@backstage/catalog-client'; +import type { Entity } from '@backstage/catalog-model'; +import { Handler } from 'express'; +import CliTable3 from 'cli-table3'; +import chalk from 'chalk'; + +export const RepositoriesRoute = ({ catalog }: { catalog: CatalogClient }) => { + const repositoriesRoute: Handler = async (req, res) => { + const { items: entities } = await catalog.getEntities(); + + const repositories = entities.flatMap(entity => { + const slug = entity.metadata + && entity.metadata.annotations + && entity.metadata.annotations["github.com/project-slug"]; + if (slug) { + return [{ + componentRef: getComponentRef(entity), + slug, + description: entity.metadata.description, + url: `https://github/${slug}`, + ssh: `git@github.com:${slug}.git`, + https: `https://github/${slug}.git` + }] + } + return []; + }); + + if (req.accepts('json')) { + res.json(repositories); + } else { + const table = new CliTable3({ + head: ['Component', 'Repository URL', 'Description'] + }); + table.push( + ...repositories.map(r => ([r.componentRef, r.url, r.description])) + ) + res.send(`\n${chalk.bold(' 🥁 Available Repositories')}\n${table}`) + } + + return repositoriesRoute; + } + return repositoriesRoute; +} + +function getComponentRef(entity: Entity) { + return [ + entity.kind !== 'Component' ? entity.kind.toLowerCase() : '', + entity.metadata.namespace && entity.metadata.namespace !== 'default' ? entity.metadata.namespace : '', + entity.metadata.name + ].join('') +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index fda6cf4884..d6ce168934 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,10 @@ "plugins/*/dev", "plugins/*/migrations" ], - "exclude": ["node_modules", "packages/graphgen"], + "exclude": [ + "node_modules", + "packages/graphgen" + ], "compilerOptions": { "outDir": "dist-types", "rootDir": "." diff --git a/yarn.lock b/yarn.lock index 5e31601b63..1b243ad729 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6222,6 +6222,11 @@ "@types/node" "*" "@types/responselike" "*" +"@types/caseless@*": + version "0.12.2" + resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8" + integrity sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w== + "@types/connect-history-api-fallback@^1.3.5": version "1.3.5" resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz#d1f7a8a09d0ed5a57aee5ae9c18ab9b803205dae" @@ -6668,6 +6673,16 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/request@^2.48.8": + version "2.48.8" + resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.8.tgz#0b90fde3b655ab50976cb8c5ac00faca22f5a82c" + integrity sha512-whjk1EDJPcAR2kYHRbFl/lKeeKYTi05A15K9bnLInCVroNDCtXce57xKdI0/rQaA3K+6q0eFyUBPmqfSndUZdQ== + 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.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" @@ -6784,6 +6799,11 @@ dependencies: "@types/node" "*" +"@types/tough-cookie@*": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397" + integrity sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw== + "@types/trusted-types@*": version "2.0.2" resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.2.tgz#fc25ad9943bcac11cceb8168db4f275e0e72e756" @@ -8716,6 +8736,15 @@ cli-spinners@^2.5.0: resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.1.tgz#adc954ebe281c37a6319bfa401e6dd2488ffb70d" integrity sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g== +cli-table3@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.3.tgz#61ab765aac156b52f222954ffc607a6f01dbeeb2" + integrity sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg== + dependencies: + string-width "^4.2.0" + optionalDependencies: + "@colors/colors" "1.5.0" + cli-table3@~0.6.0: version "0.6.2" resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.2.tgz#aaf5df9d8b5bf12634dc8b3040806a0c07120d2a" @@ -11956,7 +11985,7 @@ form-data-encoder@^1.4.3, form-data-encoder@^1.7.1: resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-1.7.2.tgz#1f1ae3dccf58ed4690b86d87e4f57c654fbab040" integrity sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A== -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.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== From 80e5b4221979df58f55d98dd2bf38e327ac3cc3d Mon Sep 17 00:00:00 2001 From: Taras Date: Sun, 16 Oct 2022 13:54:21 -0400 Subject: [PATCH 23/37] Added clone command --- plugins/platform-backend/cli/cli.ts | 63 ++++++++++++++----- plugins/platform-backend/cli/deno.json | 2 +- .../platform-backend/src/service/router.ts | 5 +- .../src/service/routes/repositories.ts | 46 ++++++++++---- 4 files changed, 88 insertions(+), 28 deletions(-) diff --git a/plugins/platform-backend/cli/cli.ts b/plugins/platform-backend/cli/cli.ts index 76c534f02e..41b8e593cd 100644 --- a/plugins/platform-backend/cli/cli.ts +++ b/plugins/platform-backend/cli/cli.ts @@ -95,27 +95,62 @@ export async function cli(options: CLIOptions) { "clone", "clone a repository associated with a component" ) - .option( - "-c --component ", - "The backstage component entity", + .option('-S, --ssh', 'Use ssh url to clone repository') + .option('-H, --https', 'Use https url to clone repository') + .arguments( + "[component:string] [directory:string]", ) - .action(async ({ component }) => { - if (!component) { + .action(async ({ ssh, https }, component, directory) => { + const output = directory ?? component; + if (ssh && https) { + throw new MainError(`Invalid options: --ssh and --https can't be used together - use one or the other.`) + } + // prefer ssh + const protocol = !(ssh && https) || ssh ? 'ssh' : 'https'; + if (component) { let response: Response; try { - response = await get(`repositories`, { - headers: { - Accept: 'text/plain' - } - }); + response = await get(`repositories/${component}/urls`); } catch (error) { throw new MainError(error.message); } - if (response.ok) { - await Deno.stdout.write( - new TextEncoder().encode(await response.text()) - ) + if (response.ok) { + const urls = await response.json(); + const url = urls[protocol]; + const clone = Deno.run({ + cmd: ['git', 'clone', url, output] + }); + if (!(await clone.status()).success) { + throw new MainError(`Encountered an error cloning "${url}" to "${output}".`) + } + } else if (response.status === 404) { + throw new MainError(`unknown component '${component}'`); + } else { + throw new MainError( + `communication error with backstage server: ${response.status} ${response.statusText}`, + ); } + return; + } + + let response: Response; + try { + response = await get(`repositories`, { + headers: { + Accept: 'text/plain' + } + }); + } catch (error) { + throw new MainError(error.message); + } + if (response.ok) { + await Deno.stdout.write( + new TextEncoder().encode(await response.text()) + ) + } else { + throw new MainError( + `communication error with backstage server: ${response.status} ${response.statusText}` + ) } }) .command( diff --git a/plugins/platform-backend/cli/deno.json b/plugins/platform-backend/cli/deno.json index 2df05e1196..a1cea42e11 100644 --- a/plugins/platform-backend/cli/deno.json +++ b/plugins/platform-backend/cli/deno.json @@ -1,6 +1,6 @@ { "tasks": { - "dev": "deno run --location=http://localhost:7007 --allow-env --allow-read --allow-net=localhost:7007 tasks/dev.ts" + "dev": "deno run --location=http://localhost:7007 --allow-env --allow-read --allow-net=localhost:7007 --allow-run tasks/dev.ts" }, "lint": { "rules": { diff --git a/plugins/platform-backend/src/service/router.ts b/plugins/platform-backend/src/service/router.ts index a195a383d4..de9d74389f 100644 --- a/plugins/platform-backend/src/service/router.ts +++ b/plugins/platform-backend/src/service/router.ts @@ -26,7 +26,7 @@ import request from 'request'; import type { Logger } from 'winston'; import { findOrCreateExecutables } from '../executables'; import { EntityRef, PlatformApi } from '../types'; -import { RepositoriesRoute } from './routes/repositories'; +import { Repositories } from './routes/repositories'; export interface RouterOptions { logger: Logger; @@ -160,10 +160,11 @@ export async function createRouter( req.pipe(request(eventStreamUrl)).pipe(res); }); - router.get('/repositories', RepositoriesRoute({ + router.use('/repositories', Repositories({ catalog })); + router.use(errorHandler()); return router; } diff --git a/plugins/platform-backend/src/service/routes/repositories.ts b/plugins/platform-backend/src/service/routes/repositories.ts index 72f39ab64b..ac84f8b0fe 100644 --- a/plugins/platform-backend/src/service/routes/repositories.ts +++ b/plugins/platform-backend/src/service/routes/repositories.ts @@ -1,25 +1,23 @@ import type { CatalogClient } from '@backstage/catalog-client'; import type { Entity } from '@backstage/catalog-model'; -import { Handler } from 'express'; import CliTable3 from 'cli-table3'; import chalk from 'chalk'; +import Router from 'express-promise-router'; -export const RepositoriesRoute = ({ catalog }: { catalog: CatalogClient }) => { - const repositoriesRoute: Handler = async (req, res) => { +export const Repositories = ({ catalog }: { catalog: CatalogClient }) => { + const router = Router(); + + router.get('/', async (req, res) => { const { items: entities } = await catalog.getEntities(); const repositories = entities.flatMap(entity => { - const slug = entity.metadata - && entity.metadata.annotations - && entity.metadata.annotations["github.com/project-slug"]; + const slug = getGithubProjectSlug(entity); if (slug) { return [{ componentRef: getComponentRef(entity), slug, description: entity.metadata.description, url: `https://github/${slug}`, - ssh: `git@github.com:${slug}.git`, - https: `https://github/${slug}.git` }] } return []; @@ -37,9 +35,35 @@ export const RepositoriesRoute = ({ catalog }: { catalog: CatalogClient }) => { res.send(`\n${chalk.bold(' 🥁 Available Repositories')}\n${table}`) } - return repositoriesRoute; - } - return repositoriesRoute; + return router; + }); + + router.get('/:component/urls', async (req, res) => { + const entity = await catalog.getEntityByRef({ + kind: 'Component', + namespace: 'default', + name: req.params.component + }); + if (entity) { + const slug = getGithubProjectSlug(entity); + res.json({ + ssh: `git@github.com:${slug}.git`, + https: `https://github/${slug}.git` + }) + return; + } + res.sendStatus(404); + res.send("Not Found"); + }); + + + return router; +} + +function getGithubProjectSlug(entity: Entity) { + return entity.metadata + && entity.metadata.annotations + && entity.metadata.annotations["github.com/project-slug"]; } function getComponentRef(entity: Entity) { From 63abcbfa52f607101e937af6bc42b11eb126b47d Mon Sep 17 00:00:00 2001 From: Taras Date: Mon, 17 Oct 2022 17:26:37 -0400 Subject: [PATCH 24/37] Refactor to use PlatformAPI --- packages/backend/src/plugins/idp.ts | 62 ++++++++++++++++- plugins/humanitec-common/src/platform-api.ts | 2 +- .../platform-backend/src/service/router.ts | 38 ++++++----- .../src/service/routes/repositories.ts | 68 +++++++------------ plugins/platform-backend/src/types.ts | 16 +++++ 5 files changed, 120 insertions(+), 66 deletions(-) diff --git a/packages/backend/src/plugins/idp.ts b/packages/backend/src/plugins/idp.ts index c8690023da..c03be27faa 100644 --- a/packages/backend/src/plugins/idp.ts +++ b/packages/backend/src/plugins/idp.ts @@ -2,6 +2,7 @@ import type { Router } from 'express'; import { createRouter } from '@frontside/backstage-plugin-platform-backend'; import { createHumanitecPlatformApi } from '@frontside/backstage-plugin-humanitec-common'; import { PluginEnvironment } from '../types'; +import { Entity } from '@backstage/catalog-model'; export default async function createPlugin({ config, @@ -15,8 +16,63 @@ export default async function createPlugin({ discovery, appURL: `${config.getString('app.baseUrl')}/platform`, catalog, - platform: createHumanitecPlatformApi({ - token: config.getString('humanitec.token') - }) + platform: { + async getRepositoryUrls(ref) { + const entity = await ref.load(); + + if (entity) { + const slug = getGithubProjectSlug(entity); + return { + ssh: `git@github.com:${slug}.git`, + https: `https://github/${slug}.git` + } + } + + return null; + }, + async getRepositories() { + const { items: entities } = await catalog.getEntities(); + + const repositories = entities.flatMap(entity => { + const slug = getGithubProjectSlug(entity); + if (slug) { + return [{ + componentRef: getComponentRef(entity), + slug, + description: entity.metadata.description, + url: `https://github/${slug}`, + }] + } + return []; + }); + return { + hasNextPage: false, + hasPreviousPage: false, + beginCursor: '', + endCursor: '', + items: repositories.map(r => ({ + cursor: '', + value: r + })) + } + }, + ...createHumanitecPlatformApi({ + token: config.getString('humanitec.token'), + }), + }, }); } + +function getGithubProjectSlug(entity: Entity) { + return entity.metadata + && entity.metadata.annotations + && entity.metadata.annotations["github.com/project-slug"]; +} + +function getComponentRef(entity: Entity) { + return [ + entity.kind !== 'Component' ? entity.kind.toLowerCase() : '', + entity.metadata.namespace && entity.metadata.namespace !== 'default' ? entity.metadata.namespace : '', + entity.metadata.name + ].join('') +} \ No newline at end of file diff --git a/plugins/humanitec-common/src/platform-api.ts b/plugins/humanitec-common/src/platform-api.ts index 0c5653ca4d..924ca61e74 100644 --- a/plugins/humanitec-common/src/platform-api.ts +++ b/plugins/humanitec-common/src/platform-api.ts @@ -4,7 +4,7 @@ import type { Entity } from '@backstage/catalog-model'; import { HUMANITEC_APP_ID_ANNOTATION, HUMANITEC_ORG_ID_ANNOTATION } from './constants'; import { createHumanitecClient } from './clients/humanitec'; -export function createHumanitecPlatformApi({ token }: { token: string }): PlatformApi { +export function createHumanitecPlatformApi({ token }: { token: string }): Pick { return { async getEnvironments(ref) { diff --git a/plugins/platform-backend/src/service/router.ts b/plugins/platform-backend/src/service/router.ts index de9d74389f..1a835935b0 100644 --- a/plugins/platform-backend/src/service/router.ts +++ b/plugins/platform-backend/src/service/router.ts @@ -25,7 +25,7 @@ import * as nunjucks from 'nunjucks'; import request from 'request'; import type { Logger } from 'winston'; import { findOrCreateExecutables } from '../executables'; -import { EntityRef, PlatformApi } from '../types'; +import { EntityRef, GetComponentRef, PlatformApi } from '../types'; import { Repositories } from './routes/repositories'; export interface RouterOptions { @@ -37,7 +37,6 @@ export interface RouterOptions { platform: PlatformApi; } - export async function createRouter( options: RouterOptions, ): Promise { @@ -54,7 +53,21 @@ export async function createRouter( downloadsURL, executableName, entrypoint: resolvePackagePath("@frontside/backstage-plugin-platform-backend", "cli", "main.ts"), - }) + }); + + const getComponentRef: GetComponentRef = async (name) => { + let component = await catalog.getEntityByRef(`component:default/${name}`); + let ref: EntityRef = { + ref: `component:default/${name}`, + compound: { + kind: 'component', + name, + namespace: 'default' + }, + load: () => Promise.resolve(component), + }; + return ref; + } const router = Router(); router.use(express.text()); @@ -94,18 +107,8 @@ export async function createRouter( router.get('/components/:name/environments', async (req, res) => { let name = req.params.name; - let component = await catalog.getEntityByRef(`component:default/${name}`); - - if (component) { - let ref: EntityRef = { - ref: `component:default/${name}`, - compound: { - kind: 'component', - name, - namespace: 'default' - }, - load: () => Promise.resolve(component), - }; + let ref = await getComponentRef(name); + if (ref) { let environments = await platform.getEnvironments(ref); let names = environments.items.map(({ value }) => value.name); @@ -161,10 +164,11 @@ export async function createRouter( }); router.use('/repositories', Repositories({ + getComponentRef, + platform, catalog })); - router.use(errorHandler()); return router; -} +} \ No newline at end of file diff --git a/plugins/platform-backend/src/service/routes/repositories.ts b/plugins/platform-backend/src/service/routes/repositories.ts index ac84f8b0fe..e0c2d89fe7 100644 --- a/plugins/platform-backend/src/service/routes/repositories.ts +++ b/plugins/platform-backend/src/service/routes/repositories.ts @@ -3,25 +3,22 @@ import type { Entity } from '@backstage/catalog-model'; import CliTable3 from 'cli-table3'; import chalk from 'chalk'; import Router from 'express-promise-router'; +import express from 'express'; +import { GetComponentRef, PlatformApi } from '../../types'; -export const Repositories = ({ catalog }: { catalog: CatalogClient }) => { +interface RouteOptions { + platform: PlatformApi; + catalog: CatalogClient; + getComponentRef: GetComponentRef; +} + +type Route = (options: RouteOptions) => express.Router; + +export const Repositories: Route = ({ platform, getComponentRef }) => { const router = Router(); router.get('/', async (req, res) => { - const { items: entities } = await catalog.getEntities(); - - const repositories = entities.flatMap(entity => { - const slug = getGithubProjectSlug(entity); - if (slug) { - return [{ - componentRef: getComponentRef(entity), - slug, - description: entity.metadata.description, - url: `https://github/${slug}`, - }] - } - return []; - }); + const repositories = await platform.getRepositories(); if (req.accepts('json')) { res.json(repositories); @@ -30,7 +27,7 @@ export const Repositories = ({ catalog }: { catalog: CatalogClient }) => { head: ['Component', 'Repository URL', 'Description'] }); table.push( - ...repositories.map(r => ([r.componentRef, r.url, r.description])) + ...repositories.items.map(({ value: r }) => ([r.componentRef, r.url, r.description])) ) res.send(`\n${chalk.bold(' 🥁 Available Repositories')}\n${table}`) } @@ -39,37 +36,18 @@ export const Repositories = ({ catalog }: { catalog: CatalogClient }) => { }); router.get('/:component/urls', async (req, res) => { - const entity = await catalog.getEntityByRef({ - kind: 'Component', - namespace: 'default', - name: req.params.component - }); - if (entity) { - const slug = getGithubProjectSlug(entity); - res.json({ - ssh: `git@github.com:${slug}.git`, - https: `https://github/${slug}.git` - }) - return; + const name = req.params.component; + + const ref = await getComponentRef(name); + const urls = await platform.getRepositoryUrls(ref); + + if (urls) { + res.json(urls) + } else { + res.sendStatus(404); + res.send("Not Found"); } - res.sendStatus(404); - res.send("Not Found"); }); - return router; } - -function getGithubProjectSlug(entity: Entity) { - return entity.metadata - && entity.metadata.annotations - && entity.metadata.annotations["github.com/project-slug"]; -} - -function getComponentRef(entity: Entity) { - return [ - entity.kind !== 'Component' ? entity.kind.toLowerCase() : '', - entity.metadata.namespace && entity.metadata.namespace !== 'default' ? entity.metadata.namespace : '', - entity.metadata.name - ].join('') -} \ No newline at end of file diff --git a/plugins/platform-backend/src/types.ts b/plugins/platform-backend/src/types.ts index 1526826948..ce9e27ad67 100644 --- a/plugins/platform-backend/src/types.ts +++ b/plugins/platform-backend/src/types.ts @@ -5,8 +5,22 @@ export interface Environment { name: string; } +export interface Repository { + componentRef: string; + slug: string; + description?: string; + url: string; +} + +export interface RepositoryUrls { + ssh: string; + https: string; +} + export interface PlatformApi { getEnvironments(ref: EntityRef, page?: PageSpec): Promise>; + getRepositories(page?: PageSpec): Promise> + getRepositoryUrls(ref: EntityRef): Promise } export interface EntityRef { @@ -33,3 +47,5 @@ export type PageSpec = { count: number; after: string; } + +export type GetComponentRef = (name: string) => Promise From afdbe3ddf4f050b4514ef114b5668060bead0d81 Mon Sep 17 00:00:00 2001 From: Taras Date: Mon, 17 Oct 2022 17:33:49 -0400 Subject: [PATCH 25/37] Upgrade node-deno --- plugins/platform-backend/package.json | 2 +- plugins/platform-backend/src/executables.ts | 2 ++ yarn.lock | 8 ++++---- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/plugins/platform-backend/package.json b/plugins/platform-backend/package.json index 041758baf3..a4bc91615f 100644 --- a/plugins/platform-backend/package.json +++ b/plugins/platform-backend/package.json @@ -32,7 +32,7 @@ "express-promise-router": "^4.1.0", "js-yaml": "^4.1.0", "node-fetch-native": "^0.1.7", - "node-deno": "^0.0.3", + "node-deno": "^0.0.4", "node-fetch": "^2.6.7", "nunjucks": "^3.2.3", "request": "^2.88.2", diff --git a/plugins/platform-backend/src/executables.ts b/plugins/platform-backend/src/executables.ts index 42edc8a868..3c6c88619f 100644 --- a/plugins/platform-backend/src/executables.ts +++ b/plugins/platform-backend/src/executables.ts @@ -78,6 +78,8 @@ function findOrCreateExecutable(target: CompilationTarget, options: FindOrCreate allowRead: true, + allowRun: true, + // fail immediately if a permission is not present noPrompt: true, }).then(result => { diff --git a/yarn.lock b/yarn.lock index 1b243ad729..28ba798a21 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16814,10 +16814,10 @@ node-cache@^5.1.2: dependencies: clone "2.x" -node-deno@^0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/node-deno/-/node-deno-0.0.3.tgz#c2cea2c42ce23daa72bc6fcce7d068c6196febef" - integrity sha512-5mJGsLXEsuTgIHiqKjclPbpkUimqRiuQ1eaQ37oxeeh3bmi7O3jP1C4aU4lpnXDrVf3dfnqzqhyjOMSAZpU1nw== +node-deno@^0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/node-deno/-/node-deno-0.0.4.tgz#62a0b7cba982c74908252d0e4d0a8c553a62b0ee" + integrity sha512-ZZe2sLojFzn9/lBMvcZ/gQ6cvs7U1IpLtxdOOXM4KUdd7QtkJ7Gb+VEJNj2JVDBT0OS8Gc090goBhKN2dsXqWg== dependencies: "@effection/process" "^2.1.1" deno-bin "^1.26.0" From 7a13dba47a532da53976cb3f94b7884a6907e7e1 Mon Sep 17 00:00:00 2001 From: Taras Date: Tue, 18 Oct 2022 08:41:44 -0400 Subject: [PATCH 26/37] Fixed some of the types --- packages/backend/src/plugins/catalog.ts | 17 ----------------- plugins/humanitec-common/src/platform-api.ts | 14 ++++++++------ .../src/service/routes/repositories.ts | 1 - 3 files changed, 8 insertions(+), 24 deletions(-) diff --git a/packages/backend/src/plugins/catalog.ts b/packages/backend/src/plugins/catalog.ts index 437c52f6e8..e09176c066 100644 --- a/packages/backend/src/plugins/catalog.ts +++ b/packages/backend/src/plugins/catalog.ts @@ -3,9 +3,7 @@ import { } from '@backstage/plugin-catalog-backend'; import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend'; import { IncrementalCatalogBuilder } from '@frontside/backstage-plugin-incremental-ingestion-backend'; -import { GithubRepositoryEntityProvider } from '@frontside/backstage-plugin-incremental-ingestion-github'; import { Router } from 'express'; -import { Duration } from 'luxon'; import { PluginEnvironment } from '../types'; export default async function createPlugin( @@ -16,21 +14,6 @@ export default async function createPlugin( // incremental builder receives builder because it'll register // incremental entity providers with the builder const incrementalBuilder = IncrementalCatalogBuilder.create(env, builder); - - const githubRepositoryProvider = GithubRepositoryEntityProvider.create({ - host: 'github.com', - searchQuery: "created:>1970-01-01 user:thefrontside", - config: env.config - }) - - // incrementalBuilder.addIncrementalEntityProvider( - // githubRepositoryProvider, - // { - // burstInterval: Duration.fromObject({ seconds: 3 }), - // burstLength: Duration.fromObject({ seconds: 3 }), - // restLength: Duration.fromObject({ day: 1 }) - // } - // ) builder.addProcessor(new ScaffolderEntitiesProcessor()); diff --git a/plugins/humanitec-common/src/platform-api.ts b/plugins/humanitec-common/src/platform-api.ts index 924ca61e74..ec3ec8d252 100644 --- a/plugins/humanitec-common/src/platform-api.ts +++ b/plugins/humanitec-common/src/platform-api.ts @@ -8,10 +8,10 @@ export function createHumanitecPlatformApi({ token }: { token: string }): Pick

Date: Tue, 18 Oct 2022 08:43:55 -0400 Subject: [PATCH 27/37] Use humanitec keys as variables --- plugins/humanitec-common/src/platform-api.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/humanitec-common/src/platform-api.ts b/plugins/humanitec-common/src/platform-api.ts index ec3ec8d252..ea6fa57350 100644 --- a/plugins/humanitec-common/src/platform-api.ts +++ b/plugins/humanitec-common/src/platform-api.ts @@ -31,8 +31,8 @@ export function createHumanitecPlatformApi({ token }: { token: string }): Pick

Date: Tue, 18 Oct 2022 09:02:16 -0400 Subject: [PATCH 28/37] Fix types --- plugins/platform-backend/src/service/router.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/plugins/platform-backend/src/service/router.ts b/plugins/platform-backend/src/service/router.ts index 1a835935b0..f690848e5d 100644 --- a/plugins/platform-backend/src/service/router.ts +++ b/plugins/platform-backend/src/service/router.ts @@ -25,7 +25,7 @@ import * as nunjucks from 'nunjucks'; import request from 'request'; import type { Logger } from 'winston'; import { findOrCreateExecutables } from '../executables'; -import { EntityRef, GetComponentRef, PlatformApi } from '../types'; +import { GetComponentRef, PlatformApi } from '../types'; import { Repositories } from './routes/repositories'; export interface RouterOptions { @@ -56,17 +56,21 @@ export async function createRouter( }); const getComponentRef: GetComponentRef = async (name) => { - let component = await catalog.getEntityByRef(`component:default/${name}`); - let ref: EntityRef = { + return { ref: `component:default/${name}`, compound: { kind: 'component', name, namespace: 'default' }, - load: () => Promise.resolve(component), + load: async () => { + const entity = await catalog.getEntityByRef(`component:default/${name}`); + if (!entity) { + throw new Error(`Component ${name} not found.`); + } + return entity; + }, }; - return ref; } const router = Router(); From 42e96d3f50a33032029c1e40df7a66bd92ac29b8 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Tue, 18 Oct 2022 09:24:32 -0500 Subject: [PATCH 29/37] Add `location` parameter --- plugins/platform-backend/src/executables.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/platform-backend/src/executables.ts b/plugins/platform-backend/src/executables.ts index 3c6c88619f..edc5226dbe 100644 --- a/plugins/platform-backend/src/executables.ts +++ b/plugins/platform-backend/src/executables.ts @@ -80,6 +80,8 @@ function findOrCreateExecutable(target: CompilationTarget, options: FindOrCreate allowRun: true, + location: baseURL, + // fail immediately if a permission is not present noPrompt: true, }).then(result => { From e04b7db130e17ba9f6b31883ef28ff92d38fbc30 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Wed, 19 Oct 2022 08:22:05 -0500 Subject: [PATCH 30/37] Add help text to the main plugin install page (#127) fixes #126 --- packages/backend/src/plugins/idp.ts | 4 +- plugins/platform-backend/package.json | 2 +- plugins/platform-backend/src/executables.ts | 64 ++++++++++++++- plugins/platform-backend/src/index.ts | 2 +- .../platform-backend/src/service/router.ts | 14 ++-- plugins/platform/package.json | 5 +- plugins/platform/src/api/executables-api.ts | 4 +- .../AllExecutables/AllExecutables.tsx | 11 +-- .../src/components/Install/Install.tsx | 78 +++++++++++++------ yarn.lock | 26 ++++++- 10 files changed, 159 insertions(+), 51 deletions(-) diff --git a/packages/backend/src/plugins/idp.ts b/packages/backend/src/plugins/idp.ts index c03be27faa..8b5ae33f94 100644 --- a/packages/backend/src/plugins/idp.ts +++ b/packages/backend/src/plugins/idp.ts @@ -25,7 +25,7 @@ export default async function createPlugin({ return { ssh: `git@github.com:${slug}.git`, https: `https://github/${slug}.git` - } + } } return null; @@ -75,4 +75,4 @@ function getComponentRef(entity: Entity) { entity.metadata.namespace && entity.metadata.namespace !== 'default' ? entity.metadata.namespace : '', entity.metadata.name ].join('') -} \ No newline at end of file +} diff --git a/plugins/platform-backend/package.json b/plugins/platform-backend/package.json index a4bc91615f..12f94ba552 100644 --- a/plugins/platform-backend/package.json +++ b/plugins/platform-backend/package.json @@ -32,7 +32,7 @@ "express-promise-router": "^4.1.0", "js-yaml": "^4.1.0", "node-fetch-native": "^0.1.7", - "node-deno": "^0.0.4", + "node-deno": "^0.1.0", "node-fetch": "^2.6.7", "nunjucks": "^3.2.3", "request": "^2.88.2", diff --git a/plugins/platform-backend/src/executables.ts b/plugins/platform-backend/src/executables.ts index edc5226dbe..d8d0656bf5 100644 --- a/plugins/platform-backend/src/executables.ts +++ b/plugins/platform-backend/src/executables.ts @@ -1,10 +1,24 @@ import type { Logger } from 'winston'; import type { CompilationTarget } from 'node-deno'; -import { CompilationTargets, compile } from 'node-deno'; +import { CompilationTargets, run, compile } from 'node-deno'; import { existsSync } from 'fs'; -export interface Executables extends Record { +export interface DownloadInfo { executableName: string; + helpText: Async; + executables: Executables; +} + +export type Executables = Record; + +type Async = { + "type": "pending"; +} | { + "type": "resolved"; + value: T; +} | { + "type": "rejected"; + error: Error; } export type Executable = { @@ -34,6 +48,50 @@ export interface FindOrCreateOptions { entrypoint: string; } +export function getDownloadInfo(options: FindOrCreateOptions): DownloadInfo { + let { executableName } = options; + + let executables = findOrCreateExecutables(options); + + let helpText = getHelpText(options); + + return { executableName, helpText, executables }; +} + +export function getHelpText(options: FindOrCreateOptions): Async { + let { executableName, entrypoint, baseURL, logger } = options; + let description = "internal developer platform"; + + let value: Async = { "type": "pending" }; + + run({ + entrypoint: [entrypoint, executableName, baseURL, `"${description}"`, "--help"], + }).then(result => { + if (result.code != 0) { + logger.info(`help text generation failed: ${result.stderr}`); + value = { "type": "rejected", error: new Error(result.stderr) }; + } else { + logger.info("help text generated for platform executable"); + value = { "type": "resolved", value: result.stdout }; + } + }).catch(error => { + logger.error(`help text generation failed: ${error}`); + value = { "type": "rejected", error }; + }); + + return new Proxy({} as Async, { + get(_, prop: keyof Executable) { + return value[prop]; + }, + ownKeys: () => Object.keys(value), + getOwnPropertyDescriptor: (_, key) => ({ + value: value[key as keyof Executable], + enumerable: true, + configurable: true, + }) + }) +} + export function findOrCreateExecutables(options: FindOrCreateOptions): Executables { options.logger.info(`generating executables for ${options.executableName}`); return CompilationTargets.reduce((executables, target) => { @@ -41,7 +99,7 @@ export function findOrCreateExecutables(options: FindOrCreateOptions): Executabl ...executables, [target]: findOrCreateExecutable(target, options), } - }, { executableName: options.executableName }) as Executables; + }, {}) as Executables; } function findOrCreateExecutable(target: CompilationTarget, options: FindOrCreateOptions): Executable { diff --git a/plugins/platform-backend/src/index.ts b/plugins/platform-backend/src/index.ts index 4ac605f78d..35b4d00bc4 100644 --- a/plugins/platform-backend/src/index.ts +++ b/plugins/platform-backend/src/index.ts @@ -16,4 +16,4 @@ export * from './service/router'; export * from './types'; -export type { Executables } from './executables'; +export type { DownloadInfo, Executables } from './executables'; diff --git a/plugins/platform-backend/src/service/router.ts b/plugins/platform-backend/src/service/router.ts index f690848e5d..eb218726df 100644 --- a/plugins/platform-backend/src/service/router.ts +++ b/plugins/platform-backend/src/service/router.ts @@ -24,7 +24,7 @@ import fetch from 'node-fetch-native'; import * as nunjucks from 'nunjucks'; import request from 'request'; import type { Logger } from 'winston'; -import { findOrCreateExecutables } from '../executables'; +import { getDownloadInfo } from '../executables'; import { GetComponentRef, PlatformApi } from '../types'; import { Repositories } from './routes/repositories'; @@ -41,12 +41,12 @@ export async function createRouter( options: RouterOptions, ): Promise { const { catalog, logger, discovery, executableName, appURL, platform } = options; - + let baseURL = await discovery.getBaseUrl('idp'); let downloadsURL = `${baseURL}/executables/dist`; let scaffolderUrl = `${await discovery.getBaseUrl('scaffolder')}/v2/tasks`; - let executables = findOrCreateExecutables({ + let executables = getDownloadInfo({ logger, distDir: 'dist-bin', baseURL, @@ -130,7 +130,7 @@ export async function createRouter( try { const values = load(req.body) as Record; - + const post = await fetch(scaffolderUrl, { method: 'POST', headers: { @@ -148,7 +148,7 @@ export async function createRouter( if(post.status !== 201) { throw new Error(`resource not created, ${post.status} - ${post.statusText}`); } - + const { id } = (await post.json()) as { id: string }; res.json({ taskId: id }); @@ -166,7 +166,7 @@ export async function createRouter( req.pipe(request(eventStreamUrl)).pipe(res); }); - + router.use('/repositories', Repositories({ getComponentRef, platform, @@ -175,4 +175,4 @@ export async function createRouter( router.use(errorHandler()); return router; -} \ No newline at end of file +} diff --git a/plugins/platform/package.json b/plugins/platform/package.json index 8c3ac8a6e4..8a164004cb 100644 --- a/plugins/platform/package.json +++ b/plugins/platform/package.json @@ -29,6 +29,7 @@ "@material-ui/core": "^4.9.13", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", + "ansi-to-react": "^6.1.6", "react-use": "^17.2.4" }, "peerDependencies": { @@ -44,8 +45,8 @@ "@testing-library/user-event": "^14.0.0", "@types/jest": "*", "@types/node": "*", - "msw": "^0.42.0", - "cross-fetch": "^3.1.5" + "cross-fetch": "^3.1.5", + "msw": "^0.42.0" }, "files": [ "dist" diff --git a/plugins/platform/src/api/executables-api.ts b/plugins/platform/src/api/executables-api.ts index 13dacbdfd2..d7fb5200ad 100644 --- a/plugins/platform/src/api/executables-api.ts +++ b/plugins/platform/src/api/executables-api.ts @@ -1,4 +1,4 @@ -import type { Executables } from '@frontside/backstage-plugin-platform-backend'; +import type { DownloadInfo } from '@frontside/backstage-plugin-platform-backend'; import { createApiRef } from '@backstage/core-plugin-api'; export const executablesApiRef = createApiRef({ @@ -6,5 +6,5 @@ export const executablesApiRef = createApiRef({ }); export interface ExecutablesAPI { - fetchExecutables(): Promise; + fetchExecutables(): Promise; } diff --git a/plugins/platform/src/components/AllExecutables/AllExecutables.tsx b/plugins/platform/src/components/AllExecutables/AllExecutables.tsx index b2375b9ec8..262f256d28 100644 --- a/plugins/platform/src/components/AllExecutables/AllExecutables.tsx +++ b/plugins/platform/src/components/AllExecutables/AllExecutables.tsx @@ -4,10 +4,10 @@ import Alert from '@material-ui/lab/Alert'; import useAsync from 'react-use/lib/useAsync'; import { useApi } from '@backstage/core-plugin-api'; -import type { Executables } from '@frontside/backstage-plugin-platform-backend'; +import type { DownloadInfo } from '@frontside/backstage-plugin-platform-backend'; import { executablesApiRef } from '../../api/executables-api'; -export const DenseTable = ({ executables }: { executables: Executables}) => { +export const DenseTable = ({ info }: { info: DownloadInfo}) => { const columns: TableColumn[] = [ { title: 'Architecture', field: 'target' }, @@ -15,10 +15,11 @@ export const DenseTable = ({ executables }: { executables: Executables}) => { { title: 'URL', field: 'url' }, ]; - let { executableName, ...binaries } = executables; + let { executableName, executables } = info; - const data = Object.entries(binaries).map(([target, executable]) => { + const data = Object.entries(executables).map(([target, executable]) => { return { + id: target, target, url: executable.type === 'compiled' ? executable.url : 'N/A', status: executable.type, @@ -44,7 +45,7 @@ export const AllExecutables = () => { } else if (error) { return {error.message}; } else { - return ; + return ; } }; diff --git a/plugins/platform/src/components/Install/Install.tsx b/plugins/platform/src/components/Install/Install.tsx index 019f4465b6..8f3c23ed68 100644 --- a/plugins/platform/src/components/Install/Install.tsx +++ b/plugins/platform/src/components/Install/Install.tsx @@ -10,29 +10,59 @@ import { SupportButton, } from '@backstage/core-components'; import { AllExecutables } from '../AllExecutables'; +import { executablesApiRef } from '../../api/executables-api'; +import { useApi } from '@backstage/core-plugin-api'; +import useAsync from 'react-use/lib/useAsync'; +import Ansi from 'ansi-to-react'; -export const Install = () => ( - -

- - -
- - - A description of your plugin goes here. - - - - - - curl -sSL http://localhost:7007/api/idp/install.sh | sh - - - - - +export const useHelp = () => { + let api = useApi(executablesApiRef); + let info = useAsync(api.fetchExecutables, []); + if (info.loading) { + return { loading: true }; + } else if (info.error) { + return info; + } else if (info.value?.helpText.type === 'rejected' ) { + return { loading: false, error: info.value.helpText.error }; + } else if (info.value?.helpText.type === 'resolved') { + return { loading: false, value: info.value?.helpText.value }; + } else { + return { loading: true }; + } +} + +export const Install = () => { + let helpText = useHelp(); + return ( + +
+ + +
+ + + Install your company's platform tool + + + + + + curl -sSL http://localhost:7007/api/idp/install.sh | sh + + + + + + + {helpText.error ? "" : helpText.value } + + + + + + -
-
- -); + + + ) +}; diff --git a/yarn.lock b/yarn.lock index 28ba798a21..f5c2dcd2d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7295,6 +7295,11 @@ alea@1.0.1: resolved "https://registry.yarnpkg.com/alea/-/alea-1.0.1.tgz#957f60741c5ad11b13f72aa02a6b89fe96a26dc4" integrity sha512-QU+wv+ziDXaMxRdsQg/aH7sVfWdhKps5YP97IIwFkHCsbDZA3k87JXoZ5/iuemf4ntytzIWeScrRpae8+lDrXA== +anser@^1.4.1: + version "1.4.10" + resolved "https://registry.yarnpkg.com/anser/-/anser-1.4.10.tgz#befa3eddf282684bd03b63dcda3927aef8c2e35b" + integrity sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww== + ansi-colors@^4.1.1, ansi-colors@^4.1.3: version "4.1.3" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" @@ -7351,6 +7356,14 @@ ansi-styles@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== +ansi-to-react@^6.1.6: + version "6.1.6" + resolved "https://registry.yarnpkg.com/ansi-to-react/-/ansi-to-react-6.1.6.tgz#d6fe15ecd4351df626a08121b1646adfe6c02ccb" + integrity sha512-+HWn72GKydtupxX9TORBedqOMsJRiKTqaLUKW8txSBZw9iBpzPKLI8KOu4WzwD4R7hSv1zEspobY6LwlWvwZ6Q== + dependencies: + anser "^1.4.1" + escape-carriage "^1.3.0" + any-promise@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" @@ -11021,6 +11034,11 @@ escalade@^3.1.1: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== +escape-carriage@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/escape-carriage/-/escape-carriage-1.3.0.tgz#71006b2d4da8cb6828686addafcb094239c742f3" + integrity sha512-ATWi5MD8QlAGQOeMgI8zTp671BG8aKvAC0M7yenlxU4CRLGO/sKthxVUyjiOFKjHdIo+6dZZUNFgHFeVEaKfGQ== + escape-html@^1.0.3, escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" @@ -16814,10 +16832,10 @@ node-cache@^5.1.2: dependencies: clone "2.x" -node-deno@^0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/node-deno/-/node-deno-0.0.4.tgz#62a0b7cba982c74908252d0e4d0a8c553a62b0ee" - integrity sha512-ZZe2sLojFzn9/lBMvcZ/gQ6cvs7U1IpLtxdOOXM4KUdd7QtkJ7Gb+VEJNj2JVDBT0OS8Gc090goBhKN2dsXqWg== +node-deno@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/node-deno/-/node-deno-0.1.0.tgz#b70a9307d93db03bca69da0377ad3b396f4486e0" + integrity sha512-RPwAQZQHaadrejl83mDOq0LbNgefveH+QYOT1r5IUKmmqc/hKizNSt0xlWEDCy+Hvb5pCurk75FGK5XFb3eD0g== dependencies: "@effection/process" "^2.1.1" deno-bin "^1.26.0" From 5f66632ece47a7ea989b089f3a665d5efcdf908f Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Sun, 23 Oct 2022 21:43:38 -0500 Subject: [PATCH 31/37] Add streaming logs command and endpoint. --- .../humanitec-common/src/clients/humanitec.ts | 16 ++- plugins/humanitec-common/src/platform-api.ts | 21 ++- plugins/platform-backend/cli/cli.ts | 124 ++++++++++++------ plugins/platform-backend/cli/deps.ts | 4 +- .../platform-backend/src/service/router.ts | 7 + .../src/service/routes/logs.ts | 51 +++++++ .../src/service/routes/repositories.ts | 8 +- plugins/platform-backend/src/types.ts | 1 + 8 files changed, 180 insertions(+), 52 deletions(-) create mode 100644 plugins/platform-backend/src/service/routes/logs.ts diff --git a/plugins/humanitec-common/src/clients/humanitec.ts b/plugins/humanitec-common/src/clients/humanitec.ts index 7b07a37a49..7b5e80a093 100644 --- a/plugins/humanitec-common/src/clients/humanitec.ts +++ b/plugins/humanitec-common/src/clients/humanitec.ts @@ -60,6 +60,18 @@ export function createHumanitecClient({ orgId, token }: { token: string; orgId: const result = await _fetch('GET', `apps/${appId}/envs/${envId}/resources`); return ResourcesResponsePayload.parse(result); }, + + async getEnvironmentLogsSnapshot(appId: string, envId: string) { + type Message = { + workload_id: string; + container_id: string; + payload: string; + level: string + }; + + return await _fetch('GET', `apps/${appId}/envs/${envId.toLowerCase()}/logs?limit=100&invert=true`); + + }, buildUrl(params: URLs) { const baseUrl = `https://app.humanitec.io/orgs/${orgId}`; switch (params.resource) { @@ -89,6 +101,8 @@ export function createHumanitecClient({ orgId, token }: { token: string; orgId: if (r.ok) { return await r.json() as R; + } else { + console.dir({ error: r.statusText }); } throw new FetchError(`Fetch ${method} to ${url} failed.`, r); @@ -98,4 +112,4 @@ export function createHumanitecClient({ orgId, token }: { token: string; orgId: retry: async (e: FetchError) => e.status === 403 }); } -} \ No newline at end of file +} diff --git a/plugins/humanitec-common/src/platform-api.ts b/plugins/humanitec-common/src/platform-api.ts index ea6fa57350..574b4d45e8 100644 --- a/plugins/humanitec-common/src/platform-api.ts +++ b/plugins/humanitec-common/src/platform-api.ts @@ -4,9 +4,26 @@ import type { Entity } from '@backstage/catalog-model'; import { HUMANITEC_APP_ID_ANNOTATION, HUMANITEC_ORG_ID_ANNOTATION } from './constants'; import { createHumanitecClient } from './clients/humanitec'; -export function createHumanitecPlatformApi({ token }: { token: string }): Pick { +export function createHumanitecPlatformApi({ token }: { token: string }): Partial { return { + async *getLogs(ref, envId) { + const entity = await ref.load(); + const { appId, orgId } = getHumanitecMetadata(entity); + const client = createHumanitecClient({ token, orgId }); + const logs = await client.getEnvironmentLogsSnapshot(appId, envId); + const bound = Math.floor(logs.length * .5); + const send = logs.slice(0, bound); + const stream = logs.slice(bound, logs.length); + for (const message of send) { + yield message.payload; + } + for (const message of stream) { + await new Promise((resolve) => setTimeout(resolve, Math.random() * 1500)); + yield message.payload; + } + }, + async getEnvironments(ref) { const entity = await ref.load(); const { appId, orgId } = getHumanitecMetadata(entity); @@ -30,7 +47,7 @@ export function createHumanitecPlatformApi({ token }: { token: string }): Pick

string> = { - 'log': blue, - 'completion': green, - 'error': red -} +const logTextColors: Record string> = { + "log": blue, + "completion": green, + "error": red, +}; function logSSEMessage(raw: string) { - const message: SSEMessage = JSON.parse(raw) + const message: SSEMessage = JSON.parse(raw); const color = logTextColors[message.type]; - const timestamp = format(new Date(message.createdAt), 'dd-MM-yyyy:hh:mm'); + const timestamp = format(new Date(message.createdAt), "dd-MM-yyyy:hh:mm"); const logType = color(`[${message.type.toLocaleUpperCase()} - ${timestamp}]`); console.log(`${logType} - ${message.body.message})`); @@ -42,21 +54,23 @@ function logSSEMessage(raw: string) { logSSEMessage(JSON.stringify({ type: "error", body: { - message: message.body.error.message + message: message.body.error.message, }, - createdAt: message.createdAt - })) + createdAt: message.createdAt, + })); } } export async function cli(options: CLIOptions) { let { apiURL, description, args, name, target } = options; - let get: typeof fetch = (endpoint, init) => fetch(`${apiURL}/${endpoint}`, init); + let get: typeof fetch = (endpoint, init) => + fetch(`${apiURL}/${endpoint}`, init); - let post = (endpoint: string, init: Omit) => fetch(`${apiURL}/${endpoint}`, { - method: 'POST', - ...init - }); + let post = (endpoint: string, init: Omit) => + fetch(`${apiURL}/${endpoint}`, { + method: "POST", + ...init, + }); const cmd = new Command() .name(name) @@ -93,20 +107,22 @@ export async function cli(options: CLIOptions) { }) .command( "clone", - "clone a repository associated with a component" + "clone a repository associated with a component", ) - .option('-S, --ssh', 'Use ssh url to clone repository') - .option('-H, --https', 'Use https url to clone repository') + .option("-S, --ssh", "Use ssh url to clone repository") + .option("-H, --https", "Use https url to clone repository") .arguments( "[component:string] [directory:string]", ) .action(async ({ ssh, https }, component, directory) => { const output = directory ?? component; if (ssh && https) { - throw new MainError(`Invalid options: --ssh and --https can't be used together - use one or the other.`) + throw new MainError( + `Invalid options: --ssh and --https can't be used together - use one or the other.`, + ); } // prefer ssh - const protocol = !(ssh && https) || ssh ? 'ssh' : 'https'; + const protocol = !(ssh && https) || ssh ? "ssh" : "https"; if (component) { let response: Response; try { @@ -118,10 +134,12 @@ export async function cli(options: CLIOptions) { const urls = await response.json(); const url = urls[protocol]; const clone = Deno.run({ - cmd: ['git', 'clone', url, output] + cmd: ["git", "clone", url, output], }); if (!(await clone.status()).success) { - throw new MainError(`Encountered an error cloning "${url}" to "${output}".`) + throw new MainError( + `Encountered an error cloning "${url}" to "${output}".`, + ); } } else if (response.status === 404) { throw new MainError(`unknown component '${component}'`); @@ -137,20 +155,20 @@ export async function cli(options: CLIOptions) { try { response = await get(`repositories`, { headers: { - Accept: 'text/plain' - } + Accept: "text/plain", + }, }); } catch (error) { throw new MainError(error.message); } if (response.ok) { await Deno.stdout.write( - new TextEncoder().encode(await response.text()) - ) + new TextEncoder().encode(await response.text()), + ); } else { throw new MainError( - `communication error with backstage server: ${response.status} ${response.statusText}` - ) + `communication error with backstage server: ${response.status} ${response.statusText}`, + ); } }) .command( @@ -185,7 +203,9 @@ export async function cli(options: CLIOptions) { } } }) - .command('create', `create something new from a template. + .command( + "create", + `create something new from a template. usage: # heredoc @@ -196,11 +216,15 @@ EOF # yaml file idp create -t standard-microservice -f ./f.yaml - `) - .option('-t --template ', 'the scaffolder template', { - default: 'standard-microservice' + `, + ) + .option("-t --template ", "the scaffolder template", { + default: "standard-microservice", }) - .option('-f --file ', `an optional file path to a file containing the template's fields`) + .option( + "-f --file ", + `an optional file path to a file containing the template's fields`, + ) .arguments("[input]") .action(async ({ template, file }, input) => { let body: string | undefined; @@ -216,13 +240,15 @@ idp create -t standard-microservice -f ./f.yaml const response = await post(`create/${template}`, { headers: { - 'Content-Type': 'text/plain', + "Content-Type": "text/plain", }, - body + body, }); if (response.status !== 200) { - throw new MainError(`create failed with ${response.status} - ${response.statusText}`) + throw new MainError( + `create failed with ${response.status} - ${response.statusText}`, + ); } // deno-lint-ignore no-explicit-any @@ -240,15 +266,27 @@ idp create -t standard-microservice -f ./f.yaml const eventSourceUrl = `${apiURL}/tasks/${taskId}/eventstream`; - const eventSource = new EventSource(eventSourceUrl, { withCredentials: true }); + const eventSource = new EventSource(eventSourceUrl, { + withCredentials: true, + }); - eventSource.addEventListener('log', sseMessageHandler); - eventSource.addEventListener('completion', (event: any) => { + eventSource.addEventListener("log", sseMessageHandler); + eventSource.addEventListener("completion", (event: any) => { sseMessageHandler(event); eventSource.close(); }); - eventSource.addEventListener('error', sseMessageHandler); + eventSource.addEventListener("error", sseMessageHandler); + }) + .command("logs") + .option( + "-c --component ", + "the component for which to fetch the logs", + ) + .action(async ({ component }) => { + let name = await findEntityContext(component); + let response = await get(`logs/${name}`); + await response.body?.pipeTo(Deno.stdout.writable); }); try { diff --git a/plugins/platform-backend/cli/deps.ts b/plugins/platform-backend/cli/deps.ts index 2f37b60597..78f79a57dc 100644 --- a/plugins/platform-backend/cli/deps.ts +++ b/plugins/platform-backend/cli/deps.ts @@ -4,8 +4,8 @@ export * as yaml from "https://deno.land/std@0.159.0/encoding/yaml.ts"; export { format } from "https://deno.land/std@0.159.0/datetime/mod.ts"; export { Command } from "https://deno.land/x/cliffy@v0.25.2/command/mod.ts"; export { EventSource } from "https://deno.land/x/eventsource@v0.0.2/mod.ts"; -export { red, blue, green } from "https://deno.land/std@0.159.0/fmt/colors.ts" -export { readAll } from "https://deno.land/std@0.159.0/streams/conversion.ts?s=copy"; +export { blue, green, red } from "https://deno.land/std@0.159.0/fmt/colors.ts"; +export { readAll } from "https://deno.land/std@0.159.0/streams/conversion.ts?s=copy"; // we tried to get this from backstage. We really did. export interface Entity { diff --git a/plugins/platform-backend/src/service/router.ts b/plugins/platform-backend/src/service/router.ts index eb218726df..d58aa11458 100644 --- a/plugins/platform-backend/src/service/router.ts +++ b/plugins/platform-backend/src/service/router.ts @@ -27,6 +27,7 @@ import type { Logger } from 'winston'; import { getDownloadInfo } from '../executables'; import { GetComponentRef, PlatformApi } from '../types'; import { Repositories } from './routes/repositories'; +import { Logs } from './routes/logs'; export interface RouterOptions { logger: Logger; @@ -173,6 +174,12 @@ export async function createRouter( catalog })); + router.use('/logs', Logs({ + getComponentRef, + platform, + catalog + })); + router.use(errorHandler()); return router; } diff --git a/plugins/platform-backend/src/service/routes/logs.ts b/plugins/platform-backend/src/service/routes/logs.ts new file mode 100644 index 0000000000..6ba6de2225 --- /dev/null +++ b/plugins/platform-backend/src/service/routes/logs.ts @@ -0,0 +1,51 @@ +import type { CatalogClient } from '@backstage/catalog-client'; +import Router from 'express-promise-router'; +import express from 'express'; +import { EntityRef, GetComponentRef, PlatformApi } from '../../types'; + +interface RouteOptions { + platform: PlatformApi; + catalog: CatalogClient; + getComponentRef: GetComponentRef; +} + +type Route = (options: RouteOptions) => express.Router; + +export const Logs: Route = ({ platform, getComponentRef }) => { + const router = Router(); + + router.get('/:component', async (req, response) => { + // Mandatory headers and http status to keep connection open + response.writeHead(200, { + 'Connection': 'keep-alive', + 'Cache-Control': 'no-cache', + 'Content-Type': 'text/event-stream', + }); + + let closed = false; + + req.on('close', () => { + closed = true; + }); + + let ref = await getComponentRef(req.params.component); + for await (const line of platform.getLogs(ref, "Development")) { + if (closed) { + return; + } else { + response.write(`${line}\n`); + flush(response); + } + } + }); + + + return router; +} + +function flush(response: express.Response) { + const flushable = response as unknown as { flush: Function }; + if (typeof flushable.flush === 'function') { + flushable.flush(); + } +} diff --git a/plugins/platform-backend/src/service/routes/repositories.ts b/plugins/platform-backend/src/service/routes/repositories.ts index e64d136437..24eea9223a 100644 --- a/plugins/platform-backend/src/service/routes/repositories.ts +++ b/plugins/platform-backend/src/service/routes/repositories.ts @@ -1,11 +1,11 @@ -import type { CatalogClient } from '@backstage/catalog-client'; +import type { CatalogClient } from '@backstage/catalog-client'; import CliTable3 from 'cli-table3'; import chalk from 'chalk'; import Router from 'express-promise-router'; import express from 'express'; import { GetComponentRef, PlatformApi } from '../../types'; -interface RouteOptions { +interface RouteOptions { platform: PlatformApi; catalog: CatalogClient; getComponentRef: GetComponentRef; @@ -36,7 +36,7 @@ export const Repositories: Route = ({ platform, getComponentRef }) => { router.get('/:component/urls', async (req, res) => { const name = req.params.component; - + const ref = await getComponentRef(name); const urls = await platform.getRepositoryUrls(ref); @@ -47,6 +47,6 @@ export const Repositories: Route = ({ platform, getComponentRef }) => { res.send("Not Found"); } }); - + return router; } diff --git a/plugins/platform-backend/src/types.ts b/plugins/platform-backend/src/types.ts index ce9e27ad67..a7e7c5b33f 100644 --- a/plugins/platform-backend/src/types.ts +++ b/plugins/platform-backend/src/types.ts @@ -18,6 +18,7 @@ export interface RepositoryUrls { } export interface PlatformApi { + getLogs(ref: EntityRef, environment: string): AsyncIterable; getEnvironments(ref: EntityRef, page?: PageSpec): Promise>; getRepositories(page?: PageSpec): Promise> getRepositoryUrls(ref: EntityRef): Promise From b8439ded27166229bb36146d063384959347fa03 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Sun, 23 Oct 2022 22:03:14 -0500 Subject: [PATCH 32/37] =?UTF-8?q?=F0=9F=9A=A8Fix=20TS=20Errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/humanitec-common/src/platform-api.ts | 4 +++- plugins/platform-backend/src/service/routes/logs.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/humanitec-common/src/platform-api.ts b/plugins/humanitec-common/src/platform-api.ts index 574b4d45e8..9999e3a93f 100644 --- a/plugins/humanitec-common/src/platform-api.ts +++ b/plugins/humanitec-common/src/platform-api.ts @@ -4,7 +4,9 @@ import type { Entity } from '@backstage/catalog-model'; import { HUMANITEC_APP_ID_ANNOTATION, HUMANITEC_ORG_ID_ANNOTATION } from './constants'; import { createHumanitecClient } from './clients/humanitec'; -export function createHumanitecPlatformApi({ token }: { token: string }): Partial { + +export type HumanitecPlatformAPI = Pick +export function createHumanitecPlatformApi({ token }: { token: string }): HumanitecPlatformAPI { return { async *getLogs(ref, envId) { diff --git a/plugins/platform-backend/src/service/routes/logs.ts b/plugins/platform-backend/src/service/routes/logs.ts index 6ba6de2225..6dc9ed53d5 100644 --- a/plugins/platform-backend/src/service/routes/logs.ts +++ b/plugins/platform-backend/src/service/routes/logs.ts @@ -1,7 +1,7 @@ import type { CatalogClient } from '@backstage/catalog-client'; import Router from 'express-promise-router'; import express from 'express'; -import { EntityRef, GetComponentRef, PlatformApi } from '../../types'; +import { GetComponentRef, PlatformApi } from '../../types'; interface RouteOptions { platform: PlatformApi; From 0a285bb23ba3500723a8defcaf06139f9b0fcbf1 Mon Sep 17 00:00:00 2001 From: minkimcello Date: Mon, 24 Oct 2022 07:48:42 -0400 Subject: [PATCH 33/37] Return more info from platform api in humanitec --- plugins/humanitec-common/src/platform-api.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/humanitec-common/src/platform-api.ts b/plugins/humanitec-common/src/platform-api.ts index 9999e3a93f..86d91a9ad0 100644 --- a/plugins/humanitec-common/src/platform-api.ts +++ b/plugins/humanitec-common/src/platform-api.ts @@ -41,6 +41,8 @@ export function createHumanitecPlatformApi({ token }: { token: string }): Humani value: { id: env.id, name: env.name, + type: env.type, + url: `https://app.humanitec.io/orgs/${orgId}/apps/${appId}/envs/${env.id}` } })), }; From 5803636b59f57a5bb06645abbe74527290cfe25e Mon Sep 17 00:00:00 2001 From: minkimcello Date: Mon, 24 Oct 2022 07:49:34 -0400 Subject: [PATCH 34/37] Print out table for idp env --- plugins/platform-backend/src/service/router.ts | 12 +++++++++--- plugins/platform-backend/src/types.ts | 2 ++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/plugins/platform-backend/src/service/router.ts b/plugins/platform-backend/src/service/router.ts index d58aa11458..441e180249 100644 --- a/plugins/platform-backend/src/service/router.ts +++ b/plugins/platform-backend/src/service/router.ts @@ -28,6 +28,8 @@ import { getDownloadInfo } from '../executables'; import { GetComponentRef, PlatformApi } from '../types'; import { Repositories } from './routes/repositories'; import { Logs } from './routes/logs'; +import CliTable3 from 'cli-table3'; +import chalk from 'chalk'; export interface RouterOptions { logger: Logger; @@ -114,10 +116,14 @@ export async function createRouter( let name = req.params.name; let ref = await getComponentRef(name); if (ref) { + const table = new CliTable3({ + head: ["ID", "Name", "Type", "URL"] + }); let environments = await platform.getEnvironments(ref); - let names = environments.items.map(({ value }) => value.name); - - res.send(`${names.join("\n")}\n`); + table.push( + ...environments.items.map(({ value: r }) => ([r.id, r.name, r.type, r.url ])) + ) + res.send(`\n${chalk.bold(' ☀️ Deployment Environments')}\n${table}\n`); } else { res.sendStatus(404); res.send("Not Found"); diff --git a/plugins/platform-backend/src/types.ts b/plugins/platform-backend/src/types.ts index a7e7c5b33f..7e03c38db2 100644 --- a/plugins/platform-backend/src/types.ts +++ b/plugins/platform-backend/src/types.ts @@ -3,6 +3,8 @@ import type { Entity, CompoundEntityRef } from '@backstage/catalog-model'; export interface Environment { id: string; name: string; + type: string; + url: string; } export interface Repository { From 499986a97b376a309784b8f6df40a8cdd69fba9c Mon Sep 17 00:00:00 2001 From: Taras Date: Wed, 26 Oct 2022 12:03:44 -0400 Subject: [PATCH 35/37] Use local template file --- app-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index 0667d77ad1..221be52452 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -81,8 +81,8 @@ catalog: locations: - type: url target: https://github.com/thefrontside/backstage/blob/main/catalog-info.yaml - - type: url - target: https://github.com/thefrontside/backstage/blob/main/templates/standard-microservice/template.yaml + - type: file + target: ../../templates/standard-microservice/template.yaml rules: - allow: [Template] From fce7f37bfaf28990f209816feb6b22794b5298a7 Mon Sep 17 00:00:00 2001 From: Taras Date: Wed, 26 Oct 2022 12:05:32 -0400 Subject: [PATCH 36/37] Added postgres option --- templates/standard-microservice/template.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/templates/standard-microservice/template.yaml b/templates/standard-microservice/template.yaml index b1dba468dd..7a2d9334a9 100644 --- a/templates/standard-microservice/template.yaml +++ b/templates/standard-microservice/template.yaml @@ -27,6 +27,15 @@ spec: ui:options: allowedHosts: - github.com + - title: Deployment + properties: + database: + title: PostgreSQL + type: number + enum: + - 15 + - 14.5 + - 13 steps: - name: Read Environment id: environment @@ -41,6 +50,7 @@ spec: orgId: ${{ steps.environment.output.orgId }} appId: ${{ parameters.componentName }} registryUrl: ${{ steps.environment.output.registryUrl }} + postgresVersion: ${{ parameters.database }} - name: Create Repository on Github id: publish action: publish:github From ba9160a2f38871390e373802f71f6419b67e1628 Mon Sep 17 00:00:00 2001 From: Taras Date: Wed, 26 Oct 2022 14:34:19 -0400 Subject: [PATCH 37/37] Pass simple string into profile --- templates/standard-microservice/template.yaml | 12 ++++++------ .../template/humanitec-apps.yaml | 1 + 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/templates/standard-microservice/template.yaml b/templates/standard-microservice/template.yaml index 7a2d9334a9..5663b482ee 100644 --- a/templates/standard-microservice/template.yaml +++ b/templates/standard-microservice/template.yaml @@ -30,12 +30,12 @@ spec: - title: Deployment properties: database: - title: PostgreSQL - type: number + title: Database + type: string enum: - - 15 - - 14.5 - - 13 + - PostgreSQL 15 + - PostgreSQL 14.5 + - PostgreSQL 13 steps: - name: Read Environment id: environment @@ -50,7 +50,7 @@ spec: orgId: ${{ steps.environment.output.orgId }} appId: ${{ parameters.componentName }} registryUrl: ${{ steps.environment.output.registryUrl }} - postgresVersion: ${{ parameters.database }} + databaseVersion: ${{ parameters.database }} - name: Create Repository on Github id: publish action: publish:github diff --git a/templates/standard-microservice/template/humanitec-apps.yaml b/templates/standard-microservice/template/humanitec-apps.yaml index 3ef76ff860..3ffe5ce151 100644 --- a/templates/standard-microservice/template/humanitec-apps.yaml +++ b/templates/standard-microservice/template/humanitec-apps.yaml @@ -14,6 +14,7 @@ environments: http: type: dns profile: humanitec/default-module + database: ${{ values.database }} spec: containers: ${{values.componentName | dump}}: