Merge branch 'master' into ndudnik/edit-yaml

This commit is contained in:
Nikita Dudnik
2020-06-03 21:45:58 +02:00
committed by GitHub
59 changed files with 1239 additions and 736 deletions
+65
View File
@@ -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
+4 -10
View File
@@ -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
+3 -3
View File
@@ -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 }}
+3 -3
View File
@@ -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 }}
+2 -2
View File
@@ -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') }}
+9
View File
@@ -0,0 +1,9 @@
app:
title: Backstage Example App
baseUrl: http://localhost:3000
backend:
baseUrl: http://localhost:7000
organization:
name: Spotify
@@ -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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

@@ -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`.
-12
View File
@@ -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();
+3 -6
View File
@@ -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,
+1
View File
@@ -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"
},
+2
View File
@@ -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(),
});
};
+2
View File
@@ -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();
+1
View File
@@ -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) {
@@ -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();
@@ -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';
@@ -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<AppConfig[]> {
// 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}`);
}
}
+17
View File
@@ -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;
+6
View File
@@ -51,6 +51,12 @@ export function createConfig(
);
}
plugins.push(
new webpack.EnvironmentPlugin({
APP_CONFIG: options.appConfig,
}),
);
return {
mode: isDev ? 'development' : 'production',
profile: false,
+4
View File
@@ -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[];
};
@@ -0,0 +1,5 @@
app:
title: Scaffolded Backstage App
organization:
name: Acme Corporation
@@ -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,
+2 -2
View File
@@ -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);
+4 -1
View File
@@ -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<AppConfig>;
export type AppConfigLoader = () => Promise<AppConfig[]>;
export type AppOptions = {
/**
@@ -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',
);
});
});
+40 -1
View File
@@ -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,
+8
View File
@@ -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]
+4
View File
@@ -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"
},
+1
View File
@@ -0,0 +1 @@
*.pem
+21
View File
@@ -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
@@ -55,7 +55,7 @@ export const executeFrameHandlerStrategy = async (
reject(new Error('Unexpected redirect'));
};
strategy.authenticate(req);
strategy.authenticate(req, {});
});
};
@@ -32,4 +32,12 @@ export const providers = [
},
disableRefresh: true,
},
{
provider: 'saml',
options: {
path: '/auth/saml/handler/frame',
entryPoint: 'http://localhost:7001/',
issuer: 'passport-saml',
},
},
];
+30 -30
View File
@@ -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;
};
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export { GithubAuthProvider } from './provider';
export { createGithubProvider } from './provider';
@@ -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;
}
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export { GoogleAuthProvider } from './provider';
export { createGoogleProvider } from './provider';
@@ -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;
}
+1 -21
View File
@@ -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';
@@ -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';
@@ -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<any> {
const { url } = await executeRedirectStrategy(req, this.strategy, {});
res.redirect(url);
}
async frameHandler(
req: express.Request,
res: express.Response,
): Promise<any> {
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<any> {
res.send('noop');
}
}
export function createSamlProvider(config: AuthProviderConfig) {
return new SamlAuthProvider(config);
}
+3 -7
View File
@@ -37,13 +37,9 @@ export interface AuthProviderRouteHandlers {
logout(req: express.Request, res: express.Response): Promise<any>;
}
export type AuthProviderFactories = {
[key: string]: AuthProviderFactory;
};
export type AuthProviderFactory = {
new (providerConfig: any): OAuthProviderHandlers;
};
export type AuthProviderFactory = (
config: AuthProviderConfig,
) => AuthProviderRouteHandlers;
export type AuthInfoBase = {
accessToken: string;
+8 -4
View File
@@ -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;
+1 -1
View File
@@ -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",
@@ -38,8 +38,8 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog {
async entityByName(
kind: string,
name: string,
namespace: string | undefined,
name: string,
): Promise<Entity | undefined> {
return await this.database.transaction(tx =>
this.entityByNameInternal(tx, kind, name, namespace),
@@ -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<void> {
await this.database.addLocationUpdateLogEvent(
locationId,
DatabaseLocationUpdateLogStatus.SUCCESS,
entityName,
);
}
async logUpdateFailure(
locationId: string,
error?: Error,
entityName?: string,
): Promise<void> {
await this.database.addLocationUpdateLogEvent(
locationId,
DatabaseLocationUpdateLogStatus.FAIL,
entityName,
error?.message,
);
}
}
@@ -62,4 +62,10 @@ export type LocationsCatalog = {
locations(): Promise<LocationResponse[]>;
location(id: string): Promise<LocationResponse>;
locationHistory(id: string): Promise<LocationUpdateLogEvent[]>;
logUpdateSuccess(locationId: string, entityName?: string): Promise<void>;
logUpdateFailure(
locationId: string,
error?: Error,
entityName?: string,
): Promise<void>;
};
@@ -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<any, any>;
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<Database>) 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<any, any>;
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<any, any>;
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<any, any>;
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,
);
});
});
});
@@ -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<void> {
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<void> {
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);
}
}
@@ -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<EntitiesCatalog>;
@@ -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' }),
);
});
});
});
@@ -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<void> {
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);
}
}
@@ -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';
@@ -28,4 +28,5 @@ export type IngestionModel = {
export type HigherOrderOperation = {
addLocation(spec: LocationSpec): Promise<AddLocationResult>;
refreshAllLocations(): Promise<void>;
};
@@ -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 },
}),
);
});
});
});
@@ -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
@@ -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();
+1 -1
View File
@@ -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",
@@ -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') }}
+259 -164
View File
@@ -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"