Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(notifications): turn plugins into dynamic ones #1104

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 24 additions & 23 deletions plugins/notifications-backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,23 @@ This Backstage backend plugin provides REST API endpoint for the notifications.

It's backed by a relational database, so far tested with PostgreSQL.

## Deploying as a dynamic plugin

The notifications backend plugin can be loaded either as a static or a dynamic plugin.

To install it as a dynamic plugin, please follow instructions here: https://github.com/janus-idp/backstage-showcase/blob/main/showcase-docs/dynamic-plugins.md#installing-a-dynamic-plugin-package-in-the-showcase

To install it as a static plugin, several steps are required as described below.

In any case, do not miss the info about configuration and especially about creating entities in Catalog as described below.

## Getting started

The plugin uses a relational database to persist messages, it has been tested with the SQLite and PostgreSQL.

Upon backend's plugin start, the `backstage_plugin_notifications` database and its tables are created automatically.

### Optional: PostgreSQL
### Optional: PostgreSQL setup

**To use the Backstage's default SQLite, no specific configuration is needed.**

Expand Down Expand Up @@ -58,6 +68,8 @@ If PostgreSQL is used, additional configuration in the in the `app-config.yaml`
store: memory
```

## Deploy as a static plugin

### Add NPM dependency

```
Expand All @@ -80,25 +92,14 @@ import { PluginEnvironment } from '../types';
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
// workaround for creating the database when client is not sqlite
const dbClient = await env.database.getClient()
dbClient.destroy()

const catalogClient = new CatalogClient({ discoveryApi: env.discovery });
mareklibra marked this conversation as resolved.
Show resolved Hide resolved
const dbConfig = env.config.getConfig('backend.database');
// Following is optional
const externalCallerSecret = env.config.getOptionalString(
'notifications.externalCallerSecret',
);

return await createRouter({
identity: env.identity,
logger: env.logger,
permissions: env.permissions,
tokenManager: env.tokenManager,
dbConfig,
catalogClient,
externalCallerSecret,
database: env.database,
mareklibra marked this conversation as resolved.
Show resolved Hide resolved
discovery: env.discovery,
config: env.config,
mareklibra marked this conversation as resolved.
Show resolved Hide resolved
});
}
```
Expand All @@ -120,7 +121,7 @@ const notificationsEnv = useHotMemoize(module, () =>
apiRouter.use('/notifications', await notifications(notificationsEnv));
```

### Configure
## Configure

#### Optional: Plugin's configuration

Expand Down Expand Up @@ -148,7 +149,7 @@ Notes:
- The `externalCallerSecret` is an workaround, exclusive use of JWT tokens will probably be required in the future.
- Sharing the same shared secret with the "auth.secret" option is not recommended.

#### Authentication
## Authentication

Please refer https://backstage.io/docs/auth/ to set-up authentication.

Expand All @@ -159,13 +160,13 @@ Refer https://backstage.io/docs/auth/identity-resolver for details.

For the purpose of development, there is `users.yaml` listing example data created.

#### Authorization
## Authorization

Every service endpoint is guarded by a permission check, enabled by default.

It is up to particular deployment to provide corresponding permission policies based on https://backstage.io/docs/permissions/writing-a-policy. To register your permission policies, refer https://backstage.io/docs/permissions/getting-started#integrating-the-permission-framework-with-your-backstage-instance.

#### Service-to-service and External Calls
### Service-to-service and External Calls

The notification-backend is expected to be called by FE plugins (including the Notifications frontend-end plugin), other backend plugins or external services.

Expand All @@ -174,9 +175,9 @@ To configure those two flows, refer
- https://backstage.io/docs/auth/service-to-service-auth.
- https://backstage.io/docs/auth/service-to-service-auth#usage-in-external-callers

#### Catalog
### Important: User entities in Catalog

The notifications require target users or groups (as receivers) to be listed in the Catalog.
_The notifications require target users or groups (as receivers) to be listed in the Catalog._

As an example how to do it, add following to the config:

Expand All @@ -186,12 +187,12 @@ catalog:
entityFilename: catalog-info.yaml
pullRequestBranchName: backstage-integration
rules:
# *** Here is new change:
# *** Here is a new change:
- allow: [Component, System, API, Resource, Location, User, Group]
locations:
# Local example data, file locations are relative to the backend process, typically `packages/backend`
- type: file
# *** Here is new change, referes to a file stored in the root of the Backstage:
# *** Here is a new change, referes to a file stored in the root of the Backstage:
target: ../../users.yaml
```

Expand Down
9 changes: 9 additions & 0 deletions plugins/notifications-backend/config.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface Config {
notifications?: {
/* Workaround for issues with external caller JWT token creation.
When following config option is not provided and the request "authentication" header is missing, the request is ALLOWED by default
When following option is present, the request must contain either a valid JWT token or that provided shared secret in the "notifications-secret" header
*/
externalCallerSecret?: string;
};
}
14 changes: 12 additions & 2 deletions plugins/notifications-backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,22 @@
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack",
"tsc": "tsc",
"openapi": "./scripts/openapi.sh"
"openapi": "./scripts/openapi.sh",
"export-dynamic": "janus-cli package export-dynamic-plugin"
},
"configSchema": "config.d.ts",
"dependencies": {
"@backstage/backend-common": "^0.19.8",
"@backstage/backend-openapi-utils": "^0.1.0",
"@backstage/catalog-client": "^1.4.5",
"@backstage/config": "^1.1.1",
"@backstage/errors": "^1.2.3",
"@backstage/backend-plugin-api": "^0.6.6",
"@backstage/backend-plugin-manager": "npm:@janus-idp/[email protected]",
"@backstage/plugin-auth-node": "^0.4.0",
"@backstage/plugin-permission-common": "^0.7.9",
"@backstage/plugin-permission-node": "^0.7.17",
"@backstage/plugin-scaffolder-node": "^0.2.6",
"ajv-formats": "^2.1.1",
"express": "^4.18.2",
"express-promise-router": "^4.1.1",
Expand All @@ -49,13 +54,18 @@
"@backstage/cli": "0.23.0",
"@types/express": "*",
"@types/supertest": "2.0.16",
"@janus-idp/cli": "1.4.7",
"js-yaml-cli": "^0.6.0",
"knex-mock-client": "2.0.0",
"msw": "1.3.2",
"openapicmd": "^2.1.0",
"supertest": "6.3.3"
},
"files": [
"dist"
"dist",
"dist-dynamic/*.*",
"dist-dynamic/dist/**",
"dist-dynamic/alpha/*",
"config.d.ts"
]
}
16 changes: 16 additions & 0 deletions plugins/notifications-backend/src/dynamic/alpha.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { createBackendModule } from '@backstage/backend-plugin-api';
import { BackendDynamicPluginInstaller } from '@backstage/backend-plugin-manager';

export const dynamicPluginInstaller: BackendDynamicPluginInstaller = {
kind: 'new',
install: createBackendModule({
moduleId: 'scaffolder-backend-notifications',
pluginId: 'scaffolder',
register(env) {
env.registerInit({
deps: {},
async init() {},
});
},
}),
};
11 changes: 11 additions & 0 deletions plugins/notifications-backend/src/dynamic/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { BackendDynamicPluginInstaller } from '@backstage/backend-plugin-manager';

import { createRouter } from '../service/router';

export const dynamicPluginInstaller: BackendDynamicPluginInstaller = {
kind: 'legacy',
router: {
pluginID: 'notifications',
createPlugin: createRouter,
},
};
1 change: 1 addition & 0 deletions plugins/notifications-backend/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './service/router';
export * from './service/permissions';
export * from './dynamic';
7 changes: 5 additions & 2 deletions plugins/notifications-backend/src/service/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { RouterOptions } from './types';

export type GetLoggedInUserOptions = Pick<
RouterOptions,
'identity' | 'tokenManager' | 'externalCallerSecret'
'identity' | 'tokenManager' | 'config'
>;
export type CheckUserPermission = GetLoggedInUserOptions &
Pick<RouterOptions, 'permissions'>;
Expand All @@ -23,9 +23,12 @@ export type CheckUserPermission = GetLoggedInUserOptions &
*/
export const getLoggedInUser = async (
request: express.Request,
{ identity, tokenManager, externalCallerSecret }: GetLoggedInUserOptions,
{ identity, tokenManager, config }: GetLoggedInUserOptions,
): Promise<string> => {
const identityResponse = await identity.getIdentity({ request });
const externalCallerSecret = config.getOptionalString(
'notifications.externalCallerSecret',
);

// To properly set identity, see packages/backend/src/plugins/auth.ts or https://backstage.io/docs/auth/identity-resolver
if (identityResponse) {
Expand Down
11 changes: 10 additions & 1 deletion plugins/notifications-backend/src/service/router.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { CatalogClient } from '@backstage/catalog-client';

import { fullFormats } from 'ajv-formats/dist/formats';
import express from 'express';
import Router from 'express-promise-router';
Expand All @@ -23,7 +25,14 @@ import { RouterOptions } from './types';
export async function createRouter(
options: RouterOptions,
): Promise<express.Router> {
const { logger, dbConfig, catalogClient } = options;
const { logger, database, discovery, config } = options;

// workaround for creating the database when client is not sqlite
mareklibra marked this conversation as resolved.
Show resolved Hide resolved
const existingDbClient = await database.getClient();
existingDbClient.destroy();

const catalogClient = new CatalogClient({ discoveryApi: discovery });
const dbConfig = config.getConfig('backend.database');

// create DB client and tables
if (!dbConfig) {
Expand Down
15 changes: 8 additions & 7 deletions plugins/notifications-backend/src/service/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { TokenManager } from '@backstage/backend-common';
import { CatalogClient } from '@backstage/catalog-client';
import {
PluginDatabaseManager,
PluginEndpointDiscovery,
TokenManager,
} from '@backstage/backend-common';
import { Config } from '@backstage/config';
import { IdentityApi } from '@backstage/plugin-auth-node';
import { PermissionEvaluator } from '@backstage/plugin-permission-common';
Expand All @@ -8,14 +11,12 @@ import { Logger } from 'winston';

export interface RouterOptions {
logger: Logger;
dbConfig: Config;
catalogClient: CatalogClient;
identity: IdentityApi;
permissions: PermissionEvaluator;
tokenManager: TokenManager;

// Workaround - see auth.ts
externalCallerSecret?: string;
database: PluginDatabaseManager;
discovery: PluginEndpointDiscovery;
config: Config;
}

export type NotificationsFilterRequest = {
Expand Down
7 changes: 7 additions & 0 deletions plugins/notifications/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ This Backstage front-end plugin provides:

Have `@janus-idp/plugin-notifications-backend` installed and running.

### Installing as a dynamic plugin?

The sections below are relevant for static plugins. If the plugin is expected to be installed as a dynamic one:

- follow https://github.com/janus-idp/backstage-showcase/blob/main/showcase-docs/dynamic-plugins.md#installing-a-dynamic-plugin-package-in-the-showcase
- add content of `app-config.janus-idp.yaml` into `app-config.local.yaml`.

### Add NPM dependency

```
Expand Down
14 changes: 14 additions & 0 deletions plugins/notifications/app-config.janus-idp.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
dynamicPlugins:
frontend:
janus-idp.backstage-plugin-notifications:
appIcons:
- name: notificationsIcon
module: NotificationsPlugin
importName: NotificationsIcon
dynamicRoutes:
- path: /notifications
importName: NotificationsPage
module: NotificationsPlugin
menuItem:
icon: notificationsIcon
text: Notifications
12 changes: 11 additions & 1 deletion plugins/notifications/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build",
"export-dynamic": "janus-cli package export-dynamic-plugin",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test --passWithNoTests --coverage",
"clean": "backstage-cli package clean",
Expand Down Expand Up @@ -45,15 +46,24 @@
"@backstage/core-app-api": "1.11.0",
"@backstage/dev-utils": "1.0.22",
"@backstage/test-utils": "^1.4.4",
"@janus-idp/cli": "1.4.7",
"@openapitools/openapi-generator-cli": "^2.7.0",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^12.1.5",
"@testing-library/user-event": "^14.5.1",
"@types/node": "*",
"msw": "1.3.2"
},
"scalprum": {
"name": "janus-idp.backstage-plugin-notifications",
"exposedModules": {
"NotificationsPlugin": "./src/index.ts"
}
},
"files": [
"dist"
"dist",
"dist-scalprum",
"app-config.janus-idp.yaml"
],
"repository": "github:janus-idp/backstage-plugins",
"keywords": [
Expand Down
2 changes: 2 additions & 0 deletions plugins/notifications/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ export { NOTIFICATIONS_ROUTE } from './constants';
// selected components for export
export { NotificationsSidebarItem } from './components/NotificationsSidebarItem';
export { usePollingEffect } from './components/usePollingEffect';

export { default as NotificationsIcon } from '@material-ui/icons/Notifications';
Loading