Merge branch 'master' of github.com:backstage/backstage into blam/move-scaffolder-to-integrations
* 'master' of github.com:backstage/backstage: (39 commits) Create tough-jars-share.md add forgotten changeset changeset: add changeset for backend https certificate generation backend-common: cache generated HTTPS certificates backend-common: replace generated cert params with a simple bool switch, and fix certificate generation docs(techradar-plugin) Update loader resp example catalog-backend: stop it with the varchar(255) catalog-backend: unbreak sqlite in create-app repos Reformat without shell Clean up testing instructions Add code fence Remove completed TODO Fix ref to exempli gratia Fix typo Remove typos in vocab rempove old changeset file remove url check to support on-prem ADO Update create-release-tag.js Fix test Fix error, add location target support ...
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/create-app': patch
|
||||
---
|
||||
|
||||
Replace `register-component` plugin with new `catalog-import` plugin
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-common': patch
|
||||
---
|
||||
|
||||
Fix HTTPS certificate generation and add new config switch, enabling it simply by setting `backend.https = true`. Also introduces caching of generated certificates in order to avoid having to add a browser override every time the backend is restarted.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': minor
|
||||
---
|
||||
|
||||
Add processor for ingesting AWS accounts from AWS Organizations
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/backend-common': patch
|
||||
'@backstage/integration': patch
|
||||
---
|
||||
|
||||
Provide support for on-prem azure devops
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-tech-radar': patch
|
||||
---
|
||||
|
||||
Updated example data in `README`.
|
||||
@@ -49,6 +49,7 @@ dataflow
|
||||
deadnaming
|
||||
destructured
|
||||
dev
|
||||
devops
|
||||
devs
|
||||
dhenneke
|
||||
discoverability
|
||||
@@ -62,7 +63,6 @@ Docusaurus
|
||||
Dominik
|
||||
dtuite
|
||||
dzolotusky
|
||||
eg
|
||||
Ek
|
||||
env
|
||||
Env
|
||||
@@ -140,7 +140,7 @@ nonces
|
||||
npm
|
||||
nvm
|
||||
oauth
|
||||
Oauth
|
||||
OAuth
|
||||
oidc
|
||||
Okta
|
||||
Oldsberg
|
||||
@@ -159,7 +159,6 @@ prebaked
|
||||
preconfigured
|
||||
prepack
|
||||
Preprarer
|
||||
Prerequisities
|
||||
productional
|
||||
Protobuf
|
||||
proxying
|
||||
@@ -193,7 +192,6 @@ semlas
|
||||
semver
|
||||
Serverless
|
||||
Sinon
|
||||
smartsymobls
|
||||
Snyk
|
||||
sourcemaps
|
||||
sparklines
|
||||
@@ -220,7 +218,6 @@ Templater
|
||||
templaters
|
||||
Templaters
|
||||
Thauer
|
||||
theres
|
||||
toc
|
||||
tolerations
|
||||
Tolerations
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
# New tags and releases are created by changeset https://github.com/atlassian/changesets
|
||||
name: Create a new release on GitHub when a new tag is created
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*' # Push events to matching v*, i.e. v0.4.0, v1.1.0
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Create a new release on GitHub when a new tag is created
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: use node.js 12.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '12.x'
|
||||
|
||||
- name: Install node dependencies
|
||||
run: npm install @octokit/rest
|
||||
|
||||
# GITHUB_REF is of the format refs/tags/vA.B.C
|
||||
# This step extracts vA.B.C from GITHUB_REF
|
||||
- name: Get the version
|
||||
id: get_version
|
||||
run: echo "::set-output name=TAG_NAME::${GITHUB_REF#refs/tags/}"
|
||||
|
||||
# TODO/Note: This will only create a Draft release, which the maintainer can see and publish.
|
||||
# If the Draft release looks good, modify the step to go ahead publish the release. (By adding the third CLI argument.)
|
||||
- name: Create release on GitHub
|
||||
run: node scripts/create-github-release.js ${{ steps.get_version.outputs.TAG_NAME }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }}
|
||||
@@ -70,21 +70,6 @@ jobs:
|
||||
bash <(curl -s https://codecov.io/bash) -f packages/core/coverage/* -F core
|
||||
bash <(curl -s https://codecov.io/bash) -f packages/core-api/coverage/* -F core-api
|
||||
|
||||
# Publishes current version of packages that are not already present in the registry
|
||||
- name: publish
|
||||
if: matrix.node-version == '12.x'
|
||||
run: yarn lerna -- publish from-package --yes
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
# Tags the commit with the version in the core package if the tag doesn't exist
|
||||
- uses: Klemensas/action-autotag@1.2.3
|
||||
if: matrix.node-version == '12.x'
|
||||
with:
|
||||
GITHUB_TOKEN: '${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }}'
|
||||
package_root: 'packages/core'
|
||||
tag_prefix: 'v'
|
||||
|
||||
- name: Discord notification
|
||||
if: ${{ failure() }}
|
||||
uses: Ilshidur/action-discord@0.2.0
|
||||
@@ -92,3 +77,85 @@ jobs:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
with:
|
||||
args: 'Master build failed https://github.com/{{GITHUB_REPOSITORY}}/actions/runs/{{GITHUB_RUN_ID}}'
|
||||
|
||||
# A separate release build that is only run for commits that are the result of merging the "Version Packages" PR
|
||||
# We can't re-use the output from the above step, but we'll have a guaranteed node_modules cache and
|
||||
# only run the build steps that are necessary for publishing
|
||||
release:
|
||||
if: ${{ endsWith(github.event.head_commit.message, 'from backstage/changeset-release/master\n\nVersion Packages') }}
|
||||
needs: build
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [14.x]
|
||||
|
||||
env:
|
||||
CI: 'true'
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
# Beginning of yarn setup, keep in sync between all workflows, see ci.yml
|
||||
- name: use node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
registry-url: https://registry.npmjs.org/ # Needed for auth
|
||||
- name: cache all node_modules
|
||||
id: cache-modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: ${{ runner.os }}-v${{ matrix.node-version }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }}
|
||||
- name: find location of global yarn cache
|
||||
id: yarn-cache
|
||||
if: steps.cache-modules.outputs.cache-hit != 'true'
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
- name: cache global yarn cache
|
||||
uses: actions/cache@v2
|
||||
if: steps.cache-modules.outputs.cache-hit != 'true'
|
||||
with:
|
||||
path: ${{ steps.yarn-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
- name: yarn install
|
||||
run: yarn install --frozen-lockfile
|
||||
# End of yarn setup
|
||||
|
||||
- name: build type declarations
|
||||
run: yarn tsc:full
|
||||
|
||||
- name: build packages
|
||||
run: yarn lerna -- run --ignore example-app build
|
||||
|
||||
# Publishes current version of packages that are not already present in the registry
|
||||
- name: publish
|
||||
run: yarn lerna -- publish from-package --yes
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
# Creates the next available tag with format "release-<year>-<month>-<day>[.<n>]"
|
||||
- name: Create a release tag
|
||||
id: create_tag
|
||||
run: node scripts/create-release-tag.js
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }}
|
||||
|
||||
# Convert the newly created tag into a release with changelog information
|
||||
- name: Create release on GitHub
|
||||
run: node scripts/create-github-release.js ${{ steps.create_tag.outputs.tag_name }} 1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }}
|
||||
|
||||
# Notify everyone about this great new release :D
|
||||
- name: Discord notification
|
||||
uses: Ilshidur/action-discord@0.2.0
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_RELEASE_WEBHOOK }}
|
||||
TAG_NAME: ${{ steps.create_tag.outputs.tag_name }}
|
||||
with:
|
||||
args: 'A new release has been published! https://github.com/backstage/backstage/releases/tag/{{TAG_NAME}}'
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ Harassment includes, but is not limited to:
|
||||
- Offensive comments related to gender, gender identity and expression, sexual orientation, disability, mental illness, neuro(a)typicality, physical appearance, body size, race, age, regional discrimination, political or religious affiliation
|
||||
- Unwelcome comments regarding a person’s lifestyle choices and practices, including those related to food, health, parenting, drugs, and employment
|
||||
- Deliberate misgendering. This includes deadnaming or persistently using a pronoun that does not correctly reflect a person's gender identity. You must address people by the name they give you when not addressing them by their username or handle
|
||||
- Physical contact and simulated physical contact (eg, textual descriptions like “_hug_” or “_backrub_”) without consent or after a request to stop
|
||||
- Physical contact and simulated physical contact (e.g., textual descriptions like “_hug_” or “_backrub_”) without consent or after a request to stop
|
||||
- Threats of violence, both physical and psychological
|
||||
- Incitement of violence towards any individual, including encouraging a person to commit suicide or to engage in self-harm
|
||||
- Deliberate intimidation
|
||||
|
||||
@@ -44,8 +44,8 @@ Backstage model and the primary way to discover existing functionality in the
|
||||
ecosystem.
|
||||
|
||||
APIs are implemented by components and form boundaries between components. They
|
||||
might be defined using an RPC IDL (eg Protobuf, GraphQL, ...), a data schema (eg
|
||||
Avro, TFRecord, ...), or as code interfaces. In any case, APIs exposed by
|
||||
might be defined using an RPC IDL (e.g., Protobuf, GraphQL, ...), a data schema
|
||||
(e.g., Avro, TFRecord, ...), or as code interfaces. In any case, APIs exposed by
|
||||
components need to be in a known machine-readable format so we can build further
|
||||
tooling and analysis on top.
|
||||
|
||||
|
||||
@@ -45,8 +45,6 @@ about TechDocs and the philosophy in its
|
||||
[v2]: https://github.com/backstage/backstage/milestone/22
|
||||
[v3]: https://github.com/backstage/backstage/milestone/17
|
||||
|
||||
<!-- TODO: Add link to milestone for v3 -->
|
||||
|
||||
## Use Cases
|
||||
|
||||
#### TechDocs V.0
|
||||
|
||||
@@ -41,7 +41,7 @@ setup for free.
|
||||
|
||||
### Manually add documentation setup to already existing repository
|
||||
|
||||
Prerequisities:
|
||||
Prerequisites:
|
||||
|
||||
- An existing component
|
||||
[registered in backstage](../software-catalog/index.md#adding-components-to-the-catalog)
|
||||
|
||||
@@ -33,4 +33,4 @@ that someone else will pick up the work.
|
||||
If your plugin isn't supposed to live as a standalone page, but rather needs to
|
||||
be presented as a part of a Service Catalog (e.g. a separate tab or a card on an
|
||||
"Overview" tab), then check out
|
||||
[the instruction](integrating-plugin-into-service-catalog.md). on how to do it.
|
||||
[the instruction](integrating-plugin-into-service-catalog.md) on how to do it.
|
||||
|
||||
+42
-33
@@ -16,15 +16,15 @@ frameworks and libraries like [Mocha](https://mochajs.org/),
|
||||
|
||||
Running all tests:
|
||||
|
||||
yarn test-react
|
||||
yarn test
|
||||
|
||||
Running an individual test (e.g. `MyComponent.test.js`):
|
||||
|
||||
yarn test-react MyComponent
|
||||
yarn test MyComponent
|
||||
|
||||
To run both `MyComponent.test.js` and `MyControl.test.js` suite of tests:
|
||||
|
||||
yarn test-react MyCo
|
||||
yarn test MyCo
|
||||
|
||||
Note: if `console.logs` are not appearing, run only the individual test you are
|
||||
working on.
|
||||
@@ -52,12 +52,12 @@ render React components.
|
||||
|
||||
TODO.
|
||||
|
||||
# Writing Unit Tests
|
||||
## Writing Unit Tests
|
||||
|
||||
The following principles are good guides for determining if you are writing high
|
||||
quality frontend unit tests.
|
||||
|
||||
## Bad Unit Test Principle
|
||||
### Bad Unit Test Principle
|
||||
|
||||
> No unit test is better than a bad one.
|
||||
|
||||
@@ -69,7 +69,7 @@ Writing a poor unit test:
|
||||
- Adds to future work by requiring updates to the unit test for irrelevant code
|
||||
changes.
|
||||
|
||||
## Input/Output Principle
|
||||
### Input/Output Principle
|
||||
|
||||
> A unit test verifies an output matches an expected input.
|
||||
|
||||
@@ -77,7 +77,7 @@ For backend, this would be that when you provide configuration X, then the
|
||||
object responds with Y. For frontend, this would be that when you provide
|
||||
properties X to a component, then the visual functionality responds with Y.
|
||||
|
||||
## Blackbox Principle
|
||||
### Blackbox Principle
|
||||
|
||||
> A good unit test does not tell the object how it should do its job but should
|
||||
> only compare inputs to outputs.
|
||||
@@ -86,7 +86,7 @@ Consider a unit test for a form. A good unit test would not test the order of
|
||||
the form fields. Instead, it would verify that the inputs to the form fields
|
||||
lead to a certain backend call when submit is clicked.
|
||||
|
||||
## Scalability Principle
|
||||
### Scalability Principle
|
||||
|
||||
> Unit test quality is directly proportionate to how much code can change
|
||||
> without having to touch the unit test.
|
||||
@@ -97,7 +97,7 @@ to the code, you have to update the unit test. A good unit test suite allows a
|
||||
lot of flexibility in _how_ the code is written so that future refactoring can
|
||||
occur without having to touch the original unit tests.
|
||||
|
||||
## Increasing Complexity Principle
|
||||
### Increasing Complexity Principle
|
||||
|
||||
> The ordering of unit tests in a suite should proceed from least specific to
|
||||
> most specific.
|
||||
@@ -116,7 +116,7 @@ throwing an error saying that output was incorrect will lead the next developer
|
||||
into thinking they may have broken the entire functionality of the object rather
|
||||
than simply letting them know they had an invalid input.
|
||||
|
||||
## Broken Functionality Principle
|
||||
### Broken Functionality Principle
|
||||
|
||||
> Generally, a unit test should not test exactly how the output appears, it
|
||||
> should test that the functionality has an expected _general_ response to an
|
||||
@@ -131,7 +131,7 @@ test a slightly different color on the button the unit test will break. A better
|
||||
unit test would verify that the button's CSS classname is assigned properly on
|
||||
hover or test for something completely different.
|
||||
|
||||
## Example: Loading Indicator
|
||||
### Example: Loading Indicator
|
||||
|
||||
A classic unit test on frontends is verifying a loading indicator displays when
|
||||
a backend request is being made.
|
||||
@@ -192,11 +192,14 @@ returns a result or displays an error or console message, like so:
|
||||
|
||||
**`StringUtil ellipsis`**
|
||||
|
||||
export function ellipsis(text, maxLength, midCharIx = 0, ellipsis = '...') {
|
||||
// Do something blackbox. We should not care about the internals, only inputs and outputs.
|
||||
...
|
||||
return someFinalValue;
|
||||
}
|
||||
```js
|
||||
export function ellipsis(text, maxLength, midCharIx = 0, ellipsis = '...') {
|
||||
// Do something blackbox. We should not care about the internals,
|
||||
// only inputs and outputs.
|
||||
...
|
||||
return someFinalValue;
|
||||
}
|
||||
```
|
||||
|
||||
There are four things to test for in a utility function:
|
||||
|
||||
@@ -207,30 +210,36 @@ There are four things to test for in a utility function:
|
||||
|
||||
> Handle Invalid Input (handle thrown errors):
|
||||
|
||||
it('Throws an error on improper arguments', () => {
|
||||
expect(() => {
|
||||
ellipsis();
|
||||
}).toThrowError('Expected \'text\' to be defined');
|
||||
});
|
||||
```js
|
||||
it('Throws an error on improper arguments', () => {
|
||||
expect(() => {
|
||||
ellipsis();
|
||||
}).toThrowError("Expected 'text' to be defined");
|
||||
});
|
||||
```
|
||||
|
||||
> Verify default input arguments:
|
||||
|
||||
it('Works with defaults', () => {
|
||||
expect(ellipsis('Hello world', 3)).toBe('Hel...');
|
||||
expect(ellipsis('', 3)).toBe('');
|
||||
expect(ellipsis('H', 3)).toBe('H');
|
||||
expect(ellipsis('Hello', 5)).toBe('Hello');
|
||||
});
|
||||
```js
|
||||
it('Works with defaults', () => {
|
||||
expect(ellipsis('Hello world', 3)).toBe('Hel...');
|
||||
expect(ellipsis('', 3)).toBe('');
|
||||
expect(ellipsis('H', 3)).toBe('H');
|
||||
expect(ellipsis('Hello', 5)).toBe('Hello');
|
||||
});
|
||||
```
|
||||
|
||||
> Verify output for expected input arguments:
|
||||
|
||||
This is especially true for edge cases!
|
||||
|
||||
it('Works with midCharIx', () => {
|
||||
expect(ellipsis('Hello world', 3, 6)).toBe('...o w...');
|
||||
expect(ellipsis('', 3, 6)).toBe('');
|
||||
expect(ellipsis('Backstage is amazing', 4, 10)).toBe('...e is...');
|
||||
});
|
||||
```js
|
||||
it('Works with midCharIx', () => {
|
||||
expect(ellipsis('Hello world', 3, 6)).toBe('...o w...');
|
||||
expect(ellipsis('', 3, 6)).toBe('');
|
||||
expect(ellipsis('Backstage is amazing', 4, 10)).toBe('...e is...');
|
||||
});
|
||||
```
|
||||
|
||||
## Non-React Classes
|
||||
|
||||
@@ -372,4 +381,4 @@ IDE.
|
||||
In most cases, we have found that using `console.log` works well.
|
||||
|
||||
Note: if your console.logs are not being displayed, focus your specific unit
|
||||
test from the command line by running them like so `yarn test-react MyTest`.
|
||||
test from the command line by running them like so `yarn test MyTest`.
|
||||
|
||||
@@ -41,8 +41,8 @@ the code.
|
||||
appreciate contributions in here and encourage them being kept up to date.
|
||||
|
||||
- [`docs/`](https://github.com/backstage/backstage/tree/master/docs) - This is
|
||||
where we keep all of our documentation Markdown files. These ends up on
|
||||
http://backstage.io/docs. Just keep in mind that changes to the
|
||||
where we keep all of our documentation Markdown files. These end up on
|
||||
https://backstage.io/docs. Just keep in mind that changes to the
|
||||
[`sidebars.json`](https://github.com/backstage/backstage/blob/master/microsite/sidebars.json)
|
||||
file may be needed as sections are added/removed.
|
||||
|
||||
|
||||
Vendored
+35
-20
@@ -32,26 +32,41 @@ export interface Config {
|
||||
port?: string | number;
|
||||
};
|
||||
|
||||
/** HTTPS configuration for the backend. If omitted the backend will serve HTTP */
|
||||
https?: {
|
||||
/** Certificate configuration or parameters for generating a self-signed certificate */
|
||||
certificate?:
|
||||
| {
|
||||
/** Algorithm to use to generate a self-signed certificate */
|
||||
algorithm: string;
|
||||
keySize?: number;
|
||||
days?: number;
|
||||
}
|
||||
| {
|
||||
/** PEM encoded certificate. Use $file to load in a file */
|
||||
cert: string;
|
||||
/**
|
||||
* PEM encoded certificate key. Use $file to load in a file.
|
||||
* @visibility secret
|
||||
*/
|
||||
key: string;
|
||||
};
|
||||
};
|
||||
/**
|
||||
* HTTPS configuration for the backend. If omitted the backend will serve HTTP.
|
||||
*
|
||||
* Setting this to `true` will cause self-signed certificates to be generated, which
|
||||
* can be useful for local development or other non-production scenarios.
|
||||
*/
|
||||
https?:
|
||||
| true
|
||||
| {
|
||||
/**
|
||||
* Certificate configuration or parameters for generating a self-signed certificate
|
||||
*
|
||||
* Setting parameters for self-signed certificates is deprecated and will be removed in
|
||||
* the future, set `backend.https = true` instead.
|
||||
*/
|
||||
certificate?:
|
||||
| {
|
||||
/** Algorithm to use to generate a self-signed certificate */
|
||||
algorithm?: string;
|
||||
keySize?: number;
|
||||
days?: number;
|
||||
attributes: {
|
||||
commonName: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
/** PEM encoded certificate. Use $file to load in a file */
|
||||
cert: string;
|
||||
/**
|
||||
* PEM encoded certificate key. Use $file to load in a file.
|
||||
* @visibility secret
|
||||
*/
|
||||
key: string;
|
||||
};
|
||||
};
|
||||
|
||||
/** Database connection configuration, select database type using the `client` field */
|
||||
database:
|
||||
|
||||
@@ -47,13 +47,7 @@ export class AzureUrlReader implements UrlReader {
|
||||
constructor(
|
||||
private readonly options: AzureIntegrationConfig,
|
||||
private readonly deps: { treeResponseFactory: ReadTreeResponseFactory },
|
||||
) {
|
||||
if (options.host !== 'dev.azure.com') {
|
||||
throw Error(
|
||||
`Azure integration currently only supports 'dev.azure.com', tried to use host '${options.host}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
) {}
|
||||
|
||||
async read(url: string): Promise<Buffer> {
|
||||
const builtUrl = getAzureFileFetchUrl(url);
|
||||
|
||||
@@ -145,7 +145,7 @@ export class ServiceBuilderImpl implements ServiceBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
start(): Promise<http.Server> {
|
||||
async start(): Promise<http.Server> {
|
||||
const app = express();
|
||||
const {
|
||||
port,
|
||||
@@ -168,16 +168,16 @@ export class ServiceBuilderImpl implements ServiceBuilder {
|
||||
app.use(notFoundHandler());
|
||||
app.use(errorHandler());
|
||||
|
||||
const server: http.Server = httpsSettings
|
||||
? await createHttpsServer(app, httpsSettings, logger)
|
||||
: createHttpServer(app, logger);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
app.on('error', e => {
|
||||
logger.error(`Failed to start up on port ${port}, ${e}`);
|
||||
reject(e);
|
||||
});
|
||||
|
||||
const server: http.Server = httpsSettings
|
||||
? createHttpsServer(app, httpsSettings, logger)
|
||||
: createHttpServer(app, logger);
|
||||
|
||||
const stoppableServer = stoppable(
|
||||
server.listen(port, host, () => {
|
||||
logger.info(`Listening on ${host}:${port}`);
|
||||
|
||||
@@ -47,14 +47,14 @@ export type CertificateReferenceOptions = {
|
||||
};
|
||||
|
||||
export type CertificateSigningOptions = {
|
||||
algorithm: string;
|
||||
algorithm?: string;
|
||||
size?: number;
|
||||
days?: number;
|
||||
attributes?: CertificateAttributes;
|
||||
attributes: CertificateAttributes;
|
||||
};
|
||||
|
||||
export type CertificateAttributes = {
|
||||
commonName?: string;
|
||||
commonName: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -193,8 +193,26 @@ export function readCspOptions(
|
||||
* ```
|
||||
*/
|
||||
export function readHttpsSettings(config: Config): HttpsSettings | undefined {
|
||||
const cc = config.getOptionalConfig('https');
|
||||
const https = config.get('https');
|
||||
if (https === true) {
|
||||
const baseUrl = config.getString('baseUrl');
|
||||
let commonName;
|
||||
try {
|
||||
commonName = new URL(baseUrl).hostname;
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid backend.baseUrl "${baseUrl}"`);
|
||||
}
|
||||
|
||||
return {
|
||||
certificate: {
|
||||
attributes: {
|
||||
commonName,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const cc = config.getOptionalConfig('https');
|
||||
if (!cc) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -13,11 +13,16 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import { resolve as resolvePath, dirname } from 'path';
|
||||
import express from 'express';
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
import { Logger } from 'winston';
|
||||
import { HttpsSettings } from './config';
|
||||
import { CertificateSigningOptions, HttpsSettings } from './config';
|
||||
|
||||
const ALMOST_MONTH_IN_MS = 25 * 24 * 60 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Creates a Http server instance based on an Express application.
|
||||
@@ -45,48 +50,152 @@ export function createHttpServer(
|
||||
* @returns A Https server instance
|
||||
*
|
||||
*/
|
||||
export function createHttpsServer(
|
||||
export async function createHttpsServer(
|
||||
app: express.Express,
|
||||
httpsSettings: HttpsSettings,
|
||||
logger?: Logger,
|
||||
): http.Server {
|
||||
): Promise<http.Server> {
|
||||
logger?.info('Initializing https server');
|
||||
|
||||
const credentials: { key: string; cert: string } = {
|
||||
key: '',
|
||||
cert: '',
|
||||
};
|
||||
let credentials: { key: string | Buffer; cert: string | Buffer };
|
||||
|
||||
const signingOptions: any = httpsSettings?.certificate;
|
||||
|
||||
if (signingOptions?.algorithm !== undefined) {
|
||||
logger?.info('Generating self-signed certificate with attributes');
|
||||
|
||||
const certificateAttributes: Array<any> = Object.entries(
|
||||
signingOptions.attributes,
|
||||
).map(([name, value]) => ({ name, value }));
|
||||
|
||||
// TODO: Create a type def for selfsigned.
|
||||
const signatures = require('selfsigned').generate(certificateAttributes, {
|
||||
algorithm: signingOptions?.algorithm,
|
||||
keySize: signingOptions?.size || 2048,
|
||||
days: signingOptions?.days || 30,
|
||||
});
|
||||
|
||||
logger?.info('Bootstrapping self-signed certificate');
|
||||
|
||||
credentials.key = signatures.private;
|
||||
credentials.cert = signatures.cert;
|
||||
// TODO(Rugvip): remove support for generated certificate params and make this a more straightforward check
|
||||
if (signingOptions?.attributes) {
|
||||
credentials = await getGeneratedCertificate(signingOptions, logger);
|
||||
} else {
|
||||
logger?.info('Bootstrapping cert from config');
|
||||
logger?.info('Loading certificate from config');
|
||||
|
||||
credentials.key = signingOptions?.key;
|
||||
credentials.cert = signingOptions?.cert;
|
||||
credentials = {
|
||||
key: signingOptions?.key,
|
||||
cert: signingOptions?.cert,
|
||||
};
|
||||
}
|
||||
|
||||
if (credentials.key === '' || credentials.cert === '') {
|
||||
throw new Error('Invalid credentials');
|
||||
if (!credentials.key || !credentials.cert) {
|
||||
throw new Error('Invalid HTTPS credentials');
|
||||
}
|
||||
|
||||
return https.createServer(credentials, app) as http.Server;
|
||||
}
|
||||
|
||||
async function getGeneratedCertificate(
|
||||
options: CertificateSigningOptions,
|
||||
logger?: Logger,
|
||||
) {
|
||||
if (options?.algorithm) {
|
||||
logger?.warn(
|
||||
'Certificate generation configuration with parameters in backend.https.certificate is deprecated, set backend.https = true instead',
|
||||
);
|
||||
}
|
||||
|
||||
const hasModules = await fs.pathExists('node_modules');
|
||||
let certPath;
|
||||
if (hasModules) {
|
||||
certPath = resolvePath(
|
||||
'node_modules/.cache/backstage-backend/dev-cert.pem',
|
||||
);
|
||||
await fs.ensureDir(dirname(certPath));
|
||||
} else {
|
||||
certPath = resolvePath('.dev-cert.pem');
|
||||
}
|
||||
|
||||
let cert = undefined;
|
||||
if (await fs.pathExists(certPath)) {
|
||||
const stat = await fs.stat(certPath);
|
||||
const ageMs = Date.now() - stat.ctimeMs;
|
||||
if (stat.isFile() && ageMs < ALMOST_MONTH_IN_MS) {
|
||||
cert = await fs.readFile(certPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (cert) {
|
||||
logger?.info('Using existing self-signed certificate');
|
||||
return {
|
||||
key: cert,
|
||||
cert: cert,
|
||||
};
|
||||
}
|
||||
|
||||
logger?.info('Generating new self-signed certificate');
|
||||
const newCert = await createCertificate(options);
|
||||
await fs.writeFile(certPath, newCert.cert + newCert.key, 'utf8');
|
||||
return newCert;
|
||||
}
|
||||
|
||||
async function createCertificate(options: CertificateSigningOptions) {
|
||||
const attributes: Array<any> = Object.entries(
|
||||
options.attributes,
|
||||
).map(([name, value]) => ({ name, value }));
|
||||
|
||||
const params = {
|
||||
algorithm: options?.algorithm || 'sha256',
|
||||
keySize: options?.size || 2048,
|
||||
days: options?.days || 30,
|
||||
extensions: [
|
||||
{
|
||||
name: 'keyUsage',
|
||||
keyCertSign: true,
|
||||
digitalSignature: true,
|
||||
nonRepudiation: true,
|
||||
keyEncipherment: true,
|
||||
dataEncipherment: true,
|
||||
},
|
||||
{
|
||||
name: 'extKeyUsage',
|
||||
serverAuth: true,
|
||||
clientAuth: true,
|
||||
codeSigning: true,
|
||||
timeStamping: true,
|
||||
},
|
||||
{
|
||||
name: 'subjectAltName',
|
||||
altNames: [
|
||||
{
|
||||
type: 2, // DNS
|
||||
value: 'localhost',
|
||||
},
|
||||
{
|
||||
type: 2,
|
||||
value: 'localhost.localdomain',
|
||||
},
|
||||
{
|
||||
type: 2,
|
||||
value: '[::1]',
|
||||
},
|
||||
{
|
||||
type: 7, // IP
|
||||
ip: '127.0.0.1',
|
||||
},
|
||||
{
|
||||
type: 7,
|
||||
ip: 'fe80::1',
|
||||
},
|
||||
...(options.attributes.commonName
|
||||
? [
|
||||
{
|
||||
type: 2, // DNS
|
||||
value: options.attributes.commonName,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return new Promise<{ key: string; cert: string }>((resolve, reject) =>
|
||||
require('selfsigned').generate(
|
||||
attributes,
|
||||
params,
|
||||
(err: Error, bundle: { private: string; cert: string }) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve({ key: bundle.private, cert: bundle.cert });
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
# @backstage/create-app
|
||||
|
||||
This package provides a CLI for creating apps.
|
||||
This package provides a CLI for creating a copy of the Backstage app.
|
||||
|
||||
You can use the flag `--skip-install` to skip the install.
|
||||
|
||||
## Installation
|
||||
## Usage
|
||||
|
||||
With `npx`:
|
||||
|
||||
@@ -11,6 +12,12 @@ With `npx`:
|
||||
$ npx @backstage/create-app
|
||||
```
|
||||
|
||||
With a local clone of this repo, from the main `create-app/` folder, run:
|
||||
|
||||
```sh
|
||||
$ yarn backstage-create-app
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md)
|
||||
|
||||
@@ -47,12 +47,12 @@
|
||||
"@backstage/plugin-auth-backend": "^0.2.8",
|
||||
"@backstage/plugin-catalog": "^0.2.9",
|
||||
"@backstage/plugin-catalog-backend": "^0.5.1",
|
||||
"@backstage/plugin-catalog-import": "^0.3.2",
|
||||
"@backstage/plugin-circleci": "^0.2.5",
|
||||
"@backstage/plugin-explore": "^0.2.2",
|
||||
"@backstage/plugin-github-actions": "^0.2.6",
|
||||
"@backstage/plugin-lighthouse": "^0.2.6",
|
||||
"@backstage/plugin-proxy-backend": "^0.2.3",
|
||||
"@backstage/plugin-register-component": "^0.2.6",
|
||||
"@backstage/plugin-rollbar-backend": "^0.1.5",
|
||||
"@backstage/plugin-scaffolder": "^0.3.5",
|
||||
"@backstage/plugin-search": "^0.2.4",
|
||||
|
||||
@@ -39,12 +39,12 @@ import { version as pluginAppBackend } from '@backstage/plugin-app-backend/packa
|
||||
import { version as pluginAuthBackend } from '@backstage/plugin-auth-backend/package.json';
|
||||
import { version as pluginCatalog } from '@backstage/plugin-catalog/package.json';
|
||||
import { version as pluginCatalogBackend } from '@backstage/plugin-catalog-backend/package.json';
|
||||
import { version as pluginCatalogImport } from '@backstage/plugin-catalog-import/package.json';
|
||||
import { version as pluginCircleci } from '@backstage/plugin-circleci/package.json';
|
||||
import { version as pluginExplore } from '@backstage/plugin-explore/package.json';
|
||||
import { version as pluginGithubActions } from '@backstage/plugin-github-actions/package.json';
|
||||
import { version as pluginLighthouse } from '@backstage/plugin-lighthouse/package.json';
|
||||
import { version as pluginProxyBackend } from '@backstage/plugin-proxy-backend/package.json';
|
||||
import { version as pluginRegisterComponent } from '@backstage/plugin-register-component/package.json';
|
||||
import { version as pluginRollbarBackend } from '@backstage/plugin-rollbar-backend/package.json';
|
||||
import { version as pluginScaffolder } from '@backstage/plugin-scaffolder/package.json';
|
||||
import { version as pluginScaffolderBackend } from '@backstage/plugin-scaffolder-backend/package.json';
|
||||
@@ -67,12 +67,12 @@ export const packageVersions = {
|
||||
'@backstage/plugin-auth-backend': pluginAuthBackend,
|
||||
'@backstage/plugin-catalog': pluginCatalog,
|
||||
'@backstage/plugin-catalog-backend': pluginCatalogBackend,
|
||||
'@backstage/plugin-catalog-import': pluginCatalogImport,
|
||||
'@backstage/plugin-circleci': pluginCircleci,
|
||||
'@backstage/plugin-explore': pluginExplore,
|
||||
'@backstage/plugin-github-actions': pluginGithubActions,
|
||||
'@backstage/plugin-lighthouse': pluginLighthouse,
|
||||
'@backstage/plugin-proxy-backend': pluginProxyBackend,
|
||||
'@backstage/plugin-register-component': pluginRegisterComponent,
|
||||
'@backstage/plugin-rollbar-backend': pluginRollbarBackend,
|
||||
'@backstage/plugin-scaffolder': pluginScaffolder,
|
||||
'@backstage/plugin-scaffolder-backend': pluginScaffolderBackend,
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"@backstage/core": "^{{version '@backstage/core'}}",
|
||||
"@backstage/plugin-api-docs": "^{{version '@backstage/plugin-api-docs'}}",
|
||||
"@backstage/plugin-catalog": "^{{version '@backstage/plugin-catalog'}}",
|
||||
"@backstage/plugin-register-component": "^{{version '@backstage/plugin-register-component'}}",
|
||||
"@backstage/plugin-catalog-import": "^{{version '@backstage/plugin-catalog-import'}}",
|
||||
"@backstage/plugin-scaffolder": "^{{version '@backstage/plugin-scaffolder'}}",
|
||||
"@backstage/plugin-techdocs": "^{{version '@backstage/plugin-techdocs'}}",
|
||||
"@backstage/catalog-model": "^{{version '@backstage/catalog-model'}}",
|
||||
|
||||
@@ -12,7 +12,7 @@ import { AppSidebar } from './sidebar';
|
||||
import { Route, Routes, Navigate } from 'react-router';
|
||||
import { Router as CatalogRouter } from '@backstage/plugin-catalog';
|
||||
import { Router as DocsRouter } from '@backstage/plugin-techdocs';
|
||||
import { Router as RegisterComponentRouter } from '@backstage/plugin-register-component';
|
||||
import { Router as ImportComponentRouter } from '@backstage/plugin-catalog-import';
|
||||
import { Router as TechRadarRouter } from '@backstage/plugin-tech-radar';
|
||||
import { SearchPage as SearchRouter } from '@backstage/plugin-search';
|
||||
|
||||
@@ -52,8 +52,8 @@ const App = () => (
|
||||
element={<TechRadarRouter width={1500} height={800} />}
|
||||
/>
|
||||
<Route
|
||||
path="/register-component"
|
||||
element={<RegisterComponentRouter catalogRouteRef={catalogRouteRef} />}
|
||||
path="/catalog-import"
|
||||
element={<ImportComponentRouter catalogRouteRef={catalogRouteRef} />}
|
||||
/>
|
||||
<Route
|
||||
path="/search"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
export { plugin as ApiDocs } from '@backstage/plugin-api-docs';
|
||||
export { plugin as CatalogPlugin } from '@backstage/plugin-catalog';
|
||||
export { plugin as RegisterComponent } from '@backstage/plugin-register-component';
|
||||
export { plugin as CatalogImport } from '@backstage/plugin-catalog-import';
|
||||
export { plugin as Circleci } from '@backstage/plugin-circleci';
|
||||
export { plugin as GithubActions } from '@backstage/plugin-github-actions';
|
||||
export { plugin as LighthousePlugin } from '@backstage/plugin-lighthouse';
|
||||
export { plugin as ScaffolderPlugin } from '@backstage/plugin-scaffolder';
|
||||
export { plugin as TechDocsPlugin } from '@backstage/plugin-techdocs';
|
||||
export { plugin as Circleci } from '@backstage/plugin-circleci';
|
||||
export { plugin as LighthousePlugin } from '@backstage/plugin-lighthouse';
|
||||
export { plugin as TechRadar } from '@backstage/plugin-tech-radar';
|
||||
export { plugin as GithubActions } from '@backstage/plugin-github-actions';
|
||||
|
||||
@@ -54,6 +54,18 @@ describe('azure core', () => {
|
||||
result:
|
||||
'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml',
|
||||
},
|
||||
{
|
||||
url:
|
||||
'https://api.com/org-name/project-name/_git/repo-name?path=my-template.yaml',
|
||||
result:
|
||||
'https://api.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml',
|
||||
},
|
||||
{
|
||||
url:
|
||||
'https://api.com/org-name/project-name/_git/repo-name?path=my-template.yaml&version=GBmaster',
|
||||
result:
|
||||
'https://api.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml&version=master',
|
||||
},
|
||||
])('should handle happy path %#', async ({ url, result }) => {
|
||||
expect(getAzureFileFetchUrl(url)).toBe(result);
|
||||
});
|
||||
|
||||
@@ -43,7 +43,6 @@ export function getAzureFileFetchUrl(url: string): string {
|
||||
const ref = parsedUrl.searchParams.get('version')?.substr(2);
|
||||
|
||||
if (
|
||||
parsedUrl.hostname !== 'dev.azure.com' ||
|
||||
empty !== '' ||
|
||||
userOrOrg === '' ||
|
||||
project === '' ||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# storybook
|
||||
|
||||
This package provides a storybook build for Backstage. See [http://backstage.io/storybook](http://http://backstage.io/storybook)
|
||||
This package provides a Storybook build for Backstage. See https://backstage.io/storybook/.
|
||||
|
||||
## Why is this not part of `@backstage/core`?
|
||||
|
||||
This separate storybook package exists because of dependency conflicts with `@backstage/cli`. It uses nohoist to avoid the conflicts, and since you can only use that in private packages it has to be separated out of `@backstage/core`.
|
||||
This separate storybook package exists because of dependency conflicts with `@backstage/cli`. It uses `nohoist` to avoid the conflicts, and since you can only use that in private packages it has to be separated out of `@backstage/core`.
|
||||
|
||||
+4
-6
@@ -1,14 +1,12 @@
|
||||
# Plugins
|
||||
|
||||
Backstage is a single-page application composed of a set of plugins.
|
||||
Backstage is a single-page application composed of a set of plugins. This folder holds numerous plugins that are managed by this repository.
|
||||
|
||||
Our goal for the plugin ecosystem is that the definition of a plugin is flexible enough to allow you to expose pretty much any kind of infrastructure or software development tool as a plugin in Backstage. By following strong [design guidelines](https://github.com/backstage/backstage/blob/master/docs/dls/design.md) we ensure the overall user experience stays consistent between plugins.
|
||||
For more information about the plugin ecosystem, see the documentation here:
|
||||
|
||||

|
||||
> https://backstage.io/docs/plugins/
|
||||
|
||||
## Creating a plugin
|
||||
|
||||
To create a plugin, follow the steps outlined [here](https://github.com/backstage/backstage/blob/master/docs/plugins/create-a-plugin.md).
|
||||
You can also see the [Plugin Marketplace](https://backstage.io/plugins) for other open source plugins you can add to your Backstage instance.
|
||||
|
||||
## Suggesting a plugin
|
||||
|
||||
|
||||
@@ -20,16 +20,14 @@
|
||||
* @param {import('knex')} knex
|
||||
*/
|
||||
exports.up = async function up(knex) {
|
||||
// Drop constraints (Postgres)
|
||||
try {
|
||||
// SQLite does not support FK and PK
|
||||
if (knex.client.config.client !== 'sqlite3') {
|
||||
await knex.schema.alterTable('entities_search', table => {
|
||||
table.dropForeign(['entity_id']);
|
||||
});
|
||||
await knex.schema.alterTable('entities', table => {
|
||||
table.dropPrimary('entities_pkey');
|
||||
});
|
||||
} catch (e) {
|
||||
// SQLite does not support FK and PK, carry on
|
||||
}
|
||||
await knex.schema.alterTable('entities', table => {
|
||||
table.dropUnique([], 'entities_unique_name');
|
||||
@@ -131,16 +129,14 @@ exports.up = async function up(knex) {
|
||||
* @param {import('knex')} knex
|
||||
*/
|
||||
exports.down = async function down(knex) {
|
||||
// Drop constraints (Postgres)
|
||||
try {
|
||||
// SQLite does not support FK and PK
|
||||
if (knex.client.config.client !== 'sqlite3') {
|
||||
await knex.schema.alterTable('entities_search', table => {
|
||||
table.dropForeign(['entity_id']);
|
||||
});
|
||||
await knex.schema.alterTable('entities', table => {
|
||||
table.dropPrimary('entities_pkey');
|
||||
});
|
||||
} catch (e) {
|
||||
// SQLite does not support FK and PK, carry on
|
||||
}
|
||||
await knex.schema.alterTable('entities', table => {
|
||||
table.dropUnique([], 'entities_unique_name');
|
||||
|
||||
@@ -20,12 +20,11 @@
|
||||
* @param {import('knex')} knex
|
||||
*/
|
||||
exports.up = async function up(knex) {
|
||||
try {
|
||||
// Sqlite does not support alter column.
|
||||
if (knex.client.config.client !== 'sqlite3') {
|
||||
await knex.schema.alterTable('entities_search', table => {
|
||||
table.text('value').nullable().alter();
|
||||
});
|
||||
} catch (e) {
|
||||
// Sqlite does not support alter column.
|
||||
}
|
||||
};
|
||||
|
||||
@@ -33,11 +32,10 @@ exports.up = async function up(knex) {
|
||||
* @param {import('knex')} knex
|
||||
*/
|
||||
exports.down = async function down(knex) {
|
||||
try {
|
||||
// Sqlite does not support alter column.
|
||||
if (knex.client.config.client !== 'sqlite3') {
|
||||
await knex.schema.alterTable('entities_search', table => {
|
||||
table.string('value').nullable().alter();
|
||||
});
|
||||
} catch (e) {
|
||||
// Sqlite does not support alter column.
|
||||
}
|
||||
};
|
||||
|
||||
@@ -30,12 +30,11 @@ exports.up = async function up(knex) {
|
||||
),
|
||||
});
|
||||
|
||||
try {
|
||||
// SQLite does not support alter column
|
||||
if (knex.client.config.client !== 'sqlite3') {
|
||||
await knex.schema.alterTable('entities', table => {
|
||||
table.text('full_name').notNullable().alter();
|
||||
});
|
||||
} catch (e) {
|
||||
// SQLite does not support alter column, ignore
|
||||
}
|
||||
|
||||
await knex.schema.alterTable('entities', table => {
|
||||
|
||||
@@ -40,10 +40,7 @@ exports.up = async function up(knex) {
|
||||
table.dropColumn('spec');
|
||||
});
|
||||
|
||||
// SQLite does not support ALTER COLUMN. Note that we do not use the try/
|
||||
// catch method as in other migrations, because if the transaction is
|
||||
// partially failed, it will further mess up the already messed-up
|
||||
// statement below this.
|
||||
// SQLite does not support ALTER COLUMN.
|
||||
if (knex.client.config.client !== 'sqlite3') {
|
||||
await knex.schema.alterTable('entities', table => {
|
||||
table.text('data').notNullable().alter();
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* @param {import('knex')} knex
|
||||
*/
|
||||
exports.up = async function up(knex) {
|
||||
if (knex.client.config.client !== 'sqlite3') {
|
||||
// We actually just want to widen columns, but can't do that while a
|
||||
// view is dependent on them - so we just reconstruct it exactly as it was
|
||||
await knex.schema
|
||||
.raw('DROP VIEW location_update_log_latest;')
|
||||
.alterTable('location_update_log', table => {
|
||||
table.text('message').alter();
|
||||
table.text('entity_name').nullable().alter();
|
||||
}).raw(`
|
||||
CREATE VIEW location_update_log_latest AS
|
||||
SELECT t1.* FROM location_update_log t1
|
||||
JOIN
|
||||
(
|
||||
SELECT location_id, MAX(id) AS MAXID
|
||||
FROM location_update_log
|
||||
GROUP BY location_id
|
||||
) t2
|
||||
ON t1.location_id = t2.location_id
|
||||
AND t1.id = t2.MAXID
|
||||
GROUP BY t1.location_id, t1.id
|
||||
ORDER BY created_at DESC;
|
||||
`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {import('knex')} knex
|
||||
*/
|
||||
exports.down = async function down(knex) {
|
||||
if (knex.client.config.client !== 'sqlite3') {
|
||||
await knex.schema
|
||||
.raw('DROP VIEW location_update_log_latest;')
|
||||
.alterTable('location_update_log', table => {
|
||||
table.string('message').alter();
|
||||
table.string('entity_name').nullable().alter();
|
||||
}).raw(`
|
||||
CREATE VIEW location_update_log_latest AS
|
||||
SELECT t1.* FROM location_update_log t1
|
||||
JOIN
|
||||
(
|
||||
SELECT location_id, MAX(id) AS MAXID
|
||||
FROM location_update_log
|
||||
GROUP BY location_id
|
||||
) t2
|
||||
ON t1.location_id = t2.location_id
|
||||
AND t1.id = t2.MAXID
|
||||
GROUP BY t1.location_id, t1.id
|
||||
ORDER BY created_at DESC;
|
||||
`);
|
||||
}
|
||||
};
|
||||
@@ -36,6 +36,7 @@
|
||||
"@octokit/graphql": "^4.5.6",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/ldapjs": "^1.0.9",
|
||||
"aws-sdk": "^2.817.0",
|
||||
"codeowners-utils": "^1.0.2",
|
||||
"core-js": "^3.6.5",
|
||||
"cross-fetch": "^3.0.6",
|
||||
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AwsOrganizationCloudAccountProcessor } from './AwsOrganizationCloudAccountProcessor';
|
||||
|
||||
describe('AwsOrganizationCloudAccountProcessor', () => {
|
||||
describe('readLocation', () => {
|
||||
const processor = new AwsOrganizationCloudAccountProcessor();
|
||||
const location = { type: 'aws-cloud-accounts', target: '' };
|
||||
const emit = jest.fn();
|
||||
const listAccounts = jest.fn();
|
||||
|
||||
processor.organizations.listAccounts = listAccounts;
|
||||
afterEach(() => jest.resetAllMocks());
|
||||
|
||||
it('generates component entities for accounts', async () => {
|
||||
listAccounts.mockImplementation(() => {
|
||||
return {
|
||||
async promise() {
|
||||
return {
|
||||
Accounts: [
|
||||
{
|
||||
Arn:
|
||||
'arn:aws:organizations::192594491037:account/o-1vl18kc5a3/957140518395',
|
||||
Name: 'testaccount',
|
||||
},
|
||||
],
|
||||
NextToken: undefined,
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
await processor.readLocation(location, false, emit);
|
||||
expect(emit).toBeCalledWith({
|
||||
type: 'entity',
|
||||
location,
|
||||
entity: {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
annotations: {
|
||||
'amazonaws.com/arn':
|
||||
'arn:aws:organizations::192594491037:account/o-1vl18kc5a3/957140518395',
|
||||
'amazonaws.com/account-id': '957140518395',
|
||||
'amazonaws.com/organization-id': 'o-1vl18kc5a3',
|
||||
},
|
||||
name: 'testaccount',
|
||||
namespace: 'default',
|
||||
},
|
||||
spec: {
|
||||
type: 'cloud-account',
|
||||
lifecycle: 'unknown',
|
||||
owner: 'unknown',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('filters out accounts not in specified location target', async () => {
|
||||
const location = { type: 'aws-cloud-accounts', target: 'o-1vl18kc5a3' };
|
||||
listAccounts.mockImplementation(() => {
|
||||
return {
|
||||
async promise() {
|
||||
return {
|
||||
Accounts: [
|
||||
{
|
||||
Arn:
|
||||
'arn:aws:organizations::192594491037:account/o-1vl18kc5a3/957140518395',
|
||||
Name: 'testaccount',
|
||||
},
|
||||
{
|
||||
Arn:
|
||||
'arn:aws:organizations::192594491037:account/o-zzzzzzzzz/957140518395',
|
||||
Name: 'testaccount2',
|
||||
},
|
||||
],
|
||||
NextToken: undefined,
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
await processor.readLocation(location, false, emit);
|
||||
expect(emit).toBeCalledTimes(1);
|
||||
expect(emit).toBeCalledWith({
|
||||
type: 'entity',
|
||||
location,
|
||||
entity: {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
annotations: {
|
||||
'amazonaws.com/arn':
|
||||
'arn:aws:organizations::192594491037:account/o-1vl18kc5a3/957140518395',
|
||||
'amazonaws.com/account-id': '957140518395',
|
||||
'amazonaws.com/organization-id': 'o-1vl18kc5a3',
|
||||
},
|
||||
name: 'testaccount',
|
||||
namespace: 'default',
|
||||
},
|
||||
spec: {
|
||||
type: 'cloud-account',
|
||||
lifecycle: 'unknown',
|
||||
owner: 'unknown',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
ComponentEntityV1alpha1,
|
||||
LocationSpec,
|
||||
} from '@backstage/catalog-model';
|
||||
import AWS, { Organizations } from 'aws-sdk';
|
||||
import { Account, ListAccountsResponse } from 'aws-sdk/clients/organizations';
|
||||
|
||||
import * as results from './results';
|
||||
import { CatalogProcessor, CatalogProcessorEmit } from './types';
|
||||
|
||||
const AWS_ORGANIZATION_REGION = 'us-east-1';
|
||||
const LOCATION_TYPE = 'aws-cloud-accounts';
|
||||
|
||||
const ACCOUNTID_ANNOTATION: string = 'amazonaws.com/account-id';
|
||||
const ARN_ANNOTATION: string = 'amazonaws.com/arn';
|
||||
const ORGANIZATION_ANNOTATION: string = 'amazonaws.com/organization-id';
|
||||
|
||||
/**
|
||||
* A processor for ingesting AWS Accounts from AWS Organizations.
|
||||
*
|
||||
* If custom authentication is needed, it can be achieved by configuring the global AWS.credentials object.
|
||||
*/
|
||||
export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor {
|
||||
organizations: Organizations;
|
||||
constructor() {
|
||||
this.organizations = new AWS.Organizations({
|
||||
region: AWS_ORGANIZATION_REGION,
|
||||
}); // Only available in us-east-1
|
||||
}
|
||||
|
||||
normalizeName(name: string): string {
|
||||
return name
|
||||
.trim()
|
||||
.toLocaleLowerCase()
|
||||
.replace(/[^a-zA-Z0-9\-]/g, '-');
|
||||
}
|
||||
|
||||
extractInformationFromArn(
|
||||
arn: string,
|
||||
): { accountId: string; organizationId: string } {
|
||||
const parts = arn.split('/');
|
||||
|
||||
return {
|
||||
accountId: parts[parts.length - 1],
|
||||
organizationId: parts[parts.length - 2],
|
||||
};
|
||||
}
|
||||
|
||||
async getAwsAccounts(): Promise<Account[]> {
|
||||
let awsAccounts: Account[] = [];
|
||||
let isInitialAttempt = true;
|
||||
let nextToken = undefined;
|
||||
while (isInitialAttempt || nextToken) {
|
||||
isInitialAttempt = false;
|
||||
const orgAccounts: ListAccountsResponse = await this.organizations
|
||||
.listAccounts({ NextToken: nextToken })
|
||||
.promise();
|
||||
if (orgAccounts.Accounts) {
|
||||
awsAccounts = awsAccounts.concat(orgAccounts.Accounts);
|
||||
}
|
||||
nextToken = orgAccounts.NextToken;
|
||||
}
|
||||
|
||||
return awsAccounts;
|
||||
}
|
||||
|
||||
mapAccountToComponent(account: Account): ComponentEntityV1alpha1 {
|
||||
const { accountId, organizationId } = this.extractInformationFromArn(
|
||||
account.Arn as string,
|
||||
);
|
||||
return {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
annotations: {
|
||||
[ACCOUNTID_ANNOTATION]: accountId,
|
||||
[ARN_ANNOTATION]: account.Arn || '',
|
||||
[ORGANIZATION_ANNOTATION]: organizationId,
|
||||
},
|
||||
name: this.normalizeName(account.Name || ''),
|
||||
namespace: 'default',
|
||||
},
|
||||
spec: {
|
||||
type: 'cloud-account',
|
||||
lifecycle: 'unknown',
|
||||
owner: 'unknown',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async readLocation(
|
||||
location: LocationSpec,
|
||||
_optional: boolean,
|
||||
emit: CatalogProcessorEmit,
|
||||
): Promise<boolean> {
|
||||
if (location.type !== LOCATION_TYPE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
(await this.getAwsAccounts())
|
||||
.map(account => this.mapAccountToComponent(account))
|
||||
.filter(entity => {
|
||||
if (location.target !== '') {
|
||||
if (entity.metadata.annotations) {
|
||||
return (
|
||||
entity.metadata.annotations[ORGANIZATION_ANNOTATION] ===
|
||||
location.target
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.forEach((entity: ComponentEntityV1alpha1) => {
|
||||
emit(results.entity(location, entity));
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
import * as results from './results';
|
||||
|
||||
export { AnnotateLocationEntityProcessor } from './AnnotateLocationEntityProcessor';
|
||||
export { AwsOrganizationCloudAccountProcessor } from './AwsOrganizationCloudAccountProcessor';
|
||||
export { BuiltinKindsEntityProcessor } from './BuiltinKindsEntityProcessor';
|
||||
export { CodeOwnersProcessor } from './CodeOwnersProcessor';
|
||||
export { FileReaderProcessor } from './FileReaderProcessor';
|
||||
|
||||
@@ -83,13 +83,20 @@ const getHardCodedData = () =>
|
||||
rings: [{ id: 'use', name: 'USE', color: '#93c47d' }],
|
||||
entries: [
|
||||
{
|
||||
moved: 0,
|
||||
ring: 'use',
|
||||
url: '#',
|
||||
key: 'github-actions',
|
||||
id: 'github-actions',
|
||||
title: 'GitHub Actions',
|
||||
quadrant: 'infrastructure',
|
||||
timeline: [
|
||||
{
|
||||
moved: 0,
|
||||
ringId: 'use',
|
||||
date: new Date('2020-08-06'),
|
||||
description:
|
||||
'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
Executable
+79
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env node
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const { Octokit } = require('@octokit/rest');
|
||||
|
||||
const baseOptions = {
|
||||
owner: 'backstage',
|
||||
repo: 'backstage',
|
||||
};
|
||||
|
||||
async function main() {
|
||||
const { GITHUB_SHA, GITHUB_TOKEN } = process.env;
|
||||
if (!GITHUB_SHA) {
|
||||
throw new Error('GITHUB_SHA is not set');
|
||||
}
|
||||
if (!GITHUB_TOKEN) {
|
||||
throw new Error('GITHUB_TOKEN is not set');
|
||||
}
|
||||
|
||||
const octokit = new Octokit({ auth: GITHUB_TOKEN });
|
||||
|
||||
const date = new Date();
|
||||
const baseTagName = `release-${date.getUTCFullYear()}-${
|
||||
date.getUTCMonth() + 1
|
||||
}-${date.getUTCDate()}`;
|
||||
|
||||
console.log('Requesting existing tags');
|
||||
|
||||
const existingTags = await octokit.repos.listTags({
|
||||
...baseOptions,
|
||||
per_page: 100,
|
||||
});
|
||||
const existingTagNames = existingTags.data.map(obj => obj.name);
|
||||
|
||||
let tagName = baseTagName;
|
||||
let index = 0;
|
||||
while (existingTagNames.includes(tagName)) {
|
||||
index += 1;
|
||||
tagName = `${baseTagName}.${index}`;
|
||||
}
|
||||
|
||||
console.log(`Creating release tag ${tagName}`);
|
||||
|
||||
const annotatedTag = await octokit.git.createTag({
|
||||
...baseOptions,
|
||||
tag: tagName,
|
||||
message: tagName,
|
||||
object: GITHUB_SHA,
|
||||
type: 'commit',
|
||||
});
|
||||
|
||||
await octokit.git.createRef({
|
||||
...baseOptions,
|
||||
ref: `refs/tags/${tagName}`,
|
||||
sha: annotatedTag.data.sha,
|
||||
});
|
||||
|
||||
console.log(`::set-output name=tag_name::${tagName}`);
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(error.stack);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -7698,6 +7698,21 @@ autoprefixer@^9.7.2:
|
||||
postcss "^7.0.32"
|
||||
postcss-value-parser "^4.1.0"
|
||||
|
||||
aws-sdk@^2.817.0:
|
||||
version "2.817.0"
|
||||
resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.817.0.tgz#3a97b690b0ec494cf8ee927affb3973cf26abcc8"
|
||||
integrity sha512-DZIdWpkcqbqsCz0MEskHsyFaqc6Tk9XIFqXAg1AKHbOgC8nU45bz+Y2osX77pU01JkS/G7OhGtGmlKDrOPvFwg==
|
||||
dependencies:
|
||||
buffer "4.9.2"
|
||||
events "1.1.1"
|
||||
ieee754 "1.1.13"
|
||||
jmespath "0.15.0"
|
||||
querystring "0.2.0"
|
||||
sax "1.2.1"
|
||||
url "0.10.3"
|
||||
uuid "3.3.2"
|
||||
xml2js "0.4.19"
|
||||
|
||||
aws-sign2@~0.7.0:
|
||||
version "0.7.0"
|
||||
resolved "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
|
||||
@@ -8564,7 +8579,7 @@ buffer-xor@^1.0.3:
|
||||
resolved "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9"
|
||||
integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=
|
||||
|
||||
buffer@^4.3.0:
|
||||
buffer@4.9.2, buffer@^4.3.0:
|
||||
version "4.9.2"
|
||||
resolved "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8"
|
||||
integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==
|
||||
@@ -11980,6 +11995,11 @@ eventemitter3@^4.0.0:
|
||||
resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.0.tgz#d65176163887ee59f386d64c82610b696a4a74eb"
|
||||
integrity sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg==
|
||||
|
||||
events@1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
|
||||
integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=
|
||||
|
||||
events@3.1.0, events@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.npmjs.org/events/-/events-3.1.0.tgz#84279af1b34cb75aa88bf5ff291f6d0bd9b31a59"
|
||||
@@ -14238,6 +14258,11 @@ identity-obj-proxy@3.0.0:
|
||||
dependencies:
|
||||
harmony-reflect "^1.4.6"
|
||||
|
||||
ieee754@1.1.13:
|
||||
version "1.1.13"
|
||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
|
||||
integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==
|
||||
|
||||
ieee754@^1.1.13, ieee754@^1.1.4, ieee754@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
||||
@@ -15657,6 +15682,11 @@ jest@^26.0.1:
|
||||
import-local "^3.0.2"
|
||||
jest-cli "^26.6.3"
|
||||
|
||||
jmespath@0.15.0:
|
||||
version "0.15.0"
|
||||
resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217"
|
||||
integrity sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=
|
||||
|
||||
jose@^1.27.1:
|
||||
version "1.27.1"
|
||||
resolved "https://registry.npmjs.org/jose/-/jose-1.27.1.tgz#a1de2ecb5b3ae1ae28f0d9d0cc536349ada27ec8"
|
||||
@@ -21916,6 +21946,11 @@ sanitize-html@^1.27.0:
|
||||
srcset "^2.0.1"
|
||||
xtend "^4.0.1"
|
||||
|
||||
sax@1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a"
|
||||
integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o=
|
||||
|
||||
sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
||||
@@ -24422,6 +24457,14 @@ url-parse@^1.4.3, url-parse@^1.4.7:
|
||||
querystringify "^2.1.1"
|
||||
requires-port "^1.0.0"
|
||||
|
||||
url@0.10.3:
|
||||
version "0.10.3"
|
||||
resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64"
|
||||
integrity sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=
|
||||
dependencies:
|
||||
punycode "1.3.2"
|
||||
querystring "0.2.0"
|
||||
|
||||
url@^0.11.0, url@~0.11.0:
|
||||
version "0.11.0"
|
||||
resolved "https://registry.npmjs.org/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
|
||||
@@ -24520,6 +24563,11 @@ utils-merge@1.0.1, utils-merge@1.x.x:
|
||||
resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
|
||||
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
|
||||
|
||||
uuid@3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
|
||||
integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
|
||||
|
||||
uuid@^3.0.1, uuid@^3.1.0, uuid@^3.3.2, uuid@^3.4.0:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
|
||||
@@ -25251,6 +25299,14 @@ xml-name-validator@^3.0.0:
|
||||
resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
|
||||
integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==
|
||||
|
||||
xml2js@0.4.19:
|
||||
version "0.4.19"
|
||||
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7"
|
||||
integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==
|
||||
dependencies:
|
||||
sax ">=0.6.0"
|
||||
xmlbuilder "~9.0.1"
|
||||
|
||||
xml2js@0.4.x:
|
||||
version "0.4.23"
|
||||
resolved "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66"
|
||||
@@ -25264,6 +25320,11 @@ xmlbuilder@^11.0.0, xmlbuilder@~11.0.0:
|
||||
resolved "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3"
|
||||
integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==
|
||||
|
||||
xmlbuilder@~9.0.1:
|
||||
version "9.0.7"
|
||||
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d"
|
||||
integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=
|
||||
|
||||
xmlchars@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
|
||||
|
||||
Reference in New Issue
Block a user