Merge remote-tracking branch 'upstream/master' into mcalus3/add-catalog-import-plugin

This commit is contained in:
Marek Calus
2020-10-28 20:49:17 +01:00
279 changed files with 2599 additions and 2483 deletions
+11
View File
@@ -0,0 +1,11 @@
---
'@backstage/catalog-model': minor
'@backstage/plugin-catalog-backend': minor
---
Changes the various kind policies into a new type `KindValidator`.
Adds `CatalogProcessor#validateEntityKind` that makes use of the above
validators. This moves entity schema validity checking away from entity
policies and into processors, centralizing the extension points into the
processor chain.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Fix `CatalogBuilder#addProcessor`.
+11
View File
@@ -0,0 +1,11 @@
---
'@backstage/plugin-catalog-backend': patch
---
Add support for `fields` sub-selection of just parts of an entity when listing
entities in the catalog backend.
Example: `.../entities?fields=metadata.name,spec.type` will return partial
entity objects with only those exact fields present and the rest cut out.
Fields do not have to be simple scalars - you can for example do
`fields=metadata`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Added .fromConfig static factories for Preparers and Publishers + read integrations config to support url location types
@@ -0,0 +1,5 @@
---
'@backstage/plugin-cost-insights': patch
---
prefer named exports
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': minor
---
Removed the parseData step from catalog processors. Locations readers should emit full entities instead.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-user-settings': minor
---
Add settings button to sidebar
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
The CodeOwnersProcessor now handles 'url' locations
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': minor
---
Removed support for deprecated `catalog.providers` config that have been moved to `integrations`
+10
View File
@@ -0,0 +1,10 @@
---
'@backstage/plugin-catalog': minor
---
The URL path for a catalog entity has changed,
- from: `/catalog/:kind/:optionalNamespaceAndName`
- to: `/catalog/:namespace/:kind/:name`
Redirects are in place, so disruptions for users should not happen.
+9
View File
@@ -0,0 +1,9 @@
---
'example-app': patch
'@backstage/core-api': patch
'@backstage/plugin-cost-insights': patch
---
Remove cost insights example client from demo app and export from plugin
Create cost insights dev plugin using example client
Make PluginConfig and dependent types public
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog': patch
---
update the EntityNotFound component
+10
View File
@@ -0,0 +1,10 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Added support for configuring the working directory of the Scaffolder:
```yaml
backend:
workingDirectory: /some-dir # Use this to configure a working directory for the scaffolder, defaults to the OS temp-dir
```
+5
View File
@@ -0,0 +1,5 @@
---
'example-app': patch
---
cleaning up because external plugins have already implemented new api for creating
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-techdocs': patch
---
While techdocs fetches site name and metadata for the component, the page title was displayed as '[object Object] | Backstage'. This has now been fixed to display the component ID if site name is not present or being fetched.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Introduce PreparerOptions for PreparerBase
+1
View File
@@ -3,6 +3,7 @@
registry "https://registry.npmjs.org/"
disable-self-update-check true
lastUpdateCheck 1580389148099
yarn-path ".yarn/releases/yarn-1.22.1.js"
network-timeout 600000
+4 -2
View File
@@ -6,6 +6,10 @@ If you encounter issues while upgrading to a newer version, don't hesitate to re
## Next Release
> Collect changes for the next release below
## v0.1.1-alpha.26
### @backstage/cli
- Configuration files are no longer selected through `APP_ENV` or `NODE_ENV`. The default configuration files are `app-config.yaml` and, fix it exists, `app-config.local.yaml` in the repo root. To load a different set of files, use `--config <path>` arguments.
@@ -16,8 +20,6 @@ If you encounter issues while upgrading to a newer version, don't hesitate to re
## v0.1.1-alpha.25
> Collect changes for the next release below
### @backstage/cli
- The recommended way to set the configuration environment is now to use `APP_ENV` instead of `NODE_ENV`.
+1 -1
View File
@@ -16,6 +16,7 @@ backend:
credentials: true
csp:
connect-src: ["'self'", 'http:', 'https:']
# workingDirectory: /tmp # Use this to configure a working direcotry for the scaffolder, defaults to the OS temp-dir
# See README.md in the proxy-backend plugin for information on the configuration format
proxy:
@@ -156,7 +157,6 @@ catalog:
scaffolder:
github:
host: https://github.com
token:
$env: GITHUB_TOKEN
visibility: public # or 'internal' or 'private'
@@ -0,0 +1,58 @@
# Running the backend behind a Corporate Proxy
Let's admit it, we've all been there. Sometimes you've gotta run stuff with no way out to the public internet, only the smallest of corporate proxy tunnels.
Whilst this isn't supported natively by Backstage, this might help you get your installation up and running making calls through the said proxy tunnel.
Unfortunately, `nodejs` does not respect `HTTP(S)_PROXY` environment variables by default, and the library that we use to provide `fetch` functionality `node-fetch` (provided by `cross-fetch`) does not also respect these environment variables.
There are however some ways to get this to work without too much effort. It's most likely that you're going to run into these issues from the `backend` part of `backstage` as that's the part that isn't helped by your browser or OS's settings for the corporate proxy.
**Note:** You're gonna want to be in your backend working directory for these solutions as that's where the requests come from that don't go through this proxy.
### Using `global-agent`
1. Install `global-agent` using `yarn install global-agent`
2. Go to the entry file for the backend (`src/index.ts`)
3. At the top of the file paste the following:
```ts
import 'global-agent/bootstrap';
```
4. Start the backend with the `global-agent` variables
```sh
export GLOBAL_AGENT_HTTP_PROXY=$HTTP_PROXY
yarn start
```
More information and more options for configuring `global-agent` including just using the default environment variables can be found here: https://github.com/gajus/global-agent
### Using `proxy-agent`
`proxy-agent` is a library that you can use to override the `globalAgents` of `node` land with a tunnel to use for each request.
1. Install `proxy-agent` using `yarn install proxy-agent`
2. Go to the entry file for the backend (`src/index.ts`)
3. At the top of the file paste the following:
```ts
import ProxyAgent from 'proxy-agent';
import http from 'http';
import https from 'https';
/*
Something to note here, this might need different configuration depending on your own setup.
If you only have an http_proxy then you'll need to set that as both the http and https globalAgent instead.
*/
if (process.env.HTTP_PROXY) {
http.globalAgent = new ProxyAgent(process.env.HTTP_PROXY);
}
if (process.env.HTTPS_PROXY) {
https.globalAgent = new ProxyAgent(process.env.HTTPS_PROXY);
}
```
4. Start the backend with `yarn start`
@@ -111,44 +111,8 @@ export default async function createPlugin({
templaters.register('cookiecutter', cookiecutterTemplater);
templaters.register('cra', craTemplater);
const filePreparer = new FilePreparer();
const githubPreparer = new GithubPreparer();
const gitlabPreparer = new GitlabPreparer(config);
const preparers = new Preparers();
preparers.register('file', filePreparer);
preparers.register('github', githubPreparer);
preparers.register('gitlab', gitlabPreparer);
preparers.register('gitlab/api', gitlabPreparer);
const publishers = new Publishers();
const githubToken = config.getString('scaffolder.github.token');
const repoVisibility = config.getString(
'scaffolder.github.visibility',
) as RepoVisibilityOptions;
const githubClient = new Octokit({ auth: githubToken });
const githubPublisher = new GithubPublisher({
client: githubClient,
token: githubToken,
repoVisibility,
});
publishers.register('file', githubPublisher);
publishers.register('github', githubPublisher);
const gitLabConfig = config.getOptionalConfig('scaffolder.gitlab.api');
if (gitLabConfig) {
const gitLabToken = gitLabConfig.getString('token');
const gitLabClient = new Gitlab({
host: gitLabConfig.getOptionalString('baseUrl'),
token: gitLabToken,
});
const gitLabPublisher = new GitlabPublisher(gitLabClient, gitLabToken);
publishers.register('gitlab', gitLabPublisher);
publishers.register('gitlab/api', gitLabPublisher);
}
const preparers = await Preparers.fromConfig(config, { logger });
const publishers = await Publishers.fromConfig(config, { logger });
const dockerClient = new Docker();
return await createRouter({
+1 -1
View File
@@ -2,5 +2,5 @@
"packages": ["packages/*", "plugins/*"],
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.1.1-alpha.25"
"version": "0.1.1-alpha.26"
}
+1 -1
View File
@@ -14,7 +14,7 @@
"rename-version": "docusaurus-rename-version"
},
"devDependencies": {
"@spotify/prettier-config": "^8.0.0",
"@spotify/prettier-config": "^9.0.0",
"docusaurus": "^2.0.0-alpha.66",
"js-yaml": "^3.14.0",
"prettier": "^2.0.5"
+4 -4
View File
@@ -959,10 +959,10 @@
resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd"
integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==
"@spotify/prettier-config@^8.0.0":
version "8.0.0"
resolved "https://registry.npmjs.org/@spotify/prettier-config/-/prettier-config-8.0.0.tgz#8b6c2bd579ddc54887155a0721fe04e96c89f7f2"
integrity sha512-so8w32ZV42CHWxOEXcBtbNO/hLXFrQNXVmhfzhUI6dVB9cq2xjRaiqu8GjFj8LvKbWpPj+S+KwTIS4aDVWqrFQ==
"@spotify/prettier-config@^9.0.0":
version "9.0.0"
resolved "https://registry.yarnpkg.com/@spotify/prettier-config/-/prettier-config-9.0.0.tgz#7b562d56573c6fc0094446fbc92b22bc318945dc"
integrity sha512-In1q0tIiqTYKAGe3KOHDcFDdZRFISyQeSeipeTHGfki23ebHRZcjxvqj5SSdBkw65D4VpSREMi0s9i5iJiMcTw==
"@types/cheerio@^0.22.8":
version "0.22.21"
+29 -31
View File
@@ -1,44 +1,44 @@
{
"name": "example-app",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"private": true,
"bundled": true,
"dependencies": {
"@backstage/catalog-model": "^0.1.1-alpha.25",
"@backstage/cli": "^0.1.1-alpha.25",
"@backstage/core": "^0.1.1-alpha.25",
"@backstage/plugin-api-docs": "^0.1.1-alpha.25",
"@backstage/plugin-catalog": "^0.1.1-alpha.25",
"@backstage/plugin-circleci": "^0.1.1-alpha.25",
"@backstage/plugin-cloudbuild": "^0.1.1-alpha.25",
"@backstage/plugin-cost-insights": "^0.1.1-alpha.25",
"@backstage/plugin-explore": "^0.1.1-alpha.25",
"@backstage/plugin-gcp-projects": "^0.1.1-alpha.25",
"@backstage/plugin-github-actions": "^0.1.1-alpha.25",
"@backstage/plugin-gitops-profiles": "^0.1.1-alpha.25",
"@backstage/plugin-graphiql": "^0.1.1-alpha.25",
"@backstage/plugin-jenkins": "^0.1.1-alpha.25",
"@backstage/plugin-kubernetes": "^0.1.1-alpha.25",
"@backstage/plugin-lighthouse": "^0.1.1-alpha.25",
"@backstage/plugin-newrelic": "^0.1.1-alpha.25",
"@backstage/plugin-register-component": "^0.1.1-alpha.25",
"@backstage/plugin-rollbar": "^0.1.1-alpha.25",
"@backstage/plugin-scaffolder": "^0.1.1-alpha.25",
"@backstage/plugin-sentry": "^0.1.1-alpha.25",
"@backstage/plugin-tech-radar": "^0.1.1-alpha.25",
"@backstage/plugin-techdocs": "^0.1.1-alpha.25",
"@backstage/plugin-user-settings": "^0.1.1-alpha.25",
"@backstage/plugin-welcome": "^0.1.1-alpha.25",
"@backstage/test-utils": "^0.1.1-alpha.25",
"@backstage/theme": "^0.1.1-alpha.25",
"@backstage/catalog-model": "^0.1.1-alpha.26",
"@backstage/cli": "^0.1.1-alpha.26",
"@backstage/core": "^0.1.1-alpha.26",
"@backstage/plugin-api-docs": "^0.1.1-alpha.26",
"@backstage/plugin-catalog": "^0.1.1-alpha.26",
"@backstage/plugin-circleci": "^0.1.1-alpha.26",
"@backstage/plugin-cloudbuild": "^0.1.1-alpha.26",
"@backstage/plugin-cost-insights": "^0.1.1-alpha.26",
"@backstage/plugin-explore": "^0.1.1-alpha.26",
"@backstage/plugin-gcp-projects": "^0.1.1-alpha.26",
"@backstage/plugin-github-actions": "^0.1.1-alpha.26",
"@backstage/plugin-gitops-profiles": "^0.1.1-alpha.26",
"@backstage/plugin-graphiql": "^0.1.1-alpha.26",
"@backstage/plugin-jenkins": "^0.1.1-alpha.26",
"@backstage/plugin-kubernetes": "^0.1.1-alpha.26",
"@backstage/plugin-lighthouse": "^0.1.1-alpha.26",
"@backstage/plugin-newrelic": "^0.1.1-alpha.26",
"@backstage/plugin-register-component": "^0.1.1-alpha.26",
"@backstage/plugin-rollbar": "^0.1.1-alpha.26",
"@backstage/plugin-scaffolder": "^0.1.1-alpha.26",
"@backstage/plugin-sentry": "^0.1.1-alpha.26",
"@backstage/plugin-tech-radar": "^0.1.1-alpha.26",
"@backstage/plugin-techdocs": "^0.1.1-alpha.26",
"@backstage/plugin-user-settings": "^0.1.1-alpha.26",
"@backstage/plugin-welcome": "^0.1.1-alpha.26",
"@backstage/test-utils": "^0.1.1-alpha.26",
"@backstage/theme": "^0.1.1-alpha.26",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@octokit/rest": "^18.0.0",
"@roadiehq/backstage-plugin-github-insights": "^0.2.7",
"@roadiehq/backstage-plugin-github-pull-requests": "^0.5.2",
"@roadiehq/backstage-plugin-travis-ci": "^0.2.3",
"@roadiehq/backstage-plugin-catalog-import": "^0.1.0",
"dayjs": "^1.9.1",
"@roadiehq/backstage-plugin-travis-ci": "^0.2.5",
"history": "^5.0.0",
"prop-types": "^15.7.2",
"react": "^16.12.0",
@@ -47,7 +47,6 @@
"react-router": "6.0.0-beta.0",
"react-router-dom": "6.0.0-beta.0",
"react-use": "^15.3.3",
"regression": "^2.0.1",
"zen-observable": "^0.8.15"
},
"devDependencies": {
@@ -59,7 +58,6 @@
"@types/jquery": "^3.3.34",
"@types/node": "^12.0.0",
"@types/react-dom": "^16.9.8",
"@types/regression": "^2.0.0",
"@types/zen-observable": "^0.8.0",
"cross-env": "^7.0.0",
"cypress": "^4.2.0",
+4 -2
View File
@@ -25,8 +25,10 @@ import {
GraphQLEndpoints,
} from '@backstage/plugin-graphiql';
import { costInsightsApiRef } from '@backstage/plugin-cost-insights';
import { ExampleCostInsightsClient } from './plugins/cost-insights';
import {
costInsightsApiRef,
ExampleCostInsightsClient,
} from '@backstage/plugin-cost-insights';
export const apis = [
createApiFactory({
+6 -6
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-common",
"description": "Common functionality library for Backstage backends",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
@@ -29,10 +29,10 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/cli-common": "^0.1.1-alpha.25",
"@backstage/config": "^0.1.1-alpha.25",
"@backstage/config-loader": "^0.1.1-alpha.25",
"@backstage/test-utils": "^0.1.1-alpha.25",
"@backstage/cli-common": "^0.1.1-alpha.26",
"@backstage/config": "^0.1.1-alpha.26",
"@backstage/config-loader": "^0.1.1-alpha.26",
"@backstage/test-utils": "^0.1.1-alpha.26",
"@types/cors": "^2.8.6",
"@types/express": "^4.17.6",
"compression": "^1.7.4",
@@ -62,7 +62,7 @@
}
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.25",
"@backstage/cli": "^0.1.1-alpha.26",
"@types/compression": "^1.7.0",
"@types/http-errors": "^1.6.3",
"@types/minimist": "^1.2.0",
+16 -16
View File
@@ -1,6 +1,6 @@
{
"name": "example-backend",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"private": true,
@@ -18,24 +18,24 @@
"migrate:create": "knex migrate:make -x ts"
},
"dependencies": {
"@backstage/backend-common": "^0.1.1-alpha.25",
"@backstage/catalog-model": "^0.1.1-alpha.25",
"@backstage/config": "^0.1.1-alpha.25",
"@backstage/plugin-app-backend": "^0.1.1-alpha.25",
"@backstage/plugin-auth-backend": "^0.1.1-alpha.25",
"@backstage/plugin-catalog-backend": "^0.1.1-alpha.25",
"@backstage/plugin-graphql-backend": "^0.1.1-alpha.25",
"@backstage/plugin-kubernetes-backend": "^0.1.1-alpha.25",
"@backstage/plugin-proxy-backend": "^0.1.1-alpha.25",
"@backstage/plugin-rollbar-backend": "^0.1.1-alpha.25",
"@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.25",
"@backstage/plugin-sentry-backend": "^0.1.1-alpha.25",
"@backstage/plugin-techdocs-backend": "^0.1.1-alpha.25",
"@backstage/backend-common": "^0.1.1-alpha.26",
"@backstage/catalog-model": "^0.1.1-alpha.26",
"@backstage/config": "^0.1.1-alpha.26",
"@backstage/plugin-app-backend": "^0.1.1-alpha.26",
"@backstage/plugin-auth-backend": "^0.1.1-alpha.26",
"@backstage/plugin-catalog-backend": "^0.1.1-alpha.26",
"@backstage/plugin-graphql-backend": "^0.1.1-alpha.26",
"@backstage/plugin-kubernetes-backend": "^0.1.1-alpha.26",
"@backstage/plugin-proxy-backend": "^0.1.1-alpha.26",
"@backstage/plugin-rollbar-backend": "^0.1.1-alpha.26",
"@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.26",
"@backstage/plugin-sentry-backend": "^0.1.1-alpha.26",
"@backstage/plugin-techdocs-backend": "^0.1.1-alpha.26",
"@gitbeaker/node": "^23.5.0",
"@octokit/rest": "^18.0.0",
"azure-devops-node-api": "^10.1.1",
"dockerode": "^3.2.0",
"example-app": "^0.1.1-alpha.25",
"example-app": "^0.1.1-alpha.26",
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
"knex": "^0.21.1",
@@ -45,7 +45,7 @@
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.25",
"@backstage/cli": "^0.1.1-alpha.26",
"@types/dockerode": "^2.5.32",
"@types/express": "^4.17.6",
"@types/express-serve-static-core": "^4.17.5",
+4 -114
View File
@@ -17,22 +17,11 @@
import {
CookieCutter,
createRouter,
FilePreparer,
GithubPreparer,
GitlabPreparer,
AzurePreparer,
Preparers,
Publishers,
GithubPublisher,
GitlabPublisher,
AzurePublisher,
CreateReactAppTemplater,
Templaters,
RepoVisibilityOptions,
} from '@backstage/plugin-scaffolder-backend';
import { Octokit } from '@octokit/rest';
import { Gitlab } from '@gitbeaker/node';
import { getPersonalAccessTokenHandler, WebApi } from 'azure-devops-node-api';
import type { PluginEnvironment } from '../types';
import Docker from 'dockerode';
@@ -46,116 +35,17 @@ export default async function createPlugin({
templaters.register('cookiecutter', cookiecutterTemplater);
templaters.register('cra', craTemplater);
const filePreparer = new FilePreparer();
const gitlabPreparer = new GitlabPreparer(config);
const azurePreparer = new AzurePreparer(config);
const preparers = new Preparers();
preparers.register('file', filePreparer);
preparers.register('gitlab', gitlabPreparer);
preparers.register('gitlab/api', gitlabPreparer);
preparers.register('azure/api', azurePreparer);
const publishers = new Publishers();
const githubConfig = config.getOptionalConfig('scaffolder.github');
if (githubConfig) {
try {
const repoVisibility = githubConfig.getString(
'visibility',
) as RepoVisibilityOptions;
const githubToken = githubConfig.getString('token');
const githubHost =
githubConfig.getOptionalString('host') ?? 'https://github.com';
const githubClient = new Octokit({
auth: githubToken,
baseUrl: githubHost,
});
const githubPublisher = new GithubPublisher({
client: githubClient,
token: githubToken,
repoVisibility,
});
const githubPreparer = new GithubPreparer({ token: githubToken });
preparers.register('github', githubPreparer);
publishers.register('file', githubPublisher);
publishers.register('github', githubPublisher);
} catch (e) {
const providerName = 'github';
if (process.env.NODE_ENV !== 'development') {
throw new Error(
`Failed to initialize ${providerName} scaffolding provider, ${e.message}`,
);
}
logger.warn(
`Skipping ${providerName} scaffolding provider, ${e.message}`,
);
}
}
const gitLabConfig = config.getOptionalConfig('scaffolder.gitlab.api');
if (gitLabConfig) {
try {
const gitLabToken = gitLabConfig.getString('token');
const gitLabClient = new Gitlab({
host: gitLabConfig.getOptionalString('baseUrl'),
token: gitLabToken,
});
const gitLabPublisher = new GitlabPublisher(gitLabClient, gitLabToken);
publishers.register('gitlab', gitLabPublisher);
publishers.register('gitlab/api', gitLabPublisher);
} catch (e) {
const providerName = 'gitlab';
if (process.env.NODE_ENV !== 'development') {
throw new Error(
`Failed to initialize ${providerName} scaffolding provider, ${e.message}`,
);
}
logger.warn(
`Skipping ${providerName} scaffolding provider, ${e.message}`,
);
}
}
const azureConfig = config.getOptionalConfig('scaffolder.azure');
if (azureConfig) {
try {
const baseUrl = azureConfig.getString('baseUrl');
const azureToken = azureConfig.getConfig('api').getString('token');
const authHandler = getPersonalAccessTokenHandler(azureToken);
const webApi = new WebApi(baseUrl, authHandler);
const azureClient = await webApi.getGitApi();
const azurePublisher = new AzurePublisher(azureClient, azureToken);
publishers.register('azure/api', azurePublisher);
} catch (e) {
const providerName = 'azure';
if (process.env.NODE_ENV !== 'development') {
throw new Error(
`Failed to initialize ${providerName} scaffolding provider, ${e.message}`,
);
}
logger.warn(
`Skipping ${providerName} scaffolding provider, ${e.message}`,
);
}
}
const preparers = await Preparers.fromConfig(config, { logger });
const publishers = await Publishers.fromConfig(config, { logger });
const dockerClient = new Docker();
return await createRouter({
preparers,
templaters,
publishers,
logger,
config,
dockerClient,
});
}
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/catalog-model",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,7 +20,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/config": "^0.1.1-alpha.25",
"@backstage/config": "^0.1.1-alpha.26",
"@types/json-schema": "^7.0.5",
"@types/yup": "^0.29.8",
"json-schema": "^0.2.5",
@@ -29,7 +29,7 @@
"yup": "^0.29.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.25",
"@backstage/cli": "^0.1.1-alpha.26",
"@types/express": "^4.17.6",
"@types/jest": "^26.0.7",
"@types/lodash": "^4.14.151",
@@ -16,10 +16,10 @@
import {
ApiEntityV1alpha1,
apiEntityV1alpha1Policy as policy,
apiEntityV1alpha1Validator as validator,
} from './ApiEntityV1alpha1';
describe('ApiV1alpha1Policy', () => {
describe('ApiV1alpha1Validator', () => {
let entity: ApiEntityV1alpha1;
beforeEach(() => {
@@ -75,81 +75,81 @@ components:
});
it('happy path: accepts valid data', async () => {
await expect(policy.enforce(entity)).resolves.toBe(entity);
await expect(validator.check(entity)).resolves.toBe(true);
});
it('silently accepts v1beta1 as well', async () => {
(entity as any).apiVersion = 'backstage.io/v1beta1';
await expect(policy.enforce(entity)).resolves.toBe(entity);
await expect(validator.check(entity)).resolves.toBe(true);
});
it('ignores unknown apiVersion', async () => {
(entity as any).apiVersion = 'backstage.io/v1beta0';
await expect(policy.enforce(entity)).resolves.toBeUndefined();
await expect(validator.check(entity)).resolves.toBe(false);
});
it('ignores unknown kind', async () => {
(entity as any).kind = 'Wizard';
await expect(policy.enforce(entity)).resolves.toBeUndefined();
await expect(validator.check(entity)).resolves.toBe(false);
});
it('rejects missing type', async () => {
delete (entity as any).spec.type;
await expect(policy.enforce(entity)).rejects.toThrow(/type/);
await expect(validator.check(entity)).rejects.toThrow(/type/);
});
it('rejects wrong type', async () => {
(entity as any).spec.type = 7;
await expect(policy.enforce(entity)).rejects.toThrow(/type/);
await expect(validator.check(entity)).rejects.toThrow(/type/);
});
it('rejects empty type', async () => {
(entity as any).spec.type = '';
await expect(policy.enforce(entity)).rejects.toThrow(/type/);
await expect(validator.check(entity)).rejects.toThrow(/type/);
});
it('rejects missing lifecycle', async () => {
delete (entity as any).spec.lifecycle;
await expect(policy.enforce(entity)).rejects.toThrow(/lifecycle/);
await expect(validator.check(entity)).rejects.toThrow(/lifecycle/);
});
it('rejects wrong lifecycle', async () => {
(entity as any).spec.lifecycle = 7;
await expect(policy.enforce(entity)).rejects.toThrow(/lifecycle/);
await expect(validator.check(entity)).rejects.toThrow(/lifecycle/);
});
it('rejects empty lifecycle', async () => {
(entity as any).spec.lifecycle = '';
await expect(policy.enforce(entity)).rejects.toThrow(/lifecycle/);
await expect(validator.check(entity)).rejects.toThrow(/lifecycle/);
});
it('rejects missing owner', async () => {
delete (entity as any).spec.owner;
await expect(policy.enforce(entity)).rejects.toThrow(/owner/);
await expect(validator.check(entity)).rejects.toThrow(/owner/);
});
it('rejects wrong owner', async () => {
(entity as any).spec.owner = 7;
await expect(policy.enforce(entity)).rejects.toThrow(/owner/);
await expect(validator.check(entity)).rejects.toThrow(/owner/);
});
it('rejects empty owner', async () => {
(entity as any).spec.owner = '';
await expect(policy.enforce(entity)).rejects.toThrow(/owner/);
await expect(validator.check(entity)).rejects.toThrow(/owner/);
});
it('rejects missing definition', async () => {
delete (entity as any).spec.definition;
await expect(policy.enforce(entity)).rejects.toThrow(/definition/);
await expect(validator.check(entity)).rejects.toThrow(/definition/);
});
it('rejects wrong definition', async () => {
(entity as any).spec.definition = 7;
await expect(policy.enforce(entity)).rejects.toThrow(/definition/);
await expect(validator.check(entity)).rejects.toThrow(/definition/);
});
it('rejects empty definition', async () => {
(entity as any).spec.definition = '';
await expect(policy.enforce(entity)).rejects.toThrow(/definition/);
await expect(validator.check(entity)).rejects.toThrow(/definition/);
});
});
@@ -16,7 +16,7 @@
import * as yup from 'yup';
import type { Entity } from '../entity/Entity';
import { schemaPolicy } from './util';
import { schemaValidator } from './util';
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
const KIND = 'API' as const;
@@ -45,4 +45,8 @@ export interface ApiEntityV1alpha1 extends Entity {
};
}
export const apiEntityV1alpha1Policy = schemaPolicy(KIND, API_VERSION, schema);
export const apiEntityV1alpha1Validator = schemaValidator(
KIND,
API_VERSION,
schema,
);
@@ -16,10 +16,10 @@
import {
ComponentEntityV1alpha1,
componentEntityV1alpha1Policy as policy,
componentEntityV1alpha1Validator as validator,
} from './ComponentEntityV1alpha1';
describe('ComponentV1alpha1Policy', () => {
describe('ComponentV1alpha1Validator', () => {
let entity: ComponentEntityV1alpha1;
beforeEach(() => {
@@ -39,86 +39,86 @@ describe('ComponentV1alpha1Policy', () => {
});
it('happy path: accepts valid data', async () => {
await expect(policy.enforce(entity)).resolves.toBe(entity);
await expect(validator.check(entity)).resolves.toBe(true);
});
it('silently accepts v1beta1 as well', async () => {
(entity as any).apiVersion = 'backstage.io/v1beta1';
await expect(policy.enforce(entity)).resolves.toBe(entity);
await expect(validator.check(entity)).resolves.toBe(true);
});
it('ignores unknown apiVersion', async () => {
(entity as any).apiVersion = 'backstage.io/v1beta0';
await expect(policy.enforce(entity)).resolves.toBeUndefined();
await expect(validator.check(entity)).resolves.toBe(false);
});
it('ignores unknown kind', async () => {
(entity as any).kind = 'Wizard';
await expect(policy.enforce(entity)).resolves.toBeUndefined();
await expect(validator.check(entity)).resolves.toBe(false);
});
it('rejects missing type', async () => {
delete (entity as any).spec.type;
await expect(policy.enforce(entity)).rejects.toThrow(/type/);
await expect(validator.check(entity)).rejects.toThrow(/type/);
});
it('rejects wrong type', async () => {
(entity as any).spec.type = 7;
await expect(policy.enforce(entity)).rejects.toThrow(/type/);
await expect(validator.check(entity)).rejects.toThrow(/type/);
});
it('rejects empty type', async () => {
(entity as any).spec.type = '';
await expect(policy.enforce(entity)).rejects.toThrow(/type/);
await expect(validator.check(entity)).rejects.toThrow(/type/);
});
it('rejects missing lifecycle', async () => {
delete (entity as any).spec.lifecycle;
await expect(policy.enforce(entity)).rejects.toThrow(/lifecycle/);
await expect(validator.check(entity)).rejects.toThrow(/lifecycle/);
});
it('rejects wrong lifecycle', async () => {
(entity as any).spec.lifecycle = 7;
await expect(policy.enforce(entity)).rejects.toThrow(/lifecycle/);
await expect(validator.check(entity)).rejects.toThrow(/lifecycle/);
});
it('rejects empty lifecycle', async () => {
(entity as any).spec.lifecycle = '';
await expect(policy.enforce(entity)).rejects.toThrow(/lifecycle/);
await expect(validator.check(entity)).rejects.toThrow(/lifecycle/);
});
it('rejects missing owner', async () => {
delete (entity as any).spec.owner;
await expect(policy.enforce(entity)).rejects.toThrow(/owner/);
await expect(validator.check(entity)).rejects.toThrow(/owner/);
});
it('rejects wrong owner', async () => {
(entity as any).spec.owner = 7;
await expect(policy.enforce(entity)).rejects.toThrow(/owner/);
await expect(validator.check(entity)).rejects.toThrow(/owner/);
});
it('rejects empty owner', async () => {
(entity as any).spec.owner = '';
await expect(policy.enforce(entity)).rejects.toThrow(/owner/);
await expect(validator.check(entity)).rejects.toThrow(/owner/);
});
it('accepts missing implementsApis', async () => {
delete (entity as any).spec.implementsApis;
await expect(policy.enforce(entity)).resolves.toBe(entity);
await expect(validator.check(entity)).resolves.toBe(true);
});
it('rejects empty implementsApis', async () => {
(entity as any).spec.implementsApis = [''];
await expect(policy.enforce(entity)).rejects.toThrow(/implementsApis/);
await expect(validator.check(entity)).rejects.toThrow(/implementsApis/);
});
it('rejects undefined implementsApis', async () => {
(entity as any).spec.implementsApis = [undefined];
await expect(policy.enforce(entity)).rejects.toThrow(/implementsApis/);
await expect(validator.check(entity)).rejects.toThrow(/implementsApis/);
});
it('accepts no implementsApis', async () => {
(entity as any).spec.implementsApis = [];
await expect(policy.enforce(entity)).resolves.toBe(entity);
await expect(validator.check(entity)).resolves.toBe(true);
});
});
@@ -16,7 +16,7 @@
import * as yup from 'yup';
import type { Entity } from '../entity/Entity';
import { schemaPolicy } from './util';
import { schemaValidator } from './util';
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
const KIND = 'Component' as const;
@@ -45,7 +45,7 @@ export interface ComponentEntityV1alpha1 extends Entity {
};
}
export const componentEntityV1alpha1Policy = schemaPolicy(
export const componentEntityV1alpha1Validator = schemaValidator(
KIND,
API_VERSION,
schema,
@@ -16,10 +16,10 @@
import {
GroupEntityV1alpha1,
groupEntityV1alpha1Policy as policy,
groupEntityV1alpha1Validator as validator,
} from './GroupEntityV1alpha1';
describe('GroupV1alpha1Policy', () => {
describe('GroupV1alpha1Validator', () => {
let entity: GroupEntityV1alpha1;
beforeEach(() => {
@@ -42,106 +42,106 @@ describe('GroupV1alpha1Policy', () => {
});
it('happy path: accepts valid data', async () => {
await expect(policy.enforce(entity)).resolves.toBe(entity);
await expect(validator.check(entity)).resolves.toBe(true);
});
it('silently accepts v1beta1 as well', async () => {
(entity as any).apiVersion = 'backstage.io/v1beta1';
await expect(policy.enforce(entity)).resolves.toBe(entity);
await expect(validator.check(entity)).resolves.toBe(true);
});
it('ignores unknown apiVersion', async () => {
(entity as any).apiVersion = 'backstage.io/v1beta0';
await expect(policy.enforce(entity)).resolves.toBeUndefined();
await expect(validator.check(entity)).resolves.toBe(false);
});
it('ignores unknown kind', async () => {
(entity as any).kind = 'Wizard';
await expect(policy.enforce(entity)).resolves.toBeUndefined();
await expect(validator.check(entity)).resolves.toBe(false);
});
it('rejects missing type', async () => {
delete (entity as any).spec.type;
await expect(policy.enforce(entity)).rejects.toThrow(/type/);
await expect(validator.check(entity)).rejects.toThrow(/type/);
});
it('rejects wrong type', async () => {
(entity as any).spec.type = 7;
await expect(policy.enforce(entity)).rejects.toThrow(/type/);
await expect(validator.check(entity)).rejects.toThrow(/type/);
});
it('rejects empty type', async () => {
(entity as any).spec.type = '';
await expect(policy.enforce(entity)).rejects.toThrow(/type/);
await expect(validator.check(entity)).rejects.toThrow(/type/);
});
it('accepts missing parent', async () => {
delete (entity as any).spec.parent;
await expect(policy.enforce(entity)).resolves.toBe(entity);
await expect(validator.check(entity)).resolves.toBe(true);
});
it('rejects empty parent', async () => {
(entity as any).spec.parent = '';
await expect(policy.enforce(entity)).rejects.toThrow(/parent/);
await expect(validator.check(entity)).rejects.toThrow(/parent/);
});
it('rejects missing ancestors', async () => {
delete (entity as any).spec.ancestors;
await expect(policy.enforce(entity)).rejects.toThrow(/ancestor/);
await expect(validator.check(entity)).rejects.toThrow(/ancestor/);
});
it('rejects empty ancestors', async () => {
(entity as any).spec.ancestors = [''];
await expect(policy.enforce(entity)).rejects.toThrow(/ancestor/);
await expect(validator.check(entity)).rejects.toThrow(/ancestor/);
});
it('rejects undefined ancestors', async () => {
(entity as any).spec.ancestors = [undefined];
await expect(policy.enforce(entity)).rejects.toThrow(/ancestor/);
await expect(validator.check(entity)).rejects.toThrow(/ancestor/);
});
it('accepts no ancestors', async () => {
(entity as any).spec.ancestors = [];
await expect(policy.enforce(entity)).resolves.toBe(entity);
await expect(validator.check(entity)).resolves.toBe(true);
});
it('rejects missing children', async () => {
delete (entity as any).spec.children;
await expect(policy.enforce(entity)).rejects.toThrow(/children/);
await expect(validator.check(entity)).rejects.toThrow(/children/);
});
it('rejects empty children', async () => {
(entity as any).spec.children = [''];
await expect(policy.enforce(entity)).rejects.toThrow(/children/);
await expect(validator.check(entity)).rejects.toThrow(/children/);
});
it('rejects undefined children', async () => {
(entity as any).spec.children = [undefined];
await expect(policy.enforce(entity)).rejects.toThrow(/children/);
await expect(validator.check(entity)).rejects.toThrow(/children/);
});
it('accepts no children', async () => {
(entity as any).spec.children = [];
await expect(policy.enforce(entity)).resolves.toBe(entity);
await expect(validator.check(entity)).resolves.toBe(true);
});
it('rejects missing descendants', async () => {
delete (entity as any).spec.descendants;
await expect(policy.enforce(entity)).rejects.toThrow(/descendants/);
await expect(validator.check(entity)).rejects.toThrow(/descendants/);
});
it('rejects empty descendants', async () => {
(entity as any).spec.descendants = [''];
await expect(policy.enforce(entity)).rejects.toThrow(/descendants/);
await expect(validator.check(entity)).rejects.toThrow(/descendants/);
});
it('rejects undefined descendants', async () => {
(entity as any).spec.descendants = [undefined];
await expect(policy.enforce(entity)).rejects.toThrow(/descendants/);
await expect(validator.check(entity)).rejects.toThrow(/descendants/);
});
it('accepts no descendants', async () => {
(entity as any).spec.descendants = [];
await expect(policy.enforce(entity)).resolves.toBe(entity);
await expect(validator.check(entity)).resolves.toBe(true);
});
});
@@ -16,7 +16,7 @@
import * as yup from 'yup';
import type { Entity } from '../entity/Entity';
import { schemaPolicy } from './util';
import { schemaValidator } from './util';
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
const KIND = 'Group' as const;
@@ -63,7 +63,7 @@ export interface GroupEntityV1alpha1 extends Entity {
};
}
export const groupEntityV1alpha1Policy = schemaPolicy(
export const groupEntityV1alpha1Validator = schemaValidator(
KIND,
API_VERSION,
schema,
@@ -16,10 +16,10 @@
import {
LocationEntityV1alpha1,
locationEntityV1alpha1Policy as policy,
locationEntityV1alpha1Validator as validator,
} from './LocationEntityV1alpha1';
describe('LocationV1alpha1Policy', () => {
describe('LocationV1alpha1Validator', () => {
let entity: LocationEntityV1alpha1;
beforeEach(() => {
@@ -36,53 +36,53 @@ describe('LocationV1alpha1Policy', () => {
});
it('happy path: accepts valid data', async () => {
await expect(policy.enforce(entity)).resolves.toBe(entity);
await expect(validator.check(entity)).resolves.toBe(true);
});
it('silently accepts v1beta1 as well', async () => {
(entity as any).apiVersion = 'backstage.io/v1beta1';
await expect(policy.enforce(entity)).resolves.toBe(entity);
await expect(validator.check(entity)).resolves.toBe(true);
});
it('ignores unknown apiVersion', async () => {
(entity as any).apiVersion = 'backstage.io/v1beta0';
await expect(policy.enforce(entity)).resolves.toBeUndefined();
await expect(validator.check(entity)).resolves.toBe(false);
});
it('ignores unknown kind', async () => {
(entity as any).kind = 'Wizard';
await expect(policy.enforce(entity)).resolves.toBeUndefined();
await expect(validator.check(entity)).resolves.toBe(false);
});
it('rejects missing type', async () => {
delete (entity as any).spec.type;
await expect(policy.enforce(entity)).rejects.toThrow(/type/);
await expect(validator.check(entity)).rejects.toThrow(/type/);
});
it('rejects wrong type', async () => {
(entity as any).spec.type = 7;
await expect(policy.enforce(entity)).rejects.toThrow(/type/);
await expect(validator.check(entity)).rejects.toThrow(/type/);
});
it('rejects empty type', async () => {
(entity as any).spec.type = '';
await expect(policy.enforce(entity)).rejects.toThrow(/type/);
await expect(validator.check(entity)).rejects.toThrow(/type/);
});
it('accepts good target', async () => {
(entity as any).spec.target =
'https://github.com/spotify/backstage/blob/master/plugins/catalog-backend/examples/artist-lookup-component.yaml';
await expect(policy.enforce(entity)).resolves.toBe(entity);
await expect(validator.check(entity)).resolves.toBe(true);
});
it('rejects wrong target', async () => {
(entity as any).spec.target = 7;
await expect(policy.enforce(entity)).rejects.toThrow(/target/);
await expect(validator.check(entity)).rejects.toThrow(/target/);
});
it('rejects empty target', async () => {
(entity as any).spec.target = '';
await expect(policy.enforce(entity)).rejects.toThrow(/target/);
await expect(validator.check(entity)).rejects.toThrow(/target/);
});
it('accepts good targets', async () => {
@@ -90,16 +90,16 @@ describe('LocationV1alpha1Policy', () => {
'https://github.com/spotify/backstage/blob/master/plugins/catalog-backend/examples/artist-lookup-component.yaml',
'https://github.com/spotify/backstage/blob/master/plugins/catalog-backend/examples/playback-order-component.yaml',
];
await expect(policy.enforce(entity)).resolves.toBe(entity);
await expect(validator.check(entity)).resolves.toBe(true);
});
it('accepts empty targets', async () => {
(entity as any).spec.targets = [];
await expect(policy.enforce(entity)).resolves.toBe(entity);
await expect(validator.check(entity)).resolves.toBe(true);
});
it('rejects wrong targets', async () => {
(entity as any).spec.targets = 7;
await expect(policy.enforce(entity)).rejects.toThrow(/targets/);
await expect(validator.check(entity)).rejects.toThrow(/targets/);
});
});
@@ -16,7 +16,7 @@
import * as yup from 'yup';
import type { Entity } from '../entity/Entity';
import { schemaPolicy } from './util';
import { schemaValidator } from './util';
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
const KIND = 'Location' as const;
@@ -43,7 +43,7 @@ export interface LocationEntityV1alpha1 extends Entity {
};
}
export const locationEntityV1alpha1Policy = schemaPolicy(
export const locationEntityV1alpha1Validator = schemaValidator(
KIND,
API_VERSION,
schema,
@@ -16,10 +16,10 @@
import {
TemplateEntityV1alpha1,
templateEntityV1alpha1Policy as policy,
templateEntityV1alpha1Validator as validator,
} from './TemplateEntityV1alpha1';
describe('templateEntityV1alpha1', () => {
describe('templateEntityV1alpha1Validator', () => {
let entity: TemplateEntityV1alpha1;
beforeEach(() => {
@@ -53,41 +53,41 @@ describe('templateEntityV1alpha1', () => {
});
it('happy path: accepts valid data', async () => {
await expect(policy.enforce(entity)).resolves.toBe(entity);
await expect(validator.check(entity)).resolves.toBe(true);
});
it('silently accepts v1beta1 as well', async () => {
(entity as any).apiVersion = 'backstage.io/v1beta1';
await expect(policy.enforce(entity)).resolves.toBe(entity);
await expect(validator.check(entity)).resolves.toBe(true);
});
it('ignores unknown apiVersion', async () => {
(entity as any).apiVersion = 'backstage.io/v1beta0';
await expect(policy.enforce(entity)).resolves.toBeUndefined();
await expect(validator.check(entity)).resolves.toBe(false);
});
it('ignores unknown kind', async () => {
(entity as any).kind = 'Wizard';
await expect(policy.enforce(entity)).resolves.toBeUndefined();
await expect(validator.check(entity)).resolves.toBe(false);
});
it('rejects missing type', async () => {
delete (entity as any).spec.type;
await expect(policy.enforce(entity)).rejects.toThrow(/type/);
await expect(validator.check(entity)).rejects.toThrow(/type/);
});
it('accepts any other type', async () => {
(entity as any).spec.type = 'hallo';
await expect(policy.enforce(entity)).resolves.toBe(entity);
await expect(validator.check(entity)).resolves.toBe(true);
});
it('rejects empty type', async () => {
(entity as any).spec.type = '';
await expect(policy.enforce(entity)).rejects.toThrow(/type/);
await expect(validator.check(entity)).rejects.toThrow(/type/);
});
it('rejects missing templater', async () => {
(entity as any).spec.templater = '';
await expect(policy.enforce(entity)).rejects.toThrow(/templater/);
await expect(validator.check(entity)).rejects.toThrow(/templater/);
});
});
@@ -17,7 +17,7 @@
import * as yup from 'yup';
import type { Entity } from '../entity/Entity';
import type { JSONSchema } from '../types';
import { schemaPolicy } from './util';
import { schemaValidator } from './util';
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
const KIND = 'Template' as const;
@@ -46,7 +46,7 @@ export interface TemplateEntityV1alpha1 extends Entity {
};
}
export const templateEntityV1alpha1Policy = schemaPolicy(
export const templateEntityV1alpha1Validator = schemaValidator(
KIND,
API_VERSION,
schema,
@@ -16,10 +16,10 @@
import {
UserEntityV1alpha1,
userEntityV1alpha1Policy as policy,
userEntityV1alpha1Validator as validator,
} from './UserEntityV1alpha1';
describe('userEntityV1alpha1Policy', () => {
describe('userEntityV1alpha1Validator', () => {
let entity: UserEntityV1alpha1;
beforeEach(() => {
@@ -41,117 +41,117 @@ describe('userEntityV1alpha1Policy', () => {
});
it('happy path: accepts valid data', async () => {
await expect(policy.enforce(entity)).resolves.toBe(entity);
await expect(validator.check(entity)).resolves.toBe(true);
});
// root
it('silently accepts v1beta1 as well', async () => {
(entity as any).apiVersion = 'backstage.io/v1beta1';
await expect(policy.enforce(entity)).resolves.toBe(entity);
await expect(validator.check(entity)).resolves.toBe(true);
});
it('ignores unknown apiVersion', async () => {
(entity as any).apiVersion = 'backstage.io/v1beta0';
await expect(policy.enforce(entity)).resolves.toBeUndefined();
await expect(validator.check(entity)).resolves.toBe(false);
});
it('ignores unknown kind', async () => {
(entity as any).kind = 'Wizard';
await expect(policy.enforce(entity)).resolves.toBeUndefined();
await expect(validator.check(entity)).resolves.toBe(false);
});
it('spec accepts unknown additional fields', async () => {
(entity as any).spec.foo = 'data';
await expect(policy.enforce(entity)).resolves.toBe(entity);
await expect(validator.check(entity)).resolves.toBe(true);
});
// profile
it('accepts missing profile', async () => {
delete (entity as any).spec.profile;
await expect(policy.enforce(entity)).resolves.toBe(entity);
await expect(validator.check(entity)).resolves.toBe(true);
});
it('rejects wrong profile', async () => {
(entity as any).spec.profile = 7;
await expect(policy.enforce(entity)).rejects.toThrow(/profile/);
await expect(validator.check(entity)).rejects.toThrow(/profile/);
});
it('profile accepts missing displayName', async () => {
delete (entity as any).spec.profile.displayName;
await expect(policy.enforce(entity)).resolves.toBe(entity);
await expect(validator.check(entity)).resolves.toBe(true);
});
it('profile rejects wrong displayName', async () => {
(entity as any).spec.profile.displayName = 7;
await expect(policy.enforce(entity)).rejects.toThrow(/displayName/);
await expect(validator.check(entity)).rejects.toThrow(/displayName/);
});
it('profile rejects empty displayName', async () => {
(entity as any).spec.profile.displayName = '';
await expect(policy.enforce(entity)).rejects.toThrow(/displayName/);
await expect(validator.check(entity)).rejects.toThrow(/displayName/);
});
it('profile accepts missing email', async () => {
delete (entity as any).spec.profile.email;
await expect(policy.enforce(entity)).resolves.toBe(entity);
await expect(validator.check(entity)).resolves.toBe(true);
});
it('profile rejects wrong email', async () => {
(entity as any).spec.profile.email = 7;
await expect(policy.enforce(entity)).rejects.toThrow(/email/);
await expect(validator.check(entity)).rejects.toThrow(/email/);
});
it('profile rejects empty email', async () => {
(entity as any).spec.profile.email = '';
await expect(policy.enforce(entity)).rejects.toThrow(/email/);
await expect(validator.check(entity)).rejects.toThrow(/email/);
});
it('profile accepts missing picture', async () => {
delete (entity as any).spec.profile.picture;
await expect(policy.enforce(entity)).resolves.toBe(entity);
await expect(validator.check(entity)).resolves.toBe(true);
});
it('profile rejects wrong picture', async () => {
(entity as any).spec.profile.picture = 7;
await expect(policy.enforce(entity)).rejects.toThrow(/picture/);
await expect(validator.check(entity)).rejects.toThrow(/picture/);
});
it('profile rejects empty picture', async () => {
(entity as any).spec.profile.picture = '';
await expect(policy.enforce(entity)).rejects.toThrow(/picture/);
await expect(validator.check(entity)).rejects.toThrow(/picture/);
});
it('profile accepts unknown additional fields', async () => {
(entity as any).spec.profile.foo = 'data';
await expect(policy.enforce(entity)).resolves.toBe(entity);
await expect(validator.check(entity)).resolves.toBe(true);
});
// memberOf
it('rejects missing memberOf', async () => {
delete (entity as any).spec.memberOf;
await expect(policy.enforce(entity)).rejects.toThrow(/memberOf/);
await expect(validator.check(entity)).rejects.toThrow(/memberOf/);
});
it('rejects wrong memberOf', async () => {
(entity as any).spec.memberOf = 7;
await expect(policy.enforce(entity)).rejects.toThrow(/memberOf/);
await expect(validator.check(entity)).rejects.toThrow(/memberOf/);
});
it('rejects wrong memberOf item', async () => {
(entity as any).spec.memberOf[0] = 7;
await expect(policy.enforce(entity)).rejects.toThrow(/memberOf/);
await expect(validator.check(entity)).rejects.toThrow(/memberOf/);
});
it('accepts empty memberOf', async () => {
(entity as any).spec.memberOf = [];
await expect(policy.enforce(entity)).resolves.toBe(entity);
await expect(validator.check(entity)).resolves.toBe(true);
});
it('rejects null memberOf', async () => {
(entity as any).spec.memberOf = null;
await expect(policy.enforce(entity)).rejects.toThrow(/memberOf/);
await expect(validator.check(entity)).rejects.toThrow(/memberOf/);
});
});
@@ -16,7 +16,7 @@
import * as yup from 'yup';
import type { Entity } from '../entity/Entity';
import { schemaPolicy } from './util';
import { schemaValidator } from './util';
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
const KIND = 'User' as const;
@@ -59,4 +59,8 @@ export interface UserEntityV1alpha1 extends Entity {
};
}
export const userEntityV1alpha1Policy = schemaPolicy(KIND, API_VERSION, schema);
export const userEntityV1alpha1Validator = schemaValidator(
KIND,
API_VERSION,
schema,
);
+8 -7
View File
@@ -14,34 +14,35 @@
* limitations under the License.
*/
export { apiEntityV1alpha1Policy } from './ApiEntityV1alpha1';
export { apiEntityV1alpha1Validator } from './ApiEntityV1alpha1';
export type {
ApiEntityV1alpha1 as ApiEntity,
ApiEntityV1alpha1,
} from './ApiEntityV1alpha1';
export { componentEntityV1alpha1Policy } from './ComponentEntityV1alpha1';
export { componentEntityV1alpha1Validator } from './ComponentEntityV1alpha1';
export type {
ComponentEntityV1alpha1 as ComponentEntity,
ComponentEntityV1alpha1,
} from './ComponentEntityV1alpha1';
export { groupEntityV1alpha1Policy } from './GroupEntityV1alpha1';
export { groupEntityV1alpha1Validator } from './GroupEntityV1alpha1';
export type {
GroupEntityV1alpha1 as GroupEntity,
GroupEntityV1alpha1,
} from './GroupEntityV1alpha1';
export { locationEntityV1alpha1Policy } from './LocationEntityV1alpha1';
export { locationEntityV1alpha1Validator } from './LocationEntityV1alpha1';
export type {
LocationEntityV1alpha1 as LocationEntity,
LocationEntityV1alpha1,
} from './LocationEntityV1alpha1';
export { templateEntityV1alpha1Policy } from './TemplateEntityV1alpha1';
export * from './relations';
export { templateEntityV1alpha1Validator } from './TemplateEntityV1alpha1';
export type {
TemplateEntityV1alpha1 as TemplateEntity,
TemplateEntityV1alpha1,
} from './TemplateEntityV1alpha1';
export { userEntityV1alpha1Policy } from './UserEntityV1alpha1';
export type { KindValidator } from './types';
export { userEntityV1alpha1Validator } from './UserEntityV1alpha1';
export type {
UserEntityV1alpha1 as UserEntity,
UserEntityV1alpha1,
} from './UserEntityV1alpha1';
export * from './relations';
+34
View File
@@ -0,0 +1,34 @@
/*
* 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 { Entity } from '../entity';
/**
* Validates entities of a certain kind.
*/
export type KindValidator = {
/**
* Validates the entity as a known entity kind.
*
* @param entity The entity to validate
* @returns Resolves to true, if the entity was of a kind that was known and
* handled by this validator, and was found to be valid. Resolves to false,
* if the entity was not of a kind that was known by this validator.
* Rejects to an Error describing the problem, if the entity was of a kind
* that was known by this validator and was not valid.
*/
check(entity: Entity): Promise<boolean>;
};
+7 -7
View File
@@ -15,23 +15,23 @@
*/
import * as yup from 'yup';
import { Entity } from '../entity';
import { EntityPolicy } from '../types';
import { KindValidator } from './types';
export function schemaPolicy(
export function schemaValidator(
kind: string,
apiVersion: readonly string[],
schema: yup.Schema<any>,
): EntityPolicy {
): KindValidator {
return {
async enforce(envelope: Entity): Promise<Entity | undefined> {
async check(envelope) {
if (
kind !== envelope.kind ||
!apiVersion.includes(envelope.apiVersion as any)
) {
return undefined;
return false;
}
return await schema.validate(envelope, { strict: true });
await schema.validate(envelope, { strict: true });
return true;
},
};
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli-common",
"description": "Common functionality used by cli, backend, and create-app",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"private": false,
"main": "src/index.ts",
"types": "src/index.ts",
+11 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli",
"description": "CLI for developing Backstage plugins and apps",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"private": false,
"publishConfig": {
"access": "public"
@@ -28,13 +28,13 @@
"backstage-cli": "bin/backstage-cli"
},
"dependencies": {
"@backstage/cli-common": "^0.1.1-alpha.25",
"@backstage/config": "^0.1.1-alpha.25",
"@backstage/config-loader": "^0.1.1-alpha.25",
"@backstage/cli-common": "^0.1.1-alpha.26",
"@backstage/config": "^0.1.1-alpha.26",
"@backstage/config-loader": "^0.1.1-alpha.26",
"@hot-loader/react-dom": "^16.13.0",
"@lerna/package-graph": "^3.18.5",
"@lerna/project": "^3.18.0",
"@rollup/plugin-commonjs": "^13.0.0",
"@rollup/plugin-commonjs": "^16.0.0",
"@rollup/plugin-json": "^4.0.2",
"@rollup/plugin-node-resolve": "^9.0.0",
"@rollup/plugin-yaml": "^2.1.1",
@@ -108,6 +108,12 @@
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/backend-common": "^0.1.1-alpha.26",
"@backstage/config": "^0.1.1-alpha.26",
"@backstage/core": "^0.1.1-alpha.26",
"@backstage/dev-utils": "^0.1.1-alpha.26",
"@backstage/test-utils": "^0.1.1-alpha.26",
"@backstage/theme": "^0.1.1-alpha.26",
"@types/diff": "^4.0.2",
"@types/fs-extra": "^9.0.1",
"@types/html-webpack-plugin": "^3.2.2",
@@ -28,8 +28,8 @@ import {
getCodeownersFilePath,
} from '../../lib/codeowners';
import { paths } from '../../lib/paths';
import { packageVersions } from '../../lib/version';
import { Task, templatingTask } from '../../lib/tasks';
import { version as backstageVersion } from '../../lib/version';
const exec = promisify(execCb);
@@ -243,7 +243,7 @@ export default async (cmd: Command) => {
? paths.resolveTargetRoot('plugins', pluginId)
: paths.resolveTargetRoot(pluginId);
const ownerIds = parseOwnerIds(answers.owner);
const { version } = isMonoRepo
const { version: pluginVersion } = isMonoRepo
? await fs.readJson(paths.resolveTargetRoot('lerna.json'))
: { version: '0.1.0' };
@@ -259,14 +259,18 @@ export default async (cmd: Command) => {
Task.section('Preparing files');
await templatingTask(templateDir, tempDir, {
...answers,
version,
backstageVersion,
name,
privatePackage,
npmRegistry,
});
await templatingTask(
templateDir,
tempDir,
{
...answers,
pluginVersion,
name,
privatePackage,
npmRegistry,
},
packageVersions,
);
Task.section('Moving to final location');
await movePlugin(tempDir, pluginDir, pluginId);
@@ -276,7 +280,7 @@ export default async (cmd: Command) => {
if ((await fs.pathExists(appPackage)) && !cmd.backend) {
Task.section('Adding plugin as dependency in app');
await addPluginDependencyToApp(paths.targetRoot, name, version);
await addPluginDependencyToApp(paths.targetRoot, name, pluginVersion);
Task.section('Import plugin in app');
await addPluginToApp(paths.targetRoot, pluginId, name);
+6 -14
View File
@@ -25,13 +25,12 @@ import {
yesPromptFunc,
} from '../../lib/diff';
import { paths } from '../../lib/paths';
import { version as backstageVersion } from '../../lib/version';
export type PluginData = {
id: string;
name: string;
privatePackage: string;
version: string;
pluginVersion: string;
npmRegistry: string;
};
@@ -40,17 +39,13 @@ const fileHandlers = [
patterns: ['package.json'],
handler: handlers.packageJson,
},
{
patterns: ['tsconfig.json'],
handler: handlers.exactMatch,
},
{
// make sure files in 1st level of src/ and dev/ exist
patterns: ['.eslintrc.js', /^(src|dev)\/[^/]+$/],
handler: handlers.exists,
},
{
patterns: ['README.md', /^src\//],
patterns: ['README.md', 'tsconfig.json', /^src\//],
handler: handlers.skip,
},
];
@@ -66,10 +61,7 @@ export default async (cmd: Command) => {
}
const data = await readPluginData();
const templateFiles = await diffTemplateFiles('default-plugin', {
backstageVersion,
...data,
});
const templateFiles = await diffTemplateFiles('default-plugin', data);
await handleAllFiles(fileHandlers, templateFiles, promptFunc);
await finalize();
};
@@ -78,13 +70,13 @@ export default async (cmd: Command) => {
async function readPluginData(): Promise<PluginData> {
let name: string;
let privatePackage: string;
let version: string;
let pluginVersion: string;
let npmRegistry: string;
try {
const pkg = require(paths.resolveTarget('package.json'));
name = pkg.name;
privatePackage = pkg.private;
version = pkg.version;
pluginVersion = pkg.version;
const scope = name.split('/')[0];
if (`${scope}:registry` in pkg.publishConfig) {
const registryURL = pkg.publishConfig[`${scope}:registry`];
@@ -106,5 +98,5 @@ async function readPluginData(): Promise<PluginData> {
const id = pluginIdMatch[1];
return { id, name, privatePackage, version, npmRegistry };
return { id, name, privatePackage, pluginVersion, npmRegistry };
}
+11 -1
View File
@@ -24,6 +24,7 @@ import handlebars from 'handlebars';
import recursiveReadDir from 'recursive-readdir';
import { paths } from '../paths';
import { FileDiff } from './types';
import { packageVersions } from '../../lib/version';
export type TemplatedFile = {
path: string;
@@ -40,7 +41,16 @@ async function readTemplateFile(
return contents;
}
return handlebars.compile(contents)(templateVars);
return handlebars.compile(contents)(templateVars, {
helpers: {
version(name: keyof typeof packageVersions) {
if (name in packageVersions) {
return packageVersions[name];
}
throw new Error(`No version available for package ${name}`);
},
},
});
}
async function readTemplate(
+11 -5
View File
@@ -33,7 +33,8 @@ describe('templatingTask', () => {
// Files content
const testFileContent = 'testing';
const testVersionFileContent = 'version: {{version}}';
const testVersionFileContent =
"version: {{pluginVersion}} {{version 'mock-pkg'}}";
mockFs({
[tmplDir]: {
@@ -45,15 +46,20 @@ describe('templatingTask', () => {
[destDir]: {},
});
await templatingTask(tmplDir, destDir, {
version: '0.0.0',
});
await templatingTask(
tmplDir,
destDir,
{
pluginVersion: '0.0.0',
},
{ 'mock-pkg': '0.1.2' },
);
await expect(
fs.readFile(resolvePath(destDir, 'test.txt'), 'utf8'),
).resolves.toBe(testFileContent);
await expect(
fs.readFile(resolvePath(destDir, 'sub/version.txt'), 'utf8'),
).resolves.toBe('version: 0.0.0');
).resolves.toBe('version: 0.0.0 0.1.2');
});
});
+20 -1
View File
@@ -20,6 +20,7 @@ import handlebars from 'handlebars';
import ora from 'ora';
import { basename, dirname } from 'path';
import recursive from 'recursive-readdir';
import { paths } from './paths';
const TASK_NAME_MAX_LENGTH = 14;
@@ -68,10 +69,12 @@ export async function templatingTask(
templateDir: string,
destinationDir: string,
context: any,
versions: { [name: string]: string },
) {
const files = await recursive(templateDir).catch(error => {
throw new Error(`Failed to read template directory: ${error.message}`);
});
const isMonoRepo = await fs.pathExists(paths.resolveTargetRoot('lerna.json'));
for (const file of files) {
const destinationFile = file.replace(templateDir, destinationDir);
@@ -83,7 +86,19 @@ export async function templatingTask(
const template = await fs.readFile(file);
const compiled = handlebars.compile(template.toString());
const contents = compiled({ name: basename(destination), ...context });
const contents = compiled(
{ name: basename(destination), ...context },
{
helpers: {
version(name: string) {
if (versions[name]) {
return versions[name];
}
throw new Error(`No version available for package ${name}`);
},
},
},
);
await fs.writeFile(destination, contents).catch(error => {
throw new Error(
@@ -92,6 +107,10 @@ export async function templatingTask(
});
});
} else {
if (isMonoRepo && file.match('tsconfig.json')) {
continue;
}
await Task.forItem('copying', basename(file), async () => {
await fs.copyFile(file, destinationFile).catch(error => {
const destination = destinationFile;
+32
View File
@@ -17,6 +17,38 @@
import fs from 'fs-extra';
import { paths } from './paths';
/* eslint-disable import/no-extraneous-dependencies,monorepo/no-internal-import */
/*
This is a list of all packages used by the templates. If dependencies are added or removed,
this list should be updated as well.
The list, and the accompanying devDependencies entries, are here to ensure correct versioning
and bumping of this package. Without this list the version would not be bumped unless we
manually trigger a release.
This does not create an actual dependency on these packages and does not bring in any code.
Rollup will extract the value of the version field in each package at build time without
leaving any imports in place.
*/
import { version as backendCommon } from '@backstage/backend-common/package.json';
import { version as cli } from '@backstage/cli/package.json';
import { version as config } from '@backstage/config/package.json';
import { version as core } from '@backstage/core/package.json';
import { version as devUtils } from '@backstage/dev-utils/package.json';
import { version as testUtils } from '@backstage/test-utils/package.json';
import { version as theme } from '@backstage/theme/package.json';
export const packageVersions = {
'@backstage/backend-common': backendCommon,
'@backstage/cli': cli,
'@backstage/config': config,
'@backstage/core': core,
'@backstage/dev-utils': devUtils,
'@backstage/test-utils': testUtils,
'@backstage/theme': theme,
};
export function findVersion() {
const pkgContent = fs.readFileSync(paths.resolveOwn('package.json'), 'utf8');
return JSON.parse(pkgContent).version;
@@ -1,6 +1,6 @@
{
"name": "{{name}}",
"version": "{{version}}",
"version": "{{pluginVersion}}",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -23,8 +23,8 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^{{backstageVersion}}",
"@backstage/config": "^{{backstageVersion}}",
"@backstage/backend-common": "^{{version '@backstage/backend-common'}}",
"@backstage/config": "^{{version '@backstage/config'}}",
"@types/express": "^4.17.6",
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
@@ -33,7 +33,7 @@
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/cli": "^{{backstageVersion}}",
"@backstage/cli": "^{{version '@backstage/cli'}}",
"@types/supertest": "^2.0.8",
"supertest": "^4.0.2",
"msw": "^0.21.2"
@@ -0,0 +1,13 @@
{
"extends": "@backstage/cli/config/tsconfig.json",
"include": [
"src",
"dev",
"migrations"
],
"exclude": ["node_modules"],
"compilerOptions": {
"outDir": "dist-types",
"rootDir": "."
}
}
@@ -1,6 +1,6 @@
{
"name": "{{name}}",
"version": "{{version}}",
"version": "{{pluginVersion}}",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -24,8 +24,8 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/core": "^{{backstageVersion}}",
"@backstage/theme": "^{{backstageVersion}}",
"@backstage/core": "^{{version '@backstage/core'}}",
"@backstage/theme": "^{{version '@backstage/theme'}}",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -34,9 +34,9 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^{{backstageVersion}}",
"@backstage/dev-utils": "^{{backstageVersion}}",
"@backstage/test-utils": "^{{backstageVersion}}",
"@backstage/cli": "^{{version '@backstage/cli'}}",
"@backstage/dev-utils": "^{{version '@backstage/dev-utils'}}",
"@backstage/test-utils": "^{{version '@backstage/test-utils'}}",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
@@ -0,0 +1,12 @@
{
"extends": "@backstage/cli/config/tsconfig.json",
"include": [
"src",
"dev"
],
"exclude": ["node_modules"],
"compilerOptions": {
"outDir": "dist-types",
"rootDir": "."
}
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/config-loader",
"description": "Config loading functionality used by Backstage backend, and CLI",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"private": false,
"publishConfig": {
"access": "public",
@@ -30,7 +30,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/config": "^0.1.1-alpha.25",
"@backstage/config": "^0.1.1-alpha.26",
"fs-extra": "^9.0.0",
"yaml": "^1.9.2",
"yup": "^0.29.3"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/config",
"description": "Config API used by Backstage core, backend, and CLI",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"private": false,
"publishConfig": {
"access": "public",
+6 -6
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/core-api",
"description": "Internal Core API used by Backstage plugins and apps",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"private": false,
"publishConfig": {
"access": "public",
@@ -29,9 +29,9 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/config": "^0.1.1-alpha.25",
"@backstage/test-utils": "^0.1.1-alpha.25",
"@backstage/theme": "^0.1.1-alpha.25",
"@backstage/config": "^0.1.1-alpha.26",
"@backstage/test-utils": "^0.1.1-alpha.26",
"@backstage/theme": "^0.1.1-alpha.26",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@types/react": "^16.9",
@@ -42,8 +42,8 @@
"zen-observable": "^0.8.15"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.25",
"@backstage/test-utils-core": "^0.1.1-alpha.25",
"@backstage/cli": "^0.1.1-alpha.26",
"@backstage/test-utils-core": "^0.1.1-alpha.26",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
+1 -41
View File
@@ -14,50 +14,10 @@
* limitations under the License.
*/
import { ComponentType } from 'react';
import {
PluginOutput,
RoutePath,
RouteOptions,
FeatureFlagName,
BackstagePlugin,
} from './types';
import { PluginConfig, PluginOutput, BackstagePlugin } from './types';
import { validateBrowserCompat, validateFlagName } from '../app/FeatureFlags';
import { RouteRef } from '../routing';
import { AnyApiFactory } from '../apis';
export type PluginConfig = {
id: string;
apis?: Iterable<AnyApiFactory>;
register?(hooks: PluginHooks): void;
};
export type PluginHooks = {
router: RouterHooks;
featureFlags: FeatureFlagsHooks;
};
export type RouterHooks = {
addRoute(
target: RouteRef,
Component: ComponentType<any>,
options?: RouteOptions,
): void;
/**
* @deprecated See the `addRoute` method
*/
registerRoute(
path: RoutePath,
Component: ComponentType<any>,
options?: RouteOptions,
): void;
};
export type FeatureFlagsHooks = {
register(name: FeatureFlagName): void;
};
export class PluginImpl {
private storedOutput?: PluginOutput[];
+32
View File
@@ -73,3 +73,35 @@ export type BackstagePlugin = {
output(): PluginOutput[];
getApis(): Iterable<AnyApiFactory>;
};
export type PluginConfig = {
id: string;
apis?: Iterable<AnyApiFactory>;
register?(hooks: PluginHooks): void;
};
export type PluginHooks = {
router: RouterHooks;
featureFlags: FeatureFlagsHooks;
};
export type RouterHooks = {
addRoute(
target: RouteRef,
Component: ComponentType<any>,
options?: RouteOptions,
): void;
/**
* @deprecated See the `addRoute` method
*/
registerRoute(
path: RoutePath,
Component: ComponentType<any>,
options?: RouteOptions,
): void;
};
export type FeatureFlagsHooks = {
register(name: FeatureFlagName): void;
};
+6 -6
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/core",
"description": "Core API used by Backstage plugins and apps",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"private": false,
"publishConfig": {
"access": "public",
@@ -29,9 +29,9 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/config": "^0.1.1-alpha.25",
"@backstage/core-api": "^0.1.1-alpha.25",
"@backstage/theme": "^0.1.1-alpha.25",
"@backstage/config": "^0.1.1-alpha.26",
"@backstage/core-api": "^0.1.1-alpha.26",
"@backstage/theme": "^0.1.1-alpha.26",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -55,8 +55,8 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.25",
"@backstage/test-utils": "^0.1.1-alpha.25",
"@backstage/cli": "^0.1.1-alpha.26",
"@backstage/test-utils": "^0.1.1-alpha.26",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
+26 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/create-app",
"description": "Create app package for Backstage",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"private": false,
"publishConfig": {
"access": "public"
@@ -27,7 +27,7 @@
"start": "nodemon --"
},
"dependencies": {
"@backstage/cli-common": "^0.1.1-alpha.25",
"@backstage/cli-common": "^0.1.1-alpha.26",
"chalk": "^4.0.0",
"commander": "^6.1.0",
"fs-extra": "^9.0.0",
@@ -37,6 +37,30 @@
"recursive-readdir": "^2.2.2"
},
"devDependencies": {
"@backstage/backend-common": "^0.1.1-alpha.26",
"@backstage/catalog-model": "^0.1.1-alpha.26",
"@backstage/cli": "^0.1.1-alpha.26",
"@backstage/config": "^0.1.1-alpha.26",
"@backstage/core": "^0.1.1-alpha.26",
"@backstage/plugin-api-docs": "^0.1.1-alpha.26",
"@backstage/plugin-auth-backend": "^0.1.1-alpha.26",
"@backstage/plugin-catalog": "^0.1.1-alpha.26",
"@backstage/plugin-catalog-backend": "^0.1.1-alpha.26",
"@backstage/plugin-circleci": "^0.1.1-alpha.26",
"@backstage/plugin-explore": "^0.1.1-alpha.26",
"@backstage/plugin-github-actions": "^0.1.1-alpha.26",
"@backstage/plugin-lighthouse": "^0.1.1-alpha.26",
"@backstage/plugin-proxy-backend": "^0.1.1-alpha.26",
"@backstage/plugin-register-component": "^0.1.1-alpha.26",
"@backstage/plugin-rollbar-backend": "^0.1.1-alpha.26",
"@backstage/plugin-scaffolder": "^0.1.1-alpha.26",
"@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.26",
"@backstage/plugin-tech-radar": "^0.1.1-alpha.26",
"@backstage/plugin-techdocs": "^0.1.1-alpha.26",
"@backstage/plugin-techdocs-backend": "^0.1.1-alpha.26",
"@backstage/plugin-user-settings": "^0.1.1-alpha.26",
"@backstage/test-utils": "^0.1.1-alpha.26",
"@backstage/theme": "^0.1.1-alpha.26",
"@types/fs-extra": "^9.0.1",
"@types/inquirer": "^7.3.1",
"@types/ora": "^3.2.0",
+1 -2
View File
@@ -22,7 +22,6 @@ import inquirer, { Answers, Question } from 'inquirer';
import { exec as execCb } from 'child_process';
import { resolve as resolvePath } from 'path';
import { findPaths } from '@backstage/cli-common';
import { version } from '../package.json';
import os from 'os';
import { Task, templatingTask } from './lib/tasks';
@@ -133,7 +132,7 @@ export default async (cmd: Command): Promise<void> => {
await createTemporaryAppFolder(tempDir);
Task.section('Preparing files');
await templatingTask(templateDir, tempDir, { ...answers, version });
await templatingTask(templateDir, tempDir, answers);
Task.section('Moving to final location');
await moveApp(tempDir, appDir, answers.name);
+14 -1
View File
@@ -20,6 +20,7 @@ import handlebars from 'handlebars';
import ora from 'ora';
import { basename, dirname } from 'path';
import recursive from 'recursive-readdir';
import { packageVersions } from './versions';
const TASK_NAME_MAX_LENGTH = 14;
@@ -83,7 +84,19 @@ export async function templatingTask(
const template = await fs.readFile(file);
const compiled = handlebars.compile(template.toString());
const contents = compiled({ name: basename(destination), ...context });
const contents = compiled(
{ name: basename(destination), ...context },
{
helpers: {
version(name: keyof typeof packageVersions) {
if (name in packageVersions) {
return packageVersions[name];
}
throw new Error(`No version available for package ${name}`);
},
},
},
);
await fs.writeFile(destination, contents).catch(error => {
throw new Error(
+82
View File
@@ -0,0 +1,82 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable import/no-extraneous-dependencies,monorepo/no-internal-import */
/*
This is a list of all packages used by the template. If dependencies are added or removed,
this list should be updated as well.
The list, and the accompanying devDependencies entries, are here to ensure correct versioning
and bumping of this package. Without this list the version would not be bumped unless we
manually trigger a release.
This does not create an actual dependency on these packages and does not bring in any code.
Rollup will extract the value of the version field in each package at build time without
leaving any imports in place.
*/
import { version as backendCommon } from '@backstage/backend-common/package.json';
import { version as catalogModel } from '@backstage/catalog-model/package.json';
import { version as cli } from '@backstage/cli/package.json';
import { version as config } from '@backstage/config/package.json';
import { version as core } from '@backstage/core/package.json';
import { version as pluginApiDocs } from '@backstage/plugin-api-docs/package.json';
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 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';
import { version as pluginTechRadar } from '@backstage/plugin-tech-radar/package.json';
import { version as pluginTechdocs } from '@backstage/plugin-techdocs/package.json';
import { version as pluginTechdocsBackend } from '@backstage/plugin-techdocs-backend/package.json';
import { version as pluginUserSettings } from '@backstage/plugin-user-settings/package.json';
import { version as testUtils } from '@backstage/test-utils/package.json';
import { version as theme } from '@backstage/theme/package.json';
export const packageVersions = {
'@backstage/backend-common': backendCommon,
'@backstage/catalog-model': catalogModel,
'@backstage/cli': cli,
'@backstage/config': config,
'@backstage/core': core,
'@backstage/plugin-api-docs': pluginApiDocs,
'@backstage/plugin-auth-backend': pluginAuthBackend,
'@backstage/plugin-catalog': pluginCatalog,
'@backstage/plugin-catalog-backend': pluginCatalogBackend,
'@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,
'@backstage/plugin-tech-radar': pluginTechRadar,
'@backstage/plugin-techdocs': pluginTechdocs,
'@backstage/plugin-techdocs-backend': pluginTechdocsBackend,
'@backstage/plugin-user-settings': pluginUserSettings,
'@backstage/test-utils': testUtils,
'@backstage/theme': theme,
};
@@ -38,6 +38,18 @@ backend:
#ca: # if you have a CA file and want to verify it you can uncomment this section
# $file: <file-path>/ca/server.crt
{{/if}}
# workingDirectory: /tmp # Use this to configure a working direcotry for the scaffolder, defaults to the OS temp-dir
integrations:
github:
- host: github.com
token:
$env: GITHUB_TOKEN
### Example for how to add your GitHub Enterprise instance using the API:
# - host: ghe.example.net
# apiBaseUrl: https://ghe.example.net/api/v3
# token:
# $env: GHE_TOKEN
proxy:
'/test':
@@ -66,17 +78,6 @@ scaffolder:
catalog:
rules:
- allow: [Component, API, Group, User, Template, Location]
processors:
github:
providers:
- target: https://github.com
token:
$env: GITHUB_TOKEN
# Example for how to add your GitHub Enterprise instance:
# - target: https://ghe.example.net
# apiBaseUrl: https://ghe.example.net/api/v3
# token:
# $env: GHE_TOKEN
locations:
# Backstage example components
- type: url
@@ -26,7 +26,7 @@
]
},
"devDependencies": {
"@backstage/cli": "^{{version}}",
"@backstage/cli": "^{{version '@backstage/cli'}}",
"@spotify/prettier-config": "^7.0.0",
"lerna": "^3.20.2",
"prettier": "^1.19.1"
@@ -6,22 +6,22 @@
"dependencies": {
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@backstage/cli": "^{{version}}",
"@backstage/core": "^{{version}}",
"@backstage/plugin-api-docs": "^{{version}}",
"@backstage/plugin-catalog": "^{{version}}",
"@backstage/plugin-register-component": "^{{version}}",
"@backstage/plugin-scaffolder": "^{{version}}",
"@backstage/plugin-techdocs": "^{{version}}",
"@backstage/catalog-model": "^{{version}}",
"@backstage/plugin-circleci": "^{{version}}",
"@backstage/plugin-explore": "^{{version}}",
"@backstage/plugin-lighthouse": "^{{version}}",
"@backstage/plugin-tech-radar": "^{{version}}",
"@backstage/plugin-github-actions": "^{{version}}",
"@backstage/plugin-user-settings": "^{{version}}",
"@backstage/test-utils": "^{{version}}",
"@backstage/theme": "^{{version}}",
"@backstage/cli": "^{{version '@backstage/cli'}}",
"@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-scaffolder": "^{{version '@backstage/plugin-scaffolder'}}",
"@backstage/plugin-techdocs": "^{{version '@backstage/plugin-techdocs'}}",
"@backstage/catalog-model": "^{{version '@backstage/catalog-model'}}",
"@backstage/plugin-circleci": "^{{version '@backstage/plugin-circleci'}}",
"@backstage/plugin-explore": "^{{version '@backstage/plugin-explore'}}",
"@backstage/plugin-lighthouse": "^{{version '@backstage/plugin-lighthouse'}}",
"@backstage/plugin-tech-radar": "^{{version '@backstage/plugin-tech-radar'}}",
"@backstage/plugin-github-actions": "^{{version '@backstage/plugin-github-actions'}}",
"@backstage/plugin-user-settings": "^{{version '@backstage/plugin-user-settings'}}",
"@backstage/test-utils": "^{{version '@backstage/test-utils'}}",
"@backstage/theme": "^{{version '@backstage/theme'}}",
"history": "^5.0.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
@@ -17,15 +17,15 @@
"migrate:create": "knex migrate:make -x ts"
},
"dependencies": {
"@backstage/backend-common": "^{{version}}",
"@backstage/catalog-model": "^{{version}}",
"@backstage/config": "^{{version}}",
"@backstage/plugin-auth-backend": "^{{version}}",
"@backstage/plugin-catalog-backend": "^{{version}}",
"@backstage/plugin-proxy-backend": "^{{version}}",
"@backstage/plugin-rollbar-backend": "^{{version}}",
"@backstage/plugin-scaffolder-backend": "^{{version}}",
"@backstage/plugin-techdocs-backend": "^{{version}}",
"@backstage/backend-common": "^{{version '@backstage/backend-common'}}",
"@backstage/catalog-model": "^{{version '@backstage/catalog-model'}}",
"@backstage/config": "^{{version '@backstage/config'}}",
"@backstage/plugin-auth-backend": "^{{version '@backstage/plugin-auth-backend'}}",
"@backstage/plugin-catalog-backend": "^{{version '@backstage/plugin-catalog-backend'}}",
"@backstage/plugin-proxy-backend": "^{{version '@backstage/plugin-proxy-backend'}}",
"@backstage/plugin-rollbar-backend": "^{{version '@backstage/plugin-rollbar-backend'}}",
"@backstage/plugin-scaffolder-backend": "^{{version '@backstage/plugin-scaffolder-backend'}}",
"@backstage/plugin-techdocs-backend": "^{{version '@backstage/plugin-techdocs-backend'}}",
"@octokit/rest": "^18.0.0",
"@gitbeaker/node": "^23.5.0",
"dockerode": "^3.2.0",
@@ -41,7 +41,7 @@
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/cli": "^{{version}}",
"@backstage/cli": "^{{version '@backstage/cli'}}",
"@types/dockerode": "^2.5.32",
"@types/express": "^4.17.6",
"@types/express-serve-static-core": "^4.17.5",
@@ -102,6 +102,7 @@ export default async function createPlugin({
templaters,
publishers,
logger,
config,
dockerClient,
});
}
+5 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/dev-utils",
"description": "Utilities for developing Backstage plugins.",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"private": false,
"publishConfig": {
"access": "public",
@@ -29,10 +29,10 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/cli": "^0.1.1-alpha.25",
"@backstage/core": "^0.1.1-alpha.25",
"@backstage/test-utils": "^0.1.1-alpha.25",
"@backstage/theme": "^0.1.1-alpha.25",
"@backstage/cli": "^0.1.1-alpha.26",
"@backstage/core": "^0.1.1-alpha.26",
"@backstage/test-utils": "^0.1.1-alpha.26",
"@backstage/theme": "^0.1.1-alpha.26",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@testing-library/jest-dom": "^5.10.1",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "docgen",
"description": "Tool for generating API Documentation for itself",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"private": true,
"homepage": "https://backstage.io",
"repository": {
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "e2e-test",
"description": "E2E test for verifying Backstage packages",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"private": true,
"homepage": "https://backstage.io",
"repository": {
@@ -24,14 +24,14 @@
"e2e-test": "bin/e2e-test"
},
"devDependencies": {
"@backstage/cli-common": "^0.1.1-alpha.25",
"@backstage/cli-common": "^0.1.1-alpha.26",
"@types/fs-extra": "^9.0.1",
"@types/node": "^13.7.2",
"chalk": "^4.0.0",
"commander": "^6.1.0",
"cross-fetch": "^3.0.6",
"fs-extra": "^9.0.0",
"handlebars": "^4.7.3",
"cross-fetch": "^3.0.6",
"pgtools": "^0.3.0",
"tree-kill": "^1.2.2",
"ts-node": "^8.6.2",
+17 -5
View File
@@ -84,11 +84,23 @@ async function buildDistWorkspace(workspaceName: string, rootDir: string) {
const path = paths.resolveOwnRoot(pkgJsonPath);
const pkgTemplate = await fs.readFile(path, 'utf8');
const { dependencies = {}, devDependencies = {} } = JSON.parse(
handlebars.compile(pkgTemplate)({
version: '0.0.0',
privatePackage: true,
scopeName: '@backstage',
}),
handlebars.compile(pkgTemplate)(
{
privatePackage: true,
scopeName: '@backstage',
},
{
helpers: {
version(name: string) {
const pkg = require(`${name}/package.json`);
if (!pkg) {
throw new Error(`No version available for package ${name}`);
}
return pkg.version;
},
},
},
),
);
Array<string>()
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "storybook",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"description": "Storybook build for core package",
"private": true,
"scripts": {
@@ -14,7 +14,7 @@
]
},
"dependencies": {
"@backstage/theme": "^0.1.1-alpha.25"
"@backstage/theme": "^0.1.1-alpha.26"
},
"devDependencies": {
"@storybook/addon-actions": "^6.0.21",
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@techdocs/cli",
"description": "CLI for running TechDocs locally.",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"private": false,
"publishConfig": {
"access": "public"
@@ -40,7 +40,7 @@
"ext": "ts"
},
"dependencies": {
"@backstage/cli": "^0.1.1-alpha.25",
"@backstage/cli": "^0.1.1-alpha.26",
"commander": "^6.1.0",
"fs-extra": "^9.0.1",
"http-proxy": "^1.18.1",
+4 -4
View File
@@ -1,11 +1,11 @@
# 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.
@@ -18,7 +18,7 @@ FROM python:3.8-alpine
RUN apk update && apk --no-cache add gcc musl-dev openjdk11-jdk curl graphviz ttf-dejavu fontconfig
RUN curl -o plantuml.jar -L http://sourceforge.net/projects/plantuml/files/plantuml.1.2020.16.jar/download && echo "c789ace48347c43073232b1458badc5810c01fe8 plantuml.jar" | sha1sum -c - && mv plantuml.jar /opt/plantuml.jar
RUN pip install --upgrade pip && pip install mkdocs-techdocs-core==0.0.8
RUN pip install --upgrade pip && pip install mkdocs-techdocs-core==0.0.10
# Create script to call plantuml.jar from a location in path
@@ -82,6 +82,10 @@ Extensions:
## Changelog
### 0.0.10
- Pin Markdown version to fix issue with Graphviz
### 0.0.9
- Change development status to 3 - Alpha
@@ -8,6 +8,7 @@ plantuml-markdown==3.1.2
markdown_inline_graphviz_extension==1.1
pygments==2.6.1
pymdown-extensions==7.1
Markdown==3.2.2
# The linter using for Python
# Note: This requires Python 3.6+ to run, but can format Python 2 code too.
@@ -23,7 +23,7 @@ with open(path.join(this_dir, "README.md"), encoding="utf-8") as file:
setup(
name="mkdocs-techdocs-core",
version="0.0.9",
version="0.0.10",
description="A Mkdocs package that contains TechDocs defaults",
long_description=long_description,
long_description_content_type="text/markdown",
@@ -41,6 +41,7 @@ setup(
"markdown_inline_graphviz_extension==1.1",
"pygments==2.6.1",
"pymdown-extensions==7.1",
"Markdown==3.2.2",
],
classifiers=[
"Development Status :: 3 - Alpha",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/test-utils-core",
"description": "Utilities to test Backstage core",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"private": false,
"publishConfig": {
"access": "public",
+5 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/test-utils",
"description": "Utilities to test Backstage plugins and apps.",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"private": false,
"publishConfig": {
"access": "public",
@@ -29,10 +29,10 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/cli": "^0.1.1-alpha.25",
"@backstage/core-api": "^0.1.1-alpha.25",
"@backstage/test-utils-core": "^0.1.1-alpha.25",
"@backstage/theme": "^0.1.1-alpha.25",
"@backstage/cli": "^0.1.1-alpha.26",
"@backstage/core-api": "^0.1.1-alpha.26",
"@backstage/test-utils-core": "^0.1.1-alpha.26",
"@backstage/theme": "^0.1.1-alpha.26",
"@material-ui/core": "^4.11.0",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/theme",
"description": "material-ui theme for use with Backstage.",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"private": false,
"publishConfig": {
"access": "public",
@@ -31,7 +31,7 @@
"@material-ui/core": "^4.11.0"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.25"
"@backstage/cli": "^0.1.1-alpha.26"
},
"files": [
"dist"
+10 -10
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-api-docs",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,10 +20,10 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/catalog-model": "^0.1.1-alpha.25",
"@backstage/core": "^0.1.1-alpha.25",
"@backstage/plugin-catalog": "^0.1.1-alpha.25",
"@backstage/theme": "^0.1.1-alpha.25",
"@backstage/catalog-model": "^0.1.1-alpha.26",
"@backstage/core": "^0.1.1-alpha.26",
"@backstage/plugin-catalog": "^0.1.1-alpha.26",
"@backstage/theme": "^0.1.1-alpha.26",
"@kyma-project/asyncapi-react": "^0.13.1",
"@material-icons/font": "^1.0.2",
"@material-ui/core": "^4.11.0",
@@ -39,9 +39,9 @@
"swagger-ui-react": "^3.31.1"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.25",
"@backstage/dev-utils": "^0.1.1-alpha.25",
"@backstage/test-utils": "^0.1.1-alpha.25",
"@backstage/cli": "^0.1.1-alpha.26",
"@backstage/dev-utils": "^0.1.1-alpha.26",
"@backstage/test-utils": "^0.1.1-alpha.26",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
@@ -49,8 +49,8 @@
"@types/node": "^12.0.0",
"@types/react": "^16.9",
"@types/swagger-ui-react": "^3.23.3",
"msw": "^0.21.2",
"cross-fetch": "^3.0.6"
"cross-fetch": "^3.0.6",
"msw": "^0.21.2"
},
"files": [
"dist"
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-app-backend",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,8 +20,8 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.1.1-alpha.25",
"@backstage/config-loader": "^0.1.1-alpha.25",
"@backstage/backend-common": "^0.1.1-alpha.26",
"@backstage/config-loader": "^0.1.1-alpha.26",
"@types/express": "^4.17.6",
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
@@ -30,7 +30,7 @@
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.25",
"@backstage/cli": "^0.1.1-alpha.26",
"@types/supertest": "^2.0.8",
"msw": "^0.20.5",
"supertest": "^4.0.2"
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-auth-backend",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,13 +20,14 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.1.1-alpha.25",
"@backstage/catalog-model": "^0.1.1-alpha.25",
"@backstage/config": "^0.1.1-alpha.25",
"@backstage/backend-common": "^0.1.1-alpha.26",
"@backstage/catalog-model": "^0.1.1-alpha.26",
"@backstage/config": "^0.1.1-alpha.26",
"@types/express": "^4.17.6",
"compression": "^1.7.4",
"cookie-parser": "^1.4.5",
"cors": "^2.8.5",
"cross-fetch": "^3.0.6",
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
"fs-extra": "^9.0.0",
@@ -47,11 +48,10 @@
"passport-saml": "^1.3.3",
"uuid": "^8.0.0",
"winston": "^3.2.1",
"cross-fetch": "^3.0.6",
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.25",
"@backstage/cli": "^0.1.1-alpha.26",
"@types/body-parser": "^1.19.0",
"@types/cookie-parser": "^1.4.2",
"@types/jwt-decode": "2.2.1",
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-catalog-backend",
"version": "0.1.1-alpha.25",
"version": "0.1.1-alpha.26",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,9 +20,9 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.1.1-alpha.25",
"@backstage/catalog-model": "^0.1.1-alpha.25",
"@backstage/config": "^0.1.1-alpha.25",
"@backstage/backend-common": "^0.1.1-alpha.26",
"@backstage/catalog-model": "^0.1.1-alpha.26",
"@backstage/config": "^0.1.1-alpha.26",
"@octokit/graphql": "^4.5.6",
"@types/express": "^4.17.6",
"codeowners-utils": "^1.0.2",
@@ -45,8 +45,8 @@
"yup": "^0.29.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.25",
"@backstage/test-utils": "^0.1.1-alpha.25",
"@backstage/cli": "^0.1.1-alpha.26",
"@backstage/test-utils": "^0.1.1-alpha.26",
"@types/core-js": "^2.5.4",
"@types/git-url-parse": "^9.0.0",
"@types/ldapjs": "^1.0.9",
@@ -28,7 +28,6 @@ import { CatalogRulesEnforcer } from './CatalogRules';
import * as result from './processors/results';
import {
CatalogProcessor,
CatalogProcessorDataResult,
CatalogProcessorEmit,
CatalogProcessorEntityResult,
CatalogProcessorErrorResult,
@@ -75,8 +74,6 @@ export class LocationReaders implements LocationReader {
for (const item of items) {
if (item.type === 'location') {
await this.handleLocation(item, emit);
} else if (item.type === 'data') {
await this.handleData(item, emit);
} else if (item.type === 'entity') {
if (rulesEnforcer.isAllowed(item.entity, item.location)) {
const relations = Array<EntityRelationSpec>();
@@ -165,40 +162,6 @@ export class LocationReaders implements LocationReader {
logger.warn(message);
}
private async handleData(
item: CatalogProcessorDataResult,
emit: CatalogProcessorEmit,
) {
const { processors, logger } = this.options;
const validatedEmit: CatalogProcessorEmit = emitResult => {
if (emitResult.type === 'relation') {
throw new Error('parseData may not emit entity relations');
}
emit(emitResult);
};
for (const processor of processors) {
if (processor.parseData) {
try {
if (
await processor.parseData(item.data, item.location, validatedEmit)
) {
return;
}
} catch (e) {
const message = `Processor ${processor.constructor.name} threw an error while parsing ${item.location.type} ${item.location.target}, ${e}`;
emit(result.generalError(item.location, message));
logger.warn(message);
}
}
}
const message = `No processor was able to parse location ${item.location.type} ${item.location.target}`;
emit(result.inputError(item.location, message));
}
private async handleEntity(
item: CatalogProcessorEntityResult,
emit: CatalogProcessorEmit,
@@ -248,6 +211,29 @@ export class LocationReaders implements LocationReader {
return undefined;
}
let handled = false;
for (const processor of processors) {
if (processor.validateEntityKind) {
try {
handled = await processor.validateEntityKind(current);
if (handled) {
break;
}
} catch (e) {
const message = `Processor ${processor.constructor.name} threw an error while validating the entity ${kind}:${namespace}/${name} at ${item.location.type} ${item.location.target}, ${e}`;
emit(result.inputError(item.location, message));
logger.warn(message);
return undefined;
}
}
}
if (!handled) {
const message = `No processor recognized the entity ${kind}:${namespace}/${name} at ${item.location.type} ${item.location.target}`;
emit(result.inputError(item.location, message));
logger.warn(message);
return undefined;
}
for (const processor of processors) {
if (processor.postProcessEntity) {
try {
@@ -1,146 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { LocationSpec } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import fetch from 'cross-fetch';
import * as result from './results';
import { CatalogProcessor, CatalogProcessorEmit } from './types';
// ***********************************************************************
// * NOTE: This has been replaced by packages/backend-common/src/reading *
// * Don't implement new functionality here as this file will be removed *
// ***********************************************************************
export class AzureApiReaderProcessor implements CatalogProcessor {
private privateToken: string;
constructor(config: Config) {
this.privateToken =
config.getOptionalString('catalog.processors.azureApi.privateToken') ??
'';
}
getRequestOptions(): RequestInit {
const headers: HeadersInit = {};
if (this.privateToken !== '') {
headers.Authorization = `Basic ${Buffer.from(
`:${this.privateToken}`,
'utf8',
).toString('base64')}`;
}
return {
headers,
};
}
async readLocation(
location: LocationSpec,
optional: boolean,
emit: CatalogProcessorEmit,
): Promise<boolean> {
if (location.type !== 'azure/api') {
return false;
}
try {
const url = this.buildRawUrl(location.target);
const response = await fetch(url.toString(), this.getRequestOptions());
// for private repos when PAT is not valid, Azure API returns a http status code 203 with sign in page html
if (response.ok && response.status !== 203) {
const data = Buffer.from(await response.text());
emit(result.data(location, data));
} else {
const message = `${location.target} could not be read as ${url}, ${response.status} ${response.statusText}`;
if (response.status === 404) {
if (!optional) {
emit(result.notFoundError(location, message));
}
} else {
emit(result.generalError(location, message));
}
}
} catch (e) {
const message = `Unable to read ${location.type} ${location.target}, ${e}`;
emit(result.generalError(location, message));
}
return true;
}
// Converts
// from: https://dev.azure.com/{organization}/{project}/_git/reponame?path={path}&version=GB{commitOrBranch}&_a=contents
// to: https://dev.azure.com/{organization}/{project}/_apis/git/repositories/reponame/items?path={path}&version={commitOrBranch}
buildRawUrl(target: string): URL {
try {
const url = new URL(target);
const [
empty,
userOrOrg,
project,
srcKeyword,
repoName,
] = url.pathname.split('/');
const path = url.searchParams.get('path') || '';
const ref = url.searchParams.get('version')?.substr(2);
if (
url.hostname !== 'dev.azure.com' ||
empty !== '' ||
userOrOrg === '' ||
project === '' ||
srcKeyword !== '_git' ||
repoName === '' ||
path === '' ||
ref === ''
) {
throw new Error('Wrong Azure Devops URL or Invalid file path');
}
// transform to api
url.pathname = [
empty,
userOrOrg,
project,
'_apis',
'git',
'repositories',
repoName,
'items',
].join('/');
const queryParams = [`path=${path}`];
if (ref) {
queryParams.push(`version=${ref}`);
}
url.search = queryParams.join('&');
url.protocol = 'https';
return url;
} catch (e) {
throw new Error(`Incorrect url: ${target}, ${e}`);
}
}
}
@@ -1,138 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { LocationSpec } from '@backstage/catalog-model';
import fetch from 'cross-fetch';
import * as result from './results';
import { Config } from '@backstage/config';
import { CatalogProcessor, CatalogProcessorEmit } from './types';
// ***********************************************************************
// * NOTE: This has been replaced by packages/backend-common/src/reading *
// * Don't implement new functionality here as this file will be removed *
// ***********************************************************************
export class BitbucketApiReaderProcessor implements CatalogProcessor {
private username: string;
private password: string;
constructor(config: Config) {
this.username =
config.getOptionalString('catalog.processors.bitbucketApi.username') ??
'';
this.password =
config.getOptionalString('catalog.processors.bitbucketApi.appPassword') ??
'';
}
getRequestOptions(): RequestInit {
const headers: HeadersInit = {};
if (this.username !== '' && this.password !== '') {
headers.Authorization = `Basic ${Buffer.from(
`${this.username}:${this.password}`,
'utf8',
).toString('base64')}`;
}
const requestOptions: RequestInit = {
headers,
};
return requestOptions;
}
async readLocation(
location: LocationSpec,
optional: boolean,
emit: CatalogProcessorEmit,
): Promise<boolean> {
if (location.type !== 'bitbucket/api') {
return false;
}
try {
const url = this.buildRawUrl(location.target);
const response = await fetch(url.toString(), this.getRequestOptions());
if (response.ok) {
const data = await response.text();
emit(result.data(location, Buffer.from(data)));
} else {
const message = `${location.target} could not be read as ${url}, ${response.status} ${response.statusText}`;
if (response.status === 404) {
if (!optional) {
emit(result.notFoundError(location, message));
}
} else {
emit(result.generalError(location, message));
}
}
} catch (e) {
const message = `Unable to read ${location.type} ${location.target}, ${e}`;
emit(result.generalError(location, message));
}
return true;
}
// Converts
// from: https://bitbucket.org/orgname/reponame/src/master/file.yaml
// to: https://api.bitbucket.org/2.0/repositories/orgname/reponame/src/master/file.yaml
buildRawUrl(target: string): URL {
try {
const url = new URL(target);
const [
empty,
userOrOrg,
repoName,
srcKeyword,
ref,
...restOfPath
] = url.pathname.split('/');
if (
url.hostname !== 'bitbucket.org' ||
empty !== '' ||
userOrOrg === '' ||
repoName === '' ||
srcKeyword !== 'src'
) {
throw new Error('Wrong Bitbucket URL or Invalid file path');
}
// transform to api
url.pathname = [
empty,
'2.0',
'repositories',
userOrOrg,
repoName,
'src',
ref,
...restOfPath,
].join('/');
url.hostname = 'api.bitbucket.org';
url.protocol = 'https';
return url;
} catch (e) {
throw new Error(`Incorrect url: ${target}, ${e}`);
}
}
}
@@ -0,0 +1,48 @@
/*
* 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 {
apiEntityV1alpha1Validator,
componentEntityV1alpha1Validator,
Entity,
groupEntityV1alpha1Validator,
locationEntityV1alpha1Validator,
templateEntityV1alpha1Validator,
userEntityV1alpha1Validator,
} from '@backstage/catalog-model';
import { CatalogProcessor } from './types';
export class BuiltinKindsEntityProcessor implements CatalogProcessor {
private readonly validators = [
apiEntityV1alpha1Validator,
componentEntityV1alpha1Validator,
groupEntityV1alpha1Validator,
locationEntityV1alpha1Validator,
templateEntityV1alpha1Validator,
userEntityV1alpha1Validator,
];
async validateEntityKind(entity: Entity): Promise<boolean> {
for (const validator of this.validators) {
const result = await validator.check(entity);
if (result) {
return true;
}
}
return false;
}
}
@@ -180,10 +180,12 @@ describe('CodeOwnersProcessor', () => {
const result = await findRawCodeOwners(mockLocation(), reader);
expect(read.mock.calls.length).toBe(3);
expect(read.mock.calls[0]).toEqual([mockReadUrl('.github/')]);
expect(read.mock.calls[1]).toEqual([mockReadUrl('')]);
expect(read.mock.calls[2]).toEqual([mockReadUrl('docs/')]);
expect(read.mock.calls.length).toBe(5);
expect(read.mock.calls[0]).toEqual([mockReadUrl('')]);
expect(read.mock.calls[1]).toEqual([mockReadUrl('docs/')]);
expect(read.mock.calls[2]).toEqual([mockReadUrl('.bitbucket/')]);
expect(read.mock.calls[3]).toEqual([mockReadUrl('.github/')]);
expect(read.mock.calls[4]).toEqual([mockReadUrl('.gitlab/')]);
expect(result).toEqual(ownersText);
});
});
@@ -236,7 +238,7 @@ describe('CodeOwnersProcessor', () => {
expect(result).toEqual(entity);
});
it('should ignore url locations', async () => {
it('should handle url locations', async () => {
const { entity, processor } = setupTest();
const result = await processor.preProcessEntity(
@@ -244,7 +246,10 @@ describe('CodeOwnersProcessor', () => {
mockLocation({ type: 'url' }),
);
expect(result).toEqual(entity);
expect(result).toEqual({
...entity,
spec: { owner: 'backstage-core' },
});
});
it('should ignore invalid kinds', async () => {
@@ -25,6 +25,7 @@ import { filter, get, head, pipe, reverse } from 'lodash/fp';
import { CatalogProcessor } from './types';
const ALLOWED_LOCATION_TYPES = [
'url',
'azure/api',
'bitbucket/api',
'github',
@@ -33,6 +34,11 @@ const ALLOWED_LOCATION_TYPES = [
'gitlab/api',
];
// TODO(Rugvip): We want to properly detect out repo provider, but for now it's
// best to wait for GitHub Apps to be properly introduced and see
// what kind of APIs that integrations will expose.
const KNOWN_LOCATIONS = ['', '/docs', '/.bitbucket', '/.github', '/.gitlab'];
type Options = {
reader: UrlReader;
};
@@ -91,13 +97,7 @@ export async function findRawCodeOwners(
return data.toString();
};
const gitProvider = location.type.split('/')[0];
return Promise.any([
readOwnerLocation(`/.${gitProvider}`),
readOwnerLocation(''),
readOwnerLocation('/docs'),
]);
return Promise.any(KNOWN_LOCATIONS.map(readOwnerLocation));
}
export function buildCodeOwnerUrl(
@@ -18,6 +18,7 @@ import { LocationSpec } from '@backstage/catalog-model';
import fs from 'fs-extra';
import * as result from './results';
import { CatalogProcessor, CatalogProcessorEmit } from './types';
import { parseEntityYaml } from './util/parse';
export class FileReaderProcessor implements CatalogProcessor {
async readLocation(
@@ -33,7 +34,10 @@ export class FileReaderProcessor implements CatalogProcessor {
const exists = await fs.pathExists(location.target);
if (exists) {
const data = await fs.readFile(location.target);
emit(result.data(location, data));
for (const parseResult of parseEntityYaml(data, location)) {
emit(parseResult);
}
} else if (!optional) {
const message = `${location.type} ${location.target} does not exist`;
emit(result.notFoundError(location, message));
@@ -1,274 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { LocationSpec } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import parseGitUri from 'git-url-parse';
import fetch from 'cross-fetch';
import { Logger } from 'winston';
import * as result from './results';
import { CatalogProcessor, CatalogProcessorEmit } from './types';
// ***********************************************************************
// * NOTE: This has been replaced by packages/backend-common/src/reading *
// * Don't implement new functionality here as this file will be removed *
// ***********************************************************************
/**
* The configuration parameters for a single GitHub API provider.
*/
export type ProviderConfig = {
/**
* The prefix of the target that this matches on, e.g. "https://github.com",
* with no trailing slash.
*/
target: string;
/**
* The base URL of the API of this provider, e.g. "https://api.github.com",
* with no trailing slash.
*
* May be omitted specifically for GitHub; then it will be deduced.
*
* The API will always be preferred if both its base URL and a token are
* present.
*/
apiBaseUrl?: string;
/**
* The base URL of the raw fetch endpoint of this provider, e.g.
* "https://raw.githubusercontent.com", with no trailing slash.
*
* May be omitted specifically for GitHub; then it will be deduced.
*
* The API will always be preferred if both its base URL and a token are
* present.
*/
rawBaseUrl?: string;
/**
* The authorization token to use for requests to this provider.
*
* If no token is specified, anonymous access is used.
*/
token?: string;
};
export function getApiRequestOptions(provider: ProviderConfig): RequestInit {
const headers: HeadersInit = {
Accept: 'application/vnd.github.v3.raw',
};
if (provider.token) {
headers.Authorization = `token ${provider.token}`;
}
return {
headers,
};
}
export function getRawRequestOptions(provider: ProviderConfig): RequestInit {
const headers: HeadersInit = {};
if (provider.token) {
headers.Authorization = `token ${provider.token}`;
}
return {
headers,
};
}
// Converts for example
// from: https://github.com/a/b/blob/branchname/path/to/c.yaml
// to: https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=branchname
export function getApiUrl(target: string, provider: ProviderConfig): URL {
try {
const { owner, name, ref, filepathtype, filepath } = parseGitUri(target);
if (
!owner ||
!name ||
!ref ||
(filepathtype !== 'blob' && filepathtype !== 'raw')
) {
throw new Error('Wrong URL or invalid file path');
}
const pathWithoutSlash = filepath.replace(/^\//, '');
return new URL(
`${provider.apiBaseUrl}/repos/${owner}/${name}/contents/${pathWithoutSlash}?ref=${ref}`,
);
} catch (e) {
throw new Error(`Incorrect URL: ${target}, ${e}`);
}
}
// Converts for example
// from: https://github.com/a/b/blob/branchname/c.yaml
// to: https://raw.githubusercontent.com/a/b/branchname/c.yaml
export function getRawUrl(target: string, provider: ProviderConfig): URL {
try {
const { owner, name, ref, filepathtype, filepath } = parseGitUri(target);
if (
!owner ||
!name ||
!ref ||
(filepathtype !== 'blob' && filepathtype !== 'raw')
) {
throw new Error('Wrong URL or invalid file path');
}
const pathWithoutSlash = filepath.replace(/^\//, '');
return new URL(
`${provider.rawBaseUrl}/${owner}/${name}/${ref}/${pathWithoutSlash}`,
);
} catch (e) {
throw new Error(`Incorrect URL: ${target}, ${e}`);
}
}
export function readConfig(config: Config, logger: Logger): ProviderConfig[] {
const providers: ProviderConfig[] = [];
// TODO(freben): Deprecate the old config root entirely in a later release
if (config.has('catalog.processors.githubApi')) {
logger.warn(
'The catalog.processors.githubApi configuration key has been deprecated, please use catalog.processors.github instead',
);
}
// In a previous version of the configuration, we only supported github,
// and the "privateToken" key held the token to use for it. The new
// configuration method is to use the "providers" key instead.
const providerConfigs =
config.getOptionalConfigArray('catalog.processors.github.providers') ??
config.getOptionalConfigArray('catalog.processors.githubApi.providers') ??
[];
const legacyToken =
config.getOptionalString('catalog.processors.github.privateToken') ??
config.getOptionalString('catalog.processors.githubApi.privateToken');
// First read all the explicit providers
for (const providerConfig of providerConfigs) {
const target = providerConfig.getString('target').replace(/\/+$/, '');
let apiBaseUrl = providerConfig.getOptionalString('apiBaseUrl');
let rawBaseUrl = providerConfig.getOptionalString('rawBaseUrl');
const token = providerConfig.getOptionalString('token');
if (apiBaseUrl) {
apiBaseUrl = apiBaseUrl.replace(/\/+$/, '');
} else if (target === 'https://github.com') {
apiBaseUrl = 'https://api.github.com';
}
if (rawBaseUrl) {
rawBaseUrl = rawBaseUrl.replace(/\/+$/, '');
} else if (target === 'https://github.com') {
rawBaseUrl = 'https://raw.githubusercontent.com';
}
if (!apiBaseUrl && !rawBaseUrl) {
throw new Error(
`Provider at ${target} must configure an explicit apiBaseUrl or rawBaseUrl`,
);
}
providers.push({ target, apiBaseUrl, rawBaseUrl, token });
}
// If no explicit github.com provider was added, put one in the list as
// a convenience
if (!providers.some(p => p.target === 'https://github.com')) {
providers.push({
target: 'https://github.com',
apiBaseUrl: 'https://api.github.com',
rawBaseUrl: 'https://raw.githubusercontent.com',
token: legacyToken,
});
}
return providers;
}
/**
* A processor that adds the ability to read files from GitHub v3 APIs, such as
* the one exposed by GitHub itself.
*/
export class GithubReaderProcessor implements CatalogProcessor {
private providers: ProviderConfig[];
static fromConfig(config: Config, logger: Logger) {
return new GithubReaderProcessor(readConfig(config, logger));
}
constructor(providers: ProviderConfig[]) {
this.providers = providers;
}
async readLocation(
location: LocationSpec,
optional: boolean,
emit: CatalogProcessorEmit,
): Promise<boolean> {
// The github/api type is for backward compatibility
if (location.type !== 'github' && location.type !== 'github/api') {
return false;
}
const provider = this.providers.find(p =>
location.target.startsWith(`${p.target}/`),
);
if (!provider) {
throw new Error(
`There is no GitHub provider that matches ${location.target}. Please add a configuration entry for it under catalog.processors.github.providers.`,
);
}
try {
const useApi =
provider.apiBaseUrl && (provider.token || !provider.rawBaseUrl);
const url = useApi
? getApiUrl(location.target, provider)
: getRawUrl(location.target, provider);
const options = useApi
? getApiRequestOptions(provider)
: getRawRequestOptions(provider);
const response = await fetch(url.toString(), options);
if (response.ok) {
const data = await response.text();
emit(result.data(location, Buffer.from(data)));
} else {
const message = `${location.target} could not be read as ${url}, ${response.status} ${response.statusText}`;
if (response.status === 404) {
if (!optional) {
emit(result.notFoundError(location, message));
}
} else {
emit(result.generalError(location, message));
}
}
} catch (e) {
const message = `Unable to read ${location.type} ${location.target}, ${e}`;
emit(result.generalError(location, message));
}
return true;
}
}
@@ -1,141 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { LocationSpec } from '@backstage/catalog-model';
import fetch from 'cross-fetch';
import { Config } from '@backstage/config';
import * as result from './results';
import { CatalogProcessor, CatalogProcessorEmit } from './types';
// ***********************************************************************
// * NOTE: This has been replaced by packages/backend-common/src/reading *
// * Don't implement new functionality here as this file will be removed *
// ***********************************************************************
export class GitlabApiReaderProcessor implements CatalogProcessor {
private privateToken: string;
constructor(config: Config) {
this.privateToken =
config.getOptionalString('catalog.processors.gitlabApi.privateToken') ??
'';
}
getRequestOptions(): RequestInit {
const headers: HeadersInit = { 'PRIVATE-TOKEN': '' };
if (this.privateToken !== '') {
headers['PRIVATE-TOKEN'] = this.privateToken;
}
const requestOptions: RequestInit = {
headers,
};
return requestOptions;
}
async readLocation(
location: LocationSpec,
optional: boolean,
emit: CatalogProcessorEmit,
): Promise<boolean> {
if (location.type !== 'gitlab/api') {
return false;
}
try {
const projectID = await this.getProjectID(location.target);
const url = this.buildRawUrl(location.target, projectID);
const response = await fetch(url.toString(), this.getRequestOptions());
if (response.ok) {
const data = await response.text();
emit(result.data(location, Buffer.from(data)));
} else {
const message = `${location.target} could not be read as ${url}, ${response.status} ${response.statusText}`;
if (response.status === 404) {
if (!optional) {
emit(result.notFoundError(location, message));
}
} else {
emit(result.generalError(location, message));
}
}
} catch (e) {
const message = `Unable to read ${location.type} ${location.target}, ${e}`;
emit(result.generalError(location, message));
}
return true;
}
// convert https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath
// to https://gitlab.com/api/v4/projects/<PROJECTID>/repository/files/filepath?ref=branch
buildRawUrl(target: string, projectID: Number): URL {
try {
const url = new URL(target);
const branchAndfilePath = url.pathname.split('/-/blob/')[1];
const [branch, ...filePath] = branchAndfilePath.split('/');
url.pathname = [
'/api/v4/projects',
projectID,
'repository/files',
encodeURIComponent(filePath.join('/')),
'raw',
].join('/');
url.search = `?ref=${branch}`;
return url;
} catch (e) {
throw new Error(`Incorrect url: ${target}, ${e}`);
}
}
async getProjectID(target: string): Promise<Number> {
const url = new URL(target);
if (
// absPaths to gitlab files should contain /-/blob
// ex: https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath
!url.pathname.match(/\/\-\/blob\//)
) {
throw new Error('Please provide full path to yaml file from Gitlab');
}
try {
const repo = url.pathname.split('/-/blob/')[0];
// Find ProjectID from url
// convert 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath'
// to 'https://gitlab.com/api/v4/projects/groupA%2Fteams%2FsubgroupA%2FteamA%2Frepo'
const repoIDLookup = new URL(
`${url.protocol + url.hostname}/api/v4/projects/${encodeURIComponent(
repo.replace(/^\//, ''),
)}`,
);
const response = await fetch(
repoIDLookup.toString(),
this.getRequestOptions(),
);
const projectIDJson = await response.json();
const projectID: Number = projectIDJson.id;
return projectID;
} catch (e) {
throw new Error(`Could not get GitLab ProjectID for: ${target}, ${e}`);
}
}
}
@@ -1,94 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { LocationSpec } from '@backstage/catalog-model';
import fetch from 'cross-fetch';
import * as result from './results';
import { CatalogProcessor, CatalogProcessorEmit } from './types';
// ***********************************************************************
// * NOTE: This has been replaced by packages/backend-common/src/reading *
// * Don't implement new functionality here as this file will be removed *
// ***********************************************************************
export class GitlabReaderProcessor implements CatalogProcessor {
async readLocation(
location: LocationSpec,
optional: boolean,
emit: CatalogProcessorEmit,
): Promise<boolean> {
if (location.type !== 'gitlab') {
return false;
}
try {
const url = this.buildRawUrl(location.target);
const response = await fetch(url.toString());
if (response.ok) {
const data = await response.text();
emit(result.data(location, Buffer.from(data)));
} else {
const message = `${location.target} could not be read as ${url}, ${response.status} ${response.statusText}`;
if (response.status === 404) {
if (!optional) {
throw result.notFoundError(location, message);
}
} else {
throw result.generalError(location, message);
}
}
} catch (e) {
const message = `Unable to read ${location.type} ${location.target}, ${e}`;
emit(result.generalError(location, message));
}
return true;
}
// Converts
// from: https://gitlab.example.com/a/b/blob/master/c.yaml
// to: https://gitlab.example.com/a/b/raw/master/c.yaml
private buildRawUrl(target: string): URL {
try {
const url = new URL(target);
const [empty, userOrOrg, repoName, , ...restOfPath] = url.pathname
.split('/')
// for the common case https://gitlab.example.com/a/b/-/blob/master/c.yaml
.filter(path => path !== '-');
if (
empty !== '' ||
userOrOrg === '' ||
repoName === '' ||
!restOfPath.join('/').match(/\.yaml$/)
) {
throw new Error('Wrong GitLab URL');
}
// Replace 'blob' with 'raw'
url.pathname = [empty, userOrOrg, repoName, 'raw', ...restOfPath].join(
'/',
);
return url;
} catch (e) {
throw new Error(`Incorrect url: ${target}, ${e}`);
}
}
}
@@ -21,7 +21,7 @@ import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { msw } from '@backstage/test-utils';
import {
CatalogProcessorDataResult,
CatalogProcessorEntityResult,
CatalogProcessorErrorResult,
CatalogProcessorResult,
} from './types';
@@ -42,17 +42,17 @@ describe('UrlReaderProcessor', () => {
server.use(
rest.get(`${mockApiOrigin}/component.yaml`, (_, res, ctx) =>
res(ctx.body('Hello')),
res(ctx.json({ mock: 'entity' })),
),
);
const generated = (await new Promise<CatalogProcessorResult>(emit =>
processor.readLocation(spec, false, emit),
)) as CatalogProcessorDataResult;
)) as CatalogProcessorEntityResult;
expect(generated.type).toBe('data');
expect(generated.type).toBe('entity');
expect(generated.location).toBe(spec);
expect(generated.data.toString('utf8')).toBe('Hello');
expect(generated.entity).toEqual({ mock: 'entity' });
});
it('should fail load from url with error', async () => {

Some files were not shown because too many files have changed in this diff Show More