Merge branch 'master' of https://github.com/backstage/backstage into marley/7641-consume-exported-ado-types
This commit is contained in:
@@ -3,4 +3,6 @@
|
||||
'@backstage/theme': patch
|
||||
---
|
||||
|
||||
Will Add warning variant to `DismissableBanner` component.
|
||||
Added a warning variant to `DismissableBanner` component. If you are using a
|
||||
custom theme, you will need to add the optional `palette.banner.warning` color,
|
||||
otherwise this variant will fall back to the `palette.banner.error` color.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-common': patch
|
||||
---
|
||||
|
||||
AWSS3UrlReader now throws a `NotModifiedError` (exported from @backstage/backend-common) when s3 returns a 304 response.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-jenkins-backend': patch
|
||||
---
|
||||
|
||||
Don't require a validation pattern for the Jenkins base URL.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/backend-common': patch
|
||||
'@backstage/cli': patch
|
||||
'@backstage/config-loader': patch
|
||||
---
|
||||
|
||||
Reading app config from a remote server
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-ilert': patch
|
||||
---
|
||||
|
||||
Change the version of `@date-io/luxon` from 2.x to 1.x to make it compatible with material-ui-pickers
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-sentry': patch
|
||||
---
|
||||
|
||||
fix: sentry-plugin can forward identity token to backend (for case when it requires authorization)
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Make `ExitCodeError` call `super` early to avoid compiler warnings
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
Adds a new endpoint for consuming logs from the Scaffolder that uses long polling instead of Server Sent Events.
|
||||
|
||||
This is useful if Backstage is accessed from an environment that doesn't support SSE correctly, which happens in combination with certain enterprise HTTP Proxy servers.
|
||||
|
||||
It is intended to switch the endpoint globally for the whole instance.
|
||||
If you want to use it, you can provide a reconfigured API to the `scaffolderApiRef`:
|
||||
|
||||
```tsx
|
||||
// packages/app/src/apis.ts
|
||||
|
||||
// ...
|
||||
import {
|
||||
scaffolderApiRef,
|
||||
ScaffolderClient,
|
||||
} from '@backstage/plugin-scaffolder';
|
||||
|
||||
export const apis: AnyApiFactory[] = [
|
||||
// ...
|
||||
|
||||
createApiFactory({
|
||||
api: scaffolderApiRef,
|
||||
deps: {
|
||||
discoveryApi: discoveryApiRef,
|
||||
identityApi: identityApiRef,
|
||||
scmIntegrationsApi: scmIntegrationsApiRef,
|
||||
},
|
||||
factory: ({ discoveryApi, identityApi, scmIntegrationsApi }) =>
|
||||
new ScaffolderClient({
|
||||
discoveryApi,
|
||||
identityApi,
|
||||
scmIntegrationsApi,
|
||||
// use long polling instead of an eventsource
|
||||
useLongPollingLogs: true,
|
||||
}),
|
||||
}),
|
||||
];
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/techdocs-common': patch
|
||||
---
|
||||
|
||||
Updated to properly join URL segments under any OS for both AWS S3 and GCP
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch
|
||||
'@backstage/plugin-scaffolder-backend-module-rails': patch
|
||||
'@backstage/plugin-scaffolder-backend-module-yeoman': patch
|
||||
---
|
||||
|
||||
Add missing API docs to scaffolder action plugins
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
# [Backstage](https://backstage.io)
|
||||
|
||||
> 🏖 All of the maintainers will be taking a wellness break Nov. 1–5. The repo and Discord may be quieter than usual, but not to worry. We’ll have coverage plans in place and be back in full force, rested and restored, on Nov. 8. 🏖
|
||||
|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||
[](https://www.cncf.io/projects)
|
||||
[](https://github.com/backstage/backstage/actions?query=workflow%3A%22Main+Master+Build%22)
|
||||
|
||||
+10
-8
@@ -67,13 +67,15 @@ production build.
|
||||
|
||||
## Configuration Files
|
||||
|
||||
It is possible to have multiple configuration files, both to support different
|
||||
environments, but also to define configuration that is local to specific
|
||||
packages. The configuration files to load are selected using a `--config <path>`
|
||||
flag, and it is possible to load any number of files. Paths are relative to the
|
||||
working directory of the executed process, for example `package/backend`. This
|
||||
means that to select a config file in the repo root when running the backend,
|
||||
you would use `--config ../../my-config.yaml`.
|
||||
It is possible to have multiple configuration files (bundled and/or remote),
|
||||
both to support different environments, but also to define configuration that is
|
||||
local to specific packages. The configuration files to load are selected using a
|
||||
`--config <local-path|url>` flag, and it is possible to load any number of
|
||||
files. Paths are relative to the working directory of the executed process, for
|
||||
example `package/backend`. This means that to select a config file in the repo
|
||||
root when running the backend, you would use `--config ../../my-config.yaml`,
|
||||
and for config file on a config server you would use
|
||||
`--config https://some.domain.io/app-config.yaml`
|
||||
|
||||
If no `config` flags are specified, the default behavior is to load
|
||||
`app-config.yaml` and, if it exists, `app-config.local.yaml` from the repo root.
|
||||
@@ -85,7 +87,7 @@ are NOT loaded. To include them you need to explicitly include them with a flag,
|
||||
for example:
|
||||
|
||||
```shell
|
||||
yarn start --config ../../app-config.yaml --config ../../app-config.staging.yaml
|
||||
yarn start --config ../../app-config.yaml --config ../../app-config.staging.yaml --config https://some.domain.io/app-config.yaml
|
||||
```
|
||||
|
||||
All loaded configuration files are merged together using the following rules:
|
||||
|
||||
@@ -22,10 +22,13 @@ import {
|
||||
loadConfigSchema,
|
||||
loadConfig,
|
||||
ConfigSchema,
|
||||
ConfigTarget,
|
||||
} from '@backstage/config-loader';
|
||||
import { AppConfig, Config, ConfigReader } from '@backstage/config';
|
||||
import { JsonValue } from '@backstage/types';
|
||||
|
||||
import { isValidUrl } from './urls';
|
||||
|
||||
import { setRootLoggerRedactionList } from './logging/rootLogger';
|
||||
|
||||
// Fetch the schema and get all the secrets to pass to the rootLogger for redaction
|
||||
@@ -178,7 +181,10 @@ export async function loadBackendConfig(options: {
|
||||
argv: string[];
|
||||
}): Promise<Config> {
|
||||
const args = parseArgs(options.argv);
|
||||
const configPaths: string[] = [args.config ?? []].flat();
|
||||
|
||||
const configTargets: ConfigTarget[] = [args.config ?? []]
|
||||
.flat()
|
||||
.map(arg => (isValidUrl(arg) ? { url: arg } : { path: resolvePath(arg) }));
|
||||
|
||||
/* eslint-disable-next-line no-restricted-syntax */
|
||||
const paths = findPaths(__dirname);
|
||||
@@ -196,7 +202,8 @@ export async function loadBackendConfig(options: {
|
||||
const config = new ObservableConfigProxy(options.logger);
|
||||
const configs = await loadConfig({
|
||||
configRoot: paths.targetRoot,
|
||||
configPaths: configPaths.map(opt => resolvePath(opt)),
|
||||
configPaths: [],
|
||||
configTargets: configTargets,
|
||||
watch: {
|
||||
onChange(newConfigs) {
|
||||
options.logger.info(
|
||||
|
||||
@@ -27,6 +27,7 @@ import { UrlReaderPredicateTuple } from './types';
|
||||
import AWSMock from 'aws-sdk-mock';
|
||||
import aws from 'aws-sdk';
|
||||
import path from 'path';
|
||||
import { NotModifiedError } from '@backstage/errors';
|
||||
|
||||
const treeResponseFactory = DefaultReadTreeResponseFactory.create({
|
||||
config: new ConfigReader({}),
|
||||
@@ -131,29 +132,37 @@ describe('AwsS3UrlReader', () => {
|
||||
});
|
||||
|
||||
describe('read', () => {
|
||||
AWSMock.setSDKInstance(aws);
|
||||
AWSMock.mock(
|
||||
'S3',
|
||||
'getObject',
|
||||
Buffer.from(
|
||||
require('fs').readFileSync(
|
||||
path.resolve(__dirname, '__fixtures__/awsS3/awsS3-mock-object.yaml'),
|
||||
let awsS3UrlReader: AwsS3UrlReader;
|
||||
|
||||
beforeAll(() => {
|
||||
AWSMock.setSDKInstance(aws);
|
||||
AWSMock.mock(
|
||||
'S3',
|
||||
'getObject',
|
||||
Buffer.from(
|
||||
require('fs').readFileSync(
|
||||
path.resolve(
|
||||
__dirname,
|
||||
'__fixtures__/awsS3/awsS3-mock-object.yaml',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
const s3 = new aws.S3();
|
||||
const awsS3UrlReader = new AwsS3UrlReader(
|
||||
new AwsS3Integration(
|
||||
readAwsS3IntegrationConfig(
|
||||
new ConfigReader({
|
||||
host: 'amazonaws.com',
|
||||
accessKeyId: 'fake-access-key',
|
||||
secretAccessKey: 'fake-secret-key',
|
||||
}),
|
||||
);
|
||||
|
||||
const s3 = new aws.S3();
|
||||
awsS3UrlReader = new AwsS3UrlReader(
|
||||
new AwsS3Integration(
|
||||
readAwsS3IntegrationConfig(
|
||||
new ConfigReader({
|
||||
host: 'amazonaws.com',
|
||||
accessKeyId: 'fake-access-key',
|
||||
secretAccessKey: 'fake-secret-key',
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
{ s3, treeResponseFactory },
|
||||
);
|
||||
{ s3, treeResponseFactory },
|
||||
);
|
||||
});
|
||||
|
||||
it('returns contents of an object in a bucket', async () => {
|
||||
const response = await awsS3UrlReader.read(
|
||||
@@ -176,32 +185,39 @@ describe('AwsS3UrlReader', () => {
|
||||
});
|
||||
|
||||
describe('readUrl', () => {
|
||||
AWSMock.setSDKInstance(aws);
|
||||
let awsS3UrlReader: AwsS3UrlReader;
|
||||
|
||||
AWSMock.mock(
|
||||
'S3',
|
||||
'getObject',
|
||||
Buffer.from(
|
||||
require('fs').readFileSync(
|
||||
path.resolve(__dirname, '__fixtures__/awsS3/awsS3-mock-object.yaml'),
|
||||
beforeAll(() => {
|
||||
AWSMock.setSDKInstance(aws);
|
||||
|
||||
AWSMock.mock(
|
||||
'S3',
|
||||
'getObject',
|
||||
Buffer.from(
|
||||
require('fs').readFileSync(
|
||||
path.resolve(
|
||||
__dirname,
|
||||
'__fixtures__/awsS3/awsS3-mock-object.yaml',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
|
||||
const s3 = new aws.S3();
|
||||
const s3 = new aws.S3();
|
||||
|
||||
const awsS3UrlReader = new AwsS3UrlReader(
|
||||
new AwsS3Integration(
|
||||
readAwsS3IntegrationConfig(
|
||||
new ConfigReader({
|
||||
host: 'amazonaws.com',
|
||||
accessKeyId: 'fake-access-key',
|
||||
secretAccessKey: 'fake-secret-key',
|
||||
}),
|
||||
awsS3UrlReader = new AwsS3UrlReader(
|
||||
new AwsS3Integration(
|
||||
readAwsS3IntegrationConfig(
|
||||
new ConfigReader({
|
||||
host: 'amazonaws.com',
|
||||
accessKeyId: 'fake-access-key',
|
||||
secretAccessKey: 'fake-secret-key',
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
{ s3, treeResponseFactory },
|
||||
);
|
||||
{ s3, treeResponseFactory },
|
||||
);
|
||||
});
|
||||
|
||||
it('returns contents of an object in a bucket', async () => {
|
||||
const response = await awsS3UrlReader.readUrl(
|
||||
@@ -223,40 +239,89 @@ describe('AwsS3UrlReader', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('readUrl with etag', () => {
|
||||
let awsS3UrlReader: AwsS3UrlReader;
|
||||
|
||||
beforeAll(() => {
|
||||
AWSMock.setSDKInstance(aws);
|
||||
|
||||
AWSMock.mock('S3', 'getObject', (_, callback) => {
|
||||
callback({ statusCode: 304 }, null);
|
||||
});
|
||||
|
||||
const s3 = new aws.S3();
|
||||
|
||||
awsS3UrlReader = new AwsS3UrlReader(
|
||||
new AwsS3Integration(
|
||||
readAwsS3IntegrationConfig(
|
||||
new ConfigReader({
|
||||
host: 'amazonaws.com',
|
||||
accessKeyId: 'fake-access-key',
|
||||
secretAccessKey: 'fake-secret-key',
|
||||
}),
|
||||
),
|
||||
),
|
||||
{ s3, treeResponseFactory },
|
||||
);
|
||||
});
|
||||
|
||||
it('returns contents of an object in a bucket', async () => {
|
||||
await expect(
|
||||
awsS3UrlReader.readUrl(
|
||||
'https://test-bucket.s3.us-east-2.amazonaws.com/awsS3-mock-object.yaml',
|
||||
{
|
||||
etag: 'abc123',
|
||||
},
|
||||
),
|
||||
).rejects.toThrow(NotModifiedError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('readTree', () => {
|
||||
const object: aws.S3.Types.Object = {
|
||||
Key: 'awsS3-mock-object.yaml',
|
||||
};
|
||||
const objectList: aws.S3.ObjectList = [object];
|
||||
const output: aws.S3.Types.ListObjectsV2Output = {
|
||||
Contents: objectList,
|
||||
};
|
||||
AWSMock.setSDKInstance(aws);
|
||||
AWSMock.mock('S3', 'listObjectsV2', output);
|
||||
let awsS3UrlReader: AwsS3UrlReader;
|
||||
|
||||
AWSMock.mock(
|
||||
'S3',
|
||||
'getObject',
|
||||
Buffer.from(
|
||||
require('fs').readFileSync(
|
||||
path.resolve(__dirname, '__fixtures__/awsS3/awsS3-mock-object.yaml'),
|
||||
),
|
||||
),
|
||||
);
|
||||
beforeAll(() => {
|
||||
const object: aws.S3.Types.Object = {
|
||||
Key: 'awsS3-mock-object.yaml',
|
||||
};
|
||||
|
||||
const s3 = new aws.S3();
|
||||
const awsS3UrlReader = new AwsS3UrlReader(
|
||||
new AwsS3Integration(
|
||||
readAwsS3IntegrationConfig(
|
||||
new ConfigReader({
|
||||
host: '.amazonaws.com',
|
||||
accessKeyId: 'fake-access-key',
|
||||
secretAccessKey: 'fake-secret-key',
|
||||
}),
|
||||
const objectList: aws.S3.ObjectList = [object];
|
||||
const output: aws.S3.Types.ListObjectsV2Output = {
|
||||
Contents: objectList,
|
||||
};
|
||||
|
||||
AWSMock.setSDKInstance(aws);
|
||||
AWSMock.mock('S3', 'listObjectsV2', output);
|
||||
|
||||
AWSMock.mock(
|
||||
'S3',
|
||||
'getObject',
|
||||
Buffer.from(
|
||||
require('fs').readFileSync(
|
||||
path.resolve(
|
||||
__dirname,
|
||||
'__fixtures__/awsS3/awsS3-mock-object.yaml',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
{ s3, treeResponseFactory },
|
||||
);
|
||||
);
|
||||
|
||||
const s3 = new aws.S3();
|
||||
awsS3UrlReader = new AwsS3UrlReader(
|
||||
new AwsS3Integration(
|
||||
readAwsS3IntegrationConfig(
|
||||
new ConfigReader({
|
||||
host: '.amazonaws.com',
|
||||
accessKeyId: 'fake-access-key',
|
||||
secretAccessKey: 'fake-secret-key',
|
||||
}),
|
||||
),
|
||||
),
|
||||
{ s3, treeResponseFactory },
|
||||
);
|
||||
});
|
||||
|
||||
it('returns contents of an object in a bucket', async () => {
|
||||
const response = await awsS3UrlReader.readTree(
|
||||
'https://test.s3.us-east-2.amazonaws.com',
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
} from './types';
|
||||
import getRawBody from 'raw-body';
|
||||
import { AwsS3Integration, ScmIntegrations } from '@backstage/integration';
|
||||
import { ForwardedError } from '@backstage/errors';
|
||||
import { ForwardedError, NotModifiedError } from '@backstage/errors';
|
||||
import { ListObjectsV2Output, ObjectList } from 'aws-sdk/clients/s3';
|
||||
|
||||
const parseURL = (
|
||||
@@ -163,6 +163,10 @@ export class AwsS3UrlReader implements UrlReader {
|
||||
etag: etag,
|
||||
};
|
||||
} catch (e) {
|
||||
if (e.statusCode === 304) {
|
||||
throw new NotModifiedError();
|
||||
}
|
||||
|
||||
throw new ForwardedError('Could not retrieve file from S3', e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { isValidUrl } from './urls';
|
||||
|
||||
describe('isValidUrl', () => {
|
||||
it('should return true for url', () => {
|
||||
const validUrl = isValidUrl('http://some.valid.url');
|
||||
expect(validUrl).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for absolute path', () => {
|
||||
const validUrl = isValidUrl('/some/absolute/path');
|
||||
expect(validUrl).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for relative path', () => {
|
||||
const validUrl = isValidUrl('../some/relative/path');
|
||||
expect(validUrl).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export function isValidUrl(url: string): boolean {
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { loadConfig, loadConfigSchema } from '@backstage/config-loader';
|
||||
import {
|
||||
ConfigTarget,
|
||||
loadConfig,
|
||||
loadConfigSchema,
|
||||
} from '@backstage/config-loader';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { paths } from './paths';
|
||||
import { isValidUrl } from './urls';
|
||||
|
||||
type Options = {
|
||||
args: string[];
|
||||
@@ -26,7 +31,12 @@ type Options = {
|
||||
};
|
||||
|
||||
export async function loadCliConfig(options: Options) {
|
||||
const configPaths = options.args.map(arg => paths.resolveTarget(arg));
|
||||
const configTargets: ConfigTarget[] = [];
|
||||
options.args.forEach(arg => {
|
||||
if (!isValidUrl(arg)) {
|
||||
configTargets.push({ path: paths.resolveTarget(arg) });
|
||||
}
|
||||
});
|
||||
|
||||
// Consider all packages in the monorepo when loading in config
|
||||
const { Project } = require('@lerna/project');
|
||||
@@ -48,7 +58,8 @@ export async function loadCliConfig(options: Options) {
|
||||
? async name => process.env[name] || 'x'
|
||||
: undefined,
|
||||
configRoot: paths.targetRoot,
|
||||
configPaths,
|
||||
configPaths: [],
|
||||
configTargets: configTargets,
|
||||
});
|
||||
|
||||
// printing to stderr to not clobber stdout in case the cli command
|
||||
|
||||
@@ -26,11 +26,11 @@ export class ExitCodeError extends CustomError {
|
||||
readonly code: number;
|
||||
|
||||
constructor(code: number, command?: string) {
|
||||
if (command) {
|
||||
super(`Command '${command}' exited with code ${code}`);
|
||||
} else {
|
||||
super(`Child exited with code ${code}`);
|
||||
}
|
||||
super(
|
||||
command
|
||||
? `Command '${command}' exited with code ${code}`
|
||||
: `Child exited with code ${code}`,
|
||||
);
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { isValidUrl } from './urls';
|
||||
|
||||
describe('isValidUrl', () => {
|
||||
it('should return true for url', () => {
|
||||
const validUrl = isValidUrl('http://some.valid.url');
|
||||
expect(validUrl).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for absolute path', () => {
|
||||
const validUrl = isValidUrl('/some/absolute/path');
|
||||
expect(validUrl).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for relative path', () => {
|
||||
const validUrl = isValidUrl('../some/relative/path');
|
||||
expect(validUrl).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export function isValidUrl(url: string): boolean {
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,17 @@ export type ConfigSchemaProcessingOptions = {
|
||||
withFilteredKeys?: boolean;
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "ConfigTarget" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export type ConfigTarget =
|
||||
| {
|
||||
path: string;
|
||||
}
|
||||
| {
|
||||
url: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type ConfigVisibility = 'frontend' | 'backend' | 'secret';
|
||||
|
||||
@@ -33,12 +44,26 @@ export function loadConfig(options: LoadConfigOptions): Promise<AppConfig[]>;
|
||||
export type LoadConfigOptions = {
|
||||
configRoot: string;
|
||||
configPaths: string[];
|
||||
configTargets: ConfigTarget[];
|
||||
env?: string;
|
||||
experimentalEnvFunc?: (name: string) => Promise<string | undefined>;
|
||||
watch?: {
|
||||
onChange: (configs: AppConfig[]) => void;
|
||||
stopSignal?: Promise<void>;
|
||||
};
|
||||
remote?: LoadConfigOptionsRemote;
|
||||
watch?: LoadConfigOptionsWatch;
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "LoadConfigOptionsRemote" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export type LoadConfigOptionsRemote = {
|
||||
reloadIntervalSeconds: number;
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "LoadConfigOptionsWatch" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export type LoadConfigOptionsWatch = {
|
||||
onChange: (configs: AppConfig[]) => void;
|
||||
stopSignal?: Promise<void>;
|
||||
};
|
||||
|
||||
// @public
|
||||
@@ -71,4 +96,8 @@ export type TransformFunc<T extends number | string | boolean> = (
|
||||
visibility: ConfigVisibility;
|
||||
},
|
||||
) => T | undefined;
|
||||
|
||||
// Warnings were encountered during analysis:
|
||||
//
|
||||
// src/loader.d.ts:33:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/config-loader" does not have an export "configTargets"
|
||||
```
|
||||
|
||||
@@ -43,7 +43,8 @@
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
"typescript-json-schema": "^0.51.0",
|
||||
"yaml": "^1.9.2",
|
||||
"yup": "^0.32.9"
|
||||
"yup": "^0.32.9",
|
||||
"node-fetch": "2.6.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^26.0.7",
|
||||
@@ -51,7 +52,8 @@
|
||||
"@types/mock-fs": "^4.10.0",
|
||||
"@types/node": "^14.14.32",
|
||||
"@types/yup": "^0.29.13",
|
||||
"mock-fs": "^5.1.0"
|
||||
"mock-fs": "^5.1.0",
|
||||
"msw": "^0.29.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -29,4 +29,9 @@ export type {
|
||||
TransformFunc,
|
||||
} from './lib';
|
||||
export { loadConfig } from './loader';
|
||||
export type { LoadConfigOptions } from './loader';
|
||||
export type {
|
||||
ConfigTarget,
|
||||
LoadConfigOptions,
|
||||
LoadConfigOptionsWatch,
|
||||
LoadConfigOptionsRemote,
|
||||
} from './loader';
|
||||
|
||||
@@ -17,3 +17,4 @@
|
||||
export { readEnvConfig } from './env';
|
||||
export * from './transform';
|
||||
export * from './schema';
|
||||
export { isValidUrl } from './urls';
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { isValidUrl } from './urls';
|
||||
|
||||
describe('isValidUrl', () => {
|
||||
it('should return true for url', () => {
|
||||
const validUrl = isValidUrl('http://some.valid.url');
|
||||
expect(validUrl).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for absolute path', () => {
|
||||
const validUrl = isValidUrl('/some/absolute/path');
|
||||
expect(validUrl).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for relative path', () => {
|
||||
const validUrl = isValidUrl('../some/relative/path');
|
||||
expect(validUrl).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export function isValidUrl(url: string): boolean {
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,43 @@ import { AppConfig } from '@backstage/config';
|
||||
import { loadConfig } from './loader';
|
||||
import mockFs from 'mock-fs';
|
||||
import fs from 'fs-extra';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
|
||||
describe('loadConfig', () => {
|
||||
const server = setupServer();
|
||||
const initialLoaderHandler = rest.get(
|
||||
`https://some.domain.io/app-config.yaml`,
|
||||
(_req, res, ctx) => {
|
||||
return res(
|
||||
ctx.body(
|
||||
`app:
|
||||
title: Remote Example App
|
||||
sessionKey: 'abc123'
|
||||
escaped: \$\${Escaped}
|
||||
`,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const reloadHandler = rest.get(
|
||||
`https://some.domain.io/app-config.yaml`,
|
||||
(_req, res, ctx) => {
|
||||
return res(
|
||||
ctx.body(
|
||||
`app:
|
||||
title: NEW ReMOTe ExaMPLe App
|
||||
sessionKey: 'abc123'
|
||||
escaped: \$\${Escaped}
|
||||
`,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
beforeAll(() => server.listen());
|
||||
|
||||
beforeEach(() => {
|
||||
process.env.MY_SECRET = 'is-secret';
|
||||
process.env.SUBSTITUTE_ME = 'substituted';
|
||||
@@ -32,6 +67,13 @@ describe('loadConfig', () => {
|
||||
$file: secrets/session-key.txt
|
||||
escaped: \$\${Escaped}
|
||||
`,
|
||||
'/root/app-config2.yaml': `
|
||||
app:
|
||||
title: Example App 2
|
||||
sessionKey:
|
||||
$file: secrets/session-key.txt
|
||||
escaped: \$\${Escaped}
|
||||
`,
|
||||
'/root/app-config.development.yaml': `
|
||||
app:
|
||||
sessionKey: development-key
|
||||
@@ -67,13 +109,17 @@ describe('loadConfig', () => {
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
afterAll(() => server.close());
|
||||
|
||||
it('load config from default path', async () => {
|
||||
await expect(
|
||||
loadConfig({
|
||||
configRoot: '/root',
|
||||
configPaths: [],
|
||||
configTargets: [],
|
||||
env: 'production',
|
||||
}),
|
||||
).resolves.toEqual([
|
||||
@@ -90,11 +136,73 @@ describe('loadConfig', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('loads config with secrets', async () => {
|
||||
it('load config from remote path', async () => {
|
||||
server.use(initialLoaderHandler);
|
||||
|
||||
const configUrl = 'https://some.domain.io/app-config.yaml';
|
||||
|
||||
await expect(
|
||||
loadConfig({
|
||||
configRoot: '/root',
|
||||
configPaths: [],
|
||||
configTargets: [{ url: configUrl }],
|
||||
env: 'production',
|
||||
remote: {
|
||||
reloadIntervalSeconds: 30,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual([
|
||||
{
|
||||
context: configUrl,
|
||||
data: {
|
||||
app: {
|
||||
title: 'Remote Example App',
|
||||
sessionKey: 'abc123',
|
||||
escaped: '${Escaped}',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('loads config with secrets from two different files', async () => {
|
||||
await expect(
|
||||
loadConfig({
|
||||
configRoot: '/root',
|
||||
configPaths: ['/root/app-config2.yaml'],
|
||||
configTargets: [{ path: '/root/app-config.yaml' }],
|
||||
env: 'production',
|
||||
}),
|
||||
).resolves.toEqual([
|
||||
{
|
||||
context: 'app-config.yaml',
|
||||
data: {
|
||||
app: {
|
||||
title: 'Example App',
|
||||
sessionKey: 'abc123',
|
||||
escaped: '${Escaped}',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
context: 'app-config2.yaml',
|
||||
data: {
|
||||
app: {
|
||||
title: 'Example App 2',
|
||||
sessionKey: 'abc123',
|
||||
escaped: '${Escaped}',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('loads config with secrets from single file', async () => {
|
||||
await expect(
|
||||
loadConfig({
|
||||
configRoot: '/root',
|
||||
configPaths: ['/root/app-config.yaml'],
|
||||
configTargets: [{ path: '/root/app-config.yaml' }],
|
||||
env: 'production',
|
||||
}),
|
||||
).resolves.toEqual([
|
||||
@@ -115,9 +223,10 @@ describe('loadConfig', () => {
|
||||
await expect(
|
||||
loadConfig({
|
||||
configRoot: '/root',
|
||||
configPaths: [
|
||||
'/root/app-config.yaml',
|
||||
'/root/app-config.development.yaml',
|
||||
configPaths: [],
|
||||
configTargets: [
|
||||
{ path: '/root/app-config.yaml' },
|
||||
{ path: '/root/app-config.development.yaml' },
|
||||
],
|
||||
env: 'development',
|
||||
}),
|
||||
@@ -155,7 +264,8 @@ describe('loadConfig', () => {
|
||||
await expect(
|
||||
loadConfig({
|
||||
configRoot: '/root',
|
||||
configPaths: ['/root/app-config.substitute.yaml'],
|
||||
configPaths: [],
|
||||
configTargets: [{ path: '/root/app-config.substitute.yaml' }],
|
||||
env: 'development',
|
||||
}),
|
||||
).resolves.toEqual([
|
||||
@@ -181,6 +291,7 @@ describe('loadConfig', () => {
|
||||
loadConfig({
|
||||
configRoot: '/root',
|
||||
configPaths: [],
|
||||
configTargets: [],
|
||||
watch: {
|
||||
onChange: onChange.resolve,
|
||||
stopSignal: stopSignal.promise,
|
||||
@@ -218,12 +329,64 @@ describe('loadConfig', () => {
|
||||
stopSignal.resolve();
|
||||
});
|
||||
|
||||
it('watches remote config urls', async () => {
|
||||
server.use(initialLoaderHandler);
|
||||
|
||||
const onChange = defer<AppConfig[]>();
|
||||
const stopSignal = defer<void>();
|
||||
|
||||
const configUrl = 'https://some.domain.io/app-config.yaml';
|
||||
await expect(
|
||||
loadConfig({
|
||||
configRoot: '/root',
|
||||
configPaths: [],
|
||||
configTargets: [{ url: configUrl }],
|
||||
watch: {
|
||||
onChange: onChange.resolve,
|
||||
stopSignal: stopSignal.promise,
|
||||
},
|
||||
remote: {
|
||||
reloadIntervalSeconds: 1,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual([
|
||||
{
|
||||
context: configUrl,
|
||||
data: {
|
||||
app: {
|
||||
title: 'Remote Example App',
|
||||
sessionKey: 'abc123',
|
||||
escaped: '${Escaped}',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
server.use(reloadHandler);
|
||||
|
||||
await expect(onChange.promise).resolves.toEqual([
|
||||
{
|
||||
context: configUrl,
|
||||
data: {
|
||||
app: {
|
||||
title: 'NEW ReMOTe ExaMPLe App',
|
||||
sessionKey: 'abc123',
|
||||
escaped: '${Escaped}',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
stopSignal.resolve();
|
||||
});
|
||||
|
||||
it('stops watching config files', async () => {
|
||||
const stopSignal = defer<void>();
|
||||
|
||||
await loadConfig({
|
||||
configRoot: '/root',
|
||||
configPaths: [],
|
||||
configTargets: [],
|
||||
watch: {
|
||||
onChange: () => {
|
||||
expect('not').toBe('called');
|
||||
|
||||
@@ -17,15 +17,38 @@
|
||||
import fs from 'fs-extra';
|
||||
import yaml from 'yaml';
|
||||
import chokidar from 'chokidar';
|
||||
import { resolve as resolvePath, dirname, isAbsolute, basename } from 'path';
|
||||
import { basename, dirname, isAbsolute, resolve as resolvePath } from 'path';
|
||||
import { AppConfig } from '@backstage/config';
|
||||
import { ForwardedError } from '@backstage/errors';
|
||||
import {
|
||||
applyConfigTransforms,
|
||||
readEnvConfig,
|
||||
createIncludeTransform,
|
||||
createSubstitutionTransform,
|
||||
isValidUrl,
|
||||
readEnvConfig,
|
||||
} from './lib';
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
export type ConfigTarget = { path: string } | { url: string };
|
||||
|
||||
export type LoadConfigOptionsWatch = {
|
||||
/**
|
||||
* A listener that is called when a config file is changed.
|
||||
*/
|
||||
onChange: (configs: AppConfig[]) => void;
|
||||
|
||||
/**
|
||||
* An optional signal that stops the watcher once the promise resolves.
|
||||
*/
|
||||
stopSignal?: Promise<void>;
|
||||
};
|
||||
|
||||
export type LoadConfigOptionsRemote = {
|
||||
/**
|
||||
* An optional remote config reloading period, in seconds
|
||||
*/
|
||||
reloadIntervalSeconds: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Options that control the loading of configuration files in the backend.
|
||||
@@ -36,9 +59,14 @@ export type LoadConfigOptions = {
|
||||
// The root directory of the config loading context. Used to find default configs.
|
||||
configRoot: string;
|
||||
|
||||
// Absolute paths to load config files from. Configs from earlier paths have lower priority.
|
||||
/** Absolute paths to load config files from. Configs from earlier paths have lower priority.
|
||||
* @deprecated Use {@link configTargets} instead.
|
||||
*/
|
||||
configPaths: string[];
|
||||
|
||||
// Paths to load config files from. Configs from earlier paths have lower priority.
|
||||
configTargets: ConfigTarget[];
|
||||
|
||||
/** @deprecated This option has been removed */
|
||||
env?: string;
|
||||
|
||||
@@ -49,20 +77,15 @@ export type LoadConfigOptions = {
|
||||
*/
|
||||
experimentalEnvFunc?: (name: string) => Promise<string | undefined>;
|
||||
|
||||
/**
|
||||
* An optional remote config
|
||||
*/
|
||||
remote?: LoadConfigOptionsRemote;
|
||||
|
||||
/**
|
||||
* An optional configuration that enables watching of config files.
|
||||
*/
|
||||
watch?: {
|
||||
/**
|
||||
* A listener that is called when a config file is changed.
|
||||
*/
|
||||
onChange: (configs: AppConfig[]) => void;
|
||||
|
||||
/**
|
||||
* An optional signal that stops the watcher once the promise resolves.
|
||||
*/
|
||||
stopSignal?: Promise<void>;
|
||||
};
|
||||
watch?: LoadConfigOptionsWatch;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -73,12 +96,32 @@ export type LoadConfigOptions = {
|
||||
export async function loadConfig(
|
||||
options: LoadConfigOptions,
|
||||
): Promise<AppConfig[]> {
|
||||
const { configRoot, experimentalEnvFunc: envFunc, watch } = options;
|
||||
const configPaths = options.configPaths.slice();
|
||||
const { configRoot, experimentalEnvFunc: envFunc, watch, remote } = options;
|
||||
|
||||
const configPaths: string[] = options.configTargets
|
||||
.slice()
|
||||
.filter((e): e is { path: string } => e.hasOwnProperty('path'))
|
||||
.map(configTarget => configTarget.path);
|
||||
|
||||
// Append deprecated configPaths to the absolute config paths received via configTargets.
|
||||
options.configPaths.forEach(cp => {
|
||||
if (!configPaths.includes(cp)) {
|
||||
configPaths.push(cp);
|
||||
}
|
||||
});
|
||||
|
||||
const configUrls: string[] = options.configTargets
|
||||
.slice()
|
||||
.filter((e): e is { url: string } => e.hasOwnProperty('url'))
|
||||
.map(configTarget => configTarget.url);
|
||||
|
||||
if (remote === undefined && configUrls.length > 0) {
|
||||
throw new Error(`Remote config detected but this feature is turned off`);
|
||||
}
|
||||
|
||||
// If no paths are provided, we default to reading
|
||||
// `app-config.yaml` and, if it exists, `app-config.local.yaml`
|
||||
if (configPaths.length === 0) {
|
||||
if (configPaths.length === 0 && configUrls.length === 0) {
|
||||
configPaths.push(resolvePath(configRoot, 'app-config.yaml'));
|
||||
|
||||
const localConfig = resolvePath(configRoot, 'app-config.local.yaml');
|
||||
@@ -114,22 +157,67 @@ export async function loadConfig(
|
||||
return configs;
|
||||
};
|
||||
|
||||
let fileConfigs;
|
||||
const loadRemoteConfigFiles = async () => {
|
||||
const configs: AppConfig[] = [];
|
||||
|
||||
const readConfigFromUrl = async (url: string) => {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Could not read config file at ${url}`);
|
||||
}
|
||||
|
||||
return await response.text();
|
||||
};
|
||||
|
||||
for (let i = 0; i < configUrls.length; i++) {
|
||||
const configUrl = configUrls[i];
|
||||
if (!isValidUrl(configUrl)) {
|
||||
throw new Error(`Config load path is not valid: '${configUrl}'`);
|
||||
}
|
||||
|
||||
const remoteConfigContent = await readConfigFromUrl(configUrl);
|
||||
if (!remoteConfigContent) {
|
||||
throw new Error(`Config is not valid`);
|
||||
}
|
||||
const configYaml = yaml.parse(remoteConfigContent);
|
||||
const substitutionTransform = createSubstitutionTransform(env);
|
||||
const data = await applyConfigTransforms(configRoot, configYaml, [
|
||||
substitutionTransform,
|
||||
]);
|
||||
|
||||
configs.push({ data, context: configUrl });
|
||||
}
|
||||
|
||||
return configs;
|
||||
};
|
||||
|
||||
let fileConfigs: AppConfig[];
|
||||
try {
|
||||
fileConfigs = await loadConfigFiles();
|
||||
} catch (error) {
|
||||
throw new ForwardedError('Failed to read static configuration file', error);
|
||||
}
|
||||
|
||||
let remoteConfigs: AppConfig[] = [];
|
||||
if (remote) {
|
||||
try {
|
||||
remoteConfigs = await loadRemoteConfigFiles();
|
||||
} catch (error) {
|
||||
throw new ForwardedError(
|
||||
`Failed to read remote configuration file`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const envConfigs = await readEnvConfig(process.env);
|
||||
|
||||
// Set up config file watching if requested by the caller
|
||||
if (watch) {
|
||||
let currentSerializedConfig = JSON.stringify(fileConfigs);
|
||||
|
||||
const watchConfigFile = (watchProp: LoadConfigOptionsWatch) => {
|
||||
const watcher = chokidar.watch(configPaths, {
|
||||
usePolling: process.env.NODE_ENV === 'test',
|
||||
});
|
||||
|
||||
let currentSerializedConfig = JSON.stringify(fileConfigs);
|
||||
watcher.on('change', async () => {
|
||||
try {
|
||||
const newConfigs = await loadConfigFiles();
|
||||
@@ -140,18 +228,69 @@ export async function loadConfig(
|
||||
}
|
||||
currentSerializedConfig = newSerializedConfig;
|
||||
|
||||
watch.onChange([...newConfigs, ...envConfigs]);
|
||||
watchProp.onChange([...remoteConfigs, ...newConfigs, ...envConfigs]);
|
||||
} catch (error) {
|
||||
console.error(`Failed to reload configuration files, ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
if (watch.stopSignal) {
|
||||
watch.stopSignal.then(() => {
|
||||
if (watchProp.stopSignal) {
|
||||
watchProp.stopSignal.then(() => {
|
||||
watcher.close();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const watchRemoteConfig = (
|
||||
watchProp: LoadConfigOptionsWatch,
|
||||
remoteProp: LoadConfigOptionsRemote,
|
||||
) => {
|
||||
const hasConfigChanged = async (
|
||||
oldRemoteConfigs: AppConfig[],
|
||||
newRemoteConfigs: AppConfig[],
|
||||
) => {
|
||||
return (
|
||||
JSON.stringify(oldRemoteConfigs) !== JSON.stringify(newRemoteConfigs)
|
||||
);
|
||||
};
|
||||
|
||||
let handle: NodeJS.Timeout | undefined;
|
||||
try {
|
||||
handle = setInterval(async () => {
|
||||
console.info(`Checking for config update`);
|
||||
const newRemoteConfigs = await loadRemoteConfigFiles();
|
||||
if (await hasConfigChanged(remoteConfigs, newRemoteConfigs)) {
|
||||
remoteConfigs = newRemoteConfigs;
|
||||
console.info(`Remote config change, reloading config ...`);
|
||||
watchProp.onChange([...remoteConfigs, ...fileConfigs, ...envConfigs]);
|
||||
console.info(`Remote config reloaded`);
|
||||
}
|
||||
}, remoteProp.reloadIntervalSeconds * 1000);
|
||||
} catch (error) {
|
||||
console.error(`Failed to reload configuration files, ${error}`);
|
||||
}
|
||||
|
||||
if (watchProp.stopSignal) {
|
||||
watchProp.stopSignal.then(() => {
|
||||
if (handle !== undefined) {
|
||||
console.info(`Stopping remote config watch`);
|
||||
clearInterval(handle);
|
||||
handle = undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Set up config file watching if requested by the caller
|
||||
if (watch) {
|
||||
watchConfigFile(watch);
|
||||
}
|
||||
|
||||
return [...fileConfigs, ...envConfigs];
|
||||
if (watch && remote) {
|
||||
watchRemoteConfig(watch, remote);
|
||||
}
|
||||
|
||||
return remote
|
||||
? [...remoteConfigs, ...fileConfigs, ...envConfigs]
|
||||
: [...fileConfigs, ...envConfigs];
|
||||
}
|
||||
|
||||
@@ -80,7 +80,8 @@ const useStyles = makeStyles(
|
||||
backgroundColor: theme.palette.banner.error,
|
||||
},
|
||||
warning: {
|
||||
backgroundColor: theme.palette.banner.warning,
|
||||
backgroundColor:
|
||||
theme.palette.banner.warning ?? theme.palette.banner.error,
|
||||
},
|
||||
}),
|
||||
{ name: 'BackstageDismissableBanner' },
|
||||
|
||||
@@ -306,7 +306,7 @@ export class AwsS3Publish implements PublisherBase {
|
||||
? entityTriplet
|
||||
: lowerCaseEntityTriplet(entityTriplet);
|
||||
|
||||
const entityRootDir = path.join(this.bucketRootPath, entityDir);
|
||||
const entityRootDir = path.posix.join(this.bucketRootPath, entityDir);
|
||||
|
||||
const stream = this.storageClient
|
||||
.getObject({
|
||||
@@ -357,7 +357,7 @@ export class AwsS3Publish implements PublisherBase {
|
||||
: lowerCaseEntityTripletInStoragePath(decodedUriNoRoot);
|
||||
|
||||
// Re-prepend the root path to the relative file path
|
||||
const filePath = path.join(this.bucketRootPath, filePathNoRoot);
|
||||
const filePath = path.posix.join(this.bucketRootPath, filePathNoRoot);
|
||||
|
||||
// Files with different extensions (CSS, HTML) need to be served with different headers
|
||||
const fileExtension = path.extname(filePath);
|
||||
@@ -396,7 +396,7 @@ export class AwsS3Publish implements PublisherBase {
|
||||
? entityTriplet
|
||||
: lowerCaseEntityTriplet(entityTriplet);
|
||||
|
||||
const entityRootDir = path.join(this.bucketRootPath, entityDir);
|
||||
const entityRootDir = path.posix.join(this.bucketRootPath, entityDir);
|
||||
|
||||
await this.storageClient
|
||||
.headObject({
|
||||
|
||||
@@ -231,7 +231,7 @@ export class GoogleGCSPublish implements PublisherBase {
|
||||
? entityTriplet
|
||||
: lowerCaseEntityTriplet(entityTriplet);
|
||||
|
||||
const entityRootDir = path.join(this.bucketRootPath, entityDir);
|
||||
const entityRootDir = path.posix.join(this.bucketRootPath, entityDir);
|
||||
|
||||
const fileStreamChunks: Array<any> = [];
|
||||
this.storageClient
|
||||
@@ -270,7 +270,7 @@ export class GoogleGCSPublish implements PublisherBase {
|
||||
: lowerCaseEntityTripletInStoragePath(decodedUriNoRoot);
|
||||
|
||||
// Re-prepend the root path to the relative file path
|
||||
const filePath = path.join(this.bucketRootPath, filePathNoRoot);
|
||||
const filePath = path.posix.join(this.bucketRootPath, filePathNoRoot);
|
||||
|
||||
// Files with different extensions (CSS, HTML) need to be served with different headers
|
||||
const fileExtension = path.extname(filePath);
|
||||
@@ -310,7 +310,7 @@ export class GoogleGCSPublish implements PublisherBase {
|
||||
? entityTriplet
|
||||
: lowerCaseEntityTriplet(entityTriplet);
|
||||
|
||||
const entityRootDir = path.join(this.bucketRootPath, entityDir);
|
||||
const entityRootDir = path.posix.join(this.bucketRootPath, entityDir);
|
||||
|
||||
this.storageClient
|
||||
.bucket(this.bucketName)
|
||||
|
||||
@@ -64,7 +64,7 @@ export type BackstagePaletteAdditions = {
|
||||
error: string;
|
||||
text: string;
|
||||
link: string;
|
||||
warning: string;
|
||||
warning?: string;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ export type BackstagePaletteAdditions = {
|
||||
error: string;
|
||||
text: string;
|
||||
link: string;
|
||||
warning: string;
|
||||
warning?: string;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"@backstage/errors": "^0.1.3",
|
||||
"@backstage/plugin-catalog-react": "^0.6.1",
|
||||
"@backstage/theme": "^0.2.12",
|
||||
"@date-io/luxon": "2.x",
|
||||
"@date-io/luxon": "1.x",
|
||||
"@material-ui/core": "^4.12.2",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.57",
|
||||
|
||||
Vendored
-4
@@ -17,7 +17,6 @@ export interface Config {
|
||||
jenkins?: {
|
||||
/**
|
||||
* Default instance baseUrl, can be specified on a named instance called "default"
|
||||
* @pattern "^https?://"
|
||||
*/
|
||||
baseUrl?: string;
|
||||
/**
|
||||
@@ -39,9 +38,6 @@ export interface Config {
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* @pattern "^https?://"
|
||||
*/
|
||||
baseUrl: string;
|
||||
username: string;
|
||||
/** @visibility secret */
|
||||
|
||||
@@ -10,9 +10,7 @@ import { ScmIntegrations } from '@backstage/integration';
|
||||
import { TemplateAction } from '@backstage/plugin-scaffolder-backend';
|
||||
import { UrlReader } from '@backstage/backend-common';
|
||||
|
||||
// Warning: (ae-missing-release-tag) "createFetchCookiecutterAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export function createFetchCookiecutterAction(options: {
|
||||
reader: UrlReader;
|
||||
integrations: ScmIntegrations;
|
||||
|
||||
@@ -121,6 +121,15 @@ export class CookiecutterRunner {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a `fetch:cookiecutter` Scaffolder action.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* See {@link https://cookiecutter.readthedocs.io/} and {@link https://backstage.io/docs/features/software-templates/writing-custom-actions}.
|
||||
* @param options - Templating configuration.
|
||||
* @public
|
||||
*/
|
||||
export function createFetchCookiecutterAction(options: {
|
||||
reader: UrlReader;
|
||||
integrations: ScmIntegrations;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* A module for the scaffolder backend that lets you template projects using cookiecutter
|
||||
* A module for the scaffolder backend that lets you template projects using {@link https://cookiecutter.readthedocs.io/ | cookiecutter}.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
@@ -8,9 +8,7 @@ import { ScmIntegrations } from '@backstage/integration';
|
||||
import { TemplateAction } from '@backstage/plugin-scaffolder-backend';
|
||||
import { UrlReader } from '@backstage/backend-common';
|
||||
|
||||
// Warning: (ae-missing-release-tag) "createFetchRailsAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export function createFetchRailsAction(options: {
|
||||
reader: UrlReader;
|
||||
integrations: ScmIntegrations;
|
||||
|
||||
@@ -27,6 +27,16 @@ import {
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import { RailsNewRunner } from './railsNewRunner';
|
||||
|
||||
/**
|
||||
* Creates the `fetch:rails` Scaffolder action.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* See {@link https://guides.rubyonrails.org/rails_application_templates.html} and {@link https://backstage.io/docs/features/software-templates/writing-custom-actions}.
|
||||
*
|
||||
* @param options - Configuration of the templater.
|
||||
* @public
|
||||
*/
|
||||
export function createFetchRailsAction(options: {
|
||||
reader: UrlReader;
|
||||
integrations: ScmIntegrations;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* A module for the scaffolder backend that lets you template projects using Rails
|
||||
* A module for the scaffolder backend that lets you template projects using {@link https://guides.rubyonrails.org/rails_application_templates.html | Rails}.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
@@ -5,10 +5,6 @@
|
||||
```ts
|
||||
import { TemplateAction } from '@backstage/plugin-scaffolder-backend';
|
||||
|
||||
// Warning: (ae-missing-release-tag) "createRunYeomanAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export function createRunYeomanAction(): TemplateAction<any>;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
|
||||
@@ -18,6 +18,15 @@ import { JsonObject } from '@backstage/types';
|
||||
import { createTemplateAction } from '@backstage/plugin-scaffolder-backend';
|
||||
import { yeomanRun } from './yeomanRun';
|
||||
|
||||
/**
|
||||
* Creates a `run:yeoman` Scaffolder action.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* See {@link https://yeoman.io/} and {@link https://backstage.io/docs/features/software-templates/writing-custom-actions}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function createRunYeomanAction() {
|
||||
return createTemplateAction<{
|
||||
namespace: string;
|
||||
|
||||
@@ -13,4 +13,11 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A module for the scaffolder backend that lets you template projects using
|
||||
* {@link https://yeoman.io/ | Yeoman}.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
export * from './actions';
|
||||
|
||||
@@ -29,24 +29,25 @@ jest.doMock('fs-extra', () => ({
|
||||
}));
|
||||
|
||||
import {
|
||||
DatabaseManager,
|
||||
DockerContainerRunner,
|
||||
getVoidLogger,
|
||||
PluginDatabaseManager,
|
||||
DatabaseManager,
|
||||
UrlReaders,
|
||||
DockerContainerRunner,
|
||||
} from '@backstage/backend-common';
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
import { TemplateEntityV1beta2 } from '@backstage/catalog-model';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import express from 'express';
|
||||
import request from 'supertest';
|
||||
import { TemplateEntityV1beta2 } from '@backstage/catalog-model';
|
||||
/**
|
||||
* TODO: The following should import directly from the router file.
|
||||
* Due to a circular dependency between this plugin and the
|
||||
* plugin-scaffolder-backend-module-cookiecutter plugin, it results in an error:
|
||||
* TypeError: _pluginscaffolderbackend.createTemplateAction is not a function
|
||||
*/
|
||||
import { createRouter } from '../index';
|
||||
import { createRouter, DatabaseTaskStore, TaskBroker } from '../index';
|
||||
import { StorageTaskBroker } from '../scaffolder/tasks/StorageTaskBroker';
|
||||
|
||||
const createCatalogClient = (templates: any[] = []) =>
|
||||
({
|
||||
@@ -73,6 +74,7 @@ const mockUrlReader = UrlReaders.default({
|
||||
|
||||
describe('createRouter', () => {
|
||||
let app: express.Express;
|
||||
let taskBroker: TaskBroker;
|
||||
const template: TemplateEntityV1beta2 = {
|
||||
apiVersion: 'backstage.io/v1beta2',
|
||||
kind: 'Template',
|
||||
@@ -102,7 +104,17 @@ describe('createRouter', () => {
|
||||
},
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
beforeEach(async () => {
|
||||
const logger = getVoidLogger();
|
||||
const databaseTaskStore = await DatabaseTaskStore.create({
|
||||
database: await createDatabase().getClient(),
|
||||
});
|
||||
taskBroker = new StorageTaskBroker(databaseTaskStore, logger);
|
||||
|
||||
jest.spyOn(taskBroker, 'dispatch');
|
||||
jest.spyOn(taskBroker, 'get');
|
||||
jest.spyOn(taskBroker, 'observe');
|
||||
|
||||
const router = await createRouter({
|
||||
logger: getVoidLogger(),
|
||||
config: new ConfigReader({}),
|
||||
@@ -110,11 +122,12 @@ describe('createRouter', () => {
|
||||
catalogClient: createCatalogClient([template]),
|
||||
containerRunner: new DockerContainerRunner({} as any),
|
||||
reader: mockUrlReader,
|
||||
taskBroker,
|
||||
});
|
||||
app = express().use(router);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
@@ -142,6 +155,12 @@ describe('createRouter', () => {
|
||||
});
|
||||
|
||||
it('return the template id', async () => {
|
||||
(
|
||||
taskBroker.dispatch as jest.Mocked<TaskBroker>['dispatch']
|
||||
).mockResolvedValue({
|
||||
taskId: 'a-random-id',
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.post('/v2/tasks')
|
||||
.send({
|
||||
@@ -151,28 +170,241 @@ describe('createRouter', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.body.id).toBeDefined();
|
||||
expect(response.body.id).toBe('a-random-id');
|
||||
expect(response.status).toEqual(201);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /v2/tasks/:taskId', () => {
|
||||
it('does not divulge secrets', async () => {
|
||||
const postResponse = await request(app)
|
||||
.post('/v2/tasks')
|
||||
.set('Authorization', 'Bearer secret')
|
||||
.send({
|
||||
templateName: 'create-react-app-template',
|
||||
values: {
|
||||
required: 'required-value',
|
||||
},
|
||||
});
|
||||
(taskBroker.get as jest.Mocked<TaskBroker>['get']).mockResolvedValue({
|
||||
id: 'a-random-id',
|
||||
spec: {} as any,
|
||||
status: 'completed',
|
||||
createdAt: '',
|
||||
secrets: { token: 'secret' },
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.get(`/v2/tasks/${postResponse.body.id}`)
|
||||
.send();
|
||||
const response = await request(app).get(`/v2/tasks/a-random-id`);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body.status).toBe('completed');
|
||||
expect(response.body.secrets).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /v2/tasks/:taskId/eventstream', () => {
|
||||
it('should return log messages', async () => {
|
||||
const unsubscribe = jest.fn();
|
||||
(
|
||||
taskBroker.observe as jest.Mocked<TaskBroker>['observe']
|
||||
).mockImplementation(({ taskId }, callback) => {
|
||||
// emit after this function returned
|
||||
setImmediate(() => {
|
||||
callback(undefined, {
|
||||
events: [
|
||||
{
|
||||
id: 0,
|
||||
taskId,
|
||||
type: 'log',
|
||||
createdAt: '',
|
||||
body: { message: 'My log message' },
|
||||
},
|
||||
],
|
||||
});
|
||||
callback(undefined, {
|
||||
events: [
|
||||
{
|
||||
id: 1,
|
||||
taskId,
|
||||
type: 'completion',
|
||||
createdAt: '',
|
||||
body: { message: 'Finished!' },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
return { unsubscribe };
|
||||
});
|
||||
|
||||
let statusCode: any = undefined;
|
||||
let headers: any = {};
|
||||
const responseDataFn = jest.fn();
|
||||
|
||||
const req = request(app)
|
||||
.get('/v2/tasks/a-random-id/eventstream')
|
||||
.set('accept', 'text/event-stream')
|
||||
.parse((res, _) => {
|
||||
({ statusCode, headers } = res as any);
|
||||
|
||||
res.on('data', chunk => {
|
||||
responseDataFn(chunk.toString());
|
||||
|
||||
// the server expects the client to abort the request
|
||||
if (chunk.includes('completion')) {
|
||||
req.abort();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// wait for the request to finish
|
||||
await req.catch(() => {
|
||||
// ignore 'aborted' error
|
||||
});
|
||||
|
||||
expect(statusCode).toBe(200);
|
||||
expect(headers['content-type']).toBe('text/event-stream');
|
||||
expect(responseDataFn).toBeCalledTimes(2);
|
||||
expect(responseDataFn).toBeCalledWith(`event: log
|
||||
data: {"id":0,"taskId":"a-random-id","type":"log","createdAt":"","body":{"message":"My log message"}}
|
||||
|
||||
`);
|
||||
expect(responseDataFn).toBeCalledWith(`event: completion
|
||||
data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{"message":"Finished!"}}
|
||||
|
||||
`);
|
||||
|
||||
expect(taskBroker.observe).toBeCalledTimes(1);
|
||||
expect(taskBroker.observe).toBeCalledWith(
|
||||
{ taskId: 'a-random-id' },
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
expect(unsubscribe).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should return log messages with after query', async () => {
|
||||
const unsubscribe = jest.fn();
|
||||
(
|
||||
taskBroker.observe as jest.Mocked<TaskBroker>['observe']
|
||||
).mockImplementation(({ taskId }, callback) => {
|
||||
setImmediate(() => {
|
||||
callback(undefined, {
|
||||
events: [
|
||||
{
|
||||
id: 1,
|
||||
taskId,
|
||||
type: 'completion',
|
||||
createdAt: '',
|
||||
body: { message: 'Finished!' },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
return { unsubscribe };
|
||||
});
|
||||
|
||||
let statusCode: any = undefined;
|
||||
let headers: any = {};
|
||||
|
||||
const req = request(app)
|
||||
.get('/v2/tasks/a-random-id/eventstream')
|
||||
.query({ after: 10 })
|
||||
.set('accept', 'text/event-stream')
|
||||
.parse((res, _) => {
|
||||
({ statusCode, headers } = res as any);
|
||||
|
||||
res.on('data', () => {
|
||||
// close immediately
|
||||
req.abort();
|
||||
});
|
||||
});
|
||||
|
||||
// wait for the request to finish
|
||||
await req.catch(() => {
|
||||
// ignore 'aborted' error
|
||||
});
|
||||
|
||||
expect(statusCode).toBe(200);
|
||||
expect(headers['content-type']).toBe('text/event-stream');
|
||||
|
||||
expect(taskBroker.observe).toBeCalledTimes(1);
|
||||
expect(taskBroker.observe).toBeCalledWith(
|
||||
{ taskId: 'a-random-id', after: 10 },
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
expect(unsubscribe).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /v2/tasks/:taskId/events', () => {
|
||||
it('should return log messages', async () => {
|
||||
const unsubscribe = jest.fn();
|
||||
(
|
||||
taskBroker.observe as jest.Mocked<TaskBroker>['observe']
|
||||
).mockImplementation(({ taskId }, callback) => {
|
||||
callback(undefined, {
|
||||
events: [
|
||||
{
|
||||
id: 0,
|
||||
taskId,
|
||||
type: 'log',
|
||||
createdAt: '',
|
||||
body: { message: 'My log message' },
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
taskId,
|
||||
type: 'completion',
|
||||
createdAt: '',
|
||||
body: { message: 'Finished!' },
|
||||
},
|
||||
],
|
||||
});
|
||||
return { unsubscribe };
|
||||
});
|
||||
|
||||
const response = await request(app).get('/v2/tasks/a-random-id/events');
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual([
|
||||
{
|
||||
id: 0,
|
||||
taskId: 'a-random-id',
|
||||
type: 'log',
|
||||
createdAt: '',
|
||||
body: { message: 'My log message' },
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
taskId: 'a-random-id',
|
||||
type: 'completion',
|
||||
createdAt: '',
|
||||
body: { message: 'Finished!' },
|
||||
},
|
||||
]);
|
||||
|
||||
expect(taskBroker.observe).toBeCalledTimes(1);
|
||||
expect(taskBroker.observe).toBeCalledWith(
|
||||
{ taskId: 'a-random-id' },
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(unsubscribe).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should return log messages with after query', async () => {
|
||||
const unsubscribe = jest.fn();
|
||||
(
|
||||
taskBroker.observe as jest.Mocked<TaskBroker>['observe']
|
||||
).mockImplementation((_, callback) => {
|
||||
callback(undefined, { events: [] });
|
||||
return { unsubscribe };
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.get('/v2/tasks/a-random-id/events')
|
||||
.query({ after: 10 });
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual([]);
|
||||
|
||||
expect(taskBroker.observe).toBeCalledTimes(1);
|
||||
expect(taskBroker.observe).toBeCalledWith(
|
||||
{ taskId: 'a-random-id', after: 10 },
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(unsubscribe).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,33 +14,33 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import express from 'express';
|
||||
import Router from 'express-promise-router';
|
||||
import { Logger } from 'winston';
|
||||
import { CatalogEntityClient } from '../lib/catalog';
|
||||
import { validate } from 'jsonschema';
|
||||
import {
|
||||
DatabaseTaskStore,
|
||||
TemplateActionRegistry,
|
||||
TaskWorker,
|
||||
TemplateAction,
|
||||
createBuiltinActions,
|
||||
} from '../scaffolder';
|
||||
import { StorageTaskBroker } from '../scaffolder/tasks/StorageTaskBroker';
|
||||
import { getEntityBaseUrl, getWorkingDirectory } from './helpers';
|
||||
import {
|
||||
ContainerRunner,
|
||||
PluginDatabaseManager,
|
||||
UrlReader,
|
||||
} from '@backstage/backend-common';
|
||||
import { InputError, NotFoundError } from '@backstage/errors';
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
import { TemplateEntityV1beta2, Entity } from '@backstage/catalog-model';
|
||||
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
|
||||
|
||||
import { Entity, TemplateEntityV1beta2 } from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import { InputError, NotFoundError } from '@backstage/errors';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { TaskBroker, TaskSpec } from '../scaffolder/tasks/types';
|
||||
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
|
||||
import express from 'express';
|
||||
import Router from 'express-promise-router';
|
||||
import { validate } from 'jsonschema';
|
||||
import { Logger } from 'winston';
|
||||
import { CatalogEntityClient } from '../lib/catalog';
|
||||
import {
|
||||
createBuiltinActions,
|
||||
DatabaseTaskStore,
|
||||
TaskBroker,
|
||||
TaskSpec,
|
||||
TaskWorker,
|
||||
TemplateAction,
|
||||
TemplateActionRegistry,
|
||||
} from '../scaffolder';
|
||||
import { StorageTaskBroker } from '../scaffolder/tasks/StorageTaskBroker';
|
||||
import { getEntityBaseUrl, getWorkingDirectory } from './helpers';
|
||||
|
||||
/**
|
||||
* RouterOptions
|
||||
@@ -247,7 +247,9 @@ export async function createRouter(
|
||||
})
|
||||
.get('/v2/tasks/:taskId/eventstream', async (req, res) => {
|
||||
const { taskId } = req.params;
|
||||
const after = Number(req.query.after) || undefined;
|
||||
const after =
|
||||
req.query.after !== undefined ? Number(req.query.after) : undefined;
|
||||
|
||||
logger.debug(`Event stream observing taskId '${taskId}' opened`);
|
||||
|
||||
// Mandatory headers and http status to keep connection open
|
||||
@@ -278,7 +280,8 @@ export async function createRouter(
|
||||
// to automatically reconnect because it lost connection.
|
||||
}
|
||||
}
|
||||
res.flush();
|
||||
// res.flush() is only available with the compression middleware
|
||||
res.flush?.();
|
||||
if (shouldUnsubscribe) unsubscribe();
|
||||
},
|
||||
);
|
||||
@@ -288,6 +291,43 @@ export async function createRouter(
|
||||
unsubscribe();
|
||||
logger.debug(`Event stream observing taskId '${taskId}' closed`);
|
||||
});
|
||||
})
|
||||
.get('/v2/tasks/:taskId/events', async (req, res) => {
|
||||
const { taskId } = req.params;
|
||||
const after = Number(req.query.after) || undefined;
|
||||
|
||||
let unsubscribe = () => {};
|
||||
|
||||
// cancel the request after 30 seconds. this aligns with the recommendations of RFC 6202.
|
||||
const timeout = setTimeout(() => {
|
||||
unsubscribe();
|
||||
res.json([]);
|
||||
}, 30_000);
|
||||
|
||||
// Get all known events after an id (always includes the completion event) and return the first callback
|
||||
({ unsubscribe } = taskBroker.observe(
|
||||
{ taskId, after },
|
||||
(error, { events }) => {
|
||||
// stop the timeout
|
||||
clearTimeout(timeout);
|
||||
unsubscribe();
|
||||
|
||||
if (error) {
|
||||
logger.error(
|
||||
`Received error from log when observing taskId '${taskId}', ${error}`,
|
||||
);
|
||||
}
|
||||
|
||||
res.json(events);
|
||||
},
|
||||
));
|
||||
|
||||
// When client closes connection we update the clients list
|
||||
// avoiding the disconnected one
|
||||
req.on('close', () => {
|
||||
unsubscribe();
|
||||
clearTimeout(timeout);
|
||||
});
|
||||
});
|
||||
|
||||
const app = express();
|
||||
|
||||
@@ -178,6 +178,7 @@ export class ScaffolderClient implements ScaffolderApi {
|
||||
discoveryApi: DiscoveryApi;
|
||||
identityApi: IdentityApi;
|
||||
scmIntegrationsApi: ScmIntegrationRegistry;
|
||||
useLongPollingLogs?: boolean;
|
||||
});
|
||||
// (undocumented)
|
||||
getIntegrationsList(options: { allowedHosts: string[] }): Promise<
|
||||
@@ -199,13 +200,7 @@ export class ScaffolderClient implements ScaffolderApi {
|
||||
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
scaffold(templateName: string, values: Record<string, any>): Promise<string>;
|
||||
// (undocumented)
|
||||
streamLogs({
|
||||
taskId,
|
||||
after,
|
||||
}: {
|
||||
taskId: string;
|
||||
after?: number;
|
||||
}): Observable<LogEvent>;
|
||||
streamLogs(opts: { taskId: string; after?: number }): Observable<LogEvent>;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "ScaffolderFieldExtensions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
"json-schema": "^0.3.0",
|
||||
"lodash": "^4.17.21",
|
||||
"luxon": "^2.0.2",
|
||||
"qs": "^6.9.4",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-lazylog": "^4.5.2",
|
||||
@@ -77,6 +78,7 @@
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/node": "^14.14.32",
|
||||
"cross-fetch": "^3.0.6",
|
||||
"event-source-polyfill": "^1.0.25",
|
||||
"msw": "^0.35.0"
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -14,12 +14,24 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { ScaffolderClient } from './api';
|
||||
import { ConfigReader } from '@backstage/core-app-api';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { setupRequestMockHandlers } from '@backstage/test-utils';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { ScaffolderClient } from './api';
|
||||
|
||||
const MockedEventSource = global.EventSource as jest.MockedClass<
|
||||
typeof EventSource
|
||||
>;
|
||||
|
||||
const server = setupServer();
|
||||
|
||||
describe('api', () => {
|
||||
const discoveryApi = {} as any;
|
||||
setupRequestMockHandlers(server);
|
||||
const mockBaseUrl = 'http://backstage/api';
|
||||
|
||||
const discoveryApi = { getBaseUrl: async () => mockBaseUrl };
|
||||
const identityApi = {} as any;
|
||||
const scmIntegrationsApi = ScmIntegrations.fromConfig(
|
||||
new ConfigReader({
|
||||
@@ -32,10 +44,14 @@ describe('api', () => {
|
||||
},
|
||||
}),
|
||||
);
|
||||
const apiClient = new ScaffolderClient({
|
||||
scmIntegrationsApi,
|
||||
discoveryApi,
|
||||
identityApi,
|
||||
|
||||
let apiClient: ScaffolderClient;
|
||||
beforeEach(() => {
|
||||
apiClient = new ScaffolderClient({
|
||||
scmIntegrationsApi,
|
||||
discoveryApi,
|
||||
identityApi,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return default and custom integrations', async () => {
|
||||
@@ -51,4 +67,240 @@ describe('api', () => {
|
||||
expect(allowedHosts).toContain(integration.host),
|
||||
);
|
||||
});
|
||||
|
||||
describe('streamEvents', () => {
|
||||
describe('eventsource', () => {
|
||||
it('should work', async () => {
|
||||
MockedEventSource.prototype.addEventListener.mockImplementation(
|
||||
(type, fn) => {
|
||||
if (typeof fn !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'log') {
|
||||
fn({
|
||||
data: '{"id":1,"taskId":"a-random-id","type":"log","createdAt":"","body":{"message":"My log message"}}',
|
||||
} as any);
|
||||
} else if (type === 'completion') {
|
||||
fn({
|
||||
data: '{"id":2,"taskId":"a-random-id","type":"completion","createdAt":"","body":{"message":"Finished!"}}',
|
||||
} as any);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const next = jest.fn();
|
||||
|
||||
await new Promise<void>(complete => {
|
||||
apiClient
|
||||
.streamLogs({ taskId: 'a-random-task-id' })
|
||||
.subscribe({ next, complete });
|
||||
});
|
||||
|
||||
expect(MockedEventSource).toBeCalledWith(
|
||||
'http://backstage/api/v2/tasks/a-random-task-id/eventstream',
|
||||
{ withCredentials: true },
|
||||
);
|
||||
expect(MockedEventSource.prototype.close).toBeCalled();
|
||||
|
||||
expect(next).toBeCalledTimes(2);
|
||||
expect(next).toBeCalledWith({
|
||||
id: 1,
|
||||
taskId: 'a-random-id',
|
||||
type: 'log',
|
||||
createdAt: '',
|
||||
body: { message: 'My log message' },
|
||||
});
|
||||
expect(next).toBeCalledWith({
|
||||
id: 2,
|
||||
taskId: 'a-random-id',
|
||||
type: 'completion',
|
||||
createdAt: '',
|
||||
body: { message: 'Finished!' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('longPolling', () => {
|
||||
beforeEach(() => {
|
||||
apiClient = new ScaffolderClient({
|
||||
scmIntegrationsApi,
|
||||
discoveryApi,
|
||||
identityApi,
|
||||
useLongPollingLogs: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should work', async () => {
|
||||
server.use(
|
||||
rest.get(
|
||||
`${mockBaseUrl}/v2/tasks/:taskId/events`,
|
||||
(req, res, ctx) => {
|
||||
const { taskId } = req.params;
|
||||
const after = req.url.searchParams.get('after');
|
||||
|
||||
if (taskId === 'a-random-task-id') {
|
||||
if (!after) {
|
||||
return res(
|
||||
ctx.json([
|
||||
{
|
||||
id: 1,
|
||||
taskId: 'a-random-id',
|
||||
type: 'log',
|
||||
createdAt: '',
|
||||
body: { message: 'My log message' },
|
||||
},
|
||||
]),
|
||||
);
|
||||
} else if (after === '1') {
|
||||
return res(
|
||||
ctx.json([
|
||||
{
|
||||
id: 2,
|
||||
taskId: 'a-random-id',
|
||||
type: 'completion',
|
||||
createdAt: '',
|
||||
body: { message: 'Finished!' },
|
||||
},
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return res(ctx.status(500));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const next = jest.fn();
|
||||
|
||||
await new Promise<void>(complete =>
|
||||
apiClient
|
||||
.streamLogs({ taskId: 'a-random-task-id' })
|
||||
.subscribe({ next, complete }),
|
||||
);
|
||||
|
||||
expect(next).toBeCalledTimes(2);
|
||||
expect(next).toBeCalledWith({
|
||||
id: 1,
|
||||
taskId: 'a-random-id',
|
||||
type: 'log',
|
||||
createdAt: '',
|
||||
body: { message: 'My log message' },
|
||||
});
|
||||
expect(next).toBeCalledWith({
|
||||
id: 2,
|
||||
taskId: 'a-random-id',
|
||||
type: 'completion',
|
||||
createdAt: '',
|
||||
body: { message: 'Finished!' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should unsubscribe', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
server.use(
|
||||
rest.get(
|
||||
`${mockBaseUrl}/v2/tasks/:taskId/events`,
|
||||
(req, res, ctx) => {
|
||||
const { taskId } = req.params;
|
||||
|
||||
const after = req.url.searchParams.get('after');
|
||||
|
||||
// use assertion to make sure it is not called after unsubscribing
|
||||
expect(after).toBe(null);
|
||||
|
||||
if (taskId === 'a-random-task-id') {
|
||||
return res(
|
||||
ctx.json([
|
||||
{
|
||||
id: 1,
|
||||
taskId: 'a-random-id',
|
||||
type: 'log',
|
||||
createdAt: '',
|
||||
body: { message: 'My log message' },
|
||||
},
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
return res(ctx.status(500));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const next = jest.fn();
|
||||
|
||||
await new Promise<void>(complete => {
|
||||
const subscription = apiClient
|
||||
.streamLogs({ taskId: 'a-random-task-id' })
|
||||
.subscribe({
|
||||
next: (...args) => {
|
||||
next(...args);
|
||||
subscription.unsubscribe();
|
||||
complete();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(next).toBeCalledTimes(1);
|
||||
expect(next).toBeCalledWith({
|
||||
id: 1,
|
||||
taskId: 'a-random-id',
|
||||
type: 'log',
|
||||
createdAt: '',
|
||||
body: { message: 'My log message' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should continue after error', async () => {
|
||||
const called = jest.fn();
|
||||
|
||||
server.use(
|
||||
rest.get(
|
||||
`${mockBaseUrl}/v2/tasks/:taskId/events`,
|
||||
(_req, res, ctx) => {
|
||||
called();
|
||||
|
||||
if (called.mock.calls.length > 1) {
|
||||
return res(
|
||||
ctx.json([
|
||||
{
|
||||
id: 2,
|
||||
taskId: 'a-random-id',
|
||||
type: 'completion',
|
||||
createdAt: '',
|
||||
body: { message: 'Finished!' },
|
||||
},
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
return res(ctx.status(500));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const next = jest.fn();
|
||||
|
||||
await new Promise<void>(complete =>
|
||||
apiClient
|
||||
.streamLogs({ taskId: 'a-random-task-id' })
|
||||
.subscribe({ next, complete }),
|
||||
);
|
||||
|
||||
expect(called).toBeCalledTimes(2);
|
||||
|
||||
expect(next).toBeCalledTimes(1);
|
||||
expect(next).toBeCalledWith({
|
||||
id: 2,
|
||||
taskId: 'a-random-id',
|
||||
type: 'completion',
|
||||
createdAt: '',
|
||||
body: { message: 'Finished!' },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,17 +15,18 @@
|
||||
*/
|
||||
|
||||
import { EntityName } from '@backstage/catalog-model';
|
||||
import { JsonObject, JsonValue, Observable } from '@backstage/types';
|
||||
import { ResponseError } from '@backstage/errors';
|
||||
import { ScmIntegrationRegistry } from '@backstage/integration';
|
||||
import { Field, FieldValidation } from '@rjsf/core';
|
||||
import ObservableImpl from 'zen-observable';
|
||||
import { ListActionsResponse, ScaffolderTask, Status } from './types';
|
||||
import {
|
||||
createApiRef,
|
||||
DiscoveryApi,
|
||||
IdentityApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { ResponseError } from '@backstage/errors';
|
||||
import { ScmIntegrationRegistry } from '@backstage/integration';
|
||||
import { JsonObject, JsonValue, Observable } from '@backstage/types';
|
||||
import { Field, FieldValidation } from '@rjsf/core';
|
||||
import qs from 'qs';
|
||||
import ObservableImpl from 'zen-observable';
|
||||
import { ListActionsResponse, ScaffolderTask, Status } from './types';
|
||||
|
||||
export const scaffolderApiRef = createApiRef<ScaffolderApi>({
|
||||
id: 'plugin.scaffolder.service',
|
||||
@@ -94,15 +95,18 @@ export class ScaffolderClient implements ScaffolderApi {
|
||||
private readonly discoveryApi: DiscoveryApi;
|
||||
private readonly identityApi: IdentityApi;
|
||||
private readonly scmIntegrationsApi: ScmIntegrationRegistry;
|
||||
private readonly useLongPollingLogs: boolean;
|
||||
|
||||
constructor(options: {
|
||||
discoveryApi: DiscoveryApi;
|
||||
identityApi: IdentityApi;
|
||||
scmIntegrationsApi: ScmIntegrationRegistry;
|
||||
useLongPollingLogs?: boolean;
|
||||
}) {
|
||||
this.discoveryApi = options.discoveryApi;
|
||||
this.identityApi = options.identityApi;
|
||||
this.scmIntegrationsApi = options.scmIntegrationsApi;
|
||||
this.useLongPollingLogs = options.useLongPollingLogs ?? false;
|
||||
}
|
||||
|
||||
async getIntegrationsList(options: { allowedHosts: string[] }) {
|
||||
@@ -189,7 +193,15 @@ export class ScaffolderClient implements ScaffolderApi {
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
streamLogs({
|
||||
streamLogs(opts: { taskId: string; after?: number }): Observable<LogEvent> {
|
||||
if (this.useLongPollingLogs) {
|
||||
return this.streamLogsPolling(opts);
|
||||
}
|
||||
|
||||
return this.streamLogsEventStream(opts);
|
||||
}
|
||||
|
||||
private streamLogsEventStream({
|
||||
taskId,
|
||||
after,
|
||||
}: {
|
||||
@@ -239,6 +251,46 @@ export class ScaffolderClient implements ScaffolderApi {
|
||||
});
|
||||
}
|
||||
|
||||
private streamLogsPolling({
|
||||
taskId,
|
||||
after: inputAfter,
|
||||
}: {
|
||||
taskId: string;
|
||||
after?: number;
|
||||
}): Observable<LogEvent> {
|
||||
let after = inputAfter;
|
||||
|
||||
return new ObservableImpl(subscriber => {
|
||||
this.discoveryApi.getBaseUrl('scaffolder').then(async baseUrl => {
|
||||
while (!subscriber.closed) {
|
||||
const url = `${baseUrl}/v2/tasks/${encodeURIComponent(
|
||||
taskId,
|
||||
)}/events?${qs.stringify({ after })}`;
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
// wait for one second to not run into an
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
continue;
|
||||
}
|
||||
|
||||
const logs = (await response.json()) as LogEvent[];
|
||||
|
||||
for (const event of logs) {
|
||||
after = Number(event.id);
|
||||
|
||||
subscriber.next(event);
|
||||
|
||||
if (event.type === 'completion') {
|
||||
subscriber.complete();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns ListActionsResponse containing all registered actions.
|
||||
*/
|
||||
|
||||
@@ -15,3 +15,7 @@
|
||||
*/
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
import 'cross-fetch/polyfill';
|
||||
|
||||
const { EventSourcePolyfill } = jest.requireMock('event-source-polyfill');
|
||||
global.EventSource = EventSourcePolyfill;
|
||||
|
||||
@@ -9,6 +9,7 @@ import { ApiRef } from '@backstage/core-plugin-api';
|
||||
import { BackstagePlugin } from '@backstage/core-plugin-api';
|
||||
import { DiscoveryApi } from '@backstage/core-plugin-api';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { IdentityApi } from '@backstage/core-plugin-api';
|
||||
import { InfoCardVariants } from '@backstage/core-components';
|
||||
import { RouteRef } from '@backstage/core-plugin-api';
|
||||
|
||||
@@ -34,7 +35,11 @@ export class MockSentryApi implements SentryApi {
|
||||
//
|
||||
// @public (undocumented)
|
||||
export class ProductionSentryApi implements SentryApi {
|
||||
constructor(discoveryApi: DiscoveryApi, organization: string);
|
||||
constructor(
|
||||
discoveryApi: DiscoveryApi,
|
||||
organization: string,
|
||||
identityApi?: IdentityApi | undefined,
|
||||
);
|
||||
// (undocumented)
|
||||
fetchIssues(
|
||||
project: string,
|
||||
|
||||
@@ -16,12 +16,13 @@
|
||||
|
||||
import { SentryIssue } from './sentry-issue';
|
||||
import { SentryApi } from './sentry-api';
|
||||
import { DiscoveryApi } from '@backstage/core-plugin-api';
|
||||
import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
|
||||
|
||||
export class ProductionSentryApi implements SentryApi {
|
||||
constructor(
|
||||
private readonly discoveryApi: DiscoveryApi,
|
||||
private readonly organization: string,
|
||||
private readonly identityApi?: IdentityApi,
|
||||
) {}
|
||||
|
||||
async fetchIssues(
|
||||
@@ -34,11 +35,13 @@ export class ProductionSentryApi implements SentryApi {
|
||||
}
|
||||
|
||||
const apiUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/sentry/api`;
|
||||
const options = await this.authOptions();
|
||||
|
||||
const queryPart = query ? `&query=${query}` : '';
|
||||
|
||||
const response = await fetch(
|
||||
`${apiUrl}/0/projects/${this.organization}/${project}/issues/?statsPeriod=${statsFor}${queryPart}`,
|
||||
options,
|
||||
);
|
||||
|
||||
if (response.status >= 400 && response.status < 600) {
|
||||
@@ -47,4 +50,16 @@ export class ProductionSentryApi implements SentryApi {
|
||||
|
||||
return (await response.json()) as SentryIssue[];
|
||||
}
|
||||
|
||||
private async authOptions() {
|
||||
if (!this.identityApi) {
|
||||
return {};
|
||||
}
|
||||
const token = await this.identityApi.getIdToken();
|
||||
return {
|
||||
headers: {
|
||||
authorization: `Bearer ${token}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
createPlugin,
|
||||
createRouteRef,
|
||||
discoveryApiRef,
|
||||
identityApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
export const rootRouteRef = createRouteRef({
|
||||
@@ -33,11 +34,16 @@ export const sentryPlugin = createPlugin({
|
||||
apis: [
|
||||
createApiFactory({
|
||||
api: sentryApiRef,
|
||||
deps: { configApi: configApiRef, discoveryApi: discoveryApiRef },
|
||||
factory: ({ configApi, discoveryApi }) =>
|
||||
deps: {
|
||||
configApi: configApiRef,
|
||||
discoveryApi: discoveryApiRef,
|
||||
identityApi: identityApiRef,
|
||||
},
|
||||
factory: ({ configApi, discoveryApi, identityApi }) =>
|
||||
new ProductionSentryApi(
|
||||
discoveryApi,
|
||||
configApi.getString('sentry.organization'),
|
||||
identityApi,
|
||||
),
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -2684,11 +2684,6 @@
|
||||
resolved "https://registry.npmjs.org/@date-io/core/-/core-2.10.7.tgz#0fe1fa0ef02c827919e23c2802a4b25589ac522d"
|
||||
integrity sha512-EG/1qDiQvd12RoNJ6H+sZcHVswC/3uMx/ySvfaJ24vB30rLjkgHggEXbgMbfgki7wMuiQ/zXI8QlmF1k3kWRGQ==
|
||||
|
||||
"@date-io/core@^2.10.11":
|
||||
version "2.10.11"
|
||||
resolved "https://registry.npmjs.org/@date-io/core/-/core-2.10.11.tgz#b1a3d57730f3eaaab54d5658be4a71727297e357"
|
||||
integrity sha512-keXQnwH0LM8wyvu+j5Z2KGK56D+eItjy7DnwuWl/oV+DM2UEYl0z5WhdPMpfswSyt/kjuPOzcVF/7u/skMLaoA==
|
||||
|
||||
"@date-io/date-fns@^1.3.13":
|
||||
version "1.3.13"
|
||||
resolved "https://registry.npmjs.org/@date-io/date-fns/-/date-fns-1.3.13.tgz#7798844041640ab393f7e21a7769a65d672f4735"
|
||||
@@ -2696,12 +2691,12 @@
|
||||
dependencies:
|
||||
"@date-io/core" "^1.3.13"
|
||||
|
||||
"@date-io/luxon@2.x":
|
||||
version "2.10.11"
|
||||
resolved "https://registry.npmjs.org/@date-io/luxon/-/luxon-2.10.11.tgz#d0981b9fdf5e5f17f8ce59265a3ac6c335565fac"
|
||||
integrity sha512-SS6SIkp0Y9GFwpQycCTUAyW3OZTW05CWI1DJu10hUzcg8SmjJfhjs7hQY3TOeW+JT6VtXGTVGwbWPUBJsNkhZg==
|
||||
"@date-io/luxon@1.x":
|
||||
version "1.3.13"
|
||||
resolved "https://registry.npmjs.org/@date-io/luxon/-/luxon-1.3.13.tgz#68f0134bb38ef486b2ed6df01981f814c633e28a"
|
||||
integrity sha512-9wUrJCNSMZJeYAiH+dbb45oGpnHeFP7TOH/Lt26If47gjFCkjvyINzWx+K5AGsnlP0Qosxc7hkF1yLi6ecutxw==
|
||||
dependencies:
|
||||
"@date-io/core" "^2.10.11"
|
||||
"@date-io/core" "^1.3.13"
|
||||
|
||||
"@discoveryjs/json-ext@^0.5.3":
|
||||
version "0.5.5"
|
||||
@@ -5449,18 +5444,18 @@
|
||||
integrity sha512-dOI13j1uHMZkRxhZuge/ugOE7Aqcg7Nxki932lDZuXyY4G8CGxkc/66PeQ8pR4PCzThHORXo7Ptvau6bh101lQ==
|
||||
|
||||
"@storybook/addon-a11y@^6.3.4":
|
||||
version "6.3.7"
|
||||
resolved "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-6.3.7.tgz#a802455f2d932eda07314e3d44a96c94bbd22b3d"
|
||||
integrity sha512-Z5Lhxm8r5CkPW9FYf6zmAk9c7IhUeUQZxKZeEWGZdOvcjQ32rtg4IYvO2SHgWNrEKBdxxFm3pMiyK3wylQLfsQ==
|
||||
version "6.3.12"
|
||||
resolved "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-6.3.12.tgz#2f930fc84fc275a4ed43a716fc09cc12caf4e110"
|
||||
integrity sha512-q1NdRHFJV6sLEEJw0hatCc5ZIthELqM/AWdrEWDyhcJNyiq7Tq4nKqQBMTQSYwHiUAmxVgw7i4oa1vM2M51/3g==
|
||||
dependencies:
|
||||
"@storybook/addons" "6.3.7"
|
||||
"@storybook/api" "6.3.7"
|
||||
"@storybook/channels" "6.3.7"
|
||||
"@storybook/client-api" "6.3.7"
|
||||
"@storybook/client-logger" "6.3.7"
|
||||
"@storybook/components" "6.3.7"
|
||||
"@storybook/core-events" "6.3.7"
|
||||
"@storybook/theming" "6.3.7"
|
||||
"@storybook/addons" "6.3.12"
|
||||
"@storybook/api" "6.3.12"
|
||||
"@storybook/channels" "6.3.12"
|
||||
"@storybook/client-api" "6.3.12"
|
||||
"@storybook/client-logger" "6.3.12"
|
||||
"@storybook/components" "6.3.12"
|
||||
"@storybook/core-events" "6.3.12"
|
||||
"@storybook/theming" "6.3.12"
|
||||
axe-core "^4.2.0"
|
||||
core-js "^3.8.2"
|
||||
global "^4.4.0"
|
||||
@@ -5531,7 +5526,7 @@
|
||||
react-syntax-highlighter "^13.5.3"
|
||||
regenerator-runtime "^0.13.7"
|
||||
|
||||
"@storybook/addons@6.3.11", "@storybook/addons@^6.1.11":
|
||||
"@storybook/addons@6.3.11":
|
||||
version "6.3.11"
|
||||
resolved "https://registry.npmjs.org/@storybook/addons/-/addons-6.3.11.tgz#4b5e88793bcce7ef823340e9010a96e35e3284cc"
|
||||
integrity sha512-2Y03lOwzWDRB/glISa/4luBMM5uyYhkIBixbZF9miIb2SCWRlNmom5NCnKsR18Wu6g7zI7os3aAMfKr24aSofQ==
|
||||
@@ -5546,6 +5541,21 @@
|
||||
global "^4.4.0"
|
||||
regenerator-runtime "^0.13.7"
|
||||
|
||||
"@storybook/addons@6.3.12", "@storybook/addons@^6.1.11":
|
||||
version "6.3.12"
|
||||
resolved "https://registry.npmjs.org/@storybook/addons/-/addons-6.3.12.tgz#8773dcc113c5086dfff722388b7b65580e43b65b"
|
||||
integrity sha512-UgoMyr7Qr0FS3ezt8u6hMEcHgyynQS9ucr5mAwZky3wpXRPFyUTmMto9r4BBUdqyUvTUj/LRKIcmLBfj+/l0Fg==
|
||||
dependencies:
|
||||
"@storybook/api" "6.3.12"
|
||||
"@storybook/channels" "6.3.12"
|
||||
"@storybook/client-logger" "6.3.12"
|
||||
"@storybook/core-events" "6.3.12"
|
||||
"@storybook/router" "6.3.12"
|
||||
"@storybook/theming" "6.3.12"
|
||||
core-js "^3.8.2"
|
||||
global "^4.4.0"
|
||||
regenerator-runtime "^0.13.7"
|
||||
|
||||
"@storybook/addons@6.3.7":
|
||||
version "6.3.7"
|
||||
resolved "https://registry.npmjs.org/@storybook/addons/-/addons-6.3.7.tgz#7c6b8d11b65f67b1884f6140437fe996dc39537a"
|
||||
@@ -5587,6 +5597,32 @@
|
||||
ts-dedent "^2.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
"@storybook/api@6.3.12":
|
||||
version "6.3.12"
|
||||
resolved "https://registry.npmjs.org/@storybook/api/-/api-6.3.12.tgz#2845c20464d5348d676d09665e8ab527825ed7b5"
|
||||
integrity sha512-LScRXUeCWEW/OP+jiooNMQICVdusv7azTmULxtm72fhkXFRiQs2CdRNTiqNg46JLLC9z95f1W+pGK66X6HiiQA==
|
||||
dependencies:
|
||||
"@reach/router" "^1.3.4"
|
||||
"@storybook/channels" "6.3.12"
|
||||
"@storybook/client-logger" "6.3.12"
|
||||
"@storybook/core-events" "6.3.12"
|
||||
"@storybook/csf" "0.0.1"
|
||||
"@storybook/router" "6.3.12"
|
||||
"@storybook/semver" "^7.3.2"
|
||||
"@storybook/theming" "6.3.12"
|
||||
"@types/reach__router" "^1.3.7"
|
||||
core-js "^3.8.2"
|
||||
fast-deep-equal "^3.1.3"
|
||||
global "^4.4.0"
|
||||
lodash "^4.17.20"
|
||||
memoizerific "^1.11.3"
|
||||
qs "^6.10.0"
|
||||
regenerator-runtime "^0.13.7"
|
||||
store2 "^2.12.0"
|
||||
telejson "^5.3.2"
|
||||
ts-dedent "^2.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
"@storybook/api@6.3.7":
|
||||
version "6.3.7"
|
||||
resolved "https://registry.npmjs.org/@storybook/api/-/api-6.3.7.tgz#88b8a51422cd0739c91bde0b1d65fb6d8a8485d0"
|
||||
@@ -5702,6 +5738,19 @@
|
||||
qs "^6.10.0"
|
||||
telejson "^5.3.2"
|
||||
|
||||
"@storybook/channel-postmessage@6.3.12":
|
||||
version "6.3.12"
|
||||
resolved "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-6.3.12.tgz#3ff9412ac0f445e3b8b44dd414e783a5a47ff7c1"
|
||||
integrity sha512-Ou/2Ga3JRTZ/4sSv7ikMgUgLTeZMsXXWLXuscz4oaYhmOqAU9CrJw0G1NitwBgK/+qC83lEFSLujHkWcoQDOKg==
|
||||
dependencies:
|
||||
"@storybook/channels" "6.3.12"
|
||||
"@storybook/client-logger" "6.3.12"
|
||||
"@storybook/core-events" "6.3.12"
|
||||
core-js "^3.8.2"
|
||||
global "^4.4.0"
|
||||
qs "^6.10.0"
|
||||
telejson "^5.3.2"
|
||||
|
||||
"@storybook/channel-postmessage@6.3.7":
|
||||
version "6.3.7"
|
||||
resolved "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-6.3.7.tgz#bd4edf84a29aa2cd4a22d26115c60194d289a840"
|
||||
@@ -5724,6 +5773,15 @@
|
||||
ts-dedent "^2.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
"@storybook/channels@6.3.12":
|
||||
version "6.3.12"
|
||||
resolved "https://registry.npmjs.org/@storybook/channels/-/channels-6.3.12.tgz#aa0d793895a8b211f0ad3459c61c1bcafd0093c7"
|
||||
integrity sha512-l4sA+g1PdUV8YCbgs47fIKREdEQAKNdQIZw0b7BfTvY9t0x5yfBywgQhYON/lIeiNGz2OlIuD+VUtqYfCtNSyw==
|
||||
dependencies:
|
||||
core-js "^3.8.2"
|
||||
ts-dedent "^2.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
"@storybook/channels@6.3.7":
|
||||
version "6.3.7"
|
||||
resolved "https://registry.npmjs.org/@storybook/channels/-/channels-6.3.7.tgz#85ed5925522b802d959810f78d37aacde7fea66e"
|
||||
@@ -5757,6 +5815,30 @@
|
||||
ts-dedent "^2.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
"@storybook/client-api@6.3.12":
|
||||
version "6.3.12"
|
||||
resolved "https://registry.npmjs.org/@storybook/client-api/-/client-api-6.3.12.tgz#a0c6d72a871d1cb02b4b98675472839061e39b5b"
|
||||
integrity sha512-xnW+lKKK2T774z+rOr9Wopt1aYTStfb86PSs9p3Fpnc2Btcftln+C3NtiHZl8Ccqft8Mz/chLGgewRui6tNI8g==
|
||||
dependencies:
|
||||
"@storybook/addons" "6.3.12"
|
||||
"@storybook/channel-postmessage" "6.3.12"
|
||||
"@storybook/channels" "6.3.12"
|
||||
"@storybook/client-logger" "6.3.12"
|
||||
"@storybook/core-events" "6.3.12"
|
||||
"@storybook/csf" "0.0.1"
|
||||
"@types/qs" "^6.9.5"
|
||||
"@types/webpack-env" "^1.16.0"
|
||||
core-js "^3.8.2"
|
||||
global "^4.4.0"
|
||||
lodash "^4.17.20"
|
||||
memoizerific "^1.11.3"
|
||||
qs "^6.10.0"
|
||||
regenerator-runtime "^0.13.7"
|
||||
stable "^0.1.8"
|
||||
store2 "^2.12.0"
|
||||
ts-dedent "^2.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
"@storybook/client-api@6.3.7":
|
||||
version "6.3.7"
|
||||
resolved "https://registry.npmjs.org/@storybook/client-api/-/client-api-6.3.7.tgz#cb1dca05467d777bd09aadbbdd1dd22ca537ce14"
|
||||
@@ -5789,6 +5871,14 @@
|
||||
core-js "^3.8.2"
|
||||
global "^4.4.0"
|
||||
|
||||
"@storybook/client-logger@6.3.12":
|
||||
version "6.3.12"
|
||||
resolved "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.3.12.tgz#6585c98923b49fcb25dbceeeb96ef2a83e28e0f4"
|
||||
integrity sha512-zNDsamZvHnuqLznDdP9dUeGgQ9TyFh4ray3t1VGO7ZqWVZ2xtVCCXjDvMnOXI2ifMpX5UsrOvshIPeE9fMBmiQ==
|
||||
dependencies:
|
||||
core-js "^3.8.2"
|
||||
global "^4.4.0"
|
||||
|
||||
"@storybook/client-logger@6.3.7":
|
||||
version "6.3.7"
|
||||
resolved "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.3.7.tgz#ff17b7494e7e9e23089b0d5c5364c371c726bdd1"
|
||||
@@ -5827,6 +5917,36 @@
|
||||
ts-dedent "^2.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
"@storybook/components@6.3.12":
|
||||
version "6.3.12"
|
||||
resolved "https://registry.npmjs.org/@storybook/components/-/components-6.3.12.tgz#0c7967c60354c84afa20dfab4753105e49b1927d"
|
||||
integrity sha512-kdQt8toUjynYAxDLrJzuG7YSNL6as1wJoyzNUaCfG06YPhvIAlKo7le9tS2mThVFN5e9nbKrW3N1V1sp6ypZXQ==
|
||||
dependencies:
|
||||
"@popperjs/core" "^2.6.0"
|
||||
"@storybook/client-logger" "6.3.12"
|
||||
"@storybook/csf" "0.0.1"
|
||||
"@storybook/theming" "6.3.12"
|
||||
"@types/color-convert" "^2.0.0"
|
||||
"@types/overlayscrollbars" "^1.12.0"
|
||||
"@types/react-syntax-highlighter" "11.0.5"
|
||||
color-convert "^2.0.1"
|
||||
core-js "^3.8.2"
|
||||
fast-deep-equal "^3.1.3"
|
||||
global "^4.4.0"
|
||||
lodash "^4.17.20"
|
||||
markdown-to-jsx "^7.1.3"
|
||||
memoizerific "^1.11.3"
|
||||
overlayscrollbars "^1.13.1"
|
||||
polished "^4.0.5"
|
||||
prop-types "^15.7.2"
|
||||
react-colorful "^5.1.2"
|
||||
react-popper-tooltip "^3.1.1"
|
||||
react-syntax-highlighter "^13.5.3"
|
||||
react-textarea-autosize "^8.3.0"
|
||||
regenerator-runtime "^0.13.7"
|
||||
ts-dedent "^2.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
"@storybook/components@6.3.7":
|
||||
version "6.3.7"
|
||||
resolved "https://registry.npmjs.org/@storybook/components/-/components-6.3.7.tgz#42b1ca6d24e388e02eab82aa9ed3365db2266ecc"
|
||||
@@ -5941,6 +6061,13 @@
|
||||
dependencies:
|
||||
core-js "^3.8.2"
|
||||
|
||||
"@storybook/core-events@6.3.12":
|
||||
version "6.3.12"
|
||||
resolved "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.3.12.tgz#73f6271d485ef2576234e578bb07705b92805290"
|
||||
integrity sha512-SXfD7xUUMazaeFkB92qOTUV8Y/RghE4SkEYe5slAdjeocSaH7Nz2WV0rqNEgChg0AQc+JUI66no8L9g0+lw4Gw==
|
||||
dependencies:
|
||||
core-js "^3.8.2"
|
||||
|
||||
"@storybook/core-events@6.3.7":
|
||||
version "6.3.7"
|
||||
resolved "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.3.7.tgz#c5bc7cae7dc295de73b6b9f671ecbe582582e9bd"
|
||||
@@ -6135,6 +6262,22 @@
|
||||
qs "^6.10.0"
|
||||
ts-dedent "^2.0.0"
|
||||
|
||||
"@storybook/router@6.3.12":
|
||||
version "6.3.12"
|
||||
resolved "https://registry.npmjs.org/@storybook/router/-/router-6.3.12.tgz#0d572ec795f588ca886f39cb9b27b94ff3683f84"
|
||||
integrity sha512-G/pNGCnrJRetCwyEZulHPT+YOcqEj/vkPVDTUfii2qgqukup6K0cjwgd7IukAURnAnnzTi1gmgFuEKUi8GE/KA==
|
||||
dependencies:
|
||||
"@reach/router" "^1.3.4"
|
||||
"@storybook/client-logger" "6.3.12"
|
||||
"@types/reach__router" "^1.3.7"
|
||||
core-js "^3.8.2"
|
||||
fast-deep-equal "^3.1.3"
|
||||
global "^4.4.0"
|
||||
lodash "^4.17.20"
|
||||
memoizerific "^1.11.3"
|
||||
qs "^6.10.0"
|
||||
ts-dedent "^2.0.0"
|
||||
|
||||
"@storybook/router@6.3.7":
|
||||
version "6.3.7"
|
||||
resolved "https://registry.npmjs.org/@storybook/router/-/router-6.3.7.tgz#1714a99a58a7b9f08b6fcfe2b678dad6ca896736"
|
||||
@@ -6193,6 +6336,24 @@
|
||||
resolve-from "^5.0.0"
|
||||
ts-dedent "^2.0.0"
|
||||
|
||||
"@storybook/theming@6.3.12":
|
||||
version "6.3.12"
|
||||
resolved "https://registry.npmjs.org/@storybook/theming/-/theming-6.3.12.tgz#5bddf9bd90a60709b5ab238ecdb7d9055dd7862e"
|
||||
integrity sha512-wOJdTEa/VFyFB2UyoqyYGaZdym6EN7RALuQOAMT6zHA282FBmKw8nL5DETHEbctpnHdcrMC/391teK4nNSrdOA==
|
||||
dependencies:
|
||||
"@emotion/core" "^10.1.1"
|
||||
"@emotion/is-prop-valid" "^0.8.6"
|
||||
"@emotion/styled" "^10.0.27"
|
||||
"@storybook/client-logger" "6.3.12"
|
||||
core-js "^3.8.2"
|
||||
deep-object-diff "^1.1.0"
|
||||
emotion-theming "^10.0.27"
|
||||
global "^4.4.0"
|
||||
memoizerific "^1.11.3"
|
||||
polished "^4.0.5"
|
||||
resolve-from "^5.0.0"
|
||||
ts-dedent "^2.0.0"
|
||||
|
||||
"@storybook/theming@6.3.7":
|
||||
version "6.3.7"
|
||||
resolved "https://registry.npmjs.org/@storybook/theming/-/theming-6.3.7.tgz#6daf9a21b26ed607f3c28a82acd90c0248e76d8b"
|
||||
@@ -14211,6 +14372,11 @@ fecha@^4.2.0:
|
||||
resolved "https://registry.npmjs.org/fecha/-/fecha-4.2.0.tgz#3ffb6395453e3f3efff850404f0a59b6747f5f41"
|
||||
integrity sha512-aN3pcx/DSmtyoovUudctc8+6Hl4T+hI9GBBHLjA76jdZl7+b1sgh5g4k+u/GL3dTy1/pnYzKp69FpJ0OicE3Wg==
|
||||
|
||||
fetch-blob@2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.npmjs.org/fetch-blob/-/fetch-blob-2.1.2.tgz#a7805db1361bd44c1ef62bb57fb5fe8ea173ef3c"
|
||||
integrity sha512-YKqtUDwqLyfyMnmbw8XD6Q8j9i/HggKtPEI+pZ1+8bvheBu78biSmNaXWusx1TauGqtUUGx/cBb1mKdq2rLYow==
|
||||
|
||||
fetch-readablestream@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.npmjs.org/fetch-readablestream/-/fetch-readablestream-0.2.0.tgz#eaa6d1a76b12de2d4731a343393c6ccdcfe2c795"
|
||||
@@ -20703,6 +20869,13 @@ node-fetch@2.6.1, node-fetch@^2.3.0, node-fetch@^2.6.0, node-fetch@^2.6.1:
|
||||
resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
|
||||
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
|
||||
|
||||
node-fetch@2.6.5:
|
||||
version "2.6.5"
|
||||
resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.5.tgz#42735537d7f080a7e5f78b6c549b7146be1742fd"
|
||||
integrity sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==
|
||||
dependencies:
|
||||
whatwg-url "^5.0.0"
|
||||
|
||||
node-forge@^0.10.0:
|
||||
version "0.10.0"
|
||||
resolved "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3"
|
||||
@@ -27091,6 +27264,11 @@ tr46@^2.0.2:
|
||||
dependencies:
|
||||
punycode "^2.1.1"
|
||||
|
||||
tr46@~0.0.3:
|
||||
version "0.0.3"
|
||||
resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
|
||||
integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
|
||||
|
||||
"traverse@>=0.3.0 <0.4":
|
||||
version "0.3.9"
|
||||
resolved "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9"
|
||||
@@ -28340,6 +28518,16 @@ web-streams-polyfill@4.0.0-beta.1:
|
||||
resolved "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.1.tgz#3b19b9817374b7cee06d374ba7eeb3aeb80e8c95"
|
||||
integrity sha512-3ux37gEX670UUphBF9AMCq8XM6iQ8Ac6A+DSRRjDoRBm1ufCkaCDdNVbaqq60PsEkdNlLKrGtv/YBP4EJXqNtQ==
|
||||
|
||||
webidl-conversions@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
|
||||
integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=
|
||||
|
||||
webidl-conversions@^4.0.2:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
|
||||
integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==
|
||||
|
||||
webidl-conversions@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff"
|
||||
@@ -28555,6 +28743,32 @@ whatwg-mimetype@^2.3.0:
|
||||
resolved "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
|
||||
integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
|
||||
|
||||
whatwg-url@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
|
||||
integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0=
|
||||
dependencies:
|
||||
tr46 "~0.0.3"
|
||||
webidl-conversions "^3.0.0"
|
||||
|
||||
whatwg-url@^6.4.1:
|
||||
version "6.5.0"
|
||||
resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8"
|
||||
integrity sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==
|
||||
dependencies:
|
||||
lodash.sortby "^4.7.0"
|
||||
tr46 "^1.0.1"
|
||||
webidl-conversions "^4.0.2"
|
||||
|
||||
whatwg-url@^7.0.0:
|
||||
version "7.1.0"
|
||||
resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06"
|
||||
integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==
|
||||
dependencies:
|
||||
lodash.sortby "^4.7.0"
|
||||
tr46 "^1.0.1"
|
||||
webidl-conversions "^4.0.2"
|
||||
|
||||
whatwg-url@^8.0.0, whatwg-url@^8.4.0:
|
||||
version "8.4.0"
|
||||
resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.4.0.tgz#50fb9615b05469591d2b2bd6dfaed2942ed72837"
|
||||
|
||||
Reference in New Issue
Block a user