diff --git a/.changeset/brown-peaches-hunt.md b/.changeset/brown-peaches-hunt.md new file mode 100644 index 0000000000..fc10acc211 --- /dev/null +++ b/.changeset/brown-peaches-hunt.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Fix a bug with S3 Fetch that caused all objects to be flattened within a single folder on the local file system. diff --git a/.changeset/hungry-days-remain.md b/.changeset/hungry-days-remain.md new file mode 100644 index 0000000000..b0c3642a35 --- /dev/null +++ b/.changeset/hungry-days-remain.md @@ -0,0 +1,5 @@ +--- +'@backstage/theme': patch +--- + +Fixed missing extra variables like `applyDarkStyles` in Mui V5 theme after calling `createUnifiedThemeFromV4` function diff --git a/.changeset/late-poets-love.md b/.changeset/late-poets-love.md new file mode 100644 index 0000000000..03b975b2b5 --- /dev/null +++ b/.changeset/late-poets-love.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Allow the `createConfigSecretEnumerator` to take an optional `schema` argument with an already-loaded global configuration schema. diff --git a/.changeset/mean-apes-wait.md b/.changeset/mean-apes-wait.md new file mode 100644 index 0000000000..838f1f2286 --- /dev/null +++ b/.changeset/mean-apes-wait.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-github': patch +--- + +Added `validateLocationsExist` to the config diff --git a/.changeset/rich-bees-fry.md b/.changeset/rich-bees-fry.md new file mode 100644 index 0000000000..281719340b --- /dev/null +++ b/.changeset/rich-bees-fry.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-app-backend': patch +'@backstage/plugin-app-node': patch +--- + +Allow the `app-backend` plugin to use a global configuration schema provided externally through an extension. diff --git a/.changeset/six-emus-buy.md b/.changeset/six-emus-buy.md new file mode 100644 index 0000000000..1ef2a8e124 --- /dev/null +++ b/.changeset/six-emus-buy.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-signals-react': patch +'@backstage/plugin-signals-node': patch +'@backstage/plugin-signals': patch +--- + +Allow defining signal type to publish and receive diff --git a/.changeset/sour-ligers-hunt.md b/.changeset/sour-ligers-hunt.md new file mode 100644 index 0000000000..5642494c24 --- /dev/null +++ b/.changeset/sour-ligers-hunt.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Added `experimentalExtraAllowedOrigins` to config diff --git a/.changeset/spicy-bobcats-return.md b/.changeset/spicy-bobcats-return.md new file mode 100644 index 0000000000..9bd4b33784 --- /dev/null +++ b/.changeset/spicy-bobcats-return.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-dynatrace': patch +--- + +Fixed Dynatrace plugin proxy configuration diff --git a/.changeset/swift-pumpkins-shake.md b/.changeset/swift-pumpkins-shake.md new file mode 100644 index 0000000000..c7a0a44ade --- /dev/null +++ b/.changeset/swift-pumpkins-shake.md @@ -0,0 +1,8 @@ +--- +'@backstage/backend-dynamic-feature-service': minor +--- + +Implement the discovery of additional individual configuration schemas for dynamic plugins, and provide: + +- an alternate implementation of the root logger service that takes them into account, +- an extension to the App backend plugin to set a global configuration schema that takes them into account. diff --git a/beps/0003-auth-architecture-evolution/README.md b/beps/0003-auth-architecture-evolution/README.md index ec2660a9cc..2dbb6e0666 100644 --- a/beps/0003-auth-architecture-evolution/README.md +++ b/beps/0003-auth-architecture-evolution/README.md @@ -14,7 +14,7 @@ creation-date: 2024-01-28 -[**Discussion Issue**](https://github.com/backstage/backstage/issues/NNNNN) +[**Discussion Issue**](https://github.com/backstage/backstage/issues/22605) - [Summary](#summary) - [Motivation](#motivation) diff --git a/docs/deployment/flightcontrol.md b/docs/deployment/flightcontrol.md index 482e9341ea..6078d931fb 100644 --- a/docs/deployment/flightcontrol.md +++ b/docs/deployment/flightcontrol.md @@ -9,8 +9,159 @@ This guide explains how to deploy Backstage to [Flightcontrol](https://www.fligh Before you begin, make sure you have a [Flightcontrol account](https://app.flightcontrol.dev/signup?ref=backstage) and a [Github account](https://github.com/login) to follow this guide. +# Creating a Dockerfile and .dockerignore + +Once your Flightcontrol account is setup, you will need to prepare a `Dockerfile` and `.dockerignore` to deploy Backstage to your target AWS environment. The standard `Dockerfile` included in the Backstage repository assumes that the `skeleton.tar.gz` and `bundle.tar.gz` already exist. When integrating Flightcontrol directly with a GitHub repository, changes to files within this repository will automatically start a Flightcontrol deployment, unless you have configured deployments via a webhook. Regardless of which method you have chosen, when Flightcontrol starts a deployment process it will pull all files directly from the repository, and not from a GitHub runner or other location. As Flightcontrol does not contain CI steps this results in the zip files having to be committed to the repository in order to be available for the `Dockerfile` to copy. + +A simple work around for this is to use a `Dockerfile` with a [multi-stage build](https://docs.docker.com/build/building/multi-stage/). Using this approach we can create our zip files in the first build stage, and copy them into the second stage ready for deployment. + +The following two code snippets demonstrate this method. The first block of code is an example of a multi-stage build approach and should be added to a file called `Dockerfile.flightcontrol` in the `packages/backend` directory. + +```dockerfile filename="Dockerfile.flightcontrol" +# This dockerfile builds an image for the backend package. +# It should be executed with the root of the repo as docker context. +# +# Before building this image, be sure to have run the following commands in the repo root: +# +# yarn install +# yarn tsc +# yarn build:backend +# +# Once the commands have been run, you can build the image using `yarn build-image` +FROM node:18-bookworm-slim as build + +USER node +WORKDIR /app +COPY --chown=node:node . . + +RUN yarn install --frozen-lockfile + +# tsc outputs type definitions to dist-types/ in the repo root, which are then consumed by the build +RUN yarn tsc + +# Build the backend, which bundles it all up into the packages/backend/dist folder. +# The configuration files here should match the one you use inside the Dockerfile below. +RUN yarn build:backend --config ../../app-config.yaml + +FROM node:18-bookworm-slim + +# Install isolate-vm dependencies, these are needed by the @backstage/plugin-scaffolder-backend. +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt-get update && \ + apt-get install -y --no-install-recommends python3 g++ build-essential && \ + yarn config set python /usr/bin/python3 + +# Install sqlite3 dependencies. You can skip this if you don't use sqlite3 in the image, +# in which case you should also move better-sqlite3 to "devDependencies" in package.json. +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt-get update && \ + apt-get install -y --no-install-recommends libsqlite3-dev + +# From here on we use the least-privileged `node` user to run the backend. +USER node + +# This should create the app dir as `node`. +# If it is instead created as `root` then the `tar` command below will fail: `can't create directory 'packages/': Permission denied`. +# If this occurs, then ensure BuildKit is enabled (`DOCKER_BUILDKIT=1`) so the app dir is correctly created as `node`. +WORKDIR /app + +# This switches many Node.js dependencies to production mode. +ENV NODE_ENV production + +# Copy repo skeleton first, to avoid unnecessary docker cache invalidation. +# The skeleton contains the package.json of each package in the monorepo, +# and along with yarn.lock and the root package.json, that's enough to run yarn install. +COPY --from=build app/yarn.lock app/package.json app/packages/backend/dist/skeleton.tar.gz ./ +RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz + +RUN --mount=type=cache,target=/home/node/.cache/yarn,sharing=locked,uid=1000,gid=1000 \ + yarn install --frozen-lockfile --production --network-timeout 300000 + +# Then copy the rest of the backend bundle, along with any other files we might want. +COPY --from=build app/packages/backend/dist/bundle.tar.gz app/app-config*.yaml ./ +RUN tar xzf bundle.tar.gz && rm bundle.tar.gz + +CMD ["node", "packages/backend", "--config", "app-config.yaml", "--config", "app-config.production.yaml"] +``` + +In order to prevent the default `.dockerignore` file from preventing the copying of the relevant files into the first stage of our build, we need to override it with a custom one. This can also be added to the `packages/backend` directory, it should be called `Dockerfile.flightcontrol.dockerignore` and contain the following. + +```ssh filename="Dockerfile.flightcontrol.dockerignore" +.git +.yarn/cache +.yarn/install-state.gz +node_modules +packages/*/node_modules +*.local.yaml +``` + +With these two new files, you will now have the necessary steps in place to build the zip files, copy them into the second stage of the build, and deploy them via Flightcontrol. + +# Creating a custom app-config.production file + +Our next steps before switching to the Flightcontrol dashboard is to create a custom app-config.production file. The purposes of this is to update how Backstage will connect to the RDS hosted Postgres DB. The default `app-config.production` file uses the `host`, `port`, `user` and `password` environment variables. + +When creating a database via the Flightcontrol dashboard, we are given an environment variable with the single connection string. Therefore we need an `app-config.production` file which uses this variable. + +Within your Backstage project create a new file called `app-config.production.flightcontrol.yaml`. Add the following configuration to it: + +```yaml filename="app-config.production.flightcontrol.yaml" +app: + # Should be the same as backend.baseUrl when using the `app-backend` plugin. + baseUrl: http://localhost:7007 + +backend: + # Note that the baseUrl should be the URL that the browser and other clients + # should use when communicating with the backend, i.e. it needs to be + # reachable not just from within the backend host, but from all of your + # callers. When its value is "http://localhost:7007", it's strictly private + # and can't be reached by others. + baseUrl: http://localhost:7007 + # The listener can also be expressed as a single : string. In this case we bind to + # all interfaces, the most permissive setting. The right value depends on your specific deployment. + listen: ':7007' + + # config options: https://node-postgres.com/api/client + database: + client: pg + pluginDivisionMode: 'schema' + connection: + connectionString: ${POSTGRES_CON_STRING} + ssl: + require: true + rejectUnauthorized: false + + # Flightcontrol provides a single DB connection string + # environment variable which can be renamed to POSTGRES_CON_STRING. + # the following default values are therefore not used: + # host: ${POSTGRES_HOST} + # port: ${POSTGRES_PORT} + # user: ${POSTGRES_USER} + # password: ${POSTGRES_PASSWORD} + # https://node-postgres.com/features/ssl + # you can set the sslmode configuration option via the `PGSSLMODE` environment variable + # see https://www.postgresql.org/docs/current/libpq-ssl.html Table 33.1. SSL Mode Descriptions (e.g. require) + # ssl: + # ca: # if you have a CA file and want to verify it you can uncomment this section + # $file: /ca/server.crt + +catalog: + # Overrides the default list locations from app-config.yaml as these contain example data. + # See https://backstage.io/docs/features/software-catalog/#adding-components-to-the-catalog for more details + # on how to get entities into the catalog. + locations: [] +``` + +Make a note of the environment variable used in the `connectionString`. In our example above this is `POSTGRES_CON_STRING`. You will need this later when you have deployed the database via the Flightcontrol console, and wish to connect to it from Backstage. + # Deployment Via Dashboard +Ensure that custom `Dockerfile.flightcontrol`, `Dockerfile.flightcontrol.dockerignore` and `app-config.production.flightcontrol.yaml` have been committed to your repository before following the next steps. + +Login into the Flightcontrol Dashboard and complete each of the following: + 1. Create a new project from the Flightcontrol Dashboard 2. Select the GitHub repo for your Backstage project @@ -25,7 +176,7 @@ Before you begin, make sure you have a [Flightcontrol account](https://app.fligh | Health Check Path | /catalog | | Port | 7007 | -5. Click `Create Project` and complete any required steps (like linking your AWS account). +5. Click `Create Project` and complete any required steps (like linking your AWS account). Remember to point the project to your custom files. # Deployment via Code @@ -33,7 +184,7 @@ Before you begin, make sure you have a [Flightcontrol account](https://app.fligh 2. Select the GitHub repo for your Backstage project -3. Select the `flightcontrol.json` Config Type. +3. Select the `flightcontrol.json` Config Type. Update the example JSON below where applicable to your custom Dockerfile. ```json filename="flightcontrol.json" { @@ -72,6 +223,8 @@ Before you begin, make sure you have a [Flightcontrol account](https://app.fligh If you need a database or Redis for your Backstage plugins, you can easily add those to your Flightcontrol deployment. For more information, see [the flightcontrol docs](https://www.flightcontrol.dev/docs/guides/flightcontrol/using-code?ref=backstage#redis). +When creating a Postgres RDS database you will need to update the connection string variable name to the one included in the `app-config.production.flightcontrol.yaml`. As noted above in our example we called this `POSTGRES_CON_STRING`. + ## Troubleshooting - [Flightcontrol Documentation](https://www.flightcontrol.dev/docs?ref=backstage) diff --git a/docs/deployment/k8s.md b/docs/deployment/k8s.md index b0218c8657..3dd2b8227e 100644 --- a/docs/deployment/k8s.md +++ b/docs/deployment/k8s.md @@ -384,7 +384,7 @@ $ yarn build-image --tag backstage:1.0.0 There is no special wiring needed to access the PostgreSQL service. Since it's running on the same cluster, Kubernetes will inject `POSTGRES_SERVICE_HOST` and `POSTGRES_SERVICE_PORT` environment variables into our Backstage container. -These can be used in the Backstage `app-config.yaml` along with the secrets: +These can be used in the Backstage `app-config.yaml` along with the secrets. Apply this to app-config.production.yaml as well if you have one: ```yaml backend: diff --git a/packages/backend-app-api/api-report.md b/packages/backend-app-api/api-report.md index e478a91bb7..f41a38de7b 100644 --- a/packages/backend-app-api/api-report.md +++ b/packages/backend-app-api/api-report.md @@ -9,6 +9,7 @@ import type { AppConfig } from '@backstage/config'; import { BackendFeature } from '@backstage/backend-plugin-api'; import { CacheClient } from '@backstage/backend-common'; import { Config } from '@backstage/config'; +import { ConfigSchema } from '@backstage/config-loader'; import { CorsOptions } from 'cors'; import { DiscoveryService } from '@backstage/backend-plugin-api'; import { ErrorRequestHandler } from 'express'; @@ -64,6 +65,7 @@ export const cacheServiceFactory: () => ServiceFactory; export function createConfigSecretEnumerator(options: { logger: LoggerService; dir?: string; + schema?: ConfigSchema; }): Promise<(config: Config) => Iterable>; // @public diff --git a/packages/backend-app-api/src/config/config.ts b/packages/backend-app-api/src/config/config.ts index 8ecc015b66..8f98c5597c 100644 --- a/packages/backend-app-api/src/config/config.ts +++ b/packages/backend-app-api/src/config/config.ts @@ -23,6 +23,7 @@ import { loadConfig, ConfigTarget, LoadConfigOptionsRemote, + ConfigSchema, } from '@backstage/config-loader'; import { ConfigReader } from '@backstage/config'; import type { Config, AppConfig } from '@backstage/config'; @@ -34,12 +35,15 @@ import { isValidUrl } from '../lib/urls'; export async function createConfigSecretEnumerator(options: { logger: LoggerService; dir?: string; + schema?: ConfigSchema; }): Promise<(config: Config) => Iterable> { const { logger, dir = process.cwd() } = options; const { packages } = await getPackages(dir); - const schema = await loadConfigSchema({ - dependencies: packages.map(p => p.packageJson.name), - }); + const schema = + options.schema ?? + (await loadConfigSchema({ + dependencies: packages.map(p => p.packageJson.name).filter(() => false), + })); return (config: Config) => { const [secretsData] = schema.process( diff --git a/packages/backend-common/src/reading/AwsS3UrlReader.ts b/packages/backend-common/src/reading/AwsS3UrlReader.ts index 23ef0f7b27..3ba4eebb48 100644 --- a/packages/backend-common/src/reading/AwsS3UrlReader.ts +++ b/packages/backend-common/src/reading/AwsS3UrlReader.ts @@ -46,6 +46,7 @@ import { import { AbortController } from '@aws-sdk/abort-controller'; import { ReadUrlResponseFactory } from './ReadUrlResponseFactory'; import { Readable } from 'stream'; +import { relative } from 'path/posix'; export const DEFAULT_REGION = 'us-east-1'; @@ -345,7 +346,7 @@ export class AwsS3UrlReader implements UrlReader { responses.push({ data: s3ObjectData, - path: String(allObjects[i]), + path: relative(path, String(allObjects[i])), lastModifiedAt: response?.LastModified ?? undefined, }); } diff --git a/packages/backend-common/src/reading/tree/ReadableArrayResponse.test.ts b/packages/backend-common/src/reading/tree/ReadableArrayResponse.test.ts index 0a27bdf09c..e2bfe1af59 100644 --- a/packages/backend-common/src/reading/tree/ReadableArrayResponse.test.ts +++ b/packages/backend-common/src/reading/tree/ReadableArrayResponse.test.ts @@ -19,6 +19,7 @@ import path from 'path'; import { FromReadableArrayOptions } from '../types'; import { ReadableArrayResponse } from './ReadableArrayResponse'; import { createMockDirectory } from '@backstage/backend-test-utils'; +import { relative } from 'path/posix'; const name1 = 'file1.yaml'; const file1 = fs.readFileSync( @@ -74,9 +75,12 @@ describe('ReadableArrayResponse', () => { }); it('should extract entire archive into directory', async () => { + const relativePath1 = relative(sourceDir.path, path1); + const relativePath2 = relative(sourceDir.path, path2); + const arr: FromReadableArrayOptions = [ - { data: createReadStream(path1), path: path1 }, - { data: createReadStream(path2), path: path2 }, + { data: createReadStream(path1), path: relativePath1 }, + { data: createReadStream(path2), path: relativePath2 }, ]; const res = new ReadableArrayResponse(arr, targetDir.path, 'etag'); diff --git a/packages/backend-common/src/reading/tree/ReadableArrayResponse.ts b/packages/backend-common/src/reading/tree/ReadableArrayResponse.ts index 1429931142..3c8bfd1fbc 100644 --- a/packages/backend-common/src/reading/tree/ReadableArrayResponse.ts +++ b/packages/backend-common/src/reading/tree/ReadableArrayResponse.ts @@ -15,7 +15,7 @@ */ import concatStream from 'concat-stream'; -import platformPath, { basename } from 'path'; +import platformPath, { dirname } from 'path'; import getRawBody from 'raw-body'; import fs from 'fs-extra'; @@ -96,12 +96,9 @@ export class ReadableArrayResponse implements ReadTreeResponse { for (let i = 0; i < this.stream.length; i++) { if (!this.stream[i].path.endsWith('/')) { - await pipeline( - this.stream[i].data, - fs.createWriteStream( - platformPath.join(dir, basename(this.stream[i].path)), - ), - ); + const filePath = platformPath.join(dir, this.stream[i].path); + await fs.mkdir(dirname(filePath), { recursive: true }); + await pipeline(this.stream[i].data, fs.createWriteStream(filePath)); } } diff --git a/packages/backend-dynamic-feature-service/api-report.md b/packages/backend-dynamic-feature-service/api-report.md index caf2cf01ec..64bffb468c 100644 --- a/packages/backend-dynamic-feature-service/api-report.md +++ b/packages/backend-dynamic-feature-service/api-report.md @@ -7,6 +7,7 @@ import { BackendFeature } from '@backstage/backend-plugin-api'; import { BackstagePackageJson } from '@backstage/cli-node'; import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; import { Config } from '@backstage/config'; +import { ConfigSchema } from '@backstage/config-loader'; import { EventBroker } from '@backstage/plugin-events-node'; import { EventsBackend } from '@backstage/plugin-events-backend'; import { FeatureDiscoveryService } from '@backstage/backend-plugin-api/alpha'; @@ -23,6 +24,7 @@ import { PluginCacheManager } from '@backstage/backend-common'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; +import { RootLoggerService } from '@backstage/backend-plugin-api'; import { Router } from 'express'; import { ServiceFactory } from '@backstage/backend-plugin-api'; import { ServiceRef } from '@backstage/backend-plugin-api'; @@ -115,6 +117,33 @@ export const dynamicPluginsFeatureDiscoveryServiceFactory: () => ServiceFactory< 'root' >; +// @public (undocumented) +export const dynamicPluginsFrontendSchemas: () => BackendFeature; + +// @public (undocumented) +export const dynamicPluginsRootLoggerServiceFactory: () => ServiceFactory< + RootLoggerService, + 'root' +>; + +// @public (undocumented) +export interface DynamicPluginsSchemasOptions { + schemaLocator?: (pluginPackage: ScannedPluginPackage) => string; +} + +// @public (undocumented) +export interface DynamicPluginsSchemasService { + // (undocumented) + addDynamicPluginsSchemas(configSchema: ConfigSchema): Promise<{ + schema: ConfigSchema; + }>; +} + +// @public (undocumented) +export const dynamicPluginsSchemasServiceFactory: ( + options?: DynamicPluginsSchemasOptions | undefined, +) => ServiceFactory; + // @public (undocumented) export const dynamicPluginsServiceFactory: ( options?: DynamicPluginsFactoryOptions | undefined, diff --git a/packages/backend-dynamic-feature-service/package.json b/packages/backend-dynamic-feature-service/package.json index 01616e38b8..a647dcbf45 100644 --- a/packages/backend-dynamic-feature-service/package.json +++ b/packages/backend-dynamic-feature-service/package.json @@ -5,9 +5,7 @@ "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { - "access": "public", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "access": "public" }, "repository": { "type": "git", @@ -17,6 +15,23 @@ "backstage": { "role": "node-library" }, + "exports": { + ".": "./src/index.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "package.json": [ + "package.json" + ] + } + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/backend-dynamic-feature-service" + }, "keywords": [ "backstage" ], @@ -31,13 +46,16 @@ "start": "backstage-cli package start" }, "dependencies": { + "@backstage/backend-app-api": "workspace:^", "@backstage/backend-common": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", "@backstage/backend-tasks": "workspace:^", "@backstage/cli-common": "workspace:^", "@backstage/cli-node": "workspace:^", "@backstage/config": "workspace:^", + "@backstage/config-loader": "workspace:^", "@backstage/errors": "workspace:^", + "@backstage/plugin-app-node": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", "@backstage/plugin-catalog-backend": "workspace:^", "@backstage/plugin-events-backend": "workspace:^", @@ -48,20 +66,21 @@ "@backstage/plugin-search-backend-node": "workspace:^", "@backstage/plugin-search-common": "workspace:^", "@backstage/types": "workspace:^", + "@manypkg/get-packages": "^1.1.3", "@types/express": "^4.17.6", "chokidar": "^3.5.3", "express": "^4.17.1", + "fs-extra": "10.1.0", "lodash": "^4.17.21", "winston": "^3.2.1" }, "devDependencies": { - "@backstage/backend-app-api": "workspace:^", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", - "@backstage/config-loader": "workspace:^", "wait-for-expect": "^3.0.2" }, "files": [ - "dist" + "dist", + "config.d.ts" ] } diff --git a/packages/backend-dynamic-feature-service/src/index.ts b/packages/backend-dynamic-feature-service/src/index.ts index 5570d8ee29..3768146124 100644 --- a/packages/backend-dynamic-feature-service/src/index.ts +++ b/packages/backend-dynamic-feature-service/src/index.ts @@ -17,3 +17,4 @@ export * from './loader'; export * from './scanner'; export * from './manager'; +export * from './schemas'; diff --git a/packages/backend-dynamic-feature-service/src/schemas/appBackendModule.ts b/packages/backend-dynamic-feature-service/src/schemas/appBackendModule.ts new file mode 100644 index 0000000000..b44913edfd --- /dev/null +++ b/packages/backend-dynamic-feature-service/src/schemas/appBackendModule.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2024 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 { + coreServices, + createBackendModule, +} from '@backstage/backend-plugin-api'; +import { dynamicPluginsSchemasServiceRef } from './schemas'; +import { + configSchemaExtensionPoint, + loadCompiledConfigSchema, +} from '@backstage/plugin-app-node'; +import { resolvePackagePath } from '@backstage/backend-common'; + +/** @public */ +export const dynamicPluginsFrontendSchemas = createBackendModule({ + pluginId: 'app', + moduleId: 'core.dynamicplugins.frontendSchemas', + register(reg) { + reg.registerInit({ + deps: { + config: coreServices.rootConfig, + schemas: dynamicPluginsSchemasServiceRef, + configSchemaExtension: configSchemaExtensionPoint, + }, + async init({ config, schemas, configSchemaExtension }) { + const appPackageName = + config.getOptionalString('app.packageName') ?? 'app'; + const appDistDir = resolvePackagePath(appPackageName, 'dist'); + const compiledConfigSchema = await loadCompiledConfigSchema(appDistDir); + if (compiledConfigSchema) { + configSchemaExtension.setConfigSchema( + (await schemas.addDynamicPluginsSchemas(compiledConfigSchema)) + .schema, + ); + } + }, + }); + }, +}); diff --git a/packages/backend-dynamic-feature-service/src/schemas/index.ts b/packages/backend-dynamic-feature-service/src/schemas/index.ts new file mode 100644 index 0000000000..94c167679b --- /dev/null +++ b/packages/backend-dynamic-feature-service/src/schemas/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2024 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 { + dynamicPluginsSchemasServiceFactory, + type DynamicPluginsSchemasService, + type DynamicPluginsSchemasOptions, +} from './schemas'; + +export { dynamicPluginsFrontendSchemas } from './appBackendModule'; +export { dynamicPluginsRootLoggerServiceFactory } from './rootLoggerServiceFactory'; diff --git a/packages/backend-dynamic-feature-service/src/schemas/rootLoggerServiceFactory.ts b/packages/backend-dynamic-feature-service/src/schemas/rootLoggerServiceFactory.ts new file mode 100644 index 0000000000..6681a65a75 --- /dev/null +++ b/packages/backend-dynamic-feature-service/src/schemas/rootLoggerServiceFactory.ts @@ -0,0 +1,63 @@ +/* + * Copyright 2024 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 { + createServiceFactory, + coreServices, +} from '@backstage/backend-plugin-api'; +import { WinstonLogger } from '@backstage/backend-app-api'; +import { transports, format } from 'winston'; +import { createConfigSecretEnumerator } from '@backstage/backend-app-api'; +import { loadConfigSchema } from '@backstage/config-loader'; +import { getPackages } from '@manypkg/get-packages'; +import { dynamicPluginsSchemasServiceRef } from './schemas'; + +/** @public */ +export const dynamicPluginsRootLoggerServiceFactory = createServiceFactory({ + service: coreServices.rootLogger, + deps: { + config: coreServices.rootConfig, + schemas: dynamicPluginsSchemasServiceRef, + }, + async factory({ config, schemas }) { + const logger = WinstonLogger.create({ + meta: { + service: 'backstage', + }, + level: process.env.LOG_LEVEL || 'info', + format: + process.env.NODE_ENV === 'production' + ? format.json() + : WinstonLogger.colorFormat(), + transports: [new transports.Console()], + }); + + const configSchema = await loadConfigSchema({ + dependencies: ( + await getPackages(process.cwd()) + ).packages.map(p => p.packageJson.name), + }); + + const secretEnumerator = await createConfigSecretEnumerator({ + logger, + schema: (await schemas.addDynamicPluginsSchemas(configSchema)).schema, + }); + logger.addRedactions(secretEnumerator(config)); + config.subscribe?.(() => logger.addRedactions(secretEnumerator(config))); + + return logger; + }, +}); diff --git a/packages/backend-dynamic-feature-service/src/schemas/schemas.ts b/packages/backend-dynamic-feature-service/src/schemas/schemas.ts new file mode 100644 index 0000000000..0c03e93ada --- /dev/null +++ b/packages/backend-dynamic-feature-service/src/schemas/schemas.ts @@ -0,0 +1,187 @@ +/* + * Copyright 2024 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 { ScannedPluginPackage } from '../scanner/types'; +import { + coreServices, + createServiceFactory, + createServiceRef, +} from '@backstage/backend-plugin-api'; +import { findPaths } from '@backstage/cli-common'; + +import fs from 'fs-extra'; +import * as path from 'path'; +import * as url from 'url'; +import { isEmpty } from 'lodash'; +import { LoggerService } from '@backstage/backend-plugin-api'; +import { JsonObject } from '@backstage/types'; +import { PluginScanner } from '../scanner/plugin-scanner'; +import { ConfigSchema, loadConfigSchema } from '@backstage/config-loader'; + +/** + * + * @public + * */ +export interface DynamicPluginsSchemasService { + addDynamicPluginsSchemas(configSchema: ConfigSchema): Promise<{ + schema: ConfigSchema; + }>; +} + +/** + * A service that provides the config schemas of scanned dynamic plugins. + * + * @public + */ +export const dynamicPluginsSchemasServiceRef = + createServiceRef({ + id: 'core.dynamicplugins.schemas', + scope: 'root', + }); + +/** + * @public + */ +export interface DynamicPluginsSchemasOptions { + /** + * Function that returns the path to the Json schema file for a given scanned plugin package. + * The path is either absolute, or relative to the plugin package root directory. + * + * Default behavior is to look for the `dist/configSchema.json` relative path. + * + * @param pluginPackage - The scanned plugin package. + * @returns the absolute or plugin-relative path to the Json schema file. + */ + schemaLocator?: (pluginPackage: ScannedPluginPackage) => string; +} + +/** + * @public + */ +export const dynamicPluginsSchemasServiceFactory = createServiceFactory( + (options?: DynamicPluginsSchemasOptions) => ({ + service: dynamicPluginsSchemasServiceRef, + deps: { + config: coreServices.rootConfig, + }, + factory({ config }) { + let additionalSchemas: { [context: string]: JsonObject } | undefined; + + return { + async addDynamicPluginsSchemas(configSchema: ConfigSchema): Promise<{ + schema: ConfigSchema; + }> { + if (!additionalSchemas) { + const logger = { + ...console, + child() { + return this; + }, + }; + + const scanner = PluginScanner.create({ + config, + logger, + // eslint-disable-next-line no-restricted-syntax + backstageRoot: findPaths(__dirname).targetRoot, + preferAlpha: true, + }); + + const { packages } = await scanner.scanRoot(); + + additionalSchemas = await gatherDynamicPluginsSchemas( + packages, + logger, + options?.schemaLocator, + ); + } + + const serialized = configSchema.serialize(); + if (serialized?.backstageConfigSchemaVersion !== 1) { + throw new Error( + 'Serialized configuration schema is invalid or has an invalid version number', + ); + } + const schemas = serialized.schemas as { + value: JsonObject; + path: string; + }[]; + + schemas.push( + ...Object.keys(additionalSchemas).map(context => { + return { + path: context, + value: additionalSchemas![context], + }; + }), + ); + serialized.schemas = schemas; + return { + schema: await loadConfigSchema({ + serialized, + }), + }; + }, + }; + }, + }), +); + +/** @internal */ +async function gatherDynamicPluginsSchemas( + packages: ScannedPluginPackage[], + logger: LoggerService, + schemaLocator: (pluginPackage: ScannedPluginPackage) => string = () => + path.join('dist', 'configSchema.json'), +): Promise<{ [context: string]: JsonObject }> { + const allSchemas: { [context: string]: JsonObject } = {}; + + for (const pluginPackage of packages) { + let schemaLocation = schemaLocator(pluginPackage); + + if (!path.isAbsolute(schemaLocation)) { + let pluginLocation = url.fileURLToPath(pluginPackage.location); + if (path.basename(pluginLocation) === 'alpha') { + pluginLocation = path.dirname(pluginLocation); + } + schemaLocation = path.resolve(pluginLocation, schemaLocation); + } + + if (!(await fs.pathExists(schemaLocation))) { + continue; + } + + const serialized = await fs.readJson(schemaLocation); + if (!serialized) { + continue; + } + + if (isEmpty(serialized)) { + continue; + } + + if (!serialized?.$schema || serialized?.type !== 'object') { + logger.error( + `Serialized configuration schema is invalid for plugin ${pluginPackage.manifest.name}`, + ); + continue; + } + + allSchemas[schemaLocation] = serialized; + } + + return allSchemas; +} diff --git a/packages/theme/src/unified/UnifiedTheme.tsx b/packages/theme/src/unified/UnifiedTheme.tsx index aeae9694ee..d6612d0a1d 100644 --- a/packages/theme/src/unified/UnifiedTheme.tsx +++ b/packages/theme/src/unified/UnifiedTheme.tsx @@ -93,5 +93,5 @@ export function createUnifiedThemeFromV4( ): UnifiedTheme { const v5Theme = adaptV4Theme(options as DeprecatedThemeOptions); const v4Theme = createTheme(options); - return new UnifiedThemeHolder(v4Theme, v5Theme); + return new UnifiedThemeHolder(v4Theme, createV5Theme(v5Theme)); } diff --git a/plugins/app-backend/api-report.md b/plugins/app-backend/api-report.md index 0383129823..0afc7021ad 100644 --- a/plugins/app-backend/api-report.md +++ b/plugins/app-backend/api-report.md @@ -4,6 +4,7 @@ ```ts import { Config } from '@backstage/config'; +import { ConfigSchema } from '@backstage/config-loader'; import express from 'express'; import { Logger } from 'winston'; import { PluginDatabaseManager } from '@backstage/backend-common'; @@ -20,6 +21,7 @@ export interface RouterOptions { disableConfigInjection?: boolean; // (undocumented) logger: Logger; + schema?: ConfigSchema; staticFallbackHandler?: express.Handler; } ``` diff --git a/plugins/app-backend/src/lib/config.ts b/plugins/app-backend/src/lib/config.ts index 63912df5c4..f09ba5c5ec 100644 --- a/plugins/app-backend/src/lib/config.ts +++ b/plugins/app-backend/src/lib/config.ts @@ -19,7 +19,11 @@ import { resolve as resolvePath } from 'path'; import { Logger } from 'winston'; import { AppConfig, Config } from '@backstage/config'; import { JsonObject } from '@backstage/types'; -import { loadConfigSchema, readEnvConfig } from '@backstage/config-loader'; +import { + ConfigSchema, + loadConfigSchema, + readEnvConfig, +} from '@backstage/config-loader'; type InjectOptions = { appConfigs: AppConfig[]; @@ -74,6 +78,7 @@ type ReadOptions = { env: { [name: string]: string | undefined }; appDistDir: string; config: Config; + schema?: ConfigSchema; }; /** @@ -90,7 +95,11 @@ export async function readConfigs(options: ReadOptions): Promise { const serializedSchema = await fs.readJson(schemaPath); try { - const schema = await loadConfigSchema({ serialized: serializedSchema }); + const schema = + options.schema || + (await loadConfigSchema({ + serialized: serializedSchema, + })); const frontendConfigs = await schema.process( [{ data: config.get() as JsonObject, context: 'app' }], diff --git a/plugins/app-backend/src/service/appPlugin.ts b/plugins/app-backend/src/service/appPlugin.ts index 9ba2ea6d6c..a2ea05623f 100644 --- a/plugins/app-backend/src/service/appPlugin.ts +++ b/plugins/app-backend/src/service/appPlugin.ts @@ -21,7 +21,11 @@ import { } from '@backstage/backend-plugin-api'; import { createRouter } from './router'; import { loggerToWinstonLogger } from '@backstage/backend-common'; -import { staticFallbackHandlerExtensionPoint } from '@backstage/plugin-app-node'; +import { + configSchemaExtensionPoint, + staticFallbackHandlerExtensionPoint, +} from '@backstage/plugin-app-node'; +import { ConfigSchema } from '@backstage/config-loader'; /** * The App plugin is responsible for serving the frontend app bundle and static assets. @@ -31,6 +35,7 @@ export const appPlugin = createBackendPlugin({ pluginId: 'app', register(env) { let staticFallbackHandler: express.Handler | undefined; + let schema: ConfigSchema | undefined; env.registerExtensionPoint(staticFallbackHandlerExtensionPoint, { setStaticFallbackHandler(handler) { @@ -43,6 +48,17 @@ export const appPlugin = createBackendPlugin({ }, }); + env.registerExtensionPoint(configSchemaExtensionPoint, { + setConfigSchema(configSchema) { + if (schema) { + throw new Error( + 'Attempted to set config schema for the app-backend twice', + ); + } + schema = configSchema; + }, + }); + env.registerInit({ deps: { logger: coreServices.logger, @@ -53,6 +69,7 @@ export const appPlugin = createBackendPlugin({ async init({ logger, config, database, httpRouter }) { const appPackageName = config.getOptionalString('app.packageName') ?? 'app'; + const winstonLogger = loggerToWinstonLogger(logger); const router = await createRouter({ @@ -61,6 +78,7 @@ export const appPlugin = createBackendPlugin({ database, appPackageName, staticFallbackHandler, + schema, }); httpRouter.use(router); }, diff --git a/plugins/app-backend/src/service/router.ts b/plugins/app-backend/src/service/router.ts index 5574892d37..17c5fd4f84 100644 --- a/plugins/app-backend/src/service/router.ts +++ b/plugins/app-backend/src/service/router.ts @@ -37,6 +37,7 @@ import { CACHE_CONTROL_NO_CACHE, CACHE_CONTROL_REVALIDATE_CACHE, } from '../lib/headers'; +import { ConfigSchema } from '@backstage/config-loader'; // express uses mime v1 while we only have types for mime v2 type Mime = { lookup(arg0: string): string }; @@ -85,6 +86,13 @@ export interface RouterOptions { * This also disables configuration injection though `APP_CONFIG_` environment variables. */ disableConfigInjection?: boolean; + + /** + * + * Provides a ConfigSchema. + * + */ + schema?: ConfigSchema; } /** @public */ @@ -121,6 +129,7 @@ export async function createRouter( config, appDistDir, env: process.env, + schema: options.schema, }); injectedConfigPath = await injectConfig({ appConfigs, logger, staticDir }); diff --git a/plugins/app-node/api-report.md b/plugins/app-node/api-report.md index 504c9d48c1..58e51e2b5c 100644 --- a/plugins/app-node/api-report.md +++ b/plugins/app-node/api-report.md @@ -3,9 +3,23 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { ConfigSchema } from '@backstage/config-loader'; import { ExtensionPoint } from '@backstage/backend-plugin-api'; import { Handler } from 'express'; +// @public +export interface ConfigSchemaExtensionPoint { + setConfigSchema(configSchema: ConfigSchema): void; +} + +// @public +export const configSchemaExtensionPoint: ExtensionPoint; + +// @public +export function loadCompiledConfigSchema( + appDistDir: string, +): Promise; + // @public export interface StaticFallbackHandlerExtensionPoint { setStaticFallbackHandler(handler: Handler): void; diff --git a/plugins/app-node/package.json b/plugins/app-node/package.json index 7ea9fa1d3c..01acf88e7b 100644 --- a/plugins/app-node/package.json +++ b/plugins/app-node/package.json @@ -34,7 +34,9 @@ ], "dependencies": { "@backstage/backend-plugin-api": "workspace:^", + "@backstage/config-loader": "workspace:^", "@types/express": "^4.17.6", - "express": "^4.17.1" + "express": "^4.17.1", + "fs-extra": "10.1.0" } } diff --git a/plugins/app-node/src/extensions.ts b/plugins/app-node/src/extensions.ts index 6a75a5b791..6cb612f8df 100644 --- a/plugins/app-node/src/extensions.ts +++ b/plugins/app-node/src/extensions.ts @@ -15,6 +15,7 @@ */ import { createExtensionPoint } from '@backstage/backend-plugin-api'; +import { ConfigSchema } from '@backstage/config-loader'; import { Handler } from 'express'; /** @@ -50,3 +51,25 @@ export const staticFallbackHandlerExtensionPoint = createExtensionPoint({ id: 'app.staticFallbackHandler', }); + +/** + * The interface for {@link configSchemaExtensionPoint}. + * + * @public + */ +export interface ConfigSchemaExtensionPoint { + /** + * Sets the config schema. This can only be done once. + */ + setConfigSchema(configSchema: ConfigSchema): void; +} + +/** + * An extension point the exposes the ability to override the config schema used by the frontend application. + * + * @public + */ +export const configSchemaExtensionPoint = + createExtensionPoint({ + id: 'app.configSchema', + }); diff --git a/plugins/app-node/src/index.ts b/plugins/app-node/src/index.ts index c5189fa8b9..4695471cf8 100644 --- a/plugins/app-node/src/index.ts +++ b/plugins/app-node/src/index.ts @@ -23,4 +23,8 @@ export { staticFallbackHandlerExtensionPoint, type StaticFallbackHandlerExtensionPoint, + configSchemaExtensionPoint, + type ConfigSchemaExtensionPoint, } from './extensions'; + +export { loadCompiledConfigSchema } from './schema'; diff --git a/plugins/app-node/src/schema.ts b/plugins/app-node/src/schema.ts new file mode 100644 index 0000000000..a38fd585d9 --- /dev/null +++ b/plugins/app-node/src/schema.ts @@ -0,0 +1,39 @@ +/* + * Copyright 2024 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 fs from 'fs-extra'; +import { resolve as resolvePath } from 'path'; +import { ConfigSchema, loadConfigSchema } from '@backstage/config-loader'; + +/** + * Loads the config schema that is embedded in the frontend build. + * + * @public + */ +export async function loadCompiledConfigSchema( + appDistDir: string, +): Promise { + const schemaPath = resolvePath(appDistDir, '.config-schema.json'); + if (await fs.pathExists(schemaPath)) { + const serializedSchema = await fs.readJson(schemaPath); + + return await loadConfigSchema({ + serialized: serializedSchema, + }); + } + + return undefined; +} diff --git a/plugins/auth-backend/config.d.ts b/plugins/auth-backend/config.d.ts index 25cd8d88ec..4c8430b33f 100644 --- a/plugins/auth-backend/config.d.ts +++ b/plugins/auth-backend/config.d.ts @@ -191,5 +191,9 @@ export interface Config { */ backstageTokenExpiration?: HumanDuration; }; + /** + * Additional app origins to allow for authenticating + */ + experimentalExtraAllowedOrigins?: string[]; }; } diff --git a/plugins/catalog-backend-module-github/config.d.ts b/plugins/catalog-backend-module-github/config.d.ts index ad4d2d6ec5..8b0f266c14 100644 --- a/plugins/catalog-backend-module-github/config.d.ts +++ b/plugins/catalog-backend-module-github/config.d.ts @@ -135,6 +135,11 @@ export interface Config { * Default: `/catalog-info.yaml`. */ catalogPath?: string; + /** + * (Optional) Whether to validate locations that exist before emitting them. + * Default: `false`. + */ + validateLocationsExist?: boolean; /** * (Optional) Filter configuration. */ diff --git a/plugins/devtools/README.md b/plugins/devtools/README.md index f6db8d1417..ca9b436acc 100644 --- a/plugins/devtools/README.md +++ b/plugins/devtools/README.md @@ -418,8 +418,10 @@ By default, only packages with names starting with `@backstage` and `@internal` devTools: info: packagePrefixes: - - @roadiehq/backstage- - - @spotify/backstage- + # Note that you MUST have quotes around these. The YAML won't be valid + # if you don't, because of the leading at-symbols. + - '@roadiehq/backstage-' + - '@spotify/backstage-' ``` ### External Dependencies Configuration diff --git a/plugins/dynatrace/README.md b/plugins/dynatrace/README.md index 5e2394cee3..73ac545265 100644 --- a/plugins/dynatrace/README.md +++ b/plugins/dynatrace/README.md @@ -62,10 +62,11 @@ This plugin requires a proxy endpoint for Dynatrace configured in `app-config.ya ```yaml proxy: - '/dynatrace': - target: 'https://example.dynatrace.com/api/v2' - headers: - Authorization: 'Api-Token ${DYNATRACE_ACCESS_TOKEN}' + endpoints: + '/dynatrace': + target: 'https://example.dynatrace.com/api/v2' + headers: + Authorization: 'Api-Token ${DYNATRACE_ACCESS_TOKEN}' ``` It also requires a `baseUrl` for rendering links to problems in the table like so: diff --git a/plugins/signals-node/api-report.md b/plugins/signals-node/api-report.md index cbf4142daf..9d67e24732 100644 --- a/plugins/signals-node/api-report.md +++ b/plugins/signals-node/api-report.md @@ -11,19 +11,23 @@ import { ServiceRef } from '@backstage/backend-plugin-api'; export class DefaultSignalService implements SignalService { // (undocumented) static create(options: SignalServiceOptions): DefaultSignalService; - publish(signal: SignalPayload): Promise; + publish( + signal: SignalPayload, + ): Promise; } // @public (undocumented) -export type SignalPayload = { +export type SignalPayload = { recipients: string[] | string | null; channel: string; - message: JsonObject; + message: TMessage; }; // @public (undocumented) export interface SignalService { - publish(signal: SignalPayload): Promise; + publish( + signal: SignalPayload, + ): Promise; } // @public (undocumented) diff --git a/plugins/signals-node/src/DefaultSignalService.ts b/plugins/signals-node/src/DefaultSignalService.ts index 2a234fe5d4..d1bc14e166 100644 --- a/plugins/signals-node/src/DefaultSignalService.ts +++ b/plugins/signals-node/src/DefaultSignalService.ts @@ -16,6 +16,7 @@ import { EventBroker } from '@backstage/plugin-events-node'; import { SignalPayload, SignalServiceOptions } from './types'; import { SignalService } from './SignalService'; +import { JsonObject } from '@backstage/types'; /** @public */ export class DefaultSignalService implements SignalService { @@ -31,12 +32,12 @@ export class DefaultSignalService implements SignalService { } /** - * Publishes a message to user refs to specific topic - * @param recipients - string or array of user ref strings to publish message to - * @param topic - message topic - * @param message - message to publish + * Publishes a signal to user refs to specific topic + * @param signal - Signal to publish */ - async publish(signal: SignalPayload) { + async publish( + signal: SignalPayload, + ) { await this.eventBroker?.publish({ topic: 'signals', eventPayload: signal, diff --git a/plugins/signals-node/src/SignalService.ts b/plugins/signals-node/src/SignalService.ts index 7d021ccf4e..7011c6c525 100644 --- a/plugins/signals-node/src/SignalService.ts +++ b/plugins/signals-node/src/SignalService.ts @@ -14,11 +14,15 @@ * limitations under the License. */ import { SignalPayload } from './types'; +import { JsonObject } from '@backstage/types'; /** @public */ export interface SignalService { /** - * Publishes a message to user refs to specific topic + * Publishes a signal to user refs to specific topic + * @param signal - Signal to publish */ - publish(signal: SignalPayload): Promise; + publish( + signal: SignalPayload, + ): Promise; } diff --git a/plugins/signals-node/src/types.ts b/plugins/signals-node/src/types.ts index 7e61aea320..ec6e815b29 100644 --- a/plugins/signals-node/src/types.ts +++ b/plugins/signals-node/src/types.ts @@ -24,8 +24,8 @@ export type SignalServiceOptions = { }; /** @public */ -export type SignalPayload = { +export type SignalPayload = { recipients: string[] | string | null; channel: string; - message: JsonObject; + message: TMessage; }; diff --git a/plugins/signals-react/api-report.md b/plugins/signals-react/api-report.md index ef4bc1b7fc..87d856288b 100644 --- a/plugins/signals-react/api-report.md +++ b/plugins/signals-react/api-report.md @@ -9,9 +9,9 @@ import { JsonObject } from '@backstage/types'; // @public (undocumented) export interface SignalApi { // (undocumented) - subscribe( + subscribe( channel: string, - onMessage: (message: JsonObject) => void, + onMessage: (message: TMessage) => void, ): SignalSubscriber; } @@ -25,8 +25,10 @@ export interface SignalSubscriber { } // @public (undocumented) -export const useSignal: (channel: string) => { - lastSignal: JsonObject | null; +export const useSignal: ( + channel: string, +) => { + lastSignal: TMessage | null; isSignalsAvailable: boolean; }; diff --git a/plugins/signals-react/src/api/SignalApi.ts b/plugins/signals-react/src/api/SignalApi.ts index b67b2ea0dc..66f2c7e881 100644 --- a/plugins/signals-react/src/api/SignalApi.ts +++ b/plugins/signals-react/src/api/SignalApi.ts @@ -28,8 +28,8 @@ export interface SignalSubscriber { /** @public */ export interface SignalApi { - subscribe( + subscribe( channel: string, - onMessage: (message: JsonObject) => void, + onMessage: (message: TMessage) => void, ): SignalSubscriber; } diff --git a/plugins/signals-react/src/hooks/useSignal.ts b/plugins/signals-react/src/hooks/useSignal.ts index 084427e2cc..6fb0f4696f 100644 --- a/plugins/signals-react/src/hooks/useSignal.ts +++ b/plugins/signals-react/src/hooks/useSignal.ts @@ -19,18 +19,23 @@ import { JsonObject } from '@backstage/types'; import { useEffect, useMemo, useState } from 'react'; /** @public */ -export const useSignal = (channel: string) => { +export const useSignal = ( + channel: string, +): { lastSignal: TMessage | null; isSignalsAvailable: boolean } => { const apiHolder = useApiHolder(); // Use apiHolder instead useApi in case signalApi is not available in the // backstage instance this is used const signals = apiHolder.get(signalApiRef); - const [lastSignal, setLastSignal] = useState(null); + const [lastSignal, setLastSignal] = useState(null); useEffect(() => { let unsub: null | (() => void) = null; if (signals) { - const { unsubscribe } = signals.subscribe(channel, (msg: JsonObject) => { - setLastSignal(msg); - }); + const { unsubscribe } = signals.subscribe( + channel, + (msg: TMessage) => { + setLastSignal(msg); + }, + ); unsub = unsubscribe; } return () => { diff --git a/plugins/signals/api-report.md b/plugins/signals/api-report.md index 398d5fbfe6..1af1063433 100644 --- a/plugins/signals/api-report.md +++ b/plugins/signals/api-report.md @@ -8,6 +8,7 @@ import { DiscoveryApi } from '@backstage/core-plugin-api'; import { IdentityApi } from '@backstage/core-plugin-api'; import { JsonObject } from '@backstage/types'; import { SignalApi } from '@backstage/plugin-signals-react'; +import { SignalSubscriber } from '@backstage/plugin-signals-react'; // @public (undocumented) export class SignalClient implements SignalApi { @@ -23,12 +24,10 @@ export class SignalClient implements SignalApi { // (undocumented) static readonly DEFAULT_RECONNECT_TIMEOUT_MS: number; // (undocumented) - subscribe( + subscribe( channel: string, - onMessage: (message: JsonObject) => void, - ): { - unsubscribe: () => void; - }; + onMessage: (message: TMessage) => void, + ): SignalSubscriber; } // @public (undocumented) diff --git a/plugins/signals/src/api/SignalClient.ts b/plugins/signals/src/api/SignalClient.ts index d6c4634264..feccf4a105 100644 --- a/plugins/signals/src/api/SignalClient.ts +++ b/plugins/signals/src/api/SignalClient.ts @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { SignalApi } from '@backstage/plugin-signals-react'; +import { SignalApi, SignalSubscriber } from '@backstage/plugin-signals-react'; import { JsonObject } from '@backstage/types'; import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api'; import { v4 as uuid } from 'uuid'; type Subscription = { channel: string; - callback: (message: JsonObject) => void; + callback: (message: any) => void; }; const WS_CLOSE_NORMAL = 1000; @@ -62,16 +62,16 @@ export class SignalClient implements SignalApi { private reconnectTimeout: number, ) {} - subscribe( + subscribe( channel: string, - onMessage: (message: JsonObject) => void, - ): { unsubscribe: () => void } { + onMessage: (message: TMessage) => void, + ): SignalSubscriber { const subscriptionId = uuid(); const exists = [...this.subscriptions.values()].find( sub => sub.channel === channel, ); this.subscriptions.set(subscriptionId, { - channel: channel, + channel, callback: onMessage, }); @@ -178,12 +178,14 @@ export class SignalClient implements SignalApi { private handleMessage(data: MessageEvent) { try { - const json = JSON.parse(data.data) as JsonObject; - if (json.channel) { - for (const sub of this.subscriptions.values()) { - if (sub.channel === json.channel) { - sub.callback(json.message as JsonObject); - } + const json = JSON.parse(data.data); + if (!json.channel) { + return; + } + + for (const sub of this.subscriptions.values()) { + if (sub.channel === json.channel) { + sub.callback(json.message); } } } catch (e) { diff --git a/scripts/verify-local-dependencies.js b/scripts/verify-local-dependencies.js index 5d75348681..e2aaf8c33a 100755 --- a/scripts/verify-local-dependencies.js +++ b/scripts/verify-local-dependencies.js @@ -90,6 +90,7 @@ const roleRules = [ '@backstage/backend-common', '@backstage/backend-defaults', '@backstage/backend-test-utils', + '@backstage/backend-dynamic-feature-service', ], message: `Plugin package SOURCE_NAME with role SOURCE_ROLE has a runtime dependency on package TARGET_NAME, which is not permitted. If you are using this dependency for dev server purposes, you can move it to devDependencies instead.`, }, diff --git a/yarn.lock b/yarn.lock index d4c7e23be6..04c963a30d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3383,6 +3383,7 @@ __metadata: "@backstage/config": "workspace:^" "@backstage/config-loader": "workspace:^" "@backstage/errors": "workspace:^" + "@backstage/plugin-app-node": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" "@backstage/plugin-catalog-backend": "workspace:^" "@backstage/plugin-events-backend": "workspace:^" @@ -3393,9 +3394,11 @@ __metadata: "@backstage/plugin-search-backend-node": "workspace:^" "@backstage/plugin-search-common": "workspace:^" "@backstage/types": "workspace:^" + "@manypkg/get-packages": ^1.1.3 "@types/express": ^4.17.6 chokidar: ^3.5.3 express: ^4.17.1 + fs-extra: 10.1.0 lodash: ^4.17.21 wait-for-expect: ^3.0.2 winston: ^3.2.1 @@ -4647,8 +4650,10 @@ __metadata: dependencies: "@backstage/backend-plugin-api": "workspace:^" "@backstage/cli": "workspace:^" + "@backstage/config-loader": "workspace:^" "@types/express": ^4.17.6 express: ^4.17.1 + fs-extra: 10.1.0 languageName: unknown linkType: soft