diff --git a/.github/workflows/cli-win.yml b/.github/workflows/cli-win.yml new file mode 100644 index 0000000000..52ec1b8b27 --- /dev/null +++ b/.github/workflows/cli-win.yml @@ -0,0 +1,65 @@ +name: CLI Test Windows + +# Building on windows is really slow, so this workflow is separate from cli.yml and only builds on changes +# to the cli itself. They're more likely to introduce issues on windows, compared to changes to core and yarn.lock. +on: + pull_request: + paths: + - '.github/workflows/cli-win.yml' + - 'packages/cli/**' + +jobs: + build: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [windows-latest] + node-version: [12.x] + + env: + CI: true + NODE_OPTIONS: --max-old-space-size=4096 + + name: Node ${{ matrix.node-version }} on ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + - name: find location of global yarn cache + id: yarn-cache + run: echo "::set-output name=dir::$(yarn cache dir)" + - name: cache global yarn cache + uses: actions/cache@v2 + with: + path: ${{ steps.yarn-cache.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: use node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - name: yarn install + run: yarn install --frozen-lockfile + - run: yarn tsc + - run: yarn build + - name: verify app and plugin creation + working-directory: ${{ runner.temp }} + run: node ${{ github.workspace }}/packages/cli/e2e-test/cli-e2e-test.js + env: + BACKSTAGE_E2E_CLI_TEST: true + - name: lint newly created app and plugin + run: yarn lint:all + working-directory: ${{ runner.temp }}/test-app + env: + BACKSTAGE_E2E_CLI_TEST: true + - name: test newly created app and plugin + run: yarn test:all + working-directory: ${{ runner.temp }}/test-app + env: + BACKSTAGE_E2E_CLI_TEST: true + - name: e2e test newly created app + run: yarn test:e2e:ci + working-directory: ${{ runner.temp }}/test-app/packages/app + env: + PORT: 3001 + BACKSTAGE_E2E_CLI_TEST: true diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 24ff2a734c..62f91e3259 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -6,6 +6,7 @@ on: - '.github/workflows/cli.yml' - 'packages/cli/**' - 'packages/core/**' + - 'packages/core-api/**' - 'yarn.lock' jobs: @@ -14,7 +15,7 @@ jobs: strategy: matrix: - os: [ubuntu-latest, windows-latest] + os: [ubuntu-latest] node-version: [12.x] env: @@ -28,7 +29,7 @@ jobs: id: yarn-cache run: echo "::set-output name=dir::$(yarn cache dir)" - name: cache global yarn cache - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: ${{ steps.yarn-cache.outputs.dir }} key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} @@ -42,15 +43,8 @@ jobs: run: yarn install --frozen-lockfile - run: yarn tsc - run: yarn build - - name: verify app and plugin creation on Windows + - name: verify app and plugin creation working-directory: ${{ runner.temp }} - if: runner.os == 'Windows' - run: node ${{ github.workspace }}/packages/cli/e2e-test/cli-e2e-test.js - env: - BACKSTAGE_E2E_CLI_TEST: true - - name: verify app and plugin creation on Linux - working-directory: ${{ runner.temp }} - if: runner.os == 'Linux' run: | sudo sysctl fs.inotify.max_user_watches=524288 node ${{ github.workspace }}/packages/cli/e2e-test/cli-e2e-test.js diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 55a594d55e..c3fa6e4e23 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -25,19 +25,19 @@ jobs: id: yarn-cache run: echo "::set-output name=dir::$(yarn cache dir)" - name: cache global yarn cache - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: ${{ steps.yarn-cache.outputs.dir }} key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} restore-keys: | ${{ runner.os }}-yarn- - name: cache node_modules - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: node_modules key: ${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }} - name: cache build cache - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: .backstage-build-cache key: build-cache-${{ github.sha }} diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 14619305cd..52f2760469 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -24,19 +24,19 @@ jobs: id: yarn-cache run: echo "::set-output name=dir::$(yarn cache dir)" - name: cache global yarn cache - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: ${{ steps.yarn-cache.outputs.dir }} key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} restore-keys: | ${{ runner.os }}-yarn- - name: cache node_modules - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: node_modules key: ${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }} - name: cache build cache - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: .backstage-build-cache key: build-cache-${{ github.sha }} diff --git a/.github/workflows/storybook-deploy.yml b/.github/workflows/storybook-deploy.yml index d6eef29744..33ffba5ccd 100644 --- a/.github/workflows/storybook-deploy.yml +++ b/.github/workflows/storybook-deploy.yml @@ -27,14 +27,14 @@ jobs: id: yarn-cache run: echo "::set-output name=dir::$(yarn cache dir)" - name: cache global yarn cache - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: ${{ steps.yarn-cache.outputs.dir }} key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} restore-keys: | ${{ runner.os }}-yarn- - name: cache node_modules - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: node_modules key: ${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }} diff --git a/app-config.yaml b/app-config.yaml new file mode 100644 index 0000000000..6ff336c727 --- /dev/null +++ b/app-config.yaml @@ -0,0 +1,9 @@ +app: + title: Backstage Example App + baseUrl: http://localhost:3000 + +backend: + baseUrl: http://localhost:7000 + +organization: + name: Spotify diff --git a/docs/architecture-decisions/adr005-catalog-core-entities.md b/docs/architecture-decisions/adr005-catalog-core-entities.md new file mode 100644 index 0000000000..8e20827280 --- /dev/null +++ b/docs/architecture-decisions/adr005-catalog-core-entities.md @@ -0,0 +1,77 @@ +# ADR005: Catalog Core Entities + +| Created | Status | +| ---------- | ------ | +| 2020-05-29 | Open | + +## Context + +We want to standardize on a few core entities that we are tracking in the Backstage catalog. This allows us to build specific plugins around them. + +## Decision + +Backstage should eventually support the following core entities: + +* **Components** are individual pieces of software +* **APIs** are the boundaries between different components +* **Resources** are physical or virtual infrastructure needed to operate a component + +![Catalog Core Entities](catalog-core-entities.png) + +For now, we'll start by only implementing support for the Component entity in the Backstage catalog. This can later be extended to APIs, Resources and other potentially useful entities. + +### Component +A component is a piece of software, for example a mobile application feature, web site, backend service or data pipeline (list not exhaustive). A component can be tracked in source control, or use some existing open source or commercial software. It can implement APIs for other components to consume. In turn it might depend on APIs implemented by other components, or resources that are attached to it at runtime. + +Component entities are typically defined in YAML descriptor files next to the code of the component, and could look like this (actual schema will evolve): +```yaml +apiVersion: backstage.io/v1beta1 +kind: Component +metadata: + name: my-component-name +spec: + type: service +``` + +### API +APIs form an abstraction that allows large software ecosystems to scale. Thus, APIs are a first class citizen in the Backstage model and the primary way to discover existing functionality in the ecosystem. + +APIs are implemented by components and make their boundaries explicit. They might be defined using an RPC IDL (e.g. in Protobuf, GraphQL or similar), a data schema (e.g. in Avro, TFRecord or similar), or as code interfaces (e.g. framework APIs in Swift, Kotlin, Java, C++, Typescript etc). In any case, APIs exposed by components need to be in a known machine-readable format so we can build further tooling and analysis on top. + +APIs are typically indexed from existing definitions in source control and thus wouldn't need their own descriptor files, but would be stored in the catalog somewhat like this (actual schema will evolve): +```yaml +apiVersion: backstage.io/v1beta1 +kind: API +metadata: + name: my-component-api +spec: + type: grpc + definition: > + service HelloService { + rpc SayHello (HelloRequest) returns (HelloResponse); + } + message HelloRequest { + string greeting = 1; + } + message HelloResponse { + string reply = 1; + } +``` + +### Resource +Resources are the infrastructure your software needs to operate at runtime like Bigtable databases, Pub/Sub topics, S3 buckets or CDNs. Modelling them together with components and APIs will allow us to visualize and create tooling around them in Backstage. + +Resources are typically indexed from declarative definitions (e.g. Terraform, GCP Config Connector, AWS Cloud Formation) and/or inventories from cloud providers (e.g. GCP Asset Inventory) and thus wouldn't need their own descriptor files, but would be stored in the catalog somewhat like this (actual schema will evolve): +```yaml +apiVersion: backstage.io/v1beta1 +kind: Resource +metadata: + name: my-component-db +spec: + type: gcp-spanner + url: spanner.googleapis.com/projects/prj/instances/my-component-db/databases/my-db +``` + +## Consequences + +We will continue fleshing out support for the Component entity in the Backstage catalog. diff --git a/docs/architecture-decisions/catalog-core-entities.png b/docs/architecture-decisions/catalog-core-entities.png new file mode 100644 index 0000000000..b0c7cb4575 Binary files /dev/null and b/docs/architecture-decisions/catalog-core-entities.png differ diff --git a/docs/getting-started/development-environment.md b/docs/getting-started/development-environment.md index 489116fcc8..26b432570f 100644 --- a/docs/getting-started/development-environment.md +++ b/docs/getting-started/development-environment.md @@ -1,16 +1,28 @@ # Development Environment +This section describes how to get set up for doing development on the Backstage repository. + +## Cloning the Repository + +After you have cloned the Backstage repository, you should run the following commands +once to set things up for development: + +```bash +$ yarn install # fetch dependency packages - may take a while + +$ yarn tsc # does a first run of type generation and checks +``` + ## Serving the Example App -Open a terminal window and start the web app using the following commands from the project root: +Open a terminal window and start the web app by using the following command from the project root. +Make sure you have run the above mentioned commands first. ```bash -$ yarn install # may take a while - $ yarn start ``` -The final `yarn start` command should open a local instance of Backstage in your browser, otherwise open one of the URLs printed in the terminal. +This should open a local instance of Backstage in your browser, otherwise open one of the URLs printed in the terminal. By default, backstage will start on port 3000, however you can override this by setting an environment variable `PORT` on your local machine. e.g. `export PORT=8080` then running `yarn start`. Or `PORT=8080 yarn start`. diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index c18169ae03..b4d01e067b 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -24,18 +24,6 @@ import apis from './apis'; const app = createApp({ apis, plugins: Object.values(plugins), - configLoader: async () => ({ - app: { - title: 'Backstage Example App', - baseUrl: 'http://localhost:3000', - }, - backend: { - baseUrl: 'http://localhost:7000', - }, - organization: { - name: 'Spotify', - }, - }), }); const AppProvider = app.getProvider(); diff --git a/packages/backend/src/plugins/catalog.ts b/packages/backend/src/plugins/catalog.ts index 150b9d1826..906e8c2d11 100644 --- a/packages/backend/src/plugins/catalog.ts +++ b/packages/backend/src/plugins/catalog.ts @@ -32,7 +32,6 @@ export default async function createPlugin({ logger, database, }: PluginEnvironment) { - const policy = new EntityPolicies(); const ingestionModel = new IngestionModels( new LocationReaders(), new DescriptorParsers(), @@ -40,19 +39,17 @@ export default async function createPlugin({ ); const db = await DatabaseManager.createDatabase(database, logger); - runPeriodically( - () => DatabaseManager.refreshLocations(db, ingestionModel, policy, logger), - 10000, - ); - const entitiesCatalog = new DatabaseEntitiesCatalog(db); const locationsCatalog = new DatabaseLocationsCatalog(db); const higherOrderOperation = new HigherOrderOperations( entitiesCatalog, locationsCatalog, ingestionModel, + logger, ); + runPeriodically(() => higherOrderOperation.refreshAllLocations(), 10000); + return await createRouter({ entitiesCatalog, locationsCatalog, diff --git a/packages/cli/package.json b/packages/cli/package.json index 20d9893a37..784df5de6b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -79,6 +79,7 @@ "url-loader": "^4.1.0", "webpack": "^4.41.6", "webpack-dev-server": "^3.10.3", + "yaml": "^1.10.0", "yml-loader": "^2.1.0", "yn": "^4.0.0" }, diff --git a/packages/cli/src/commands/app/build.ts b/packages/cli/src/commands/app/build.ts index c654baa439..fad3e6db13 100644 --- a/packages/cli/src/commands/app/build.ts +++ b/packages/cli/src/commands/app/build.ts @@ -16,10 +16,12 @@ import { buildBundle } from '../../lib/bundler'; import { Command } from 'commander'; +import { loadConfig } from '../../lib/app-config'; export default async (cmd: Command) => { await buildBundle({ entry: 'src/index', statsJsonEnabled: cmd.stats, + appConfig: await loadConfig(), }); }; diff --git a/packages/cli/src/commands/app/serve.ts b/packages/cli/src/commands/app/serve.ts index 416f8f0151..19dfd9a7be 100644 --- a/packages/cli/src/commands/app/serve.ts +++ b/packages/cli/src/commands/app/serve.ts @@ -16,11 +16,13 @@ import { Command } from 'commander'; import { serveBundle } from '../../lib/bundler'; +import { loadConfig } from '../../lib/app-config'; export default async (cmd: Command) => { const waitForExit = await serveBundle({ entry: 'src/index', checksEnabled: cmd.check, + appConfig: await loadConfig(), }); await waitForExit(); diff --git a/packages/cli/src/commands/clean/clean.ts b/packages/cli/src/commands/clean/clean.ts index 2fa49fcc4a..91531a53ed 100644 --- a/packages/cli/src/commands/clean/clean.ts +++ b/packages/cli/src/commands/clean/clean.ts @@ -24,6 +24,7 @@ export default async function clean() { const packagePath = getPackagePath(cacheOptions.cacheDir); await fs.remove(cacheOptions.output); await fs.remove(packagePath); + await fs.remove(paths.resolveTarget('coverage')); } function getPackagePath(cacheDir: string) { diff --git a/packages/cli/src/commands/plugin/serve.ts b/packages/cli/src/commands/plugin/serve.ts index 8abbd92440..174d1fe4af 100644 --- a/packages/cli/src/commands/plugin/serve.ts +++ b/packages/cli/src/commands/plugin/serve.ts @@ -16,11 +16,13 @@ import { Command } from 'commander'; import { serveBundle } from '../../lib/bundler'; +import { loadConfig } from '../../lib/app-config'; export default async (cmd: Command) => { const waitForExit = await serveBundle({ entry: 'dev/index', checksEnabled: cmd.check, + appConfig: await loadConfig(), }); await waitForExit(); diff --git a/plugins/auth-backend/src/providers/index.test.ts b/packages/cli/src/lib/app-config/index.ts similarity index 79% rename from plugins/auth-backend/src/providers/index.test.ts rename to packages/cli/src/lib/app-config/index.ts index 7f39d9de57..e2c80f89e1 100644 --- a/plugins/auth-backend/src/providers/index.test.ts +++ b/packages/cli/src/lib/app-config/index.ts @@ -14,10 +14,5 @@ * limitations under the License. */ -import { defaultRouter } from '.'; - -describe('test', () => { - it('unbreaks the test runner', () => { - expect(defaultRouter).toBeDefined(); - }); -}); +export type { AppConfig } from './types'; +export { loadConfig } from './loaders'; diff --git a/packages/cli/src/lib/app-config/loaders.ts b/packages/cli/src/lib/app-config/loaders.ts new file mode 100644 index 0000000000..6e6a56e6c5 --- /dev/null +++ b/packages/cli/src/lib/app-config/loaders.ts @@ -0,0 +1,41 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AppConfig } from './types'; +import fs from 'fs-extra'; +import yaml from 'yaml'; +import { paths } from '../paths'; + +type LoadConfigOptions = { + // Config path, defaults to app-config.yaml in project root + configPath?: string; +}; + +export async function loadConfig( + options: LoadConfigOptions = {}, +): Promise { + // TODO: We'll want this to be a bit more elaborate, probably adding configs for + // specific env, and maybe local config for plugins. + const { configPath = paths.resolveTargetRoot('app-config.yaml') } = options; + + try { + const configYaml = await fs.readFile(configPath, 'utf8'); + const config = yaml.parse(configYaml); + return [config]; + } catch (error) { + throw new Error(`Failed to read static configuration file, ${error}`); + } +} diff --git a/packages/cli/src/lib/app-config/types.ts b/packages/cli/src/lib/app-config/types.ts new file mode 100644 index 0000000000..d15cbe3787 --- /dev/null +++ b/packages/cli/src/lib/app-config/types.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type AppConfig = any; diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index a35c22e3bd..deb02a38c6 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -51,6 +51,12 @@ export function createConfig( ); } + plugins.push( + new webpack.EnvironmentPlugin({ + APP_CONFIG: options.appConfig, + }), + ); + return { mode: isDev ? 'development' : 'production', profile: false, diff --git a/packages/cli/src/lib/bundler/types.ts b/packages/cli/src/lib/bundler/types.ts index 4182226d69..1a9f49701c 100644 --- a/packages/cli/src/lib/bundler/types.ts +++ b/packages/cli/src/lib/bundler/types.ts @@ -15,16 +15,20 @@ */ import { BundlingPathsOptions } from './paths'; +import { AppConfig } from '../app-config'; export type BundlingOptions = { checksEnabled: boolean; isDev: boolean; + appConfig: AppConfig[]; }; export type ServeOptions = BundlingPathsOptions & { checksEnabled: boolean; + appConfig: AppConfig[]; }; export type BuildOptions = BundlingPathsOptions & { statsJsonEnabled: boolean; + appConfig: AppConfig[]; }; diff --git a/packages/cli/templates/default-app/app-config.yaml b/packages/cli/templates/default-app/app-config.yaml new file mode 100644 index 0000000000..b4c53905de --- /dev/null +++ b/packages/cli/templates/default-app/app-config.yaml @@ -0,0 +1,5 @@ +app: + title: Scaffolded Backstage App + +organization: + name: Acme Corporation diff --git a/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.ts b/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.ts index 7a5cf3b185..f6a8bce17b 100644 --- a/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.ts +++ b/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.ts @@ -15,6 +15,7 @@ */ import { ConfigApi, Config } from '../../definitions/ConfigApi'; +import { AppConfig } from '../../../app'; const CONFIG_KEY_PART_PATTERN = /^[a-z][a-z0-9]*(?:[-_][a-z][a-z0-9]*)*$/i; @@ -62,6 +63,18 @@ function validateString( export class ConfigReader implements ConfigApi { static nullReader = new ConfigReader({}); + static fromConfigs(configs: AppConfig[]): ConfigReader { + if (configs.length === 0) { + return new ConfigReader({}); + } + + // Merge together all configs info a single config with recursive fallback + // readers, giving the first config object in the array the highest priority. + return configs.reduceRight((previousReader, nextConfig) => { + return new ConfigReader(nextConfig, previousReader); + }, undefined); + } + constructor( private readonly data: JsonObject, private readonly fallback?: ConfigApi, diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx index 4207a267af..282e0aa474 100644 --- a/packages/core-api/src/app/App.tsx +++ b/packages/core-api/src/app/App.tsx @@ -150,7 +150,7 @@ export class PrivateAppImpl implements BackstageApp { const Provider: FC<{}> = ({ children }) => { // Keeping this synchronous when a config loader isn't set simplifies tests a lot const hasConfig = Boolean(this.configLoader); - const config = useAsync(this.configLoader || (() => Promise.resolve({}))); + const config = useAsync(this.configLoader || (() => Promise.resolve([]))); let childNode = children; @@ -164,7 +164,7 @@ export class PrivateAppImpl implements BackstageApp { const appApis = ApiRegistry.from([ [appThemeApiRef, AppThemeSelector.createWithStorage(this.themes)], - [configApiRef, new ConfigReader(config.value ?? {})], + [configApiRef, ConfigReader.fromConfigs(config.value ?? [])], ]); const apis = new ApiAggregator(this.apis, appApis); diff --git a/packages/core-api/src/app/types.ts b/packages/core-api/src/app/types.ts index defc155a82..953a10cbb2 100644 --- a/packages/core-api/src/app/types.ts +++ b/packages/core-api/src/app/types.ts @@ -38,8 +38,11 @@ export type AppConfig = any; /** * A function that loads in the App config that will be accessible via the ConfigApi. + * + * If multiple config objects are returned in the array, values in the earlier configs + * will override later ones. */ -export type AppConfigLoader = () => Promise; +export type AppConfigLoader = () => Promise; export type AppOptions = { /** diff --git a/packages/core/src/api-wrappers/createApp.test.tsx b/packages/core/src/api-wrappers/createApp.test.tsx new file mode 100644 index 0000000000..30553d84a7 --- /dev/null +++ b/packages/core/src/api-wrappers/createApp.test.tsx @@ -0,0 +1,74 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { defaultConfigLoader } from './createApp'; + +describe('defaultConfigLoader', () => { + afterEach(() => { + delete process.env.APP_CONFIG; + }); + + it('loads static config', async () => { + Object.defineProperty(process.env, 'APP_CONFIG', { + configurable: true, + value: [{ my: 'config' }, { my: 'override-config' }] as any, + }); + const configs = await defaultConfigLoader(); + expect(configs).toEqual([{ my: 'config' }, { my: 'override-config' }]); + }); + + it('loads runtime config', async () => { + Object.defineProperty(process.env, 'APP_CONFIG', { + configurable: true, + value: [{ my: 'override-config' }, { my: 'config' }] as any, + }); + const configs = await (defaultConfigLoader as any)( + '{"my":"runtime-config"}', + ); + expect(configs).toEqual([ + { my: 'runtime-config' }, + { my: 'override-config' }, + { my: 'config' }, + ]); + }); + + it('fails to load invalid missing config', async () => { + await expect(defaultConfigLoader()).rejects.toThrow( + 'No static configuration provided', + ); + }); + + it('fails to load invalid static config', async () => { + Object.defineProperty(process.env, 'APP_CONFIG', { + configurable: true, + value: { my: 'invalid-config' } as any, + }); + await expect(defaultConfigLoader()).rejects.toThrow( + 'Static configuration has invalid format', + ); + }); + + it('fails to load bad runtime config', async () => { + Object.defineProperty(process.env, 'APP_CONFIG', { + configurable: true, + value: [{ my: 'config' }] as any, + }); + + await expect((defaultConfigLoader as any)('}')).rejects.toThrow( + 'Failed to load runtime configuration, SyntaxError: Unexpected token } in JSON at position 0', + ); + }); +}); diff --git a/packages/core/src/api-wrappers/createApp.tsx b/packages/core/src/api-wrappers/createApp.tsx index 7c605c4e18..c53a4765b7 100644 --- a/packages/core/src/api-wrappers/createApp.tsx +++ b/packages/core/src/api-wrappers/createApp.tsx @@ -20,6 +20,8 @@ import privateExports, { ApiRegistry, defaultSystemIcons, BootErrorPageProps, + AppConfigLoader, + AppConfig, } from '@backstage/core-api'; import { BrowserRouter as Router } from 'react-router-dom'; @@ -29,6 +31,43 @@ import { lightTheme, darkTheme } from '@backstage/theme'; const { PrivateAppImpl } = privateExports; +/** + * The default config loader, which expects that config is available at compile-time + * in `process.env.APP_CONFIG`. APP_CONFIG should be an array of config objects as + * returned by the config loader. + * + * It will also load runtime config from the __APP_INJECTED_RUNTIME_CONFIG__ string, + * which can be rewritten at runtime to contain an additional JSON config object. + * If runtime config is present, it will be placed first in the config array, overriding + * other config values. + */ +export const defaultConfigLoader: AppConfigLoader = async ( + // This string may be replaced at runtime to provide additional config. + // It should be replaced by a JSON-serialized config object. + // It's a param so we can test it, but at runtime this will always fall back to default. + runtimeConfigJson: string = '__APP_INJECTED_RUNTIME_CONFIG__', +) => { + const appConfig = process.env.APP_CONFIG; + if (!appConfig) { + throw new Error('No static configuration provided'); + } + if (!Array.isArray(appConfig)) { + throw new Error('Static configuration has invalid format'); + } + const configs = (appConfig.slice() as unknown) as AppConfig[]; + + // Avoiding this string also being replaced at runtime + if (runtimeConfigJson !== '__app_injected_runtime_config__'.toUpperCase()) { + try { + configs.unshift(JSON.parse(runtimeConfigJson)); + } catch (error) { + throw new Error(`Failed to load runtime configuration, ${error}`); + } + } + + return configs; +}; + // createApp is defined in core, and not core-api, since we need access // to the components inside core to provide defaults. // The actual implementation of the app class still lives in core-api, @@ -77,7 +116,7 @@ export function createApp(options?: AppOptions) { theme: darkTheme, }, ]; - const configLoader = options?.configLoader ?? (async () => ({})); + const configLoader = options?.configLoader ?? defaultConfigLoader; const app = new PrivateAppImpl({ apis, diff --git a/plugins/auth-backend/README.md b/plugins/auth-backend/README.md index 46bfef8b3c..ae17a556c9 100644 --- a/plugins/auth-backend/README.md +++ b/plugins/auth-backend/README.md @@ -19,6 +19,14 @@ read -r AUTH_GOOGLE_CLIENT_SECRET export AUTH_GOOGLE_CLIENT_SECRET run `yarn start` in packages/backend folder +### SAML + +To try out SAML, you can use the mock identity provider: + +```bash +./scripts/start-saml-idp.sh +``` + ## Links - (The Backstage homepage)[https://backstage.io] diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 505240853d..3e902304db 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -20,6 +20,7 @@ "@types/passport": "^1.0.3", "@types/passport-github2": "^1.2.4", "@types/passport-google-oauth20": "^2.0.3", + "body-parser": "^1.19.0", "compression": "^1.7.4", "cookie-parser": "^1.4.5", "cors": "^2.8.5", @@ -31,11 +32,14 @@ "passport": "^0.4.1", "passport-github2": "^0.1.12", "passport-google-oauth20": "^2.0.0", + "passport-saml": "^1.3.3", "winston": "^3.2.1", "yn": "^4.0.0" }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.6", + "@types/body-parser": "^1.19.0", + "@types/passport-saml": "^1.1.2", "jest-fetch-mock": "^3.0.3", "tsc-watch": "^4.2.3" }, diff --git a/plugins/auth-backend/scripts/.gitignore b/plugins/auth-backend/scripts/.gitignore new file mode 100644 index 0000000000..cfaad76118 --- /dev/null +++ b/plugins/auth-backend/scripts/.gitignore @@ -0,0 +1 @@ +*.pem diff --git a/plugins/auth-backend/scripts/start-saml-idp.sh b/plugins/auth-backend/scripts/start-saml-idp.sh new file mode 100755 index 0000000000..33217f7978 --- /dev/null +++ b/plugins/auth-backend/scripts/start-saml-idp.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +cd "$DIR" + +if [[ ! -f idp-public-cert.pem ]]; then + echo "Generating new SAML Certificates" + openssl req \ + -x509 \ + -newkey rsa:1024 \ + -days 3650 \ + -nodes \ + -subj '/CN=localhost' \ + -keyout "idp-private-key.pem" \ + -out "idp-public-cert.pem" +fi + +echo "Downloading and starting SAML-IdP" +export NPM_CONFIG_REGISTRY=https://registry.npmjs.org +exec npx saml-idp --acsUrl "http://localhost:7000/auth/saml/handler/frame" --audience "http://localhost:7000" --port 7001 diff --git a/plugins/auth-backend/src/providers/PassportStrategyHelper.ts b/plugins/auth-backend/src/providers/PassportStrategyHelper.ts index 3ec8539330..5c02930c2c 100644 --- a/plugins/auth-backend/src/providers/PassportStrategyHelper.ts +++ b/plugins/auth-backend/src/providers/PassportStrategyHelper.ts @@ -55,7 +55,7 @@ export const executeFrameHandlerStrategy = async ( reject(new Error('Unexpected redirect')); }; - strategy.authenticate(req); + strategy.authenticate(req, {}); }); }; diff --git a/plugins/auth-backend/src/providers/config.ts b/plugins/auth-backend/src/providers/config.ts index 8d04dc5a1f..5ec73b7827 100644 --- a/plugins/auth-backend/src/providers/config.ts +++ b/plugins/auth-backend/src/providers/config.ts @@ -32,4 +32,12 @@ export const providers = [ }, disableRefresh: true, }, + { + provider: 'saml', + options: { + path: '/auth/saml/handler/frame', + entryPoint: 'http://localhost:7001/', + issuer: 'passport-saml', + }, + }, ]; diff --git a/plugins/auth-backend/src/providers/factories.ts b/plugins/auth-backend/src/providers/factories.ts index 0c9f99e878..f7560174ef 100644 --- a/plugins/auth-backend/src/providers/factories.ts +++ b/plugins/auth-backend/src/providers/factories.ts @@ -14,37 +14,37 @@ * limitations under the License. */ -import { - AuthProviderFactories, - AuthProviderRouteHandlers, - AuthProviderConfig, -} from './types'; -import { GoogleAuthProvider } from './google'; -import { GithubAuthProvider } from './github'; -import { OAuthProvider } from './OAuthProvider'; +import Router from 'express-promise-router'; +import { createGithubProvider } from './github'; +import { createGoogleProvider } from './google'; +import { createSamlProvider } from './saml'; +import { AuthProviderFactory, AuthProviderConfig } from './types'; -export class ProviderFactories { - private static readonly providerFactories: AuthProviderFactories = { - google: GoogleAuthProvider, - github: GithubAuthProvider, - }; +const factories: { [providerId: string]: AuthProviderFactory } = { + google: createGoogleProvider, + github: createGithubProvider, + saml: createSamlProvider, +}; - public static getProviderFactory( - config: AuthProviderConfig, - ): AuthProviderRouteHandlers { - const providerId = config.provider; - const ProviderImpl = ProviderFactories.providerFactories[providerId]; - if (!ProviderImpl) { - throw Error( - `Provider Implementation missing for : ${providerId} auth provider`, - ); - } - const providerInstance = new ProviderImpl(config); - const oauthProvider = new OAuthProvider( - providerInstance, - providerId, - config.disableRefresh, - ); - return oauthProvider; +export function createAuthProvider(providerId: string, config: any) { + const factory = factories[providerId]; + if (!factory) { + throw Error(`No auth provider available for '${providerId}'`); } + return factory(config); } + +export const createAuthProviderRouter = (config: AuthProviderConfig) => { + const providerId = config.provider; + const provider = createAuthProvider(providerId, config); + + const router = Router(); + router.get('/start', provider.start.bind(provider)); + router.get('/handler/frame', provider.frameHandler.bind(provider)); + router.post('/handler/frame', provider.frameHandler.bind(provider)); + router.get('/logout', provider.logout.bind(provider)); + if (provider.refresh) { + router.get('/refresh', provider.refresh.bind(provider)); + } + return router; +}; diff --git a/plugins/auth-backend/src/providers/github/index.ts b/plugins/auth-backend/src/providers/github/index.ts index c3a48d35e0..60ad6998b7 100644 --- a/plugins/auth-backend/src/providers/github/index.ts +++ b/plugins/auth-backend/src/providers/github/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { GithubAuthProvider } from './provider'; +export { createGithubProvider } from './provider'; diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index 4445e98451..09622d9303 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -27,6 +27,7 @@ import { AuthInfoBase, AuthInfoPrivate, } from '../types'; +import { OAuthProvider } from '../OAuthProvider'; export class GithubAuthProvider implements OAuthProviderHandlers { private readonly providerConfig: AuthProviderConfig; @@ -57,3 +58,9 @@ export class GithubAuthProvider implements OAuthProviderHandlers { return await executeFrameHandlerStrategy(req, this._strategy); } } + +export function createGithubProvider(config: AuthProviderConfig) { + const provider = new GithubAuthProvider(config); + const oauthProvider = new OAuthProvider(provider, config.provider, true); + return oauthProvider; +} diff --git a/plugins/auth-backend/src/providers/google/index.ts b/plugins/auth-backend/src/providers/google/index.ts index 0ec98bef89..b2cd85e6de 100644 --- a/plugins/auth-backend/src/providers/google/index.ts +++ b/plugins/auth-backend/src/providers/google/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { GoogleAuthProvider } from './provider'; +export { createGoogleProvider } from './provider'; diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 90d33652a5..e3969b14c8 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -28,6 +28,7 @@ import { RedirectInfo, AuthProviderConfig, } from '../types'; +import { OAuthProvider } from '../OAuthProvider'; export class GoogleAuthProvider implements OAuthProviderHandlers { private readonly providerConfig: AuthProviderConfig; @@ -87,3 +88,9 @@ export class GoogleAuthProvider implements OAuthProviderHandlers { }; } } + +export function createGoogleProvider(config: AuthProviderConfig) { + const provider = new GoogleAuthProvider(config); + const oauthProvider = new OAuthProvider(provider, config.provider); + return oauthProvider; +} diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index cfbabbbad1..d210bfd1bb 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -14,24 +14,4 @@ * limitations under the License. */ -import Router from 'express-promise-router'; -import { AuthProviderRouteHandlers, AuthProviderConfig } from './types'; -import { ProviderFactories } from './factories'; - -export const defaultRouter = (provider: AuthProviderRouteHandlers) => { - const router = Router(); - router.get('/start', provider.start.bind(provider)); - router.get('/handler/frame', provider.frameHandler.bind(provider)); - router.get('/logout', provider.logout.bind(provider)); - if (provider.refresh) { - router.get('/refresh', provider.refresh.bind(provider)); - } - return router; -}; - -export const makeProvider = (config: AuthProviderConfig) => { - const providerId = config.provider; - const oauthProvider = ProviderFactories.getProviderFactory(config); - const providerRouter = defaultRouter(oauthProvider); - return { providerId, providerRouter }; -}; +export { createAuthProviderRouter } from './factories'; diff --git a/plugins/auth-backend/src/providers/saml/index.ts b/plugins/auth-backend/src/providers/saml/index.ts new file mode 100644 index 0000000000..582deb1608 --- /dev/null +++ b/plugins/auth-backend/src/providers/saml/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { createSamlProvider } from './provider'; diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts new file mode 100644 index 0000000000..50bea3495e --- /dev/null +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -0,0 +1,82 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import express from 'express'; +import { Strategy as SamlStrategy } from 'passport-saml'; +import { + executeFrameHandlerStrategy, + executeRedirectStrategy, +} from '../PassportStrategyHelper'; +import { AuthProviderConfig, AuthProviderRouteHandlers } from '../types'; +import { postMessageResponse } from '../OAuthProvider'; + +export class SamlAuthProvider implements AuthProviderRouteHandlers { + private readonly strategy: SamlStrategy; + + constructor(providerConfig: AuthProviderConfig) { + this.strategy = new SamlStrategy( + { ...providerConfig.options }, + (profile: any, done: any) => { + // TODO: There's plenty more validation and profile handling to do here, + // this provider is currently only intended to validate the provider pattern + // for non-oauth auth flows. + // TODO: This flow doesn't issue an identity token that can be used to validate + // the identity of the user in other backends, which we need in some form. + done(undefined, { + email: profile.email, + firstName: profile.firstName, + lastName: profile.lastName, + displayName: profile.displayName, + }); + }, + ); + } + + async start(req: express.Request, res: express.Response): Promise { + const { url } = await executeRedirectStrategy(req, this.strategy, {}); + res.redirect(url); + } + + async frameHandler( + req: express.Request, + res: express.Response, + ): Promise { + try { + const { user } = await executeFrameHandlerStrategy(req, this.strategy); + + return postMessageResponse(res, { + type: 'auth-result', + payload: user, + }); + } catch (error) { + return postMessageResponse(res, { + type: 'auth-result', + error: { + name: error.name, + message: error.message, + }, + }); + } + } + + async logout(_req: express.Request, res: express.Response): Promise { + res.send('noop'); + } +} + +export function createSamlProvider(config: AuthProviderConfig) { + return new SamlAuthProvider(config); +} diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index dc83c19dd0..661435e74b 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -37,13 +37,9 @@ export interface AuthProviderRouteHandlers { logout(req: express.Request, res: express.Response): Promise; } -export type AuthProviderFactories = { - [key: string]: AuthProviderFactory; -}; - -export type AuthProviderFactory = { - new (providerConfig: any): OAuthProviderHandlers; -}; +export type AuthProviderFactory = ( + config: AuthProviderConfig, +) => AuthProviderRouteHandlers; export type AuthInfoBase = { accessToken: string; diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 49487d551a..9de5fb80a5 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -17,9 +17,10 @@ import express from 'express'; import Router from 'express-promise-router'; import cookieParser from 'cookie-parser'; +import bodyParser from 'body-parser'; import { Logger } from 'winston'; import { providers } from './../providers/config'; -import { makeProvider } from '../providers'; +import { createAuthProviderRouter } from '../providers'; export interface RouterOptions { logger: Logger; @@ -32,12 +33,15 @@ export async function createRouter( const logger = options.logger.child({ plugin: 'auth' }); router.use(cookieParser()); + router.use(bodyParser.urlencoded({ extended: false })); + router.use(bodyParser.json()); // configure all the providers for (const providerConfig of providers) { - const { providerId, providerRouter } = makeProvider(providerConfig); - logger.info(`Configuring provider, ${providerId}`); - router.use(`/${providerId}`, providerRouter); + const { provider } = providerConfig; + const providerRouter = createAuthProviderRouter(providerConfig); + logger.info(`Configuring provider, ${provider}`); + router.use(`/${provider}`, providerRouter); } return router; diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index a5f283abba..436afbfcd2 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -6,7 +6,7 @@ "license": "Apache-2.0", "private": true, "scripts": { - "start": "tsc-watch --onFirstSuccess \"cross-env NODE_ENV=development nodemon -r esm dist/run.js\"", + "start": "backstage-cli watch-deps --build -- tsc-watch --onFirstSuccess \\\"cross-env NODE_ENV=development nodemon -r esm dist/run.js\\\"", "build": "tsc", "lint": "backstage-cli lint", "test": "backstage-cli test", diff --git a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts index 6a7a51a168..1ec1ebf6e5 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts @@ -38,8 +38,8 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog { async entityByName( kind: string, - name: string, namespace: string | undefined, + name: string, ): Promise { return await this.database.transaction(tx => this.entityByNameInternal(tx, kind, name, namespace), diff --git a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts index 066fab8464..a1e76679f7 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts @@ -16,7 +16,10 @@ import { Location } from '@backstage/catalog-model'; import type { Database } from '../database'; -import { DatabaseLocationUpdateLogEvent } from '../database/types'; +import { + DatabaseLocationUpdateLogEvent, + DatabaseLocationUpdateLogStatus, +} from '../database/types'; import { LocationResponse, LocationsCatalog } from './types'; export class DatabaseLocationsCatalog implements LocationsCatalog { @@ -63,4 +66,28 @@ export class DatabaseLocationsCatalog implements LocationsCatalog { data, }; } + + async logUpdateSuccess( + locationId: string, + entityName?: string, + ): Promise { + await this.database.addLocationUpdateLogEvent( + locationId, + DatabaseLocationUpdateLogStatus.SUCCESS, + entityName, + ); + } + + async logUpdateFailure( + locationId: string, + error?: Error, + entityName?: string, + ): Promise { + await this.database.addLocationUpdateLogEvent( + locationId, + DatabaseLocationUpdateLogStatus.FAIL, + entityName, + error?.message, + ); + } } diff --git a/plugins/catalog-backend/src/catalog/types.ts b/plugins/catalog-backend/src/catalog/types.ts index 0499b8a409..b0d3dde0fb 100644 --- a/plugins/catalog-backend/src/catalog/types.ts +++ b/plugins/catalog-backend/src/catalog/types.ts @@ -62,4 +62,10 @@ export type LocationsCatalog = { locations(): Promise; location(id: string): Promise; locationHistory(id: string): Promise; + logUpdateSuccess(locationId: string, entityName?: string): Promise; + logUpdateFailure( + locationId: string, + error?: Error, + entityName?: string, + ): Promise; }; diff --git a/plugins/catalog-backend/src/database/DatabaseManager.test.ts b/plugins/catalog-backend/src/database/DatabaseManager.test.ts deleted file mode 100644 index 0c28524f8d..0000000000 --- a/plugins/catalog-backend/src/database/DatabaseManager.test.ts +++ /dev/null @@ -1,248 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { getVoidLogger } from '@backstage/backend-common'; -import type { Entity, EntityPolicy } from '@backstage/catalog-model'; -import Knex from 'knex'; -import type { IngestionModel } from '../ingestion/types'; -import { DatabaseManager } from './DatabaseManager'; -import { DatabaseLocationUpdateLogStatus } from './types'; -import type { - Database, - DbLocationsRow, - DbLocationsRowWithStatus, -} from './types'; - -describe('DatabaseManager', () => { - describe('refreshLocations', () => { - it('works with no locations added', async () => { - const db = ({ - locations: jest.fn().mockResolvedValue([]), - } as unknown) as Database; - const reader: IngestionModel = { - readLocation: jest.fn(), - }; - const policy: EntityPolicy = { - enforce: jest.fn(), - }; - - await expect( - DatabaseManager.refreshLocations(db, reader, policy, getVoidLogger()), - ).resolves.toBeUndefined(); - expect(reader.readLocation).not.toHaveBeenCalled(); - expect(policy.enforce).not.toHaveBeenCalled(); - }); - - it('can update a single location', async () => { - const location: DbLocationsRowWithStatus = { - id: '123', - type: 'some', - target: 'thing', - message: '', - status: DatabaseLocationUpdateLogStatus.SUCCESS, - timestamp: new Date(314159265).toISOString(), - }; - const desc: Entity = { - apiVersion: 'backstage.io/v1beta1', - kind: 'Component', - metadata: { name: 'c1' }, - spec: { type: 'service' }, - }; - - const tx = (undefined as unknown) as Knex.Transaction; - - const db = ({ - transaction: jest.fn(f => f(tx)), - entity: jest.fn(() => Promise.resolve(undefined)), - addEntity: jest.fn(), - locations: jest.fn(() => Promise.resolve([location])), - addLocationUpdateLogEvent: jest.fn(), - } as Partial) as Database; - - const reader: IngestionModel = { - readLocation: jest.fn(() => - Promise.resolve([{ type: 'data', data: desc }]), - ), - }; - const policy: EntityPolicy = { - enforce: jest.fn(() => Promise.resolve(desc)), - }; - - await expect( - DatabaseManager.refreshLocations(db, reader, policy, getVoidLogger()), - ).resolves.toBeUndefined(); - expect(reader.readLocation).toHaveBeenCalledTimes(1); - expect(reader.readLocation).toHaveBeenNthCalledWith(1, 'some', 'thing'); - expect(db.addEntity).toHaveBeenCalledTimes(1); - expect(db.addEntity).toHaveBeenNthCalledWith(1, undefined, { - locationId: '123', - entity: expect.objectContaining({ - metadata: expect.objectContaining({ name: 'c1' }), - }), - }); - }); - - it('logs successful updates', async () => { - const tx = (undefined as unknown) as Knex.Transaction; - - const db = ({ - transaction: jest.fn(f => f(tx)), - addEntity: jest.fn(), - entity: jest.fn(() => Promise.resolve(undefined)), - locations: jest.fn(() => - Promise.resolve([ - { - id: '123', - type: 'some', - target: 'thing', - } as DbLocationsRow, - ]), - ), - addLocationUpdateLogEvent: jest.fn(), - } as unknown) as Database; - - const desc: Entity = { - apiVersion: 'backstage.io/v1beta1', - kind: 'Component', - metadata: { name: 'c1' }, - spec: { type: 'service' }, - }; - const reader: IngestionModel = { - readLocation: jest.fn(() => - Promise.resolve([{ type: 'data', data: desc }]), - ), - }; - const policy: EntityPolicy = { - enforce: jest.fn(() => Promise.resolve(desc)), - }; - - await expect( - DatabaseManager.refreshLocations(db, reader, policy, getVoidLogger()), - ).resolves.toBeUndefined(); - - expect(db.addLocationUpdateLogEvent).toHaveBeenNthCalledWith( - 1, - '123', - DatabaseLocationUpdateLogStatus.SUCCESS, - 'c1', - ); - - expect(db.addLocationUpdateLogEvent).toHaveBeenNthCalledWith( - 2, - '123', - DatabaseLocationUpdateLogStatus.SUCCESS, - undefined, - ); - }); - - it('logs unsuccessful updates when parser fails', async () => { - const tx = (undefined as unknown) as Knex.Transaction; - - const db = ({ - transaction: jest.fn(f => f(tx)), - addEntity: jest.fn(), - locations: jest.fn(() => - Promise.resolve([ - { - id: '123', - type: 'some', - target: 'thing', - } as DbLocationsRow, - ]), - ), - addLocationUpdateLogEvent: jest.fn(), - } as unknown) as Database; - - const desc: Entity = { - apiVersion: 'backstage.io/v1beta1', - kind: 'Component', - metadata: { name: 'c1' }, - spec: { type: 'service' }, - }; - const reader: IngestionModel = { - readLocation: jest.fn(() => - Promise.resolve([{ type: 'data', data: desc }]), - ), - }; - const policy: EntityPolicy = { - enforce: jest.fn(() => - Promise.reject(new Error('parser error message')), - ), - }; - - await expect( - DatabaseManager.refreshLocations(db, reader, policy, getVoidLogger()), - ).resolves.toBeUndefined(); - - expect(db.addLocationUpdateLogEvent).toHaveBeenNthCalledWith( - 1, - '123', - DatabaseLocationUpdateLogStatus.FAIL, - 'c1', - 'parser error message', - ); - - expect(db.addLocationUpdateLogEvent).toHaveBeenNthCalledWith( - 2, - '123', - DatabaseLocationUpdateLogStatus.SUCCESS, - undefined, - ); - }); - - it('logs unsuccessful updates when reader fails', async () => { - const tx = (undefined as unknown) as Knex.Transaction; - - const db = ({ - transaction: jest.fn(f => f(tx)), - addEntity: jest.fn(), - locations: jest.fn(() => - Promise.resolve([ - { - id: '123', - type: 'some', - target: 'thing', - } as DbLocationsRow, - ]), - ), - addLocationUpdateLogEvent: jest.fn(), - } as unknown) as Database; - - const reader: IngestionModel = { - readLocation: jest.fn(() => - Promise.reject([{ type: 'error', error: new Error('test message') }]), - ), - }; - const policy: EntityPolicy = { - enforce: jest.fn(() => - Promise.reject(new Error('parser error message')), - ), - }; - - await expect( - DatabaseManager.refreshLocations(db, reader, policy, getVoidLogger()), - ).resolves.toBeUndefined(); - - expect(db.addLocationUpdateLogEvent).toHaveBeenNthCalledWith( - 1, - '123', - DatabaseLocationUpdateLogStatus.FAIL, - undefined, - undefined, - ); - }); - }); -}); diff --git a/plugins/catalog-backend/src/database/DatabaseManager.ts b/plugins/catalog-backend/src/database/DatabaseManager.ts index 9d7a79d57a..349df0b114 100644 --- a/plugins/catalog-backend/src/database/DatabaseManager.ts +++ b/plugins/catalog-backend/src/database/DatabaseManager.ts @@ -14,18 +14,11 @@ * limitations under the License. */ -import { Entity, EntityPolicy } from '@backstage/catalog-model'; import Knex from 'knex'; -import lodash from 'lodash'; import path from 'path'; import { Logger } from 'winston'; -import { IngestionModel } from '../ingestion/types'; import { CommonDatabase } from './CommonDatabase'; -import { - DatabaseLocationUpdateLogStatus, - Database, - DbEntityRequest, -} from './types'; +import { Database } from './types'; export class DatabaseManager { public static async createDatabase( @@ -57,182 +50,4 @@ export class DatabaseManager { }); return new CommonDatabase(database, logger); } - - private static async logUpdateSuccess( - database: Database, - locationId: string, - entityName?: string, - ) { - return database.addLocationUpdateLogEvent( - locationId, - DatabaseLocationUpdateLogStatus.SUCCESS, - entityName, - ); - } - - private static async logUpdateFailure( - database: Database, - locationId: string, - error?: Error, - entityName?: string, - ) { - return database.addLocationUpdateLogEvent( - locationId, - DatabaseLocationUpdateLogStatus.FAIL, - entityName, - error?.message, - ); - } - - public static async refreshLocations( - database: Database, - ingestionModel: IngestionModel, - entityPolicy: EntityPolicy, - logger: Logger, - ): Promise { - const locations = await database.locations(); - for (const location of locations) { - try { - logger.debug( - `Refreshing location id="${location.id}" type="${location.type}" target="${location.target}"`, - ); - - const readerOutput = await ingestionModel.readLocation( - location.type, - location.target, - ); - - for (const readerItem of readerOutput) { - if (readerItem.type === 'error') { - logger.info(readerItem.error); - continue; - } - - try { - const entity = await entityPolicy.enforce(readerItem.data); - await DatabaseManager.refreshSingleEntity( - database, - location.id, - entity, - logger, - ); - await DatabaseManager.logUpdateSuccess( - database, - location.id, - entity.metadata.name, - ); - } catch (error) { - await DatabaseManager.logUpdateFailure( - database, - location.id, - error, - readerItem.data.metadata.name, - ); - } - } - await DatabaseManager.logUpdateSuccess( - database, - location.id, - undefined, - ); - } catch (error) { - logger.debug( - `Failed to refresh location id="${location.id}", ${error}`, - ); - await DatabaseManager.logUpdateFailure(database, location.id, error); - } - } - } - - private static async refreshSingleEntity( - database: Database, - locationId: string, - entity: Entity, - logger: Logger, - ): Promise { - const { kind } = entity; - const { name, namespace } = entity.metadata || {}; - if (!name) { - throw new Error('Entities without names are not yet supported'); - } - - const request: DbEntityRequest = { - locationId: locationId, - entity: entity, - }; - - logger.debug( - `Read entity kind="${kind}" name="${name}" namespace="${namespace}"`, - ); - - await database.transaction(async tx => { - const previous = await database.entity(tx, kind, name, namespace); - if (!previous) { - logger.debug(`No such entity found, adding`); - await database.addEntity(tx, request); - } else if ( - !DatabaseManager.entitiesAreEqual(previous.entity, request.entity) - ) { - logger.debug(`Different from existing entity, updating`); - await database.updateEntity(tx, request); - } else { - logger.debug(`Equal to existing entity, skipping update`); - } - }); - } - - private static entitiesAreEqual(previous: Entity, next: Entity) { - if ( - previous.apiVersion !== next.apiVersion || - previous.kind !== next.kind || - !lodash.isEqual(previous.spec, next.spec) // Accept that {} !== undefined - ) { - return false; - } - - // Since the next annotations get merged into the previous, extract only - // the overlapping keys and check if their values match. - if (next.metadata.annotations) { - if (!previous.metadata.annotations) { - return false; - } - if ( - !lodash.isEqual( - next.metadata.annotations, - lodash.pick( - previous.metadata.annotations, - Object.keys(next.metadata.annotations), - ), - ) - ) { - return false; - } - } - - const e1 = lodash.cloneDeep(previous); - const e2 = lodash.cloneDeep(next); - - if (!e1.metadata.labels) { - e1.metadata.labels = {}; - } - if (!e2.metadata.labels) { - e2.metadata.labels = {}; - } - - // Remove generated fields - delete e1.metadata.uid; - delete e1.metadata.etag; - delete e1.metadata.generation; - delete e2.metadata.uid; - delete e2.metadata.etag; - delete e2.metadata.generation; - - // Remove already compared things - delete e1.metadata.annotations; - delete e1.spec; - delete e2.metadata.annotations; - delete e2.spec; - - return lodash.isEqual(e1, e2); - } } diff --git a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts index 7f1e890fbf..bd8dbedb27 100644 --- a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts +++ b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts @@ -14,10 +14,13 @@ * limitations under the License. */ +import { getVoidLogger } from '@backstage/backend-common'; +import { Entity, Location } from '@backstage/catalog-model'; import { EntitiesCatalog, LocationsCatalog } from '../catalog'; -import { IngestionModel } from './types'; +import { LocationUpdateStatus } from '../catalog/types'; +import { DatabaseLocationUpdateLogStatus } from '../database/types'; import { HigherOrderOperations } from './HigherOrderOperations'; -import { Entity } from '@backstage/catalog-model'; +import { IngestionModel } from './types'; describe('HigherOrderOperations', () => { let entitiesCatalog: jest.Mocked; @@ -39,6 +42,8 @@ describe('HigherOrderOperations', () => { locations: jest.fn(), location: jest.fn(), locationHistory: jest.fn(), + logUpdateSuccess: jest.fn(), + logUpdateFailure: jest.fn(), }; ingestionModel = { readLocation: jest.fn(), @@ -47,6 +52,7 @@ describe('HigherOrderOperations', () => { entitiesCatalog, locationsCatalog, ingestionModel, + getVoidLogger(), ); }); @@ -140,4 +146,147 @@ describe('HigherOrderOperations', () => { expect(locationsCatalog.addLocation).not.toBeCalled(); }); }); + + describe('refreshLocations', () => { + it('works with no locations added', async () => { + locationsCatalog.locations.mockResolvedValue([]); + + await expect( + higherOrderOperation.refreshAllLocations(), + ).resolves.toBeUndefined(); + + expect(locationsCatalog.locations).toHaveBeenCalledTimes(1); + expect(ingestionModel.readLocation).not.toHaveBeenCalled(); + expect(entitiesCatalog.addOrUpdateEntity).not.toHaveBeenCalled(); + }); + + it('can update a single location where a matching entity did not exist', async () => { + const locationStatus: LocationUpdateStatus = { + message: '', + status: DatabaseLocationUpdateLogStatus.SUCCESS, + timestamp: new Date(314159265).toISOString(), + }; + const location: Location = { + id: '123', + type: 'some', + target: 'thing', + }; + const desc: Entity = { + apiVersion: 'backstage.io/v1beta1', + kind: 'Component', + metadata: { name: 'c1' }, + spec: { type: 'service' }, + }; + + locationsCatalog.locations.mockResolvedValue([ + { currentStatus: locationStatus, data: location }, + ]); + ingestionModel.readLocation.mockResolvedValue([ + { type: 'data', data: desc }, + ]); + entitiesCatalog.entityByName.mockResolvedValue(undefined); + entitiesCatalog.addOrUpdateEntity.mockResolvedValue(desc); + + await expect( + higherOrderOperation.refreshAllLocations(), + ).resolves.toBeUndefined(); + + expect(locationsCatalog.locations).toHaveBeenCalledTimes(1); + expect(ingestionModel.readLocation).toHaveBeenCalledTimes(1); + expect(ingestionModel.readLocation).toHaveBeenNthCalledWith( + 1, + 'some', + 'thing', + ); + expect(entitiesCatalog.entityByName).toHaveBeenCalledTimes(1); + expect(entitiesCatalog.entityByName).toHaveBeenNthCalledWith( + 1, + 'Component', + undefined, + 'c1', + ); + expect(entitiesCatalog.addOrUpdateEntity).toHaveBeenCalledTimes(1); + expect(entitiesCatalog.addOrUpdateEntity).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + metadata: expect.objectContaining({ name: 'c1' }), + }), + '123', + ); + }); + + it('logs successful updates', async () => { + const locationStatus: LocationUpdateStatus = { + message: '', + status: DatabaseLocationUpdateLogStatus.SUCCESS, + timestamp: new Date(314159265).toISOString(), + }; + const location: Location = { + id: '123', + type: 'some', + target: 'thing', + }; + const desc: Entity = { + apiVersion: 'backstage.io/v1beta1', + kind: 'Component', + metadata: { name: 'c1' }, + spec: { type: 'service' }, + }; + + locationsCatalog.locations.mockResolvedValue([ + { currentStatus: locationStatus, data: location }, + ]); + ingestionModel.readLocation.mockResolvedValue([ + { type: 'data', data: desc }, + ]); + entitiesCatalog.entityByName.mockResolvedValue(undefined); + entitiesCatalog.addOrUpdateEntity.mockResolvedValue(desc); + + await expect( + higherOrderOperation.refreshAllLocations(), + ).resolves.toBeUndefined(); + + expect(locationsCatalog.logUpdateSuccess).toHaveBeenCalledTimes(2); + expect(locationsCatalog.logUpdateSuccess).toHaveBeenCalledWith( + '123', + undefined, + ); + expect(locationsCatalog.logUpdateSuccess).toHaveBeenCalledWith( + '123', + 'c1', + ); + }); + + it('logs unsuccessful updates when reader fails', async () => { + const locationStatus: LocationUpdateStatus = { + message: '', + status: DatabaseLocationUpdateLogStatus.SUCCESS, + timestamp: new Date(314159265).toISOString(), + }; + const location: Location = { + id: '123', + type: 'some', + target: 'thing', + }; + + locationsCatalog.locations.mockResolvedValue([ + { currentStatus: locationStatus, data: location }, + ]); + ingestionModel.readLocation.mockRejectedValue( + new Error('reader error message'), + ); + + await expect( + higherOrderOperation.refreshAllLocations(), + ).resolves.toBeUndefined(); + + expect(ingestionModel.readLocation).toHaveBeenCalledTimes(1); + expect(locationsCatalog.logUpdateFailure).toHaveBeenCalledTimes(1); + expect(locationsCatalog.logUpdateSuccess).not.toHaveBeenCalled(); + expect(locationsCatalog.logUpdateFailure).toHaveBeenCalledWith( + '123', + expect.objectContaining({ message: 'reader error message' }), + ); + }); + }); }); diff --git a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts index e28d1e722a..e9051d1f78 100644 --- a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts +++ b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts @@ -16,10 +16,12 @@ import { InputError } from '@backstage/backend-common'; import { Entity, Location, LocationSpec } from '@backstage/catalog-model'; +import lodash from 'lodash'; import { v4 as uuidv4 } from 'uuid'; import { EntitiesCatalog, LocationsCatalog } from '../catalog'; import { IngestionModel } from '../ingestion'; import { AddLocationResult, HigherOrderOperation } from './types'; +import { Logger } from 'winston'; const LOCATION_ANNOTATION = 'backstage.io/managed-by-location'; @@ -34,15 +36,18 @@ export class HigherOrderOperations implements HigherOrderOperation { private readonly entitiesCatalog: EntitiesCatalog; private readonly locationsCatalog: LocationsCatalog; private readonly ingestionModel: IngestionModel; + private readonly logger: Logger; constructor( entitiesCatalog: EntitiesCatalog, locationsCatalog: LocationsCatalog, ingestionModel: IngestionModel, + logger: Logger, ) { this.entitiesCatalog = entitiesCatalog; this.locationsCatalog = locationsCatalog; this.ingestionModel = ingestionModel; + this.logger = logger; } /** @@ -111,4 +116,154 @@ export class HigherOrderOperations implements HigherOrderOperation { return { location, entities: outputEntities }; } + + /** + * Goes through all registered locations, and performs a refresh of each one. + * + * Entities are read from their respective sources, are parsed and validated + * according to the entity policy, and get inserted or updated in the catalog. + * Entities that have disappeared from their location are left orphaned, + * without changes. + */ + async refreshAllLocations(): Promise { + const startTimestamp = new Date().valueOf(); + this.logger.info('Beginning locations refresh'); + + const locations = await this.locationsCatalog.locations(); + this.logger.info(`Visiting ${locations.length} locations`); + + for (const { data: location } of locations) { + this.logger.debug( + `Refreshing location id="${location.id}" type="${location.type}" target="${location.target}"`, + ); + try { + await this.refreshSingleLocation(location); + await this.locationsCatalog.logUpdateSuccess(location.id, undefined); + } catch (e) { + this.logger.debug( + `Failed to refresh location id="${location.id}" type="${location.type}" target="${location.target}", ${e}`, + ); + await this.locationsCatalog.logUpdateFailure(location.id, e); + } + } + + const endTimestamp = new Date().valueOf(); + const duration = ((endTimestamp - startTimestamp) / 1000).toFixed(1); + this.logger.debug(`Completed locations refresh in ${duration} seconds`); + } + + // Performs a full refresh of a single location + private async refreshSingleLocation(location: Location) { + const readerOutput = await this.ingestionModel.readLocation( + location.type, + location.target, + ); + + for (const readerItem of readerOutput) { + if (readerItem.type === 'error') { + this.logger.debug( + `Failed item in location id="${location.id}" type="${location.type}" target="${location.target}", ${readerItem.error}`, + ); + continue; + } + + const entity = readerItem.data; + this.logger.debug( + `Read entity kind="${entity.kind}" name="${ + entity.metadata.name + }" namespace="${entity.metadata.namespace || ''}"`, + ); + + try { + const previous = await this.entitiesCatalog.entityByName( + entity.kind, + entity.metadata.namespace, + entity.metadata.name, + ); + + if (!previous) { + this.logger.debug(`No such entity found, adding`); + await this.entitiesCatalog.addOrUpdateEntity(entity, location.id); + } else if (!this.entitiesAreEqual(previous, entity)) { + this.logger.debug(`Different from existing entity, updating`); + await this.entitiesCatalog.addOrUpdateEntity(entity, location.id); + } else { + this.logger.debug(`Equal to existing entity, skipping update`); + } + + await this.locationsCatalog.logUpdateSuccess( + location.id, + entity.metadata.name, + ); + } catch (error) { + this.logger.debug( + `Failed refresh of entity kind="${entity.kind}" name="${ + entity.metadata.name + }" namespace="${entity.metadata.namespace || ''}", ${error}`, + ); + + await this.locationsCatalog.logUpdateFailure( + location.id, + error, + entity.metadata.name, + ); + } + } + } + + // Compares entities, ignoring generated and irrelevant data + private entitiesAreEqual(previous: Entity, next: Entity): boolean { + if ( + previous.apiVersion !== next.apiVersion || + previous.kind !== next.kind || + !lodash.isEqual(previous.spec, next.spec) // Accept that {} !== undefined + ) { + return false; + } + + // Since the next annotations get merged into the previous, extract only + // the overlapping keys and check if their values match. + if (next.metadata.annotations) { + if (!previous.metadata.annotations) { + return false; + } + if ( + !lodash.isEqual( + next.metadata.annotations, + lodash.pick( + previous.metadata.annotations, + Object.keys(next.metadata.annotations), + ), + ) + ) { + return false; + } + } + + const e1 = lodash.cloneDeep(previous); + const e2 = lodash.cloneDeep(next); + + if (!e1.metadata.labels) { + e1.metadata.labels = {}; + } + if (!e2.metadata.labels) { + e2.metadata.labels = {}; + } + + // Remove generated fields + delete e1.metadata.uid; + delete e1.metadata.etag; + delete e1.metadata.generation; + delete e2.metadata.uid; + delete e2.metadata.etag; + delete e2.metadata.generation; + + // Remove already compared things + delete e1.metadata.annotations; + delete e1.spec; + delete e2.metadata.annotations; + delete e2.spec; + + return lodash.isEqual(e1, e2); + } } diff --git a/plugins/catalog-backend/src/ingestion/index.ts b/plugins/catalog-backend/src/ingestion/index.ts index 93af856656..6c530c7234 100644 --- a/plugins/catalog-backend/src/ingestion/index.ts +++ b/plugins/catalog-backend/src/ingestion/index.ts @@ -18,4 +18,4 @@ export * from './descriptor'; export { HigherOrderOperations } from './HigherOrderOperations'; export { IngestionModels } from './IngestionModels'; export * from './source'; -export type { IngestionModel } from './types'; +export type { HigherOrderOperation, IngestionModel } from './types'; diff --git a/plugins/catalog-backend/src/ingestion/types.ts b/plugins/catalog-backend/src/ingestion/types.ts index 018784bd0f..8fb56367a6 100644 --- a/plugins/catalog-backend/src/ingestion/types.ts +++ b/plugins/catalog-backend/src/ingestion/types.ts @@ -28,4 +28,5 @@ export type IngestionModel = { export type HigherOrderOperation = { addLocation(spec: LocationSpec): Promise; + refreshAllLocations(): Promise; }; diff --git a/plugins/catalog-backend/src/service/router.test.ts b/plugins/catalog-backend/src/service/router.test.ts index d26476bca0..29b3f7ce04 100644 --- a/plugins/catalog-backend/src/service/router.test.ts +++ b/plugins/catalog-backend/src/service/router.test.ts @@ -43,9 +43,12 @@ describe('createRouter', () => { locations: jest.fn(), location: jest.fn(), locationHistory: jest.fn(), + logUpdateSuccess: jest.fn(), + logUpdateFailure: jest.fn(), }; higherOrderOperation = { addLocation: jest.fn(), + refreshAllLocations: jest.fn(), }; const router = await createRouter({ entitiesCatalog, @@ -78,6 +81,7 @@ describe('createRouter', () => { const response = await request(app).get('/entities?a=1&a=&a=3&b=4&c='); expect(response.status).toEqual(200); + expect(entitiesCatalog.entities).toHaveBeenCalledTimes(1); expect(entitiesCatalog.entities).toHaveBeenCalledWith([ { key: 'a', values: ['1', null, '3'] }, { key: 'b', values: ['4'] }, @@ -99,14 +103,19 @@ describe('createRouter', () => { const response = await request(app).get('/entities/by-uid/zzz'); + expect(entitiesCatalog.entityByUid).toHaveBeenCalledTimes(1); + expect(entitiesCatalog.entityByUid).toHaveBeenCalledWith('zzz'); expect(response.status).toEqual(200); expect(response.body).toEqual(expect.objectContaining(entity)); }); it('responds with a 404 for missing entities', async () => { entitiesCatalog.entityByUid.mockResolvedValue(undefined); + const response = await request(app).get('/entities/by-uid/zzz'); + expect(entitiesCatalog.entityByUid).toHaveBeenCalledTimes(1); + expect(entitiesCatalog.entityByUid).toHaveBeenCalledWith('zzz'); expect(response.status).toEqual(404); expect(response.text).toMatch(/uid/); }); @@ -116,16 +125,18 @@ describe('createRouter', () => { it('can fetch entity by name', async () => { const entity: Entity = { apiVersion: 'a', - kind: 'b', + kind: 'k', metadata: { - name: 'c', - namespace: 'd', + name: 'n', + namespace: 'ns', }, }; entitiesCatalog.entityByName.mockResolvedValue(entity); - const response = await request(app).get('/entities/by-name/b/d/c'); + const response = await request(app).get('/entities/by-name/k/ns/n'); + expect(entitiesCatalog.entityByName).toHaveBeenCalledTimes(1); + expect(entitiesCatalog.entityByName).toHaveBeenCalledWith('k', 'ns', 'n'); expect(response.status).toEqual(200); expect(response.body).toEqual(expect.objectContaining(entity)); }); @@ -133,8 +144,10 @@ describe('createRouter', () => { it('responds with a 404 for missing entities', async () => { entitiesCatalog.entityByName.mockResolvedValue(undefined); - const response = await request(app).get('/entities/by-name//b/d/c'); + const response = await request(app).get('/entities/by-name/b/d/c'); + expect(entitiesCatalog.entityByName).toHaveBeenCalledTimes(1); + expect(entitiesCatalog.entityByName).toHaveBeenCalledWith('b', 'd', 'c'); expect(response.status).toEqual(404); expect(response.text).toMatch(/name/); }); @@ -147,9 +160,9 @@ describe('createRouter', () => { .set('Content-Type', 'application/json') .send(); + expect(entitiesCatalog.addOrUpdateEntity).not.toHaveBeenCalled(); expect(response.status).toEqual(400); expect(response.text).toMatch(/body/); - expect(entitiesCatalog.addOrUpdateEntity).not.toHaveBeenCalled(); }); it('passes the body down', async () => { @@ -169,13 +182,13 @@ describe('createRouter', () => { .send(entity) .set('Content-Type', 'application/json'); - expect(response.status).toEqual(200); - expect(response.body).toEqual(entity); expect(entitiesCatalog.addOrUpdateEntity).toHaveBeenCalledTimes(1); expect(entitiesCatalog.addOrUpdateEntity).toHaveBeenNthCalledWith( 1, entity, ); + expect(response.status).toEqual(200); + expect(response.body).toEqual(entity); }); }); @@ -185,8 +198,9 @@ describe('createRouter', () => { const response = await request(app).delete('/entities/by-uid/apa'); - expect(response.status).toEqual(204); expect(entitiesCatalog.removeEntityByUid).toHaveBeenCalledTimes(1); + expect(entitiesCatalog.removeEntityByUid).toHaveBeenCalledWith('apa'); + expect(response.status).toEqual(204); }); it('responds with a 404 for missing entities', async () => { @@ -196,8 +210,9 @@ describe('createRouter', () => { const response = await request(app).delete('/entities/by-uid/apa'); - expect(response.status).toEqual(404); expect(entitiesCatalog.removeEntityByUid).toHaveBeenCalledTimes(1); + expect(entitiesCatalog.removeEntityByUid).toHaveBeenCalledWith('apa'); + expect(response.status).toEqual(404); }); }); @@ -227,8 +242,8 @@ describe('createRouter', () => { const response = await request(app).post('/locations').send(spec); - expect(response.status).toEqual(400); expect(higherOrderOperation.addLocation).not.toHaveBeenCalled(); + expect(response.status).toEqual(400); }); it('passes the body down', async () => { @@ -244,9 +259,14 @@ describe('createRouter', () => { const response = await request(app).post('/locations').send(spec); - expect(response.status).toEqual(201); expect(higherOrderOperation.addLocation).toHaveBeenCalledTimes(1); expect(higherOrderOperation.addLocation).toHaveBeenCalledWith(spec); + expect(response.status).toEqual(201); + expect(response.body).toEqual( + expect.objectContaining({ + location: { id: 'a', ...spec }, + }), + ); }); }); }); diff --git a/plugins/catalog-backend/src/service/router.ts b/plugins/catalog-backend/src/service/router.ts index 22d1f73730..a3da182416 100644 --- a/plugins/catalog-backend/src/service/router.ts +++ b/plugins/catalog-backend/src/service/router.ts @@ -69,8 +69,8 @@ export async function createRouter( const { kind, namespace, name } = req.params; const entity = await entitiesCatalog.entityByName( kind, - name, namespace, + name, ); if (!entity) { res diff --git a/plugins/catalog-backend/src/service/standaloneApplication.ts b/plugins/catalog-backend/src/service/standaloneApplication.ts index ebd28a0a45..db24f9d224 100644 --- a/plugins/catalog-backend/src/service/standaloneApplication.ts +++ b/plugins/catalog-backend/src/service/standaloneApplication.ts @@ -25,6 +25,7 @@ import express from 'express'; import helmet from 'helmet'; import { Logger } from 'winston'; import { EntitiesCatalog, LocationsCatalog } from '../catalog'; +import { HigherOrderOperation } from '../ingestion'; import { createRouter } from './router'; import { HigherOrderOperation } from '../ingestion/types'; @@ -43,8 +44,8 @@ export async function createStandaloneApplication( enableCors, entitiesCatalog, locationsCatalog, - logger, higherOrderOperation, + logger, } = options; const app = express(); diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index e67461c83d..a3fee6e217 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -48,7 +48,7 @@ "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", - "@types/codemirror": "^0.0.93", + "@types/codemirror": "^0.0.95", "@types/jest": "^25.2.2", "@types/node": "^12.0.0", "@types/testing-library__jest-dom": "^5.0.4", diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/.github/workflows/build.yml b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/.github/workflows/build.yml index 9087876ce2..d1563ba3e6 100644 --- a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/.github/workflows/build.yml +++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.componentId}}/.github/workflows/build.yml @@ -19,7 +19,7 @@ jobs: - name: get yarn cache id: yarn-cache run: echo "::set-output name=dir::$(yarn cache dir)" - - uses: actions/cache@v1 + - uses: actions/cache@v2 with: path: ${{ steps.yarn-cache.outputs.dir }} key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} diff --git a/yarn.lock b/yarn.lock index 760f1421b7..40e01e639c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1482,14 +1482,14 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" -"@lerna/add@3.20.0": - version "3.20.0" - resolved "https://registry.npmjs.org/@lerna/add/-/add-3.20.0.tgz#bea7edf36fc93fb72ec34cb9ba854c48d4abf309" - integrity sha512-AnH1oRIEEg/VDa3SjYq4x1/UglEAvrZuV0WssHUMN81RTZgQk3we+Mv3qZNddrZ/fBcZu2IAdN/EQ3+ie2JxKQ== +"@lerna/add@3.21.0": + version "3.21.0" + resolved "https://registry.npmjs.org/@lerna/add/-/add-3.21.0.tgz#27007bde71cc7b0a2969ab3c2f0ae41578b4577b" + integrity sha512-vhUXXF6SpufBE1EkNEXwz1VLW03f177G9uMOFMQkp6OJ30/PWg4Ekifuz9/3YfgB2/GH8Tu4Lk3O51P2Hskg/A== dependencies: "@evocateur/pacote" "^9.6.3" - "@lerna/bootstrap" "3.20.0" - "@lerna/command" "3.18.5" + "@lerna/bootstrap" "3.21.0" + "@lerna/command" "3.21.0" "@lerna/filter-options" "3.20.0" "@lerna/npm-conf" "3.16.0" "@lerna/validation-error" "3.13.0" @@ -1498,12 +1498,12 @@ p-map "^2.1.0" semver "^6.2.0" -"@lerna/bootstrap@3.20.0": - version "3.20.0" - resolved "https://registry.npmjs.org/@lerna/bootstrap/-/bootstrap-3.20.0.tgz#635d71046830f208e851ab429a63da1747589e37" - integrity sha512-Wylullx3uthKE7r4izo09qeRGL20Y5yONlQEjPCfnbxCC2Elu+QcPu4RC6kqKQ7b+g7pdC3OOgcHZjngrwr5XQ== +"@lerna/bootstrap@3.21.0": + version "3.21.0" + resolved "https://registry.npmjs.org/@lerna/bootstrap/-/bootstrap-3.21.0.tgz#bcd1b651be5b0970b20d8fae04c864548123aed6" + integrity sha512-mtNHlXpmvJn6JTu0KcuTTPl2jLsDNud0QacV/h++qsaKbhAaJr/FElNZ5s7MwZFUM3XaDmvWzHKaszeBMHIbBw== dependencies: - "@lerna/command" "3.18.5" + "@lerna/command" "3.21.0" "@lerna/filter-options" "3.20.0" "@lerna/has-npm-version" "3.16.5" "@lerna/npm-install" "3.16.5" @@ -1527,13 +1527,13 @@ read-package-tree "^5.1.6" semver "^6.2.0" -"@lerna/changed@3.20.0": - version "3.20.0" - resolved "https://registry.npmjs.org/@lerna/changed/-/changed-3.20.0.tgz#66b97ebd6c8f8d207152ee524a0791846a9097ae" - integrity sha512-+hzMFSldbRPulZ0vbKk6RD9f36gaH3Osjx34wrrZ62VB4pKmjyuS/rxVYkCA3viPLHoiIw2F8zHM5BdYoDSbjw== +"@lerna/changed@3.21.0": + version "3.21.0" + resolved "https://registry.npmjs.org/@lerna/changed/-/changed-3.21.0.tgz#108e15f679bfe077af500f58248c634f1044ea0b" + integrity sha512-hzqoyf8MSHVjZp0gfJ7G8jaz+++mgXYiNs9iViQGA8JlN/dnWLI5sWDptEH3/B30Izo+fdVz0S0s7ydVE3pWIw== dependencies: "@lerna/collect-updates" "3.20.0" - "@lerna/command" "3.18.5" + "@lerna/command" "3.21.0" "@lerna/listable" "3.18.5" "@lerna/output" "3.13.0" @@ -1555,12 +1555,12 @@ execa "^1.0.0" strong-log-transformer "^2.0.0" -"@lerna/clean@3.20.0": - version "3.20.0" - resolved "https://registry.npmjs.org/@lerna/clean/-/clean-3.20.0.tgz#ba777e373ddeae63e57860df75d47a9e5264c5b2" - integrity sha512-9ZdYrrjQvR5wNXmHfDsfjWjp0foOkCwKe3hrckTzkAeQA1ibyz5llGwz5e1AeFrV12e2/OLajVqYfe+qdkZUgg== +"@lerna/clean@3.21.0": + version "3.21.0" + resolved "https://registry.npmjs.org/@lerna/clean/-/clean-3.21.0.tgz#c0b46b5300cc3dae2cda3bec14b803082da3856d" + integrity sha512-b/L9l+MDgE/7oGbrav6rG8RTQvRiZLO1zTcG17zgJAAuhlsPxJExMlh2DFwJEVi2les70vMhHfST3Ue1IMMjpg== dependencies: - "@lerna/command" "3.18.5" + "@lerna/command" "3.21.0" "@lerna/filter-options" "3.20.0" "@lerna/prompt" "3.18.5" "@lerna/pulse-till-done" "3.13.0" @@ -1600,14 +1600,14 @@ npmlog "^4.1.2" slash "^2.0.0" -"@lerna/command@3.18.5": - version "3.18.5" - resolved "https://registry.npmjs.org/@lerna/command/-/command-3.18.5.tgz#14c6d2454adbfd365f8027201523e6c289cd3cd9" - integrity sha512-36EnqR59yaTU4HrR1C9XDFti2jRx0BgpIUBeWn129LZZB8kAB3ov1/dJNa1KcNRKp91DncoKHLY99FZ6zTNpMQ== +"@lerna/command@3.21.0": + version "3.21.0" + resolved "https://registry.npmjs.org/@lerna/command/-/command-3.21.0.tgz#9a2383759dc7b700dacfa8a22b2f3a6e190121f7" + integrity sha512-T2bu6R8R3KkH5YoCKdutKv123iUgUbW8efVjdGCDnCMthAQzoentOJfDeodBwn0P2OqCl3ohsiNVtSn9h78fyQ== dependencies: "@lerna/child-process" "3.16.5" "@lerna/package-graph" "3.18.5" - "@lerna/project" "3.18.0" + "@lerna/project" "3.21.0" "@lerna/validation-error" "3.13.0" "@lerna/write-log-file" "3.13.0" clone-deep "^4.0.1" @@ -1616,10 +1616,10 @@ is-ci "^2.0.0" npmlog "^4.1.2" -"@lerna/conventional-commits@3.18.5": - version "3.18.5" - resolved "https://registry.npmjs.org/@lerna/conventional-commits/-/conventional-commits-3.18.5.tgz#08efd2e5b45acfaf3f151a53a3ec7ecade58a7bc" - integrity sha512-qcvXIEJ3qSgalxXnQ7Yxp5H9Ta5TVyai6vEor6AAEHc20WiO7UIdbLDCxBtiiHMdGdpH85dTYlsoYUwsCJu3HQ== +"@lerna/conventional-commits@3.22.0": + version "3.22.0" + resolved "https://registry.npmjs.org/@lerna/conventional-commits/-/conventional-commits-3.22.0.tgz#2798f4881ee2ef457bdae027ab7d0bf0af6f1e09" + integrity sha512-z4ZZk1e8Mhz7+IS8NxHr64wyklHctCJyWpJKEZZPJiLFJ8yKto/x38O80R10pIzC0rr8Sy/OsjSH4bl0TbbgqA== dependencies: "@lerna/validation-error" "3.13.0" conventional-changelog-angular "^5.0.3" @@ -1642,14 +1642,14 @@ fs-extra "^8.1.0" npmlog "^4.1.2" -"@lerna/create@3.18.5": - version "3.18.5" - resolved "https://registry.npmjs.org/@lerna/create/-/create-3.18.5.tgz#11ac539f069248eaf7bc4c42e237784330f4fc47" - integrity sha512-cHpjocbpKmLopCuZFI7cKEM3E/QY8y+yC7VtZ4FQRSaLU8D8i2xXtXmYaP1GOlVNavji0iwoXjuNpnRMInIr2g== +"@lerna/create@3.22.0": + version "3.22.0" + resolved "https://registry.npmjs.org/@lerna/create/-/create-3.22.0.tgz#d6bbd037c3dc5b425fe5f6d1b817057c278f7619" + integrity sha512-MdiQQzCcB4E9fBF1TyMOaAEz9lUjIHp1Ju9H7f3lXze5JK6Fl5NYkouAvsLgY6YSIhXMY8AHW2zzXeBDY4yWkw== dependencies: "@evocateur/pacote" "^9.6.3" "@lerna/child-process" "3.16.5" - "@lerna/command" "3.18.5" + "@lerna/command" "3.21.0" "@lerna/npm-conf" "3.16.0" "@lerna/validation-error" "3.13.0" camelcase "^5.0.0" @@ -1674,23 +1674,23 @@ "@lerna/child-process" "3.16.5" npmlog "^4.1.2" -"@lerna/diff@3.18.5": - version "3.18.5" - resolved "https://registry.npmjs.org/@lerna/diff/-/diff-3.18.5.tgz#e9e2cb882f84d5b84f0487c612137305f07accbc" - integrity sha512-u90lGs+B8DRA9Z/2xX4YaS3h9X6GbypmGV6ITzx9+1Ga12UWGTVlKaCXBgONMBjzJDzAQOK8qPTwLA57SeBLgA== +"@lerna/diff@3.21.0": + version "3.21.0" + resolved "https://registry.npmjs.org/@lerna/diff/-/diff-3.21.0.tgz#e6df0d8b9916167ff5a49fcb02ac06424280a68d" + integrity sha512-5viTR33QV3S7O+bjruo1SaR40m7F2aUHJaDAC7fL9Ca6xji+aw1KFkpCtVlISS0G8vikUREGMJh+c/VMSc8Usw== dependencies: "@lerna/child-process" "3.16.5" - "@lerna/command" "3.18.5" + "@lerna/command" "3.21.0" "@lerna/validation-error" "3.13.0" npmlog "^4.1.2" -"@lerna/exec@3.20.0": - version "3.20.0" - resolved "https://registry.npmjs.org/@lerna/exec/-/exec-3.20.0.tgz#29f0c01aee2340eb46f90706731fef2062a49639" - integrity sha512-pS1mmC7kzV668rHLWuv31ClngqeXjeHC8kJuM+W2D6IpUVMGQHLcCTYLudFgQsuKGVpl0DGNYG+sjLhAPiiu6A== +"@lerna/exec@3.21.0": + version "3.21.0" + resolved "https://registry.npmjs.org/@lerna/exec/-/exec-3.21.0.tgz#17f07533893cb918a17b41bcc566dc437016db26" + integrity sha512-iLvDBrIE6rpdd4GIKTY9mkXyhwsJ2RvQdB9ZU+/NhR3okXfqKc6py/24tV111jqpXTtZUW6HNydT4dMao2hi1Q== dependencies: "@lerna/child-process" "3.16.5" - "@lerna/command" "3.18.5" + "@lerna/command" "3.21.0" "@lerna/filter-options" "3.20.0" "@lerna/profiler" "3.20.0" "@lerna/run-topologically" "3.18.5" @@ -1733,13 +1733,13 @@ ssri "^6.0.1" tar "^4.4.8" -"@lerna/github-client@3.16.5": - version "3.16.5" - resolved "https://registry.npmjs.org/@lerna/github-client/-/github-client-3.16.5.tgz#2eb0235c3bf7a7e5d92d73e09b3761ab21f35c2e" - integrity sha512-rHQdn8Dv/CJrO3VouOP66zAcJzrHsm+wFuZ4uGAai2At2NkgKH+tpNhQy2H1PSC0Ezj9LxvdaHYrUzULqVK5Hw== +"@lerna/github-client@3.22.0": + version "3.22.0" + resolved "https://registry.npmjs.org/@lerna/github-client/-/github-client-3.22.0.tgz#5d816aa4f76747ed736ae64ff962b8f15c354d95" + integrity sha512-O/GwPW+Gzr3Eb5bk+nTzTJ3uv+jh5jGho9BOqKlajXaOkMYGBELEAqV5+uARNGWZFvYAiF4PgqHb6aCUu7XdXg== dependencies: "@lerna/child-process" "3.16.5" - "@octokit/plugin-enterprise-rest" "^3.6.1" + "@octokit/plugin-enterprise-rest" "^6.0.1" "@octokit/rest" "^16.28.4" git-url-parse "^11.1.2" npmlog "^4.1.2" @@ -1766,13 +1766,13 @@ "@lerna/child-process" "3.16.5" semver "^6.2.0" -"@lerna/import@3.18.5": - version "3.18.5" - resolved "https://registry.npmjs.org/@lerna/import/-/import-3.18.5.tgz#a9c7d8601870729851293c10abd18b3707f7ba5e" - integrity sha512-PH0WVLEgp+ORyNKbGGwUcrueW89K3Iuk/DDCz8mFyG2IG09l/jOF0vzckEyGyz6PO5CMcz4TI1al/qnp3FrahQ== +"@lerna/import@3.22.0": + version "3.22.0" + resolved "https://registry.npmjs.org/@lerna/import/-/import-3.22.0.tgz#1a5f0394f38e23c4f642a123e5e1517e70d068d2" + integrity sha512-uWOlexasM5XR6tXi4YehODtH9Y3OZrFht3mGUFFT3OIl2s+V85xIGFfqFGMTipMPAGb2oF1UBLL48kR43hRsOg== dependencies: "@lerna/child-process" "3.16.5" - "@lerna/command" "3.18.5" + "@lerna/command" "3.21.0" "@lerna/prompt" "3.18.5" "@lerna/pulse-till-done" "3.13.0" "@lerna/validation-error" "3.13.0" @@ -1780,43 +1780,43 @@ fs-extra "^8.1.0" p-map-series "^1.0.0" -"@lerna/info@3.20.0": - version "3.20.0" - resolved "https://registry.npmjs.org/@lerna/info/-/info-3.20.0.tgz#3a5212f3029f2bc6255f9533bdf4bcb120ef329a" - integrity sha512-Rsz+KQF9mczbGUbPTrtOed1N0C+cA08Qz0eX/oI+NNjvsryZIju/o7uedG4I3P55MBiAioNrJI88fHH3eTgYug== +"@lerna/info@3.21.0": + version "3.21.0" + resolved "https://registry.npmjs.org/@lerna/info/-/info-3.21.0.tgz#76696b676fdb0f35d48c83c63c1e32bb5e37814f" + integrity sha512-0XDqGYVBgWxUquFaIptW2bYSIu6jOs1BtkvRTWDDhw4zyEdp6q4eaMvqdSap1CG+7wM5jeLCi6z94wS0AuiuwA== dependencies: - "@lerna/command" "3.18.5" + "@lerna/command" "3.21.0" "@lerna/output" "3.13.0" envinfo "^7.3.1" -"@lerna/init@3.18.5": - version "3.18.5" - resolved "https://registry.npmjs.org/@lerna/init/-/init-3.18.5.tgz#86dd0b2b3290755a96975069b5cb007f775df9f5" - integrity sha512-oCwipWrha98EcJAHm8AGd2YFFLNI7AW9AWi0/LbClj1+XY9ah+uifXIgYGfTk63LbgophDd8936ZEpHMxBsbAg== +"@lerna/init@3.21.0": + version "3.21.0" + resolved "https://registry.npmjs.org/@lerna/init/-/init-3.21.0.tgz#1e810934dc8bf4e5386c031041881d3b4096aa5c" + integrity sha512-6CM0z+EFUkFfurwdJCR+LQQF6MqHbYDCBPyhu/d086LRf58GtYZYj49J8mKG9ktayp/TOIxL/pKKjgLD8QBPOg== dependencies: "@lerna/child-process" "3.16.5" - "@lerna/command" "3.18.5" + "@lerna/command" "3.21.0" fs-extra "^8.1.0" p-map "^2.1.0" write-json-file "^3.2.0" -"@lerna/link@3.18.5": - version "3.18.5" - resolved "https://registry.npmjs.org/@lerna/link/-/link-3.18.5.tgz#f24347e4f0b71d54575bd37cfa1794bc8ee91b18" - integrity sha512-xTN3vktJpkT7Nqc3QkZRtHO4bT5NvuLMtKNIBDkks0HpGxC9PRyyqwOoCoh1yOGbrWIuDezhfMg3Qow+6I69IQ== +"@lerna/link@3.21.0": + version "3.21.0" + resolved "https://registry.npmjs.org/@lerna/link/-/link-3.21.0.tgz#8be68ff0ccee104b174b5bbd606302c2f06e9d9b" + integrity sha512-tGu9GxrX7Ivs+Wl3w1+jrLi1nQ36kNI32dcOssij6bg0oZ2M2MDEFI9UF2gmoypTaN9uO5TSsjCFS7aR79HbdQ== dependencies: - "@lerna/command" "3.18.5" + "@lerna/command" "3.21.0" "@lerna/package-graph" "3.18.5" "@lerna/symlink-dependencies" "3.17.0" p-map "^2.1.0" slash "^2.0.0" -"@lerna/list@3.20.0": - version "3.20.0" - resolved "https://registry.npmjs.org/@lerna/list/-/list-3.20.0.tgz#7e67cc29c5cf661cfd097e8a7c2d3dcce7a81029" - integrity sha512-fXTicPrfioVnRzknyPawmYIVkzDRBaQqk9spejS1S3O1DOidkihK0xxNkr8HCVC0L22w6f92g83qWDp2BYRUbg== +"@lerna/list@3.21.0": + version "3.21.0" + resolved "https://registry.npmjs.org/@lerna/list/-/list-3.21.0.tgz#42f76fafa56dea13b691ec8cab13832691d61da2" + integrity sha512-KehRjE83B1VaAbRRkRy6jLX1Cin8ltsrQ7FHf2bhwhRHK0S54YuA6LOoBnY/NtA8bHDX/Z+G5sMY78X30NS9tg== dependencies: - "@lerna/command" "3.18.5" + "@lerna/command" "3.21.0" "@lerna/filter-options" "3.20.0" "@lerna/listable" "3.18.5" "@lerna/output" "3.13.0" @@ -1962,25 +1962,7 @@ npmlog "^4.1.2" upath "^1.2.0" -"@lerna/project@3.18.0": - version "3.18.0" - resolved "https://registry.npmjs.org/@lerna/project/-/project-3.18.0.tgz#56feee01daeb42c03cbdf0ed8a2a10cbce32f670" - integrity sha512-+LDwvdAp0BurOAWmeHE3uuticsq9hNxBI0+FMHiIai8jrygpJGahaQrBYWpwbshbQyVLeQgx3+YJdW2TbEdFWA== - dependencies: - "@lerna/package" "3.16.0" - "@lerna/validation-error" "3.13.0" - cosmiconfig "^5.1.0" - dedent "^0.7.0" - dot-prop "^4.2.0" - glob-parent "^5.0.0" - globby "^9.2.0" - load-json-file "^5.3.0" - npmlog "^4.1.2" - p-map "^2.1.0" - resolve-from "^4.0.0" - write-json-file "^3.2.0" - -"@lerna/project@^3.18.0": +"@lerna/project@3.21.0", "@lerna/project@^3.18.0": version "3.21.0" resolved "https://registry.npmjs.org/@lerna/project/-/project-3.21.0.tgz#5d784d2d10c561a00f20320bcdb040997c10502d" integrity sha512-xT1mrpET2BF11CY32uypV2GPtPVm6Hgtha7D81GQP9iAitk9EccrdNjYGt5UBYASl4CIDXBRxwmTTVGfrCx82A== @@ -2006,10 +1988,10 @@ inquirer "^6.2.0" npmlog "^4.1.2" -"@lerna/publish@3.20.2": - version "3.20.2" - resolved "https://registry.npmjs.org/@lerna/publish/-/publish-3.20.2.tgz#a45d29813099b3249657ea913d0dc3f8ebc5cc2e" - integrity sha512-N7Y6PdhJ+tYQPdI1tZum8W25cDlTp4D6brvRacKZusweWexxaopbV8RprBaKexkEX/KIbncuADq7qjDBdQHzaA== +"@lerna/publish@3.22.0": + version "3.22.0" + resolved "https://registry.npmjs.org/@lerna/publish/-/publish-3.22.0.tgz#7a3fb61026d3b7425f3b9a1849421f67d795c55d" + integrity sha512-8LBeTLBN8NIrCrLGykRu+PKrfrCC16sGCVY0/bzq9TDioR7g6+cY0ZAw653Qt/0Kr7rg3J7XxVNdzj3fvevlwA== dependencies: "@evocateur/libnpmaccess" "^3.1.2" "@evocateur/npm-registry-fetch" "^4.0.0" @@ -2017,7 +1999,7 @@ "@lerna/check-working-tree" "3.16.5" "@lerna/child-process" "3.16.5" "@lerna/collect-updates" "3.20.0" - "@lerna/command" "3.18.5" + "@lerna/command" "3.21.0" "@lerna/describe-ref" "3.16.5" "@lerna/log-packed" "3.16.0" "@lerna/npm-conf" "3.16.0" @@ -2032,7 +2014,7 @@ "@lerna/run-lifecycle" "3.16.2" "@lerna/run-topologically" "3.18.5" "@lerna/validation-error" "3.13.0" - "@lerna/version" "3.20.2" + "@lerna/version" "3.22.0" figgy-pudding "^3.5.1" fs-extra "^8.1.0" npm-package-arg "^6.1.0" @@ -2095,12 +2077,12 @@ figgy-pudding "^3.5.1" p-queue "^4.0.0" -"@lerna/run@3.20.0": - version "3.20.0" - resolved "https://registry.npmjs.org/@lerna/run/-/run-3.20.0.tgz#a479f7c42bdf9ebabb3a1e5a2bdebb7a8d201151" - integrity sha512-9U3AqeaCeB7KsGS9oyKNp62s9vYoULg/B4cqXTKZkc+OKL6QOEjYHYVSBcMK9lUXrMjCjDIuDSX3PnTCPxQ2Dw== +"@lerna/run@3.21.0": + version "3.21.0" + resolved "https://registry.npmjs.org/@lerna/run/-/run-3.21.0.tgz#2a35ec84979e4d6e42474fe148d32e5de1cac891" + integrity sha512-fJF68rT3veh+hkToFsBmUJ9MHc9yGXA7LSDvhziAojzOb0AI/jBDp6cEcDQyJ7dbnplba2Lj02IH61QUf9oW0Q== dependencies: - "@lerna/command" "3.18.5" + "@lerna/command" "3.21.0" "@lerna/filter-options" "3.20.0" "@lerna/npm-run-script" "3.16.5" "@lerna/output" "3.13.0" @@ -2145,17 +2127,17 @@ dependencies: npmlog "^4.1.2" -"@lerna/version@3.20.2": - version "3.20.2" - resolved "https://registry.npmjs.org/@lerna/version/-/version-3.20.2.tgz#3709141c0f537741d9bc10cb24f56897bcb30428" - integrity sha512-ckBJMaBWc+xJen0cMyCE7W67QXLLrc0ELvigPIn8p609qkfNM0L0CF803MKxjVOldJAjw84b8ucNWZLvJagP/Q== +"@lerna/version@3.22.0": + version "3.22.0" + resolved "https://registry.npmjs.org/@lerna/version/-/version-3.22.0.tgz#67e1340c1904e9b339becd66429f32dd8ad65a55" + integrity sha512-6uhL6RL7/FeW6u1INEgyKjd5dwO8+IsbLfkfC682QuoVLS7VG6OOB+JmTpCvnuyYWI6fqGh1bRk9ww8kPsj+EA== dependencies: "@lerna/check-working-tree" "3.16.5" "@lerna/child-process" "3.16.5" "@lerna/collect-updates" "3.20.0" - "@lerna/command" "3.18.5" - "@lerna/conventional-commits" "3.18.5" - "@lerna/github-client" "3.16.5" + "@lerna/command" "3.21.0" + "@lerna/conventional-commits" "3.22.0" + "@lerna/github-client" "3.22.0" "@lerna/gitlab-client" "3.15.0" "@lerna/output" "3.13.0" "@lerna/prerelease-id-from-version" "3.16.0" @@ -2340,10 +2322,10 @@ is-plain-object "^3.0.0" universal-user-agent "^5.0.0" -"@octokit/plugin-enterprise-rest@^3.6.1": - version "3.6.2" - resolved "https://registry.npmjs.org/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-3.6.2.tgz#74de25bef21e0182b4fa03a8678cd00a4e67e561" - integrity sha512-3wF5eueS5OHQYuAEudkpN+xVeUsg8vYEMMenEzLphUZ7PRZ8OJtDcsreL3ad9zxXmBbaFWzLmFcdob5CLyZftA== +"@octokit/plugin-enterprise-rest@^6.0.1": + version "6.0.1" + resolved "https://registry.npmjs.org/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-6.0.1.tgz#e07896739618dab8da7d4077c658003775f95437" + integrity sha512-93uGjlhUD+iNg1iWhUENAtJata6w5nE+V4urXOAlIXdco6xNZtUSfYY8dzp3Udy74aqO/B5UZL80x/YMa5PKRw== "@octokit/plugin-paginate-rest@^1.1.1": version "1.1.2" @@ -2585,15 +2567,15 @@ ts-dedent "^1.1.0" "@storybook/addon-storysource@^5.3.18": - version "5.3.18" - resolved "https://registry.npmjs.org/@storybook/addon-storysource/-/addon-storysource-5.3.18.tgz#003374d76c0898c3b74aa4cd008f444eb6b63c68" - integrity sha512-uMSu505rbcNErAYngV5raVRQ/eMiOtGNA88WxNzD/8h/0EVpEXlBAewiGzAmooNhCa/3O3iPuwka8Cky6aWPOA== + version "5.3.19" + resolved "https://registry.npmjs.org/@storybook/addon-storysource/-/addon-storysource-5.3.19.tgz#ae693e88db5d220cb256a9ef4a2366c300e8d88c" + integrity sha512-W7mIAHuxYT+b1huaHCHLkBAh2MbeWmF8CxeBCFiOgZaYYQUTDEh018HJF8u2AqiWSouRhcfzhTnGxOo0hNRBgw== dependencies: - "@storybook/addons" "5.3.18" - "@storybook/components" "5.3.18" - "@storybook/router" "5.3.18" - "@storybook/source-loader" "5.3.18" - "@storybook/theming" "5.3.18" + "@storybook/addons" "5.3.19" + "@storybook/components" "5.3.19" + "@storybook/router" "5.3.19" + "@storybook/source-loader" "5.3.19" + "@storybook/theming" "5.3.19" core-js "^3.0.1" estraverse "^4.2.0" loader-utils "^1.2.3" @@ -2616,7 +2598,7 @@ global "^4.3.2" util-deprecate "^1.0.2" -"@storybook/addons@^5.3.17": +"@storybook/addons@5.3.19", "@storybook/addons@^5.3.17": version "5.3.19" resolved "https://registry.npmjs.org/@storybook/addons/-/addons-5.3.19.tgz#3a7010697afd6df9a41b8c8a7351d9a06ff490a4" integrity sha512-Ky/k22p6i6FVNvs1VhuFyGvYJdcp+FgXqFgnPyY/OXJW/vPDapdElpTpHJZLFI9I2FQBDcygBPU5RXkumQ+KUQ== @@ -2770,6 +2752,33 @@ simplebar-react "^1.0.0-alpha.6" ts-dedent "^1.1.0" +"@storybook/components@5.3.19": + version "5.3.19" + resolved "https://registry.npmjs.org/@storybook/components/-/components-5.3.19.tgz#aac1f9eea1247cc85bd93b10fca803876fb84a6b" + integrity sha512-3g23/+ktlocaHLJKISu9Neu3XKa6aYP2ctDYkRtGchSB0Q55hQsUVGO+BEVuT7Pk2D59mVCxboBjxcRoPUY4pw== + dependencies: + "@storybook/client-logger" "5.3.19" + "@storybook/theming" "5.3.19" + "@types/react-syntax-highlighter" "11.0.4" + "@types/react-textarea-autosize" "^4.3.3" + core-js "^3.0.1" + global "^4.3.2" + lodash "^4.17.15" + markdown-to-jsx "^6.11.4" + memoizerific "^1.11.3" + polished "^3.3.1" + popper.js "^1.14.7" + prop-types "^15.7.2" + react "^16.8.3" + react-dom "^16.8.3" + react-focus-lock "^2.1.0" + react-helmet-async "^1.0.2" + react-popper-tooltip "^2.8.3" + react-syntax-highlighter "^11.0.2" + react-textarea-autosize "^7.1.0" + simplebar-react "^1.0.0-alpha.6" + ts-dedent "^1.1.0" + "@storybook/core-events@5.3.18": version "5.3.18" resolved "https://registry.npmjs.org/@storybook/core-events/-/core-events-5.3.18.tgz#e5d335f8a2c7dd46502b8f505006f1e111b46d49" @@ -2939,13 +2948,13 @@ qs "^6.6.0" util-deprecate "^1.0.2" -"@storybook/source-loader@5.3.18": - version "5.3.18" - resolved "https://registry.npmjs.org/@storybook/source-loader/-/source-loader-5.3.18.tgz#39ba28d9664ab8204d6b04ee757772369931e7e5" - integrity sha512-oljmLt3KMu17W9FAaEgocsI6wrloWczS26SVXadzbDbL+8Tu6vTeFJSd6HcY+e4JIIKjuze0kmaVhpi6wwPbZQ== +"@storybook/source-loader@5.3.19": + version "5.3.19" + resolved "https://registry.npmjs.org/@storybook/source-loader/-/source-loader-5.3.19.tgz#ff0a00731c24c61721d8b9d84152f8542913a3b7" + integrity sha512-srSZRPgEOUse8nRVnlazweB2QGp63mPqM0uofg8zYARyaYSOzkC155ymdeiHsmiBTS3X3I0FQE4+KnwiH7iLtw== dependencies: - "@storybook/addons" "5.3.18" - "@storybook/client-logger" "5.3.18" + "@storybook/addons" "5.3.19" + "@storybook/client-logger" "5.3.19" "@storybook/csf" "0.0.1" core-js "^3.0.1" estraverse "^4.2.0" @@ -3402,7 +3411,7 @@ dependencies: "@babel/types" "^7.3.0" -"@types/body-parser@*": +"@types/body-parser@*", "@types/body-parser@^1.19.0": version "1.19.0" resolved "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ== @@ -3422,10 +3431,10 @@ dependencies: "@types/node" "*" -"@types/codemirror@^0.0.93": - version "0.0.93" - resolved "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.93.tgz#74ce5e1132325cb2c88a9bb91fd819294fe0c8ed" - integrity sha512-7F2ksY4U5amV/bIkMTev0n22RWcFO2BBuFuFYmAJIT19gr3kDElr1phgjPsNKV/Pj301bujnMjSy5GlWla+cow== +"@types/codemirror@^0.0.95": + version "0.0.95" + resolved "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.95.tgz#8fe424989cd5a6d7848f124774d42ef34d91adb8" + integrity sha512-E7w4HS8/rR7Rxkga6j68n3/Mi4BJ870/OJJKRqytyWiM659KnbviSng/NPfM/FOjg7YL+5ruFF69FqoLChnPBw== dependencies: "@types/tern" "*" @@ -3812,6 +3821,14 @@ "@types/oauth" "*" "@types/passport" "*" +"@types/passport-saml@^1.1.2": + version "1.1.2" + resolved "https://registry.npmjs.org/@types/passport-saml/-/passport-saml-1.1.2.tgz#f32ac2321eb25ec7bdbb1f3a5313b596bb0887e6" + integrity sha512-vpSdcb7V/bFxrvZJwSqnBr0qEqIhtOnwRBxw+Dvq4UkVbEgcCOkxF4tERCCFfA+FP3lp63VCCAifZLQrF5JkXA== + dependencies: + "@types/express" "*" + "@types/passport" "*" + "@types/passport@*", "@types/passport@^1.0.3": version "1.0.3" resolved "https://registry.npmjs.org/@types/passport/-/passport-1.0.3.tgz#e459ed6c262bf0686684d1b05901be0d0b192a9c" @@ -5453,7 +5470,7 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: resolved "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA== -body-parser@1.19.0: +body-parser@1.19.0, body-parser@^1.19.0: version "1.19.0" resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== @@ -6771,7 +6788,7 @@ cross-spawn@6.0.5, cross-spawn@^6.0.0: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@7.0.1, cross-spawn@^7.0.0, cross-spawn@^7.0.1: +cross-spawn@7.0.1: version "7.0.1" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz#0ab56286e0f7c24e153d04cc2aa027e43a9a5d14" integrity sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg== @@ -6789,7 +6806,7 @@ cross-spawn@^5.1.0: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^7.0.2: +cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2: version "7.0.3" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -8065,7 +8082,7 @@ escape-goat@^2.0.0: resolved "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q== -escape-html@~1.0.3: +escape-html@^1.0.3, escape-html@~1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= @@ -12006,26 +12023,26 @@ left-pad@^1.3.0: integrity sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA== lerna@^3.20.2: - version "3.20.2" - resolved "https://registry.npmjs.org/lerna/-/lerna-3.20.2.tgz#abf84e73055fe84ee21b46e64baf37b496c24864" - integrity sha512-bjdL7hPLpU3Y8CBnw/1ys3ynQMUjiK6l9iDWnEGwFtDy48Xh5JboR9ZJwmKGCz9A/sarVVIGwf1tlRNKUG9etA== + version "3.22.0" + resolved "https://registry.npmjs.org/lerna/-/lerna-3.22.0.tgz#da14d08f183ffe6eec566a4ef3f0e11afa621183" + integrity sha512-xWlHdAStcqK/IjKvjsSMHPZjPkBV1lS60PmsIeObU8rLljTepc4Sg/hncw4HWfQxPIewHAUTqhrxPIsqf9L2Eg== dependencies: - "@lerna/add" "3.20.0" - "@lerna/bootstrap" "3.20.0" - "@lerna/changed" "3.20.0" - "@lerna/clean" "3.20.0" + "@lerna/add" "3.21.0" + "@lerna/bootstrap" "3.21.0" + "@lerna/changed" "3.21.0" + "@lerna/clean" "3.21.0" "@lerna/cli" "3.18.5" - "@lerna/create" "3.18.5" - "@lerna/diff" "3.18.5" - "@lerna/exec" "3.20.0" - "@lerna/import" "3.18.5" - "@lerna/info" "3.20.0" - "@lerna/init" "3.18.5" - "@lerna/link" "3.18.5" - "@lerna/list" "3.20.0" - "@lerna/publish" "3.20.2" - "@lerna/run" "3.20.0" - "@lerna/version" "3.20.2" + "@lerna/create" "3.22.0" + "@lerna/diff" "3.21.0" + "@lerna/exec" "3.21.0" + "@lerna/import" "3.22.0" + "@lerna/info" "3.21.0" + "@lerna/init" "3.21.0" + "@lerna/link" "3.21.0" + "@lerna/list" "3.21.0" + "@lerna/publish" "3.22.0" + "@lerna/run" "3.21.0" + "@lerna/version" "3.22.0" import-local "^2.0.0" npmlog "^4.1.2" @@ -12553,6 +12570,14 @@ markdown-it@^10.0.0: mdurl "^1.0.1" uc.micro "^1.0.5" +markdown-to-jsx@^6.11.4: + version "6.11.4" + resolved "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-6.11.4.tgz#b4528b1ab668aef7fe61c1535c27e837819392c5" + integrity sha512-3lRCD5Sh+tfA52iGgfs/XZiw33f7fFX9Bn55aNnVNUd2GzLDkOWyKYYD8Yju2B1Vn+feiEdgJs8T6Tg0xNokPw== + dependencies: + prop-types "^15.6.2" + unquote "^1.1.0" + markdown-to-jsx@^6.9.1, markdown-to-jsx@^6.9.3: version "6.11.0" resolved "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-6.11.0.tgz#a2e3f2bc781c3402d8bb0f8e0a12a186474623b0" @@ -13210,6 +13235,11 @@ node-forge@0.9.0: resolved "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579" integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ== +node-forge@^0.7.0: + version "0.7.6" + resolved "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz#fdf3b418aee1f94f0ef642cd63486c77ca9724ac" + integrity sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw== + node-gyp@^5.0.2: version "5.1.0" resolved "https://registry.npmjs.org/node-gyp/-/node-gyp-5.1.0.tgz#8e31260a7af4a2e2f994b0673d4e0b3866156332" @@ -14079,7 +14109,21 @@ passport-oauth2@1.x.x: uid2 "0.0.x" utils-merge "1.x.x" -passport-strategy@1.x.x: +passport-saml@^1.3.3: + version "1.3.3" + resolved "https://registry.npmjs.org/passport-saml/-/passport-saml-1.3.3.tgz#cbea1a2b21ff32b3bc4bfd84dc39c3a370df9935" + integrity sha512-54ecY/A6UEsyCehJws6a+J6THvwtYnGl9cnAUxx5DjsuKgZrDs0tSy58K4hCk1XG/LOcdQSF1TR3xlRXgTULhA== + dependencies: + debug "^3.1.0" + passport-strategy "*" + q "^1.5.0" + xml-crypto "^1.4.0" + xml-encryption "^1.0.0" + xml2js "0.4.x" + xmlbuilder "^11.0.0" + xmldom "0.1.x" + +passport-strategy@*, passport-strategy@1.x.x: version "1.0.0" resolved "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" integrity sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ= @@ -15067,7 +15111,7 @@ pupa@^2.0.1: dependencies: escape-goat "^2.0.0" -q@^1.1.2, q@^1.5.1: +q@^1.1.2, q@^1.5.0, q@^1.5.1: version "1.5.1" resolved "https://registry.npmjs.org/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= @@ -16405,7 +16449,7 @@ sane@^4.0.3: minimist "^1.1.1" walker "~1.0.5" -sax@^1.2.4, sax@~1.2.4: +sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4: version "1.2.4" resolved "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== @@ -18083,9 +18127,9 @@ ts-pnp@^1.1.2: integrity sha512-CrG5GqAAzMT7144Cl+UIFP7mz/iIhiy+xQ6GGcnjTezhALT02uPMRw7tgDSESgB5MsfKt55+GPWw4ir1kVtMIQ== tsc-watch@^4.2.3: - version "4.2.5" - resolved "https://registry.npmjs.org/tsc-watch/-/tsc-watch-4.2.5.tgz#3ee680cfd02087bb76cbecd2d5f290a0d6302454" - integrity sha512-scXpL5SFJevvtKOtQIRxJvyEwCKSIZS9bDrO/cy/dWlETTVwSlfRm2WmakdRPM0Rg90DbYyJjkUr2z74HXcxzQ== + version "4.2.8" + resolved "https://registry.npmjs.org/tsc-watch/-/tsc-watch-4.2.8.tgz#cb8639bf507738dfa64590504f539a2f525d3a7b" + integrity sha512-EtWdmVZYFxTxSHqtmpowtvC/son28Hgl3qxYnD6sy+uGQXsWYDQxwmL8EneWca2kzAIYNZq3CGrUdK1jjbL4Vw== dependencies: cross-spawn "^5.1.0" node-cleanup "^2.1.2" @@ -19171,16 +19215,62 @@ xdg-basedir@^4.0.0: resolved "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== +xml-crypto@^1.4.0: + version "1.5.3" + resolved "https://registry.npmjs.org/xml-crypto/-/xml-crypto-1.5.3.tgz#a8f500b90f0dfaf0efa3331c345ecb0fff993c34" + integrity sha512-uHkmpUtX15xExe5iimPmakAZN+6CqIvjmaJTy4FwqGzaTjrKRBNeqMh8zGEzVNgW0dk6beFYpyQSgqV/J6C5xA== + dependencies: + xmldom "0.1.27" + xpath "0.0.27" + +xml-encryption@^1.0.0: + version "1.2.0" + resolved "https://registry.npmjs.org/xml-encryption/-/xml-encryption-1.2.0.tgz#37c8b470beae88b4625ea8cad82f108ea0f9c364" + integrity sha512-J3NjGMY8jf6bTo15jURTYBLtsisbnyCeM+MuxtfiAkZEZBnSZpNKjUUORhiOScKvSi6tMOAaZ3r7bZOXOni+Ew== + dependencies: + escape-html "^1.0.3" + node-forge "^0.7.0" + xmldom "~0.1.15" + xpath "0.0.27" + xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== +xml2js@0.4.x: + version "0.4.23" + resolved "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" + integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== + dependencies: + sax ">=0.6.0" + xmlbuilder "~11.0.0" + +xmlbuilder@^11.0.0, xmlbuilder@~11.0.0: + version "11.0.1" + resolved "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" + integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== + xmlchars@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== +xmldom@0.1.27: + version "0.1.27" + resolved "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9" + integrity sha1-1QH5ezvbQDr4757MIFcxh6rawOk= + +xmldom@0.1.x, xmldom@~0.1.15: + version "0.1.31" + resolved "https://registry.npmjs.org/xmldom/-/xmldom-0.1.31.tgz#b76c9a1bd9f0a9737e5a72dc37231cf38375e2ff" + integrity sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ== + +xpath@0.0.27: + version "0.0.27" + resolved "https://registry.npmjs.org/xpath/-/xpath-0.0.27.tgz#dd3421fbdcc5646ac32c48531b4d7e9d0c2cfa92" + integrity sha512-fg03WRxtkCV6ohClePNAECYsmpKKTv5L8y/X3Dn1hQrec3POx2jHZ/0P2qQ6HvsrU1BmeqXcof3NGGueG6LxwQ== + xregexp@^4.3.0: version "4.3.0" resolved "https://registry.npmjs.org/xregexp/-/xregexp-4.3.0.tgz#7e92e73d9174a99a59743f67a4ce879a04b5ae50" @@ -19220,6 +19310,11 @@ yaml@*, yaml@^1.9.2: dependencies: "@babel/runtime" "^7.9.2" +yaml@^1.10.0: + version "1.10.0" + resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e" + integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg== + yaml@^1.7.2: version "1.8.3" resolved "https://registry.npmjs.org/yaml/-/yaml-1.8.3.tgz#2f420fca58b68ce3a332d0ca64be1d191dd3f87a"