Merge remote-tracking branch 'origin/master' into cost-insight-trending-line

This commit is contained in:
bogdannechyporenko
2022-11-21 21:24:52 +01:00
53 changed files with 1715 additions and 909 deletions
+21
View File
@@ -0,0 +1,21 @@
---
'@backstage/plugin-events-backend': minor
---
**BREAKING:** Remove required field `router` at `HttpPostIngressEventPublisher.fromConfig`
and replace it with `bind(router: Router)`.
Additionally, the path prefix `/http` will be added inside `HttpPostIngressEventPublisher`.
```diff
// at packages/backend/src/plugins/events.ts
const eventsRouter = Router();
- const httpRouter = Router();
- eventsRouter.use('/http', httpRouter);
const http = HttpPostIngressEventPublisher.fromConfig({
config: env.config,
logger: env.logger,
- router: httpRouter,
});
+ http.bind(eventsRouter);
```
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Use Json types from @backstage/types
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-components': patch
---
Create a variable for minimum height and add a prop named 'fit' for determining if the graph height should grow or be contained.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-explore': patch
---
Adds styling to graph forcing it to always fill out the available space.
+9
View File
@@ -0,0 +1,9 @@
---
'@backstage/plugin-events-backend': patch
'@backstage/plugin-events-node': minor
---
Introduce a new interface `RequestDetails` to abstract `Request`
providing access to request body and headers.
**BREAKING:** Replace `request: Request` with `request: RequestDetails` at `RequestValidator`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-app-api': patch
---
Added `lifecycleFactory` implementation.
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/core-components': patch
'@backstage/plugin-codescene': patch
'@backstage/plugin-sonarqube': patch
---
Updated dependency `rc-progress` to `3.4.1`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Registered shutdown hook in experimental catalog plugin.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog': patch
---
Fixed Entity kind pluralisation in the `CatalogKindHeader` component.
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-vault-backend': patch
---
Use `express-promise-router` to catch errors properly.
Add `403` error as a known one. It will now return a `NotAllowed` error.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-defaults': patch
---
Added `lifecycleFactory` to default service factories.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-plugin-api': patch
---
Added initial support for registering shutdown hooks via `lifecycleServiceRef`.
@@ -0,0 +1,63 @@
name: Automate merge message
on:
pull_request:
branches: ['master']
types: ['closed']
permissions:
pull-requests: write
actions: none
checks: none
contents: none
deployments: none
issues: none
packages: none
pages: none
repository-projects: none
security-events: none
statuses: none
jobs:
message:
# prevent running towards forks, and only run on merged PRs
if: github.repository == 'backstage/backstage' && github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
ref: '${{ github.event.pull_request.merge_commit_sha }}'
- name: fetch base
run: git fetch --depth 1 origin ${{ github.event.pull_request.base.sha }}
# We avoid using the in-source script since this workflow has elevated permissions that we don't want to expose
- name: Generate Message
id: generate-message
run: |
rm -f generate.js
wget -O generate.js https://raw.githubusercontent.com/backstage/backstage/master/scripts/generate-merge-message.js 1>&2
node generate.js FETCH_HEAD > message.txt
- name: Post Message
uses: actions/github-script@v6
env:
ISSUE_NUMBER: ${{ github.event.pull_request.number }}
with:
script: |
const owner = "backstage";
const repo = "backstage";
const body = require('fs').readFileSync('message.txt', 'utf8').trim();
const issue_number = Number(process.env.ISSUE_NUMBER);
if (!body) {
console.log(`skipping comment for #${issue_number}`);
return;
}
console.log(`creating comment for #${issue_number}`);
await github.rest.issues.createComment({
owner,
repo,
issue_number,
body,
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

@@ -159,7 +159,12 @@ Check out the numbered markings - let's go through them one by one.
the outcome of that. This example issues a `fetch` to the right service and
issues a full refresh of its entity bucket based on that.
5. The method translates the foreign data model to the native `Entity` form, as
expected by the catalog.
expected by the catalog. The `Entity` must include the
`backstage.io/managed-by-location` and
`backstage.io/managed-by-origin-location annotations`; otherwise, it will not
appear in the Catalog and will generate warning logs. The
[Well-known Annotations](./well-known-annotations.md#backstageiomanaged-by-location)
documentation has guidance on what values to use for these.
6. Finally, we issue a "mutation" to the catalog. This persists the entities in
our own bucket, along with an optional `locationKey` that's used for conflict
checks. But this is a bigger topic - mutations warrant their own explanatory
+92
View File
@@ -393,6 +393,98 @@ You can add more icons, if the [default icons](https://github.com/backstage/back
Note: If the icon is not available as one of the default icons or one you've added then it will fall back to Material UI's `LanguageIcon`
## Custom Sidebar
As you've seen there are many ways that you can customize your Backstage app. The following section will show you how you can customize the sidebar.
### Sidebar Sub-menu
For this example we'll show you how you can expand the sidebar with a sub-menu:
1. Open the `Root.tsx` file located in `packages/app/src/components/Root` as this is where the sidebar code lives
2. Then we want to add the following imports for the icons:
```ts
import ApiIcon from '@material-ui/icons/Extension';
import ComponentIcon from '@material-ui/icons/Memory';
import DomainIcon from '@material-ui/icons/Apartment';
import ResourceIcon from '@material-ui/icons/Work';
import SystemIcon from '@material-ui/icons/Category';
import UserIcon from '@material-ui/icons/Person';
```
3. Then update the `@backstage/core-components` import like this:
```diff
import {
Sidebar,
sidebarConfig,
SidebarDivider,
SidebarGroup,
SidebarItem,
SidebarPage,
SidebarScrollWrapper,
SidebarSpace,
useSidebarOpenState,
Link,
+ GroupIcon,
+ SidebarSubmenu,
+ SidebarSubmenuItem,
} from '@backstage/core-components';
```
4. Finally replace `<SidebarItem icon={HomeIcon} to="catalog" text="Home" />` with this:
```ts
<SidebarItem icon={HomeIcon} to="catalog" text="Home">
<SidebarSubmenu title="Catalog">
<SidebarSubmenuItem
title="Domains"
to="catalog?filters[kind]=domain"
icon={DomainIcon}
/>
<SidebarSubmenuItem
title="Systems"
to="catalog?filters[kind]=system"
icon={SystemIcon}
/>
<SidebarSubmenuItem
title="Components"
to="catalog?filters[kind]=component"
icon={ComponentIcon}
/>
<SidebarSubmenuItem
title="APIs"
to="catalog?filters[kind]=api"
icon={ApiIcon}
/>
<SidebarDivider />
<SidebarSubmenuItem
title="Resources"
to="catalog?filters[kind]=resource"
icon={ResourceIcon}
/>
<SidebarDivider />
<SidebarSubmenuItem
title="Groups"
to="catalog?filters[kind]=group"
icon={GroupIcon}
/>
<SidebarSubmenuItem
title="Users"
to="catalog?filters[kind]=user"
icon={UserIcon}
/>
</SidebarSubmenu>
</SidebarItem>
```
When you startup your Backstage app and hover over the Home option on the sidebar you'll now see a nice sub-menu appear with links to the various Kinds in your Catalog. It would look like this:
![Sidebar sub-menu example](./../assets/getting-started/sidebar-submenu-example.png)
You can see more ways to use this in the [Storybook Sidebar examples](https://backstage.io/storybook/?path=/story/layout-sidebar--sample-scalable-sidebar)
## Custom Homepage
In addition to a custom theme, a custom logo, you can also customize the
+13
View File
@@ -0,0 +1,13 @@
---
title: Cloudsmith
author: Roadie
authorUrl: https://roadie.io
category: CI/CD
description: Show Cloudsmith Repository stats on your backstage homepage.
documentation: https://github.com/RoadieHQ/roadie-backstage-plugins/tree/main/plugins/frontend/backstage-plugin-cloudsmith
iconUrl: https://cloudsmith.com/img/cloudsmith-mini-dark.svg
npmPackageName: '@roadiehq/backstage-plugin-cloudsmith'
tags:
- dashboards
- monitoring
addedDate: '2022-11-18'
+50 -1
View File
@@ -47,12 +47,21 @@ import {
SidebarSpace,
Link,
useSidebarOpenState,
SidebarSubmenu,
SidebarSubmenuItem,
} from '@backstage/core-components';
import { MyGroupsSidebarItem } from '@backstage/plugin-org';
import GroupIcon from '@material-ui/icons/People';
import { SearchModal } from '../search/SearchModal';
import Score from '@material-ui/icons/Score';
import ApiIcon from '@material-ui/icons/Extension';
import ComponentIcon from '@material-ui/icons/Memory';
import DomainIcon from '@material-ui/icons/Apartment';
import ResourceIcon from '@material-ui/icons/Work';
import SystemIcon from '@material-ui/icons/Category';
import UserIcon from '@material-ui/icons/Person';
const useSidebarLogoStyles = makeStyles({
root: {
width: sidebarConfig.drawerWidthClosed,
@@ -93,7 +102,47 @@ export const Root = ({ children }: PropsWithChildren<{}>) => (
<SidebarDivider />
<SidebarGroup label="Menu" icon={<MenuIcon />}>
{/* Global nav, not org-specific */}
<SidebarItem icon={HomeIcon} to="catalog" text="Home" />
<SidebarItem icon={HomeIcon} to="catalog" text="Home">
<SidebarSubmenu title="Catalog">
<SidebarSubmenuItem
title="Domains"
to="catalog?filters[kind]=domain"
icon={DomainIcon}
/>
<SidebarSubmenuItem
title="Systems"
to="catalog?filters[kind]=system"
icon={SystemIcon}
/>
<SidebarSubmenuItem
title="Components"
to="catalog?filters[kind]=component"
icon={ComponentIcon}
/>
<SidebarSubmenuItem
title="APIs"
to="catalog?filters[kind]=api"
icon={ApiIcon}
/>
<SidebarDivider />
<SidebarSubmenuItem
title="Resources"
to="catalog?filters[kind]=resource"
icon={ResourceIcon}
/>
<SidebarDivider />
<SidebarSubmenuItem
title="Groups"
to="catalog?filters[kind]=group"
icon={GroupIcon}
/>
<SidebarSubmenuItem
title="Users"
to="catalog?filters[kind]=user"
icon={UserIcon}
/>
</SidebarSubmenu>
</SidebarItem>
<MyGroupsSidebarItem
singularTitle="My Squad"
pluralTitle="My Squads"
+6
View File
@@ -4,6 +4,7 @@
```ts
import { BackendFeature } from '@backstage/backend-plugin-api';
import { BackendLifecycle } from '@backstage/backend-plugin-api';
import { Config } from '@backstage/config';
import { ExtensionPoint } from '@backstage/backend-plugin-api';
import { HttpRouterService } from '@backstage/backend-plugin-api';
@@ -66,6 +67,11 @@ export type HttpRouterFactoryOptions = {
indexPlugin?: string;
};
// @public
export const lifecycleFactory: (
options?: undefined,
) => ServiceFactory<BackendLifecycle>;
// @public (undocumented)
export const loggerFactory: (options?: undefined) => ServiceFactory<Logger>;
@@ -25,4 +25,5 @@ export { schedulerFactory } from './schedulerService';
export { tokenManagerFactory } from './tokenManagerService';
export { urlReaderFactory } from './urlReaderService';
export { httpRouterFactory } from './httpRouterService';
export { lifecycleFactory } from './lifecycleService';
export type { HttpRouterFactoryOptions } from './httpRouterService';
@@ -0,0 +1,47 @@
/*
* Copyright 2022 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 { BackendLifecycleImpl } from './lifecycleService';
describe('lifecycleService', () => {
it('should execute registered shutdown hook', async () => {
const service = new BackendLifecycleImpl(getVoidLogger());
const hook = jest.fn();
service.addShutdownHook({
pluginId: 'test',
fn: async () => {
hook();
},
});
// should not execute the hook more than once.
await service.shutdown();
await service.shutdown();
await service.shutdown();
expect(hook).toHaveBeenCalledTimes(1);
});
it('should not throw errors', async () => {
const service = new BackendLifecycleImpl(getVoidLogger());
service.addShutdownHook({
pluginId: 'test',
fn: async () => {
throw new Error('oh no');
},
});
await expect(service.shutdown()).resolves.toBeUndefined();
});
});
@@ -0,0 +1,96 @@
/*
* Copyright 2022 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 {
BackendLifecycle,
createServiceFactory,
lifecycleServiceRef,
loggerToWinstonLogger,
pluginMetadataServiceRef,
rootLoggerServiceRef,
BackendLifecycleShutdownHook,
} from '@backstage/backend-plugin-api';
import { Logger } from 'winston';
const CALLBACKS = ['SIGTERM', 'SIGINT', 'beforeExit'];
export class BackendLifecycleImpl {
constructor(private readonly logger: Logger) {
CALLBACKS.map(signal => process.on(signal, () => this.shutdown()));
}
#isCalled = false;
#shutdownTasks: Array<BackendLifecycleShutdownHook & { pluginId: string }> =
[];
addShutdownHook(
options: BackendLifecycleShutdownHook & { pluginId: string },
): void {
this.#shutdownTasks.push(options);
}
async shutdown(): Promise<void> {
if (this.#isCalled) {
return;
}
this.#isCalled = true;
this.logger.info(`Running ${this.#shutdownTasks.length} shutdown tasks...`);
await Promise.all(
this.#shutdownTasks.map(hook =>
Promise.resolve()
.then(() => hook.fn())
.catch(e => {
this.logger.error(
`Shutdown hook registered by plugin '${hook.pluginId}' failed with: ${e}`,
);
})
.then(() =>
this.logger.info(
`Successfully ran shutdown hook registered by plugin ${hook.pluginId}`,
),
),
),
);
}
}
class PluginScopedLifecycleImpl implements BackendLifecycle {
constructor(
private readonly lifecycle: BackendLifecycleImpl,
private readonly pluginId: string,
) {}
addShutdownHook(options: BackendLifecycleShutdownHook): void {
this.lifecycle.addShutdownHook({ ...options, pluginId: this.pluginId });
}
}
/**
* Allows plugins to register shutdown hooks that are run when the process is about to exit.
* @public */
export const lifecycleFactory = createServiceFactory({
service: lifecycleServiceRef,
deps: {
logger: rootLoggerServiceRef,
plugin: pluginMetadataServiceRef,
},
async factory({ logger }) {
const rootLifecycle = new BackendLifecycleImpl(
loggerToWinstonLogger(logger),
);
return async ({ plugin }) => {
return new PluginScopedLifecycleImpl(rootLifecycle, plugin.getId());
};
},
});
@@ -22,6 +22,7 @@ import {
databaseFactory,
discoveryFactory,
httpRouterFactory,
lifecycleFactory,
loggerFactory,
permissionsFactory,
rootLoggerFactory,
@@ -43,6 +44,7 @@ export const defaultServiceFactories = [
tokenManagerFactory,
urlReaderFactory,
httpRouterFactory,
lifecycleFactory,
];
/**
+13
View File
@@ -24,6 +24,16 @@ export interface BackendFeature {
register(reg: BackendRegistrationPoints): void;
}
// @public (undocumented)
export interface BackendLifecycle {
addShutdownHook(options: BackendLifecycleShutdownHook): void;
}
// @public (undocumented)
export type BackendLifecycleShutdownHook = {
fn: () => void | Promise<void>;
};
// @public (undocumented)
export interface BackendModuleConfig<TOptions> {
// (undocumented)
@@ -158,6 +168,9 @@ export interface HttpRouterService {
// @public (undocumented)
export const httpRouterServiceRef: ServiceRef<HttpRouterService, 'plugin'>;
// @public (undocumented)
export const lifecycleServiceRef: ServiceRef<BackendLifecycle, 'plugin'>;
// @public (undocumented)
export interface Logger {
// (undocumented)
@@ -28,4 +28,9 @@ export { permissionsServiceRef } from './permissionsServiceRef';
export { schedulerServiceRef } from './schedulerServiceRef';
export { rootLoggerServiceRef } from './rootLoggerServiceRef';
export { pluginMetadataServiceRef } from './pluginMetadataServiceRef';
export { lifecycleServiceRef } from './lifecycleServiceRef';
export type {
BackendLifecycle,
BackendLifecycleShutdownHook,
} from './lifecycleServiceRef';
export type { PluginMetadata } from './pluginMetadataServiceRef';
@@ -0,0 +1,42 @@
/*
* Copyright 2022 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 { createServiceRef } from '../system/types';
/**
* @public
**/
export type BackendLifecycleShutdownHook = {
fn: () => void | Promise<void>;
};
/**
* @public
**/
export interface BackendLifecycle {
/**
* Register a function to be called when the backend is shutting down.
*/
addShutdownHook(options: BackendLifecycleShutdownHook): void;
}
/**
* @public
*/
export const lifecycleServiceRef = createServiceRef<BackendLifecycle>({
id: 'core.lifecycle',
scope: 'plugin',
});
+1 -3
View File
@@ -27,14 +27,12 @@ export default async function createPlugin(
subscribers: EventSubscriber[],
): Promise<Router> {
const eventsRouter = Router();
const httpRouter = Router();
eventsRouter.use('/http', httpRouter);
const http = HttpPostIngressEventPublisher.fromConfig({
config: env.config,
logger: env.logger,
router: httpRouter,
});
http.bind(eventsRouter);
await new EventsBackend(env.logger)
.addPublishers(http)
+1
View File
@@ -250,6 +250,7 @@ export interface DependencyGraphProps<NodeData, EdgeData>
edgeRanks?: number;
edges: DependencyEdge<EdgeData>[];
edgeWeight?: number;
fit?: 'grow' | 'contain';
labelOffset?: number;
// Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver
labelPosition?: LabelPosition;
+1 -1
View File
@@ -56,7 +56,7 @@
"pluralize": "^8.0.0",
"prop-types": "^15.7.2",
"qs": "^6.9.4",
"rc-progress": "3.4.0",
"rc-progress": "3.4.1",
"react-helmet": "6.1.0",
"react-hook-form": "^7.12.2",
"react-markdown": "^8.0.0",
@@ -170,6 +170,14 @@ export interface DependencyGraphProps<NodeData, EdgeData>
* Default: 'curveMonotoneX'
*/
curve?: 'curveStepBefore' | 'curveMonotoneX';
/**
* Controls if the graph should be contained or grow
*
* @remarks
*
* Default: 'grow'
*/
fit?: 'grow' | 'contain';
}
const WORKSPACE_ID = 'workspace';
@@ -203,6 +211,7 @@ export function DependencyGraph<NodeData, EdgeData>(
defs,
zoom = 'enabled',
curve = 'curveMonotoneX',
fit = 'grow',
...svgProps
} = props;
const theme: BackstageTheme = useTheme();
@@ -223,6 +232,9 @@ export function DependencyGraph<NodeData, EdgeData>(
const maxWidth = Math.max(graphWidth, containerWidth);
const maxHeight = Math.max(graphHeight, containerHeight);
const minHeight = Math.min(graphHeight, containerHeight);
const scalableHeight = fit === 'grow' ? maxHeight : minHeight;
const containerRef = React.useMemo(
() =>
@@ -394,7 +406,7 @@ export function DependencyGraph<NodeData, EdgeData>(
ref={containerRef}
{...svgProps}
width="100%"
height={maxHeight}
height={scalableHeight}
viewBox={`0 0 ${maxWidth} ${maxHeight}`}
>
<defs>
@@ -22,6 +22,7 @@ import {
permissionsServiceRef,
urlReaderServiceRef,
httpRouterServiceRef,
lifecycleServiceRef,
} from '@backstage/backend-plugin-api';
import { CatalogBuilder } from './CatalogBuilder';
import {
@@ -78,6 +79,7 @@ export const catalogPlugin = createBackendPlugin({
permissions: permissionsServiceRef,
database: databaseServiceRef,
httpRouter: httpRouterServiceRef,
lifecycle: lifecycleServiceRef,
},
async init({
logger,
@@ -86,6 +88,7 @@ export const catalogPlugin = createBackendPlugin({
database,
permissions,
httpRouter,
lifecycle,
}) {
const winstonLogger = loggerToWinstonLogger(logger);
const builder = await CatalogBuilder.create({
@@ -100,7 +103,11 @@ export const catalogPlugin = createBackendPlugin({
const { processingEngine, router } = await builder.build();
await processingEngine.start();
lifecycle.addShutdownHook({
fn: async () => {
await processingEngine.stop();
},
});
httpRouter.use(router);
},
});
+1
View File
@@ -49,6 +49,7 @@
"@material-ui/lab": "4.0.0-alpha.57",
"history": "^5.0.0",
"lodash": "^4.17.21",
"pluralize": "^8.0.0",
"react-helmet": "6.1.0",
"react-use": "^17.2.4",
"zen-observable": "^0.9.0"
@@ -31,6 +31,7 @@ import {
} from '@backstage/test-utils';
import { CatalogKindHeader } from './CatalogKindHeader';
import { errorApiRef } from '@backstage/core-plugin-api';
import pluralize from 'pluralize';
const entities: Entity[] = [
{
@@ -96,7 +97,7 @@ describe('<CatalogKindHeader />', () => {
entities.map(entity => {
expect(
screen.getByRole('option', { name: `${entity.kind}s` }),
screen.getByRole('option', { name: `${pluralize(entity.kind)}` }),
).toBeInTheDocument();
});
});
@@ -31,6 +31,7 @@ import {
} from '@backstage/plugin-catalog-react';
import useAsync from 'react-use/lib/useAsync';
import { useApi } from '@backstage/core-plugin-api';
import pluralize from 'pluralize';
const useStyles = makeStyles((theme: Theme) =>
createStyles({
@@ -132,7 +133,7 @@ export function CatalogKindHeader(props: CatalogKindHeaderProps) {
>
{Object.keys(options).map(kind => (
<MenuItem value={kind} key={kind}>
{`${options[kind]}s`}
{`${pluralize(options[kind])}`}
</MenuItem>
))}
</Select>
+1 -1
View File
@@ -30,7 +30,7 @@
"@material-ui/core": "^4.9.10",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "^4.0.0-alpha.57",
"rc-progress": "3.4.0",
"rc-progress": "3.4.1",
"react-use": "^17.2.4"
},
"peerDependencies": {
+1 -1
View File
@@ -170,8 +170,8 @@ const http = HttpPostIngressEventPublisher.fromConfig({
},
},
logger: env.logger,
router: httpRouter,
});
http.bind(router);
await new EventsBackend(env.logger)
.addPublishers(http)
+2 -1
View File
@@ -33,6 +33,8 @@ export const eventsPlugin: (options?: undefined) => BackendFeature;
// @public
export class HttpPostIngressEventPublisher implements EventPublisher {
// (undocumented)
bind(router: express.Router): void;
// (undocumented)
static fromConfig(env: {
config: Config;
@@ -40,7 +42,6 @@ export class HttpPostIngressEventPublisher implements EventPublisher {
[topic: string]: Omit<HttpPostIngressOptions, 'topic'>;
};
logger: Logger;
router: express.Router;
}): HttpPostIngressEventPublisher;
// (undocumented)
setEventBroker(eventBroker: EventBroker): Promise<void>;
@@ -90,14 +90,11 @@ export const eventsPlugin = createBackendPlugin({
env.registerInit({
deps: {
config: configServiceRef,
httpRouter: httpRouterServiceRef,
logger: loggerServiceRef,
router: httpRouterServiceRef,
},
async init({ config, httpRouter, logger }) {
async init({ config, logger, router }) {
const winstonLogger = loggerToWinstonLogger(logger);
const eventsRouter = Router();
const router = Router();
eventsRouter.use('/http', router);
const ingresses = Object.fromEntries(
extensionPoint.httpPostIngresses.map(ingress => [
@@ -108,23 +105,20 @@ export const eventsPlugin = createBackendPlugin({
const http = HttpPostIngressEventPublisher.fromConfig({
config,
logger: winstonLogger,
router,
ingresses,
logger: winstonLogger,
});
const eventsRouter = Router();
http.bind(eventsRouter);
router.use(eventsRouter);
if (!extensionPoint.eventBroker) {
extensionPoint.setEventBroker(new InMemoryEventBroker(winstonLogger));
}
const eventBroker =
extensionPoint.eventBroker ?? new InMemoryEventBroker(winstonLogger);
extensionPoint.eventBroker!.subscribe(extensionPoint.subscribers);
eventBroker.subscribe(extensionPoint.subscribers);
[extensionPoint.publishers, http]
.flat()
.forEach(publisher =>
publisher.setEventBroker(extensionPoint.eventBroker!),
);
httpRouter.use(eventsRouter);
.forEach(publisher => publisher.setEventBroker(eventBroker));
},
});
},
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { errorHandler, getVoidLogger } from '@backstage/backend-common';
import { getVoidLogger } from '@backstage/backend-common';
import { ConfigReader } from '@backstage/config';
import { TestEventBroker } from '@backstage/plugin-events-backend-test-utils';
import express from 'express';
@@ -35,37 +35,35 @@ describe('HttpPostIngressEventPublisher', () => {
});
const router = Router();
router.use(express.json());
router.use(errorHandler());
const app = express().use(router);
const publisher = HttpPostIngressEventPublisher.fromConfig({
config,
logger,
router,
ingresses: {
testB: {},
},
logger,
});
publisher.bind(router);
const eventBroker = new TestEventBroker();
await publisher.setEventBroker(eventBroker);
const notFoundResponse = await request(app)
.post('/unknown')
.post('/http/unknown')
.timeout(100)
.send({ test: 'data' });
expect(notFoundResponse.status).toBe(404);
const response1 = await request(app)
.post('/testA')
.post('/http/testA')
.set('X-Custom-Header', 'test-value')
.timeout(100)
.send({ testA: 'data' });
expect(response1.status).toBe(202);
const response2 = await request(app)
.post('/testB')
.post('/http/testB')
.set('X-Custom-Header', 'test-value')
.timeout(100)
.send({ testB: 'data' });
@@ -100,14 +98,10 @@ describe('HttpPostIngressEventPublisher', () => {
});
const router = Router();
router.use(express.json());
router.use(errorHandler());
const app = express().use(router);
const publisher = HttpPostIngressEventPublisher.fromConfig({
config,
logger,
router,
ingresses: {
testB: {
validator: async (req, context) => {
@@ -148,26 +142,28 @@ describe('HttpPostIngressEventPublisher', () => {
},
},
},
logger,
});
publisher.bind(router);
const eventBroker = new TestEventBroker();
await publisher.setEventBroker(eventBroker);
const response1 = await request(app)
.post('/testA')
.post('/http/testA')
.timeout(100)
.send({ test: 'data' });
expect(response1.status).toBe(202);
const response2 = await request(app)
.post('/testB')
.post('/http/testB')
.timeout(100)
.send({ test: 'data' });
expect(response2.status).toBe(400);
expect(response2.body).toEqual({ message: 'wrong signature' });
const response3 = await request(app)
.post('/testB')
.post('/http/testB')
.set('X-Test-Signature', 'wrong')
.timeout(100)
.send({ test: 'data' });
@@ -175,21 +171,21 @@ describe('HttpPostIngressEventPublisher', () => {
expect(response3.body).toEqual({ message: 'wrong signature' });
const response4 = await request(app)
.post('/testB')
.post('/http/testB')
.set('X-Test-Signature', 'testB-signature')
.timeout(100)
.send({ test: 'data' });
expect(response4.status).toBe(202);
const response5 = await request(app)
.post('/testC')
.post('/http/testC')
.timeout(100)
.send({ test: 'data' });
expect(response5.status).toBe(404);
expect(response5.body).toEqual({});
const response6 = await request(app)
.post('/testD')
.post('/http/testD')
.timeout(100)
.send({ test: 'data' });
expect(response6.status).toBe(403);
@@ -210,15 +206,10 @@ describe('HttpPostIngressEventPublisher', () => {
it('without configuration', async () => {
const config = new ConfigReader({});
const router = Router();
router.use(express.json());
router.use(errorHandler());
expect(() =>
HttpPostIngressEventPublisher.fromConfig({
config,
logger,
router,
}),
).not.toThrow();
});
@@ -41,7 +41,6 @@ export class HttpPostIngressEventPublisher implements EventPublisher {
config: Config;
ingresses?: { [topic: string]: Omit<HttpPostIngressOptions, 'topic'> };
logger: Logger;
router: express.Router;
}): HttpPostIngressEventPublisher {
const topics =
env.config.getOptionalStringArray('events.http.topics') ?? [];
@@ -55,15 +54,18 @@ export class HttpPostIngressEventPublisher implements EventPublisher {
}
});
return new HttpPostIngressEventPublisher(env.logger, env.router, ingresses);
return new HttpPostIngressEventPublisher(env.logger, ingresses);
}
private constructor(
private logger: Logger,
router: express.Router,
ingresses: { [topic: string]: Omit<HttpPostIngressOptions, 'topic'> },
) {
router.use(this.createRouter(ingresses));
private readonly logger: Logger,
private readonly ingresses: {
[topic: string]: Omit<HttpPostIngressOptions, 'topic'>;
},
) {}
bind(router: express.Router): void {
router.use('/http', this.createRouter(this.ingresses));
}
async setEventBroker(eventBroker: EventBroker): Promise<void> {
@@ -92,8 +94,12 @@ export class HttpPostIngressEventPublisher implements EventPublisher {
const path = `/${topic}`;
router.post(path, async (request, response) => {
const requestDetails = {
body: request.body,
headers: request.headers,
};
const context = new RequestValidationContextImpl();
await validator?.(request, context);
await validator?.(requestDetails, context);
if (context.wasRejected()) {
response
.status(context.rejectionDetails!.status)
+7 -2
View File
@@ -4,7 +4,6 @@
```ts
import { ExtensionPoint } from '@backstage/backend-plugin-api';
import { Request as Request_2 } from 'express';
// @public
export interface EventBroker {
@@ -74,6 +73,12 @@ export interface HttpPostIngressOptions {
validator?: RequestValidator;
}
// @public (undocumented)
export interface RequestDetails {
body: unknown;
headers: Record<string, string | string[] | undefined>;
}
// @public
export interface RequestRejectionDetails {
// (undocumented)
@@ -89,7 +94,7 @@ export interface RequestValidationContext {
// @public
export type RequestValidator = (
request: Request_2,
request: RequestDetails,
context: RequestValidationContext,
) => Promise<void>;
+1 -3
View File
@@ -24,9 +24,7 @@
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/backend-plugin-api": "workspace:^",
"@types/express": "^4.17.6",
"express": "^4.17.1"
"@backstage/backend-plugin-api": "workspace:^"
},
"devDependencies": {
"@backstage/cli": "workspace:^"
@@ -0,0 +1,29 @@
/*
* Copyright 2022 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.
*/
/**
* @public
*/
export interface RequestDetails {
/**
* Request body. JSON payloads have been parsed already.
*/
body: unknown;
/**
* Key-value pairs of header names and values. Header names are lower-cased.
*/
headers: Record<string, string | string[] | undefined>;
}
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { Request } from 'express';
import { RequestDetails } from './RequestDetails';
import { RequestValidationContext } from './RequestValidationContext';
/**
@@ -29,6 +29,6 @@ import { RequestValidationContext } from './RequestValidationContext';
* @public
*/
export type RequestValidator = (
request: Request,
request: RequestDetails,
context: RequestValidationContext,
) => Promise<void>;
@@ -14,6 +14,7 @@
* limitations under the License.
*/
export type { RequestDetails } from './RequestDetails';
export type { RequestRejectionDetails } from './RequestRejectionDetails';
export type { RequestValidationContext } from './RequestValidationContext';
export type { RequestValidator } from './RequestValidator';
@@ -43,8 +43,10 @@ import useAsync from 'react-use/lib/useAsync';
const useStyles = makeStyles((theme: BackstageTheme) => ({
graph: {
flex: 1,
minHeight: 0,
minHeight: '100%',
},
graphWrapper: {
height: '100%',
},
organizationNode: {
fill: theme.palette.secondary.light,
@@ -62,6 +64,15 @@ const useStyles = makeStyles((theme: BackstageTheme) => ({
justifyContent: 'center',
color: 'black',
},
legend: {
position: 'absolute',
bottom: 0,
right: 0,
padding: theme.spacing(1),
'& .icon': {
verticalAlign: 'bottom',
},
},
textOrganization: {
color: theme.palette.secondary.contrastText,
},
@@ -221,7 +232,7 @@ export function GroupsDiagram() {
}
return (
<>
<div className={classes.graphWrapper}>
<DependencyGraph
nodes={nodes}
edges={edges}
@@ -229,14 +240,18 @@ export function GroupsDiagram() {
direction={DependencyGraphTypes.Direction.RIGHT_LEFT}
renderNode={RenderNode}
className={classes.graph}
fit="contain"
/>
<Typography
variant="caption"
style={{ display: 'block', textAlign: 'right' }}
color="textSecondary"
display="block"
className={classes.legend}
>
<ZoomOutMap style={{ verticalAlign: 'bottom' }} /> Use pinch &amp; zoom
to move around the diagram.
<ZoomOutMap className="icon" /> Use pinch &amp; zoom to move around the
diagram.
</Typography>
</>
</div>
);
}
@@ -23,9 +23,10 @@ import {
stringifyEntityRef,
UserEntity,
} from '@backstage/catalog-model';
import { Config, JsonObject, JsonValue } from '@backstage/config';
import { Config } from '@backstage/config';
import { InputError, NotFoundError, stringifyError } from '@backstage/errors';
import { ScmIntegrations } from '@backstage/integration';
import { JsonObject, JsonValue } from '@backstage/types';
import {
TaskSpec,
TemplateEntityV1beta3,
+1 -1
View File
@@ -44,7 +44,7 @@
"@material-ui/lab": "4.0.0-alpha.57",
"@material-ui/styles": "^4.10.0",
"cross-fetch": "^3.1.5",
"rc-progress": "3.4.0",
"rc-progress": "3.4.1",
"react-use": "^17.2.4"
},
"peerDependencies": {
@@ -17,7 +17,8 @@
import { Config } from '@backstage/config';
import { InputError } from '@backstage/errors';
import { Logger } from 'winston';
import express, { Router } from 'express';
import express from 'express';
import Router from 'express-promise-router';
import { VaultClient } from './vaultApi';
import { TaskRunner, PluginTaskScheduler } from '@backstage/backend-tasks';
import { errorHandler } from '@backstage/backend-common';
@@ -15,7 +15,7 @@
*/
import { Config } from '@backstage/config';
import { NotFoundError } from '@backstage/errors';
import { NotAllowedError, NotFoundError } from '@backstage/errors';
import fetch from 'node-fetch';
import plimit from 'p-limit';
import { getVaultConfig, VaultConfig } from '../config';
@@ -103,6 +103,8 @@ export class VaultClient implements VaultApi {
return (await response.json()) as T;
} else if (response.status === 404) {
throw new NotFoundError(`No secrets found in path '${path}'`);
} else if (response.status === 403) {
throw new NotAllowedError(response.statusText);
}
throw new Error(
`Unexpected error while fetching secrets from path '${path}'`,
+115
View File
@@ -0,0 +1,115 @@
#!/usr/bin/env node
/*
* Copyright 2022 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.
*/
const { execFile: execFileCb } = require('child_process');
const { promisify } = require('util');
const { resolve: resolvePath } = require('path');
const execFile = promisify(execFileCb);
async function hasNewChangesets(ref) {
if (!ref) {
throw new Error('ref is required');
}
const { stdout } = await execFile('git', [
'diff',
'--compact-summary',
ref,
'.changeset/*.md',
]);
return stdout.includes('(new)');
}
function getReleaseOfMonth(year, month) {
const base = new Date(Date.UTC(year, month));
const wednesdayOffset =
base.getUTCDay() > 3 ? 10 - base.getUTCDay() : 3 - base.getUTCDay();
const thirdWednesdayOffset = wednesdayOffset + 7 * 2;
const releaseOffset = thirdWednesdayOffset - 1;
const releaseDay = new Date(
Date.UTC(base.getUTCFullYear(), base.getUTCMonth(), releaseOffset + 1),
);
return releaseDay;
}
function getReleaseSchedule() {
const firstReleaseYear = 2022;
const firstReleaseMonth = 2;
return Array(100)
.fill(0)
.map((_, i) => {
const date = getReleaseOfMonth(firstReleaseYear, firstReleaseMonth + i);
return { version: `1.${i}.0`, date };
});
}
function getCurrentRelease() {
const { version: releaseVersion } = require(resolvePath('package.json'));
const match = releaseVersion.match(/^(\d+\.\d+\.\d+)/);
if (!match) {
throw new Error(`Failed to parse release version, '${releaseVersion}'`);
}
const [versionStr] = match;
if (versionStr === releaseVersion) {
return releaseVersion;
}
const [major, minor] = versionStr.split('.').map(Number);
return `${major}.${minor - 1}.0`;
}
function findNextRelease(currentRelease, releaseSchedule) {
const currentIndex = releaseSchedule.findIndex(
r => r.version === currentRelease,
);
if (currentIndex === -1) {
throw new Error(
`Failed to find current release '${currentRelease}' in release schedule`,
);
}
return releaseSchedule[currentIndex + 1];
}
async function main() {
const [diffRef = 'origin/master'] = process.argv.slice(2);
const needsMessage = await hasNewChangesets(diffRef);
if (!needsMessage) {
return;
}
const currentRelease = getCurrentRelease();
const releaseSchedule = getReleaseSchedule();
const nextRelease = findNextRelease(currentRelease, releaseSchedule);
const scheduledDate = nextRelease.date
.toUTCString()
.replace(/\s*\d+:\d+:\d+.*/, '');
process.stdout.write(
[
'Thank you for contributing to Backstage! The changes in this pull request will be part',
`of the \`${nextRelease.version}\` release, scheduled for ${scheduledDate}.`,
].join(' '),
);
}
main().catch(error => {
console.error(error.stack);
process.exit(1);
});
+43 -43
View File
@@ -2966,90 +2966,90 @@ __metadata:
languageName: node
linkType: hard
"@swc/core-darwin-arm64@npm:1.3.16":
version: 1.3.16
resolution: "@swc/core-darwin-arm64@npm:1.3.16"
"@swc/core-darwin-arm64@npm:1.3.19":
version: 1.3.19
resolution: "@swc/core-darwin-arm64@npm:1.3.19"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
"@swc/core-darwin-x64@npm:1.3.16":
version: 1.3.16
resolution: "@swc/core-darwin-x64@npm:1.3.16"
"@swc/core-darwin-x64@npm:1.3.19":
version: 1.3.19
resolution: "@swc/core-darwin-x64@npm:1.3.19"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
"@swc/core-linux-arm-gnueabihf@npm:1.3.16":
version: 1.3.16
resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.16"
"@swc/core-linux-arm-gnueabihf@npm:1.3.19":
version: 1.3.19
resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.19"
conditions: os=linux & cpu=arm
languageName: node
linkType: hard
"@swc/core-linux-arm64-gnu@npm:1.3.16":
version: 1.3.16
resolution: "@swc/core-linux-arm64-gnu@npm:1.3.16"
"@swc/core-linux-arm64-gnu@npm:1.3.19":
version: 1.3.19
resolution: "@swc/core-linux-arm64-gnu@npm:1.3.19"
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
"@swc/core-linux-arm64-musl@npm:1.3.16":
version: 1.3.16
resolution: "@swc/core-linux-arm64-musl@npm:1.3.16"
"@swc/core-linux-arm64-musl@npm:1.3.19":
version: 1.3.19
resolution: "@swc/core-linux-arm64-musl@npm:1.3.19"
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
"@swc/core-linux-x64-gnu@npm:1.3.16":
version: 1.3.16
resolution: "@swc/core-linux-x64-gnu@npm:1.3.16"
"@swc/core-linux-x64-gnu@npm:1.3.19":
version: 1.3.19
resolution: "@swc/core-linux-x64-gnu@npm:1.3.19"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
"@swc/core-linux-x64-musl@npm:1.3.16":
version: 1.3.16
resolution: "@swc/core-linux-x64-musl@npm:1.3.16"
"@swc/core-linux-x64-musl@npm:1.3.19":
version: 1.3.19
resolution: "@swc/core-linux-x64-musl@npm:1.3.19"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
"@swc/core-win32-arm64-msvc@npm:1.3.16":
version: 1.3.16
resolution: "@swc/core-win32-arm64-msvc@npm:1.3.16"
"@swc/core-win32-arm64-msvc@npm:1.3.19":
version: 1.3.19
resolution: "@swc/core-win32-arm64-msvc@npm:1.3.19"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
"@swc/core-win32-ia32-msvc@npm:1.3.16":
version: 1.3.16
resolution: "@swc/core-win32-ia32-msvc@npm:1.3.16"
"@swc/core-win32-ia32-msvc@npm:1.3.19":
version: 1.3.19
resolution: "@swc/core-win32-ia32-msvc@npm:1.3.19"
conditions: os=win32 & cpu=ia32
languageName: node
linkType: hard
"@swc/core-win32-x64-msvc@npm:1.3.16":
version: 1.3.16
resolution: "@swc/core-win32-x64-msvc@npm:1.3.16"
"@swc/core-win32-x64-msvc@npm:1.3.19":
version: 1.3.19
resolution: "@swc/core-win32-x64-msvc@npm:1.3.19"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
"@swc/core@npm:^1.3.9":
version: 1.3.16
resolution: "@swc/core@npm:1.3.16"
version: 1.3.19
resolution: "@swc/core@npm:1.3.19"
dependencies:
"@swc/core-darwin-arm64": 1.3.16
"@swc/core-darwin-x64": 1.3.16
"@swc/core-linux-arm-gnueabihf": 1.3.16
"@swc/core-linux-arm64-gnu": 1.3.16
"@swc/core-linux-arm64-musl": 1.3.16
"@swc/core-linux-x64-gnu": 1.3.16
"@swc/core-linux-x64-musl": 1.3.16
"@swc/core-win32-arm64-msvc": 1.3.16
"@swc/core-win32-ia32-msvc": 1.3.16
"@swc/core-win32-x64-msvc": 1.3.16
"@swc/core-darwin-arm64": 1.3.19
"@swc/core-darwin-x64": 1.3.19
"@swc/core-linux-arm-gnueabihf": 1.3.19
"@swc/core-linux-arm64-gnu": 1.3.19
"@swc/core-linux-arm64-musl": 1.3.19
"@swc/core-linux-x64-gnu": 1.3.19
"@swc/core-linux-x64-musl": 1.3.19
"@swc/core-win32-arm64-msvc": 1.3.19
"@swc/core-win32-ia32-msvc": 1.3.19
"@swc/core-win32-x64-msvc": 1.3.19
dependenciesMeta:
"@swc/core-darwin-arm64":
optional: true
@@ -3073,7 +3073,7 @@ __metadata:
optional: true
bin:
swcx: run_swcx.js
checksum: 4361252c928c487a02f526aecd8f3072b923244234c2701916944cf13c252b6d5ce2466caf3e0797d3e92e71a89d4a044f8577e51e8f7fe3af0fe30d94e94b13
checksum: 752499e18f81df789a9737936b2a83ea1db34bb2f983c80b394766796d853fa7d30701b52b9cb30ba943ad15286d8316603149683f4c553a5493aa78d89a76e0
languageName: node
linkType: hard
+897 -788
View File
File diff suppressed because it is too large Load Diff