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

This commit is contained in:
Marek Calus
2020-11-29 18:18:11 +01:00
137 changed files with 1766 additions and 495 deletions
+18
View File
@@ -1,5 +1,23 @@
# @backstage/plugin-api-docs
## 0.3.0
### Minor Changes
- f3bb55ee3: APIs now have real entity pages that are customizable in the app.
Therefore the old entity page from this plugin is removed.
See the `packages/app` on how to create and customize the API entity page.
### Patch Changes
- 6f70ed7a9: Replace usage of implementsApis with relations
- Updated dependencies [6f70ed7a9]
- Updated dependencies [ab94c9542]
- Updated dependencies [2daf18e80]
- Updated dependencies [069cda35f]
- @backstage/plugin-catalog@0.2.4
- @backstage/catalog-model@0.3.1
## 0.2.2
### Patch Changes
+1 -1
View File
@@ -21,7 +21,7 @@ Right now, the following API formats are supported:
Other formats are displayed as plain text, but this can easily be extended.
To fill the catalog with APIs, [provide entities of kind API](https://backstage.io/docs/features/software-catalog/descriptor-format#kind-api).
To link that an component implements an API, see [`implementsApis` property on components](https://backstage.io/docs/features/software-catalog/descriptor-format#specimplementsapis-optional).
To link that a component provides or consumes an API, see the [`providesApis`](https://backstage.io/docs/features/software-catalog/descriptor-format#specprovidesapis-optional) and [`consumesApis`](https://backstage.io/docs/features/software-catalog/descriptor-format#specconsumesapis-optional) properties on the Component kind.
## Links
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-api-docs",
"version": "0.2.2",
"version": "0.3.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,9 +20,9 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/catalog-model": "^0.3.0",
"@backstage/catalog-model": "^0.3.1",
"@backstage/core": "^0.3.2",
"@backstage/plugin-catalog": "^0.2.3",
"@backstage/plugin-catalog": "^0.2.4",
"@backstage/theme": "^0.2.1",
"@kyma-project/asyncapi-react": "^0.14.2",
"@material-icons/font": "^1.0.2",
@@ -40,7 +40,7 @@
"swagger-ui-react": "^3.31.1"
},
"devDependencies": {
"@backstage/cli": "^0.3.1",
"@backstage/cli": "^0.3.2",
"@backstage/dev-utils": "^0.1.4",
"@backstage/test-utils": "^0.1.3",
"@testing-library/jest-dom": "^5.10.1",
@@ -28,7 +28,7 @@ spec:
type: service
lifecycle: production
owner: guest
implementsApis:
providesApis:
- example-api
`;
@@ -47,11 +47,10 @@ export const MissingImplementsApisEmptyState = () => {
missing="field"
title="No APIs implemented by this entity"
description={
<Typography>
<>
Components can implement APIs that are displayed on this page. You
need to fill the <code>implementsApis</code> field to enable this
tool.
</Typography>
need to fill the <code>providesApis</code> field to enable this tool.
</>
}
action={
<>
@@ -71,7 +70,7 @@ export const MissingImplementsApisEmptyState = () => {
<Button
variant="contained"
color="primary"
href="https://backstage.io/docs/features/software-catalog/descriptor-format#specimplementsapis-optional"
href="https://backstage.io/docs/features/software-catalog/descriptor-format#specprovidesapis-optional"
>
Read more
</Button>
+3 -2
View File
@@ -15,14 +15,15 @@
*/
import React from 'react';
import { Entity } from '@backstage/catalog-model';
import { Entity, RELATION_PROVIDES_API } from '@backstage/catalog-model';
import { Route, Routes } from 'react-router';
import { catalogRoute } from '../routes';
import { EntityPageApi } from './EntityPageApi';
import { MissingImplementsApisEmptyState } from './MissingImplementsApisEmptyState';
const isPluginApplicableToEntity = (entity: Entity) => {
return ((entity.spec?.implementsApis as string[]) || []).length > 0;
// TODO: Also support RELATION_CONSUMES_API
return entity.relations?.some(r => r.type === RELATION_PROVIDES_API);
};
export const Router = ({ entity }: { entity: Entity }) =>
@@ -22,7 +22,7 @@ import * as React from 'react';
import { apiDocsConfigRef } from '../../config';
import { ApiExplorerTable } from './ApiExplorerTable';
const entites: Entity[] = [
const entities: Entity[] = [
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'API',
@@ -70,7 +70,7 @@ describe('ApiCatalogTable component', () => {
const rendered = render(
wrapInTestApp(
<ApiProvider apis={apiRegistry}>
<ApiExplorerTable entities={entites} loading={false} />
<ApiExplorerTable entities={entities} loading={false} />
</ApiProvider>,
),
);
@@ -14,8 +14,16 @@
* limitations under the License.
*/
import { ComponentEntity } from '@backstage/catalog-model';
import {
ComponentEntity,
RELATION_PROVIDES_API,
} from '@backstage/catalog-model';
export const useComponentApiNames = (entity: ComponentEntity) => {
return (entity.spec?.implementsApis as string[]) || [];
// TODO: This code doesn't handle namespaces and kinds correctly, but will be removed soon
return (
entity.relations
?.filter(r => r.type === RELATION_PROVIDES_API)
?.map(r => r.target.name) || []
);
};
+9
View File
@@ -1,5 +1,14 @@
# @backstage/plugin-app-backend
## 0.3.1
### Patch Changes
- ff1301d28: Warn if the app-backend can't start-up because the static directory that should be served is unavailable.
- Updated dependencies [3aa7efb3f]
- Updated dependencies [b3d4e4e57]
- @backstage/backend-common@0.3.2
## 0.3.0
### Minor Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-app-backend",
"version": "0.3.0",
"version": "0.3.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,7 +20,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.3.0",
"@backstage/backend-common": "^0.3.2",
"@backstage/config-loader": "^0.3.0",
"@backstage/config": "^0.1.1",
"@types/express": "^4.17.6",
@@ -31,7 +31,7 @@
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.3.0",
"@backstage/cli": "^0.3.2",
"@types/supertest": "^2.0.8",
"msw": "^0.20.5",
"supertest": "^4.0.2"
+14
View File
@@ -1,5 +1,19 @@
# @backstage/plugin-auth-backend
## 0.2.4
### Patch Changes
- 50eff1d00: Allow the backend to register custom AuthProviderFactories
- 700a212b4: bug fix: issue 3223 - detect mismatching origin and indicate it in the message at auth failure
- Updated dependencies [3aa7efb3f]
- Updated dependencies [ab94c9542]
- Updated dependencies [2daf18e80]
- Updated dependencies [069cda35f]
- Updated dependencies [b3d4e4e57]
- @backstage/backend-common@0.3.2
- @backstage/catalog-model@0.3.1
## 0.2.3
### Patch Changes
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-auth-backend",
"version": "0.2.3",
"version": "0.2.4",
"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.3.1",
"@backstage/backend-common": "^0.3.2",
"@backstage/catalog-client": "^0.3.1",
"@backstage/catalog-model": "^0.3.0",
"@backstage/catalog-model": "^0.3.1",
"@backstage/config": "^0.1.1",
"@types/express": "^4.17.6",
"compression": "^1.7.4",
@@ -55,7 +55,7 @@
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.3.1",
"@backstage/cli": "^0.3.2",
"@types/body-parser": "^1.19.0",
"@types/cookie-parser": "^1.4.2",
"@types/express-session": "^1.17.2",
+8
View File
@@ -15,3 +15,11 @@
*/
export * from './service/router';
export * from './providers';
// flow package provides 2 functions
// ensuresXRequestedWith and postMessageResponse to safely handle CORS requests for login. The WebMessageResponse type in flow is used to type the response from the login-popup
export * from './lib/flow';
// OAuth wrapper over a passport or a custom `startegy`.
export * from './lib/oauth';
@@ -81,6 +81,52 @@ describe('oauth helpers', () => {
expect(mockResponse.end).toBeCalledWith(expect.stringContaining(encoded));
});
it('should call postMessage twice but only one of them with target *', () => {
let responseBody = '';
const mockResponse = ({
end: jest.fn(body => {
responseBody = body;
return this;
}),
setHeader: jest.fn().mockReturnThis(),
} as unknown) as express.Response;
const data: WebMessageResponse = {
type: 'authorization_response',
response: {
providerInfo: {
accessToken: 'ACCESS_TOKEN',
idToken: 'ID_TOKEN',
expiresInSeconds: 10,
scope: 'email',
},
profile: {
email: 'foo@bar.com',
},
backstageIdentity: {
id: 'a',
idToken: 'a.b.c',
},
},
};
postMessageResponse(mockResponse, appOrigin, data);
expect(responseBody.match(/.postMessage\(/g)).toHaveLength(2);
expect(
responseBody.match(/.postMessage\([a-zA-z.()]*, \'\*\'\)/g),
).toHaveLength(1);
const errData: WebMessageResponse = {
type: 'authorization_response',
error: new Error('Unknown error occurred'),
};
postMessageResponse(mockResponse, appOrigin, errData);
expect(responseBody.match(/.postMessage\(/g)).toHaveLength(2);
expect(
responseBody.match(/.postMessage\([a-zA-z.()]*, \'\*\'\)/g),
).toHaveLength(1);
});
it('handles single quotes and unicode chars safely', () => {
const mockResponse = ({
end: jest.fn().mockReturnThis(),
@@ -38,10 +38,24 @@ export const postMessageResponse = (
// data.
// TODO: Make target app origin configurable globally
//
// postMessage fails silently if the targetOrigin is disallowed.
// So 2 postMessages are sent from the popup to the parent window.
// First, the origin being used to post the actual authorization response is
// shared with the parent window with a postMessage with targetOrigin '*'.
// Second, the actual authorization response is sent with the app origin
// as the targetOrigin.
// If the first message was received but the actual auth response was
// never received, the event listener can conclude that targetOrigin
// was disallowed, indicating potential misconfiguration.
//
const script = `
var json = decodeURIComponent('${base64Data}');
var authResponse = decodeURIComponent('${base64Data}');
var origin = decodeURIComponent('${base64Origin}');
(window.opener || window.parent).postMessage(JSON.parse(json), origin);
var originInfo = {'type': 'config_info', 'targetOrigin': origin};
(window.opener || window.parent).postMessage(originInfo, '*');
(window.opener || window.parent).postMessage(JSON.parse(authResponse), origin);
window.close();
`;
const hash = crypto.createHash('sha256').update(script).digest('base64');
@@ -15,3 +15,5 @@
*/
export { ensuresXRequestedWith, postMessageResponse } from './authFlowHelpers';
export type { WebMessageResponse } from './types';
@@ -149,12 +149,12 @@ export class Auth0AuthProvider implements OAuthHandlers {
}
export const createAuth0Provider: AuthProviderFactory = ({
providerId,
globalConfig,
config,
tokenIssuer,
}) =>
OAuthEnvironmentHandler.mapConfig(config, envConfig => {
const providerId = 'auth0';
const clientId = envConfig.getString('clientId');
const clientSecret = envConfig.getString('clientSecret');
const domain = envConfig.getString('domain');
@@ -24,9 +24,9 @@ import { createSamlProvider } from './saml';
import { createAuth0Provider } from './auth0';
import { createMicrosoftProvider } from './microsoft';
import { createOneLoginProvider } from './onelogin';
import { AuthProviderFactory, AuthProviderFactoryOptions } from './types';
import { AuthProviderFactory } from './types';
const factories: { [providerId: string]: AuthProviderFactory } = {
export const factories: { [providerId: string]: AuthProviderFactory } = {
google: createGoogleProvider,
github: createGithubProvider,
gitlab: createGitlabProvider,
@@ -38,15 +38,3 @@ const factories: { [providerId: string]: AuthProviderFactory } = {
oidc: createOidcProvider,
onelogin: createOneLoginProvider,
};
export function createAuthProvider(
providerId: string,
options: AuthProviderFactoryOptions,
) {
const factory = factories[providerId];
if (!factory) {
throw Error(`No auth provider available for '${providerId}'`);
}
return factory(options);
}
@@ -137,12 +137,12 @@ export class GithubAuthProvider implements OAuthHandlers {
}
export const createGithubProvider: AuthProviderFactory = ({
providerId,
globalConfig,
config,
tokenIssuer,
}) =>
OAuthEnvironmentHandler.mapConfig(config, envConfig => {
const providerId = 'github';
const clientId = envConfig.getString('clientId');
const clientSecret = envConfig.getString('clientSecret');
const enterpriseInstanceUrl = envConfig.getOptionalString(
@@ -140,12 +140,12 @@ export class GitlabAuthProvider implements OAuthHandlers {
}
export const createGitlabProvider: AuthProviderFactory = ({
providerId,
globalConfig,
config,
tokenIssuer,
}) =>
OAuthEnvironmentHandler.mapConfig(config, envConfig => {
const providerId = 'gitlab';
const clientId = envConfig.getString('clientId');
const clientSecret = envConfig.getString('clientSecret');
const audience = envConfig.getString('audience');
@@ -175,6 +175,7 @@ export class GoogleAuthProvider implements OAuthHandlers {
}
export const createGoogleProvider: AuthProviderFactory = ({
providerId,
globalConfig,
config,
logger,
@@ -182,7 +183,6 @@ export const createGoogleProvider: AuthProviderFactory = ({
catalogApi,
}) =>
OAuthEnvironmentHandler.mapConfig(config, envConfig => {
const providerId = 'google';
const clientId = envConfig.getString('clientId');
const clientSecret = envConfig.getString('clientSecret');
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
+13 -1
View File
@@ -14,4 +14,16 @@
* limitations under the License.
*/
export { createAuthProvider } from './factories';
export { factories as defaultAuthProviderFactories } from './factories';
// Export the minimal interface required for implementing a
// custom Authorization Handler
export type {
AuthProviderRouteHandlers,
AuthProviderFactoryOptions,
AuthProviderFactory,
} from './types';
// These types are needed for a postMessage from the login pop-up
// to the frontend
export type { AuthResponse, BackstageIdentity, ProfileInfo } from './types';
@@ -206,13 +206,12 @@ export class MicrosoftAuthProvider implements OAuthHandlers {
}
export const createMicrosoftProvider: AuthProviderFactory = ({
providerId,
globalConfig,
config,
tokenIssuer,
}) =>
OAuthEnvironmentHandler.mapConfig(config, envConfig => {
const providerId = 'microsoft';
const clientId = envConfig.getString('clientId');
const clientSecret = envConfig.getString('clientSecret');
const tenantID = envConfig.getString('tenantId');
@@ -157,12 +157,12 @@ export class OAuth2AuthProvider implements OAuthHandlers {
}
export const createOAuth2Provider: AuthProviderFactory = ({
providerId,
globalConfig,
config,
tokenIssuer,
}) =>
OAuthEnvironmentHandler.mapConfig(config, envConfig => {
const providerId = 'oauth2';
const clientId = envConfig.getString('clientId');
const clientSecret = envConfig.getString('clientSecret');
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
@@ -171,12 +171,12 @@ export class OidcAuthProvider implements OAuthHandlers {
}
export const createOidcProvider: AuthProviderFactory = ({
providerId,
globalConfig,
config,
tokenIssuer,
}) =>
OAuthEnvironmentHandler.mapConfig(config, envConfig => {
const providerId = 'oidc';
const clientId = envConfig.getString('clientId');
const clientSecret = envConfig.getString('clientSecret');
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
@@ -168,12 +168,12 @@ export class OktaAuthProvider implements OAuthHandlers {
}
export const createOktaProvider: AuthProviderFactory = ({
providerId,
globalConfig,
config,
tokenIssuer,
}) =>
OAuthEnvironmentHandler.mapConfig(config, envConfig => {
const providerId = 'okta';
const clientId = envConfig.getString('clientId');
const clientSecret = envConfig.getString('clientSecret');
const audience = envConfig.getString('audience');
@@ -147,12 +147,12 @@ export class OneLoginProvider implements OAuthHandlers {
}
export const createOneLoginProvider: AuthProviderFactory = ({
providerId,
globalConfig,
config,
tokenIssuer,
}) =>
OAuthEnvironmentHandler.mapConfig(config, envConfig => {
const providerId = 'onelogin';
const clientId = envConfig.getString('clientId');
const clientSecret = envConfig.getString('clientSecret');
const issuer = envConfig.getString('issuer');
@@ -121,12 +121,12 @@ type SAMLProviderOptions = {
};
export const createSamlProvider: AuthProviderFactory = ({
providerId,
globalConfig,
config,
tokenIssuer,
}) => {
const url = new URL(globalConfig.baseUrl);
const providerId = 'saml';
const entryPoint = config.getString('entryPoint');
const issuer = config.getString('issuer');
const opts = {
@@ -113,6 +113,7 @@ export interface AuthProviderRouteHandlers {
}
export type AuthProviderFactoryOptions = {
providerId: string;
globalConfig: AuthProviderConfig;
config: Config;
logger: Logger;
+23 -6
View File
@@ -14,6 +14,14 @@
* limitations under the License.
*/
import express from 'express';
import Router from 'express-promise-router';
import cookieParser from 'cookie-parser';
import { Logger } from 'winston';
import {
defaultAuthProviderFactories,
AuthProviderFactory,
} from '../providers';
import {
NotFoundError,
PluginDatabaseManager,
@@ -21,20 +29,18 @@ import {
} from '@backstage/backend-common';
import { CatalogClient } from '@backstage/catalog-client';
import { Config } from '@backstage/config';
import cookieParser from 'cookie-parser';
import express from 'express';
import Router from 'express-promise-router';
import { Logger } from 'winston';
import { createOidcRouter, DatabaseKeyStore, TokenFactory } from '../identity';
import { createAuthProvider } from '../providers';
import session from 'express-session';
import passport from 'passport';
type ProviderFactories = { [s: string]: AuthProviderFactory };
export interface RouterOptions {
logger: Logger;
database: PluginDatabaseManager;
config: Config;
discovery: PluginEndpointDiscovery;
providerFactories?: ProviderFactories;
}
export async function createRouter({
@@ -42,6 +48,7 @@ export async function createRouter({
config,
discovery,
database,
providerFactories,
}: RouterOptions): Promise<express.Router> {
const router = Router();
@@ -74,13 +81,23 @@ export async function createRouter({
router.use(express.urlencoded({ extended: false }));
router.use(express.json());
const allProviderFactories = {
...defaultAuthProviderFactories,
...providerFactories,
};
const providersConfig = config.getConfig('auth.providers');
const providers = providersConfig.keys();
for (const providerId of providers) {
logger.info(`Configuring provider, ${providerId}`);
try {
const provider = createAuthProvider(providerId, {
const providerFactory = allProviderFactories[providerId];
if (!providerFactory) {
throw Error(`No auth provider available for '${providerId}'`);
}
const provider = providerFactory({
providerId,
globalConfig: { baseUrl: authUrl, appUrl },
config: providersConfig.getConfig(providerId),
logger,
+29
View File
@@ -1,5 +1,34 @@
# @backstage/plugin-catalog-backend
## 0.2.3
### Patch Changes
- 1ec19a3f4: Ignore empty YAML documents. Having a YAML file like this is now ingested without an error:
```yaml
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: web
spec:
type: website
---
```
This behaves now the same way as Kubernetes handles multiple documents in a single YAML file.
- ab94c9542: Add `providesApis` and `consumesApis` to the component entity spec.
- 2daf18e80: Start emitting all known relation types from the core entity kinds, based on their spec data.
- Updated dependencies [3aa7efb3f]
- Updated dependencies [ab94c9542]
- Updated dependencies [2daf18e80]
- Updated dependencies [069cda35f]
- Updated dependencies [b3d4e4e57]
- @backstage/backend-common@0.3.2
- @backstage/catalog-model@0.3.1
## 0.2.2
### Patch Changes
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-catalog-backend",
"version": "0.2.2",
"version": "0.2.3",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,8 +21,8 @@
},
"dependencies": {
"@azure/msal-node": "^1.0.0-alpha.8",
"@backstage/backend-common": "^0.3.1",
"@backstage/catalog-model": "^0.3.0",
"@backstage/backend-common": "^0.3.2",
"@backstage/catalog-model": "^0.3.1",
"@backstage/config": "^0.1.1",
"@octokit/graphql": "^4.5.6",
"@types/express": "^4.17.6",
@@ -48,7 +48,7 @@
"yup": "^0.29.3"
},
"devDependencies": {
"@backstage/cli": "^0.3.1",
"@backstage/cli": "^0.3.2",
"@backstage/test-utils": "^0.1.3",
"@types/core-js": "^2.5.4",
"@types/git-url-parse": "^9.0.0",
@@ -68,12 +68,14 @@ describe('BuiltinKindsEntityProcessor', () => {
owner: 'o',
lifecycle: 'l',
implementsApis: ['a'],
providesApis: ['b'],
consumesApis: ['c'],
},
};
await processor.postProcessEntity(entity, location, emit);
expect(emit).toBeCalledTimes(4);
expect(emit).toBeCalledTimes(8);
expect(emit).toBeCalledWith({
type: 'relation',
relation: {
@@ -106,6 +108,38 @@ describe('BuiltinKindsEntityProcessor', () => {
target: { kind: 'API', namespace: 'default', name: 'a' },
},
});
expect(emit).toBeCalledWith({
type: 'relation',
relation: {
source: { kind: 'API', namespace: 'default', name: 'b' },
type: 'apiProvidedBy',
target: { kind: 'Component', namespace: 'default', name: 'n' },
},
});
expect(emit).toBeCalledWith({
type: 'relation',
relation: {
source: { kind: 'Component', namespace: 'default', name: 'n' },
type: 'providesApi',
target: { kind: 'API', namespace: 'default', name: 'b' },
},
});
expect(emit).toBeCalledWith({
type: 'relation',
relation: {
source: { kind: 'API', namespace: 'default', name: 'c' },
type: 'apiConsumedBy',
target: { kind: 'Component', namespace: 'default', name: 'n' },
},
});
expect(emit).toBeCalledWith({
type: 'relation',
relation: {
source: { kind: 'Component', namespace: 'default', name: 'n' },
type: 'consumesApi',
target: { kind: 'API', namespace: 'default', name: 'c' },
},
});
});
it('generates relations for api entities', async () => {
@@ -26,8 +26,10 @@ import {
locationEntityV1alpha1Validator,
LocationSpec,
parseEntityRef,
RELATION_API_CONSUMED_BY,
RELATION_API_PROVIDED_BY,
RELATION_CHILD_OF,
RELATION_CONSUMES_API,
RELATION_HAS_MEMBER,
RELATION_MEMBER_OF,
RELATION_OWNED_BY,
@@ -138,6 +140,18 @@ export class BuiltinKindsEntityProcessor implements CatalogProcessor {
RELATION_PROVIDES_API,
RELATION_API_PROVIDED_BY,
);
doEmit(
component.spec.providesApis,
{ defaultKind: 'API', defaultNamespace: selfRef.namespace },
RELATION_PROVIDES_API,
RELATION_API_PROVIDED_BY,
);
doEmit(
component.spec.consumesApis,
{ defaultKind: 'API', defaultNamespace: selfRef.namespace },
RELATION_CONSUMES_API,
RELATION_API_CONSUMED_BY,
);
}
/*
@@ -115,6 +115,41 @@ describe('parseEntityYaml', () => {
]);
});
it('should handle empty yaml documents', () => {
// This happens if the user accidentially adds a "---"
// at the end of a file
const results = Array.from(
parseEntityYaml(
Buffer.from(
`
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: web
spec:
type: website
---
`,
'utf8',
),
testLoc,
),
);
expect(results).toEqual([
result.entity(testLoc, {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'web',
},
spec: {
type: 'website',
},
}),
]);
});
it('should emit parsing errors', () => {
const results = Array.from(
parseEntityYaml(Buffer.from('`', 'utf8'), testLoc),
@@ -40,6 +40,9 @@ export function* parseEntityYaml(
const json = document.toJSON();
if (lodash.isPlainObject(json)) {
yield result.entity(location, json as Entity);
} else if (json === null) {
// Ignore null values, these happen if there is an empty document in the
// YAML file, for example if --- is added to the end of the file.
} else {
const message = `Expected object at root, got ${typeof json}`;
yield result.generalError(location, message);
+12
View File
@@ -1,5 +1,17 @@
# @backstage/plugin-catalog
## 0.2.4
### Patch Changes
- 6f70ed7a9: Replace usage of implementsApis with relations
- Updated dependencies [4b53294a6]
- Updated dependencies [ab94c9542]
- Updated dependencies [2daf18e80]
- Updated dependencies [069cda35f]
- @backstage/plugin-techdocs@0.3.0
- @backstage/catalog-model@0.3.1
## 0.2.3
### Patch Changes
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-catalog",
"version": "0.2.3",
"version": "0.2.4",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -22,10 +22,10 @@
},
"dependencies": {
"@backstage/catalog-client": "^0.3.1",
"@backstage/catalog-model": "^0.3.0",
"@backstage/catalog-model": "^0.3.1",
"@backstage/core": "^0.3.2",
"@backstage/plugin-scaffolder": "^0.3.1",
"@backstage/plugin-techdocs": "^0.2.3",
"@backstage/plugin-techdocs": "^0.3.0",
"@backstage/theme": "^0.2.1",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
@@ -43,7 +43,7 @@
"swr": "^0.3.0"
},
"devDependencies": {
"@backstage/cli": "^0.3.1",
"@backstage/cli": "^0.3.2",
"@backstage/dev-utils": "^0.1.4",
"@backstage/test-utils": "^0.1.3",
"@microsoft/microsoft-graph-types": "^1.25.0",
@@ -18,6 +18,7 @@ import {
Entity,
ENTITY_DEFAULT_NAMESPACE,
RELATION_OWNED_BY,
RELATION_PROVIDES_API,
serializeEntityRef,
} from '@backstage/catalog-model';
import {
@@ -111,6 +112,8 @@ type AboutCardProps = {
export function AboutCard({ entity, variant }: AboutCardProps) {
const classes = useStyles();
const codeLink = getCodeLinkInfo(entity);
// TODO: Also support RELATION_CONSUMES_API here
const hasApis = entity.relations?.some(r => r.type === RELATION_PROVIDES_API);
return (
<Card className={variant === 'gridItem' ? classes.gridItemCard : ''}>
@@ -146,9 +149,9 @@ export function AboutCard({ entity, variant }: AboutCardProps) {
}/${entity.kind}/${entity.metadata.name}`}
/>
<IconLinkVertical
disabled={!entity.spec?.implementsApis}
disabled={!hasApis}
label="View API"
title={!entity.spec?.implementsApis ? 'No APIs available' : ''}
title={hasApis ? '' : 'No APIs available'}
icon={<ExtensionIcon />}
href="api"
/>
+1 -1
View File
@@ -40,7 +40,7 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.3.1",
"@backstage/cli": "^0.3.2",
"@backstage/dev-utils": "^0.1.4",
"@backstage/test-utils": "^0.1.3",
"@testing-library/jest-dom": "^5.10.1",
+1 -1
View File
@@ -39,7 +39,7 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.3.1",
"@backstage/cli": "^0.3.2",
"@backstage/dev-utils": "^0.1.4",
"@backstage/test-utils": "^0.1.3",
"@testing-library/jest-dom": "^5.10.1",
+1 -1
View File
@@ -46,7 +46,7 @@
"yup": "^0.29.3"
},
"devDependencies": {
"@backstage/cli": "^0.3.1",
"@backstage/cli": "^0.3.2",
"@backstage/dev-utils": "^0.1.4",
"@backstage/test-utils": "^0.1.3",
"@testing-library/jest-dom": "^5.10.1",
+1 -1
View File
@@ -33,7 +33,7 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.3.1",
"@backstage/cli": "^0.3.2",
"@backstage/dev-utils": "^0.1.4",
"@backstage/test-utils": "^0.1.3",
"@testing-library/jest-dom": "^5.10.1",
+1 -1
View File
@@ -31,7 +31,7 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.3.1",
"@backstage/cli": "^0.3.2",
"@backstage/dev-utils": "^0.1.4",
"@backstage/test-utils": "^0.1.3",
"@testing-library/jest-dom": "^5.10.1",
+1 -1
View File
@@ -40,7 +40,7 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.3.1",
"@backstage/cli": "^0.3.2",
"@backstage/dev-utils": "^0.1.4",
"@backstage/test-utils": "^0.1.3",
"@testing-library/jest-dom": "^5.10.1",
+1 -1
View File
@@ -32,7 +32,7 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.3.1",
"@backstage/cli": "^0.3.2",
"@backstage/dev-utils": "^0.1.4",
"@backstage/test-utils": "^0.1.3",
"@testing-library/jest-dom": "^5.10.1",
+1 -1
View File
@@ -43,7 +43,7 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.3.1",
"@backstage/cli": "^0.3.2",
"@backstage/dev-utils": "^0.1.4",
"@backstage/test-utils": "^0.1.3",
"@testing-library/jest-dom": "^5.10.1",
+1 -1
View File
@@ -36,7 +36,7 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.3.1",
"@backstage/cli": "^0.3.2",
"@backstage/dev-utils": "^0.1.4",
"@backstage/test-utils": "^0.1.3",
"@testing-library/jest-dom": "^5.10.1",
+1 -1
View File
@@ -36,7 +36,7 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.3.1",
"@backstage/cli": "^0.3.2",
"@backstage/dev-utils": "^0.1.4",
"@backstage/test-utils": "^0.1.3",
"@testing-library/jest-dom": "^5.10.1",
+1 -1
View File
@@ -38,7 +38,7 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.3.1",
"@backstage/cli": "^0.3.2",
"@backstage/dev-utils": "^0.1.4",
"@backstage/test-utils": "^0.1.3",
"@testing-library/jest-dom": "^5.10.1",
+1 -1
View File
@@ -31,7 +31,7 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.3.1",
"@backstage/cli": "^0.3.2",
"@backstage/dev-utils": "^0.1.4",
"@backstage/test-utils": "^0.1.3",
"@testing-library/jest-dom": "^5.10.1",
+1 -1
View File
@@ -36,7 +36,7 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.3.1",
"@backstage/cli": "^0.3.2",
"@backstage/dev-utils": "^0.1.4",
"@backstage/test-utils": "^0.1.3",
"@testing-library/jest-dom": "^5.10.1",
+9
View File
@@ -1,5 +1,14 @@
# @backstage/plugin-rollbar-backend
## 0.1.4
### Patch Changes
- 3a201c5d5: Add config schema for the rollbar & rollbar-backend plugins
- Updated dependencies [3aa7efb3f]
- Updated dependencies [b3d4e4e57]
- @backstage/backend-common@0.3.2
## 0.1.3
### Patch Changes
-1
View File
@@ -8,7 +8,6 @@ The following values are read from the configuration file.
```yaml
rollbar:
organization: organization-name
accountToken:
$env: ROLLBAR_ACCOUNT_TOKEN
```
@@ -14,28 +14,13 @@
* limitations under the License.
*/
import fetch from 'cross-fetch';
export class TechDocsMetadata {
private async getMetadataFile(docsUrl: String) {
const metadataURL = `${docsUrl}/techdocs_metadata.json`;
try {
const req = await fetch(metadataURL);
return await req.json();
} catch (error) {
throw new Error(error);
}
}
public async getMkDocsMetaData(docsUrl: any) {
const mkDocsMetadata = await this.getMetadataFile(docsUrl);
if (!mkDocsMetadata) return null;
return {
...mkDocsMetadata,
};
}
export interface Config {
/** Configuration options for the rollbar-backend plugin */
rollbar?: {
/**
* The autentication token for accessing the Rollbar API
* @see https://explorer.docs.rollbar.com/#section/Authentication
*/
accountToken: string;
};
}
+7 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-rollbar-backend",
"version": "0.1.3",
"version": "0.1.4",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,7 +20,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.3.0",
"@backstage/backend-common": "^0.3.2",
"@backstage/config": "^0.1.1",
"@types/express": "^4.17.6",
"axios": "^0.20.0",
@@ -37,11 +37,13 @@
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.3.0",
"@backstage/cli": "^0.3.2",
"@types/supertest": "^2.0.8",
"supertest": "^4.0.2"
},
"files": [
"dist"
]
"dist",
"config.d.ts"
],
"configSchema": "config.d.ts"
}
+12
View File
@@ -1,5 +1,17 @@
# @backstage/plugin-rollbar
## 0.2.4
### Patch Changes
- 3a201c5d5: Add config schema for the rollbar & rollbar-backend plugins
- Updated dependencies [6f70ed7a9]
- Updated dependencies [ab94c9542]
- Updated dependencies [2daf18e80]
- Updated dependencies [069cda35f]
- @backstage/plugin-catalog@0.2.4
- @backstage/catalog-model@0.3.1
## 0.2.3
### Patch Changes
+1
View File
@@ -54,6 +54,7 @@ const ServiceEntityPage = ({ entity }: { entity: Entity }) => (
# app.config.yaml
rollbar:
organization: organization-name
# used by rollbar-backend
accountToken:
$env: ROLLBAR_ACCOUNT_TOKEN
```
+27
View File
@@ -0,0 +1,27 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface Config {
/** Configuration options for the rollbar plugin */
rollbar?: {
/**
* The Rollbar organization name. This can be omitted by using the `rollbar.com/project-slug` annotation.
* @see https://backstage.io/docs/features/software-catalog/well-known-annotations#rollbarcomproject-slug
* @visibility frontend
*/
organization?: string;
};
}
+7 -21
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-rollbar",
"version": "0.2.3",
"version": "0.2.4",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,9 +21,9 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/catalog-model": "^0.3.0",
"@backstage/catalog-model": "^0.3.1",
"@backstage/core": "^0.3.2",
"@backstage/plugin-catalog": "^0.2.3",
"@backstage/plugin-catalog": "^0.2.4",
"@backstage/theme": "^0.2.1",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
@@ -37,7 +37,7 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.3.1",
"@backstage/cli": "^0.3.2",
"@backstage/dev-utils": "^0.1.4",
"@backstage/test-utils": "^0.1.3",
"@testing-library/jest-dom": "^5.10.1",
@@ -51,22 +51,8 @@
"msw": "^0.21.2"
},
"files": [
"dist"
"dist",
"config.d.ts"
],
"configSchema": {
"$schema": "https://backstage.io/schema/config-v1",
"title": "@backstage/rollbar",
"type": "object",
"properties": {
"rollbar": {
"type": "object",
"properties": {
"organization": {
"type": "string",
"visibility": "frontend"
}
}
}
}
}
"configSchema": "config.d.ts"
}
+1 -1
View File
@@ -41,7 +41,7 @@
"swr": "^0.3.0"
},
"devDependencies": {
"@backstage/cli": "^0.3.1",
"@backstage/cli": "^0.3.2",
"@backstage/dev-utils": "^0.1.4",
"@backstage/test-utils": "^0.1.3",
"@testing-library/jest-dom": "^5.10.1",
+1 -1
View File
@@ -34,7 +34,7 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.3.1",
"@backstage/cli": "^0.3.2",
"@backstage/dev-utils": "^0.1.4",
"@backstage/test-utils": "^0.1.3",
"@testing-library/jest-dom": "^5.10.1",
+1 -1
View File
@@ -36,7 +36,7 @@
"timeago.js": "^4.0.2"
},
"devDependencies": {
"@backstage/cli": "^0.3.1",
"@backstage/cli": "^0.3.2",
"@backstage/dev-utils": "^0.1.4",
"@backstage/test-utils": "^0.1.3",
"@testing-library/jest-dom": "^5.10.1",
+1 -1
View File
@@ -35,7 +35,7 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.3.1",
"@backstage/cli": "^0.3.2",
"@backstage/dev-utils": "^0.1.4",
"@backstage/test-utils": "^0.1.3",
"@testing-library/jest-dom": "^5.10.1",
+1 -1
View File
@@ -35,7 +35,7 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.3.1",
"@backstage/cli": "^0.3.2",
"@backstage/dev-utils": "^0.1.4",
"@backstage/test-utils": "^0.1.3",
"@testing-library/jest-dom": "^5.10.1",
+17
View File
@@ -1,5 +1,22 @@
# @backstage/plugin-techdocs-backend
## 0.3.0
### Minor Changes
- 4b53294a6: - Use techdocs annotation to add repo_url if missing in mkdocs.yml. Having repo_url creates a Edit button on techdocs pages.
- techdocs-backend: API endpoint `/metadata/mkdocs/*` renamed to `/metadata/techdocs/*`
### Patch Changes
- Updated dependencies [3aa7efb3f]
- Updated dependencies [ab94c9542]
- Updated dependencies [2daf18e80]
- Updated dependencies [069cda35f]
- Updated dependencies [b3d4e4e57]
- @backstage/backend-common@0.3.2
- @backstage/catalog-model@0.3.1
## 0.2.2
### Patch Changes
+6 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-techdocs-backend",
"version": "0.2.2",
"version": "0.3.0",
"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.3.1",
"@backstage/catalog-model": "^0.3.0",
"@backstage/backend-common": "^0.3.2",
"@backstage/catalog-model": "^0.3.1",
"@backstage/config": "^0.1.1",
"@types/dockerode": "^2.5.34",
"@types/express": "^4.17.6",
@@ -32,12 +32,14 @@
"express-promise-router": "^3.0.3",
"fs-extra": "^9.0.1",
"git-url-parse": "^11.4.0",
"js-yaml": "^3.14.0",
"knex": "^0.21.6",
"mock-fs": "^4.13.0",
"nodegit": "^0.27.0",
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/cli": "^0.3.1",
"@backstage/cli": "^0.3.2",
"supertest": "^4.0.2"
},
"files": [
@@ -69,10 +69,13 @@ export class DocsBuilder {
this.logger.info(`Running preparer on entity ${getEntityId(this.entity)}`);
const preparedDir = await this.preparer.prepare(this.entity);
const parsedLocationAnnotation = getLocationForEntity(this.entity);
this.logger.info(`Running generator on entity ${getEntityId(this.entity)}`);
const { resultDir } = await this.generator.run({
directory: preparedDir,
dockerClient: this.dockerClient,
parsedLocationAnnotation,
});
this.logger.info(`Running publisher on entity ${getEntityId(this.entity)}`);
@@ -61,7 +61,7 @@ export async function createRouter({
}: RouterOptions): Promise<express.Router> {
const router = Router();
router.get('/metadata/mkdocs/*', async (req, res) => {
router.get('/metadata/techdocs/*', async (req, res) => {
let storageUrl = config.getString('techdocs.storageUrl');
if (publisher instanceof LocalPublish) {
storageUrl = new URL(
@@ -74,8 +74,8 @@ export async function createRouter({
const metadataURL = `${storageUrl}/${path}/techdocs_metadata.json`;
try {
const mkDocsMetadata = await (await fetch(metadataURL)).json();
res.send(mkDocsMetadata);
const techdocsMetadata = await (await fetch(metadataURL)).json();
res.send(techdocsMetadata);
} catch (err) {
logger.info(`Unable to get metadata for ${path} with error ${err}`);
throw new Error(`Unable to get metadata for ${path} with error ${err}`);
@@ -0,0 +1,2 @@
site_name: Test site name
site_description: Test site description
@@ -0,0 +1,4 @@
site_name: Test site name
site_description: Test site description
repo_url: https://github.com/backstage/backstage
@@ -13,10 +13,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Stream, { PassThrough } from 'stream';
import fs from 'fs-extra';
import os from 'os';
import { resolve as resolvePath } from 'path';
import Stream, { PassThrough } from 'stream';
import Docker from 'dockerode';
import { runDockerContainer, getGeneratorKey } from './helpers';
import mockFs from 'mock-fs';
import * as winston from 'winston';
import {
runDockerContainer,
getGeneratorKey,
isValidRepoUrlForMkdocs,
getRepoUrlFromLocationAnnotation,
patchMkdocsYmlPreBuild,
} from './helpers';
import { RemoteProtocol } from '../prepare/types';
import { ParsedLocationAnnotation } from '../../../helpers';
const mockEntity = {
apiVersion: 'version',
@@ -28,6 +40,14 @@ const mockEntity = {
const mockDocker = new Docker() as jest.Mocked<Docker>;
const mkdocsYml = fs.readFileSync(
resolvePath(__filename, '../__fixtures__/mkdocs.yml'),
);
const mkdocsYmlWithRepoUrl = fs.readFileSync(
resolvePath(__filename, '../__fixtures__/mkdocs_with_repo_url.yml'),
);
const mockLogger = winston.createLogger();
describe('helpers', () => {
describe('getGeneratorKey', () => {
it('should return techdocs as the only generator key', () => {
@@ -138,4 +158,175 @@ describe('helpers', () => {
});
});
});
describe('isValidRepoUrlForMkdocs', () => {
it('should return true for valid repo_url values for mkdocs', () => {
const validRepoUrls = [
'https://github.com/org/repo',
'https://github.com/backstage/backstage/',
'https://github.com/org123/repo1-2-3/',
'http://github.com/insecureOrg/insecureRepo',
'https://gitlab.com/org/repo',
'https://gitlab.com/backstage/backstage/',
'https://gitlab.com/org123/repo1-2-3/',
'http://gitlab.com/insecureOrg/insecureRepo',
];
const validRemoteProtocols = ['github', 'gitlab'];
validRepoUrls.forEach(url => {
validRemoteProtocols.forEach(targetType => {
expect(
isValidRepoUrlForMkdocs(url, targetType as RemoteProtocol),
).toBe(true);
});
});
});
it('should return false for invalid repo_urls values for mkdocs', () => {
const invalidRepoUrls = [
'git@github.com:org/repo',
'https://github.com/backstage/backstage/tree/master/plugins/techdocs-backend',
];
invalidRepoUrls.forEach(url => {
expect(isValidRepoUrlForMkdocs(url, 'github')).toBe(false);
});
});
it('should return false for unsupported remote protocols', () => {
const validRepoUrl = 'https://github.com/backstage/backstage';
const unsupportedRemoteProtocols = ['dir', 'file', 'url'];
unsupportedRemoteProtocols.forEach(targetType => {
expect(
isValidRepoUrlForMkdocs(validRepoUrl, targetType as RemoteProtocol),
).toBe(false);
});
});
});
describe('getRepoUrlFromLocationAnnotation', () => {
it('should return undefined for unsupported location type', () => {
const parsedLocationAnnotation1: ParsedLocationAnnotation = {
type: 'dir',
target: '/home/user/workspace/docs-repository',
};
const parsedLocationAnnotation2: ParsedLocationAnnotation = {
type: 'file',
target: '/home/user/workspace/docs-repository/catalog-info.yaml',
};
const parsedLocationAnnotation3: ParsedLocationAnnotation = {
type: 'url',
target: 'https://my-website.com/storage/this/docs/repository',
};
expect(getRepoUrlFromLocationAnnotation(parsedLocationAnnotation1)).toBe(
undefined,
);
expect(getRepoUrlFromLocationAnnotation(parsedLocationAnnotation2)).toBe(
undefined,
);
expect(getRepoUrlFromLocationAnnotation(parsedLocationAnnotation3)).toBe(
undefined,
);
});
it('should return correct target url for supported hosts', () => {
const parsedLocationAnnotation1: ParsedLocationAnnotation = {
type: 'github',
target: 'https://github.com/backstage/backstage.git',
};
expect(getRepoUrlFromLocationAnnotation(parsedLocationAnnotation1)).toBe(
'https://github.com/backstage/backstage',
);
const parsedLocationAnnotation2: ParsedLocationAnnotation = {
type: 'github',
target: 'https://github.com/org/repo',
};
expect(getRepoUrlFromLocationAnnotation(parsedLocationAnnotation2)).toBe(
'https://github.com/org/repo',
);
const parsedLocationAnnotation3: ParsedLocationAnnotation = {
type: 'gitlab',
target: 'https://gitlab.com/org/repo',
};
expect(getRepoUrlFromLocationAnnotation(parsedLocationAnnotation3)).toBe(
'https://gitlab.com/org/repo',
);
const parsedLocationAnnotation4: ParsedLocationAnnotation = {
type: 'github',
target:
'github.com/backstage/backstage/blob/master/plugins/techdocs-backend/examples/documented-component',
};
expect(getRepoUrlFromLocationAnnotation(parsedLocationAnnotation4)).toBe(
'github.com/backstage/backstage/blob/master/plugins/techdocs-backend/examples/documented-component',
);
});
});
describe('pathMkdocsPreBuild', () => {
beforeEach(() => {
mockFs({
'/mkdocs.yml': mkdocsYml,
'/mkdocs_with_repo_url.yml': mkdocsYmlWithRepoUrl,
});
});
afterEach(() => {
mockFs.restore();
});
it('should add repo_url to mkdocs.yml', async () => {
const parsedLocationAnnotation: ParsedLocationAnnotation = {
type: 'github',
target: 'https://github.com/backstage/backstage',
};
await patchMkdocsYmlPreBuild(
'/mkdocs.yml',
mockLogger,
parsedLocationAnnotation,
);
const updatedMkdocsYml = await fs.readFile('/mkdocs.yml');
expect(updatedMkdocsYml.toString()).toContain(
"repo_url: 'https://github.com/backstage/backstage'",
);
});
it('should not override existing repo_url in mkdocs.yml', async () => {
const parsedLocationAnnotation: ParsedLocationAnnotation = {
type: 'github',
target: 'https://github.com/neworg/newrepo',
};
await patchMkdocsYmlPreBuild(
'/mkdocs_with_repo_url.yml',
mockLogger,
parsedLocationAnnotation,
);
const updatedMkdocsYml = await fs.readFile('/mkdocs_with_repo_url.yml');
expect(updatedMkdocsYml.toString()).toContain(
"repo_url: 'https://github.com/backstage/backstage'",
);
expect(updatedMkdocsYml.toString()).not.toContain(
"repo_url: 'https://github.com/neworg/newrepo'",
);
});
});
});
@@ -14,11 +14,16 @@
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import fs from 'fs-extra';
import { spawn } from 'child_process';
import { Writable, PassThrough } from 'stream';
import Docker from 'dockerode';
import yaml from 'js-yaml';
import { Logger } from 'winston';
import { Entity } from '@backstage/catalog-model';
import { SupportedGeneratorKey } from './types';
import { spawn } from 'child_process';
import { ParsedLocationAnnotation } from '../../../helpers';
import { RemoteProtocol } from '../prepare/types';
// TODO: Implement proper support for more generators.
export function getGeneratorKey(entity: Entity): SupportedGeneratorKey {
@@ -142,3 +147,131 @@ export const runCommand = async ({
});
});
};
/**
* Return true if mkdocs can compile docs with provided repo_url
*
* Valid repo_url examples in mkdocs.yml
* - https://github.com/backstage/backstage
* - https://gitlab.com/org/repo/
* - http://github.com/backstage/backstage
* - A http(s) protocol URL to the root of the repository
*
* Invalid repo_url examples in mkdocs.yml
* - https://github.com/backstage/backstage/blob/master/plugins/techdocs-backend/examples/documented-component
* - (anything that is not valid as described above)
*
* @param {string} repoUrl URL supposed to be used as repo_url in mkdocs.yml
* @param {RemoteProtocol} locationType Type of source code host - github, gitlab, dir, url, etc.
* @returns {boolean}
*/
export const isValidRepoUrlForMkdocs = (
repoUrl: string,
locationType: RemoteProtocol,
): boolean => {
// Trim trailing slash
const cleanRepoUrl = repoUrl.replace(/\/$/, '');
if (locationType === 'github' || locationType === 'gitlab') {
// A valid repoUrl to the root of the repository will be split into 5 strings if split using the / delimiter.
// We do not want URLs which have more than that number of forward slashes since they will signify a non-root location
// Note: This is not the best possible implementation but will work most of the times.. Feel free to improve or
// highlight edge cases.
return cleanRepoUrl.split('/').length === 5;
}
return false;
};
/**
* Return a valid URL of the repository used in backstage.io/techdocs-ref annotation.
* Return undefined if the `target` is not valid in context of repo_url in mkdocs.yml
* Alter URL so that it is a valid repo_url config in mkdocs.yml
*
* @param {ParsedLocationAnnotation} parsedLocationAnnotation Object with location url and type
* @returns {string | undefined}
*/
export const getRepoUrlFromLocationAnnotation = (
parsedLocationAnnotation: ParsedLocationAnnotation,
): string | undefined => {
const { type: locationType, target } = parsedLocationAnnotation;
// Add more options from the RemoteProtocol type of parsedLocationAnnotation.type here
// when TechDocs supports more hosts and if mkdocs can generated an Edit URL for them.
const supportedHosts = ['github', 'gitlab'];
if (supportedHosts.includes(locationType)) {
// Trim .git or .git/ from the end of repository url
return target.replace(/.git\/*$/, '');
}
return undefined;
};
/**
* Update the mkdocs.yml file before TechDocs generator uses it to build docs site.
*
* List of tasks:
* - Add repo_url if it does not exists
* If mkdocs.yml has a repo_url, the generated docs site gets an Edit button on the pages by default.
* If repo_url is missing in mkdocs.yml, we will use techdocs annotation of the entity to possibly get
* the repository URL.
*
* This function will not throw an error since this is not critical to the whole TechDocs pipeline.
* Instead it will log warnings if there are any errors in reading, parsing or writing YAML.
*
* @param {string} mkdocsYmlPath Absolute path to mkdocs.yml or equivalent of a docs site
* @param {Logger} logger
* @param {ParsedLocationAnnotation} parsedLocationAnnotation Object with location url and type
*/
export const patchMkdocsYmlPreBuild = async (
mkdocsYmlPath: string,
logger: Logger,
parsedLocationAnnotation: ParsedLocationAnnotation,
) => {
let mkdocsYmlFileString;
try {
mkdocsYmlFileString = await fs.readFile(mkdocsYmlPath, 'utf8');
} catch (error) {
logger.warn(
`Could not read file ${mkdocsYmlPath} before running the generator. ${error.message}`,
);
return;
}
let mkdocsYml: any;
try {
mkdocsYml = yaml.safeLoad(mkdocsYmlFileString);
// mkdocsYml should be an object type after successful parsing.
// But based on its type definition, it can also be a string or undefined, which we don't want.
if (typeof mkdocsYml === 'string' || typeof mkdocsYml === 'undefined') {
throw new Error('Bad YAML format.');
}
} catch (error) {
logger.warn(
`Error in parsing YAML at ${mkdocsYmlPath} before running the generator. ${error.message}`,
);
return;
}
// Add repo_url to mkdocs.yml if it is missing. This will enable the Page edit button generated by MkDocs.
if (!('repo_url' in mkdocsYml)) {
const repoUrl = getRepoUrlFromLocationAnnotation(parsedLocationAnnotation);
if (repoUrl !== undefined) {
// mkdocs.yml will not build with invalid repo_url. So, make sure it is valid.
if (isValidRepoUrlForMkdocs(repoUrl, parsedLocationAnnotation.type)) {
mkdocsYml.repo_url = repoUrl;
}
}
}
try {
await fs.writeFile(mkdocsYmlPath, yaml.safeDump(mkdocsYml), 'utf8');
} catch (error) {
logger.warn(
`Could not write to ${mkdocsYmlPath} after updating it before running the generator. ${error.message}`,
);
return;
}
};
@@ -26,7 +26,11 @@ import {
GeneratorRunOptions,
GeneratorRunResult,
} from './types';
import { runDockerContainer, runCommand } from './helpers';
import {
runDockerContainer,
runCommand,
patchMkdocsYmlPreBuild,
} from './helpers';
type TechdocsGeneratorOptions = {
// This option enables users to configure if they want to use TechDocs container
@@ -62,6 +66,7 @@ export class TechdocsGenerator implements GeneratorBase {
public async run({
directory,
dockerClient,
parsedLocationAnnotation,
}: GeneratorRunOptions): Promise<GeneratorRunResult> {
const tmpdirPath = os.tmpdir();
// Fixes a problem with macOS returning a path that is a symlink
@@ -71,6 +76,15 @@ export class TechdocsGenerator implements GeneratorBase {
);
const [log, logStream] = createStream();
// TODO: In future mkdocs.yml can be mkdocs.yaml. So, use a config variable here to find out
// the correct file name.
// Do some updates to mkdocs.yml before generating docs e.g. adding repo_url
await patchMkdocsYmlPreBuild(
path.join(directory, 'mkdocs.yml'),
this.logger,
parsedLocationAnnotation,
);
try {
switch (this.options.runGeneratorIn) {
case 'local':
@@ -13,9 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Writable } from 'stream';
import { Writable } from 'stream';
import Docker from 'dockerode';
import { Entity } from '@backstage/catalog-model';
import { ParsedLocationAnnotation } from '../../../helpers';
/**
* The returned directory from the generator which is ready
@@ -26,14 +27,18 @@ export type GeneratorRunResult = {
};
/**
* The values that the generator will receive. The directory of the
* uncompiled documentation, with the values from the frontend. A dedicated log stream and a docker
* client to run any generator on top of your directory.
* The values that the generator will receive.
*
* @param {string} directory The directory of the uncompiled documentation, with the values from the frontend
* @param {Docker} dockerClient A docker client to run any generator on top of your directory
* @param {ParsedLocationAnnotation} parsedLocationAnnotation backstage.io/techdocs-ref annotation of an entity
* @param {Writable} [logStream] A dedicated log stream
*/
export type GeneratorRunOptions = {
directory: string;
logStream?: Writable;
dockerClient: Docker;
parsedLocationAnnotation: ParsedLocationAnnotation;
logStream?: Writable;
};
export type GeneratorBase = {
+18
View File
@@ -1,5 +1,23 @@
# @backstage/plugin-techdocs
## 0.3.0
### Minor Changes
- 4b53294a6: - Use techdocs annotation to add repo_url if missing in mkdocs.yml. Having repo_url creates a Edit button on techdocs pages.
- techdocs-backend: API endpoint `/metadata/mkdocs/*` renamed to `/metadata/techdocs/*`
### Patch Changes
- Updated dependencies [6f70ed7a9]
- Updated dependencies [ab94c9542]
- Updated dependencies [2daf18e80]
- Updated dependencies [069cda35f]
- Updated dependencies [700a212b4]
- @backstage/plugin-catalog@0.2.4
- @backstage/catalog-model@0.3.1
- @backstage/core-api@0.2.3
## 0.2.3
### Patch Changes
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-techdocs",
"version": "0.2.3",
"version": "0.3.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -21,10 +21,10 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/catalog-model": "^0.3.0",
"@backstage/catalog-model": "^0.3.1",
"@backstage/core": "^0.3.2",
"@backstage/core-api": "^0.2.1",
"@backstage/plugin-catalog": "^0.2.3",
"@backstage/core-api": "^0.2.3",
"@backstage/plugin-catalog": "^0.2.4",
"@backstage/test-utils": "^0.1.3",
"@backstage/theme": "^0.2.1",
"@material-ui/core": "^4.11.0",
@@ -39,7 +39,7 @@
"sanitize-html": "^1.27.0"
},
"devDependencies": {
"@backstage/cli": "^0.3.1",
"@backstage/cli": "^0.3.2",
"@backstage/dev-utils": "^0.1.4",
"@backstage/test-utils": "^0.1.3",
"@testing-library/jest-dom": "^5.10.1",
+32 -4
View File
@@ -15,7 +15,6 @@
*/
import { createApiRef } from '@backstage/core';
import { ParsedEntityId } from './types';
export const techdocsStorageApiRef = createApiRef<TechDocsStorageApi>({
@@ -38,7 +37,8 @@ export interface TechDocsStorage {
}
export interface TechDocs {
getMetadata(metadataType: string, entityId: ParsedEntityId): Promise<string>;
getTechDocsMetadata(entityId: ParsedEntityId): Promise<string>;
getEntityMetadata(entityId: ParsedEntityId): Promise<string>;
}
/**
@@ -53,10 +53,38 @@ export class TechDocsApi implements TechDocs {
this.apiOrigin = apiOrigin;
}
async getMetadata(metadataType: string, entityId: ParsedEntityId) {
/**
* Retrieve TechDocs metadata.
*
* When docs are built, we generate a techdocs_metadata.json and store it along with the generated
* static files. It includes necessary data about the docs site. This method requests techdocs-backend
* which retrieves the TechDocs metadata.
*
* @param {ParsedEntityId} entityId Object containing entity data like name, namespace, etc.
*/
async getTechDocsMetadata(entityId: ParsedEntityId) {
const { kind, namespace, name } = entityId;
const requestUrl = `${this.apiOrigin}/metadata/${metadataType}/${namespace}/${kind}/${name}`;
const requestUrl = `${this.apiOrigin}/metadata/techdocs/${namespace}/${kind}/${name}`;
const request = await fetch(`${requestUrl}`);
const res = await request.json();
return res;
}
/**
* Retrieve metadata about an entity.
*
* This method requests techdocs-backend which uses the catalog APIs to respond with filtered
* information required here.
*
* @param {ParsedEntityId} entityId Object containing entity data like name, namespace, etc.
*/
async getEntityMetadata(entityId: ParsedEntityId) {
const { kind, namespace, name } = entityId;
const requestUrl = `${this.apiOrigin}/metadata/entity/${namespace}/${kind}/${name}`;
const request = await fetch(`${requestUrl}`);
const res = await request.json();
@@ -50,17 +50,18 @@ describe('<TechDocsPage />', () => {
entityId: 'Component::backstage',
});
const techDocsApi: Partial<TechDocsApi> = {
getMetadata: () => Promise.resolve([]),
const techdocsApi: Partial<TechDocsApi> = {
getEntityMetadata: () => Promise.resolve([]),
getTechDocsMetadata: () => Promise.resolve([]),
};
const techDocsStorageApi: Partial<TechDocsStorageApi> = {
const techdocsStorageApi: Partial<TechDocsStorageApi> = {
getEntityDocs: (): Promise<string> => Promise.resolve('String'),
getBaseUrl: (): string => '',
};
const apiRegistry = ApiRegistry.from([
[techdocsApiRef, techDocsApi],
[techdocsStorageApiRef, techDocsStorageApi],
[techdocsApiRef, techdocsApi],
[techdocsStorageApiRef, techdocsStorageApi],
]);
await act(async () => {
@@ -26,19 +26,19 @@ export const TechDocsPage = () => {
const [documentReady, setDocumentReady] = useState<boolean>(false);
const { namespace, kind, name } = useParams();
const techDocsApi = useApi(techdocsApiRef);
const techdocsApi = useApi(techdocsApiRef);
const mkdocsMetadataRequest = useAsync(() => {
const techdocsMetadataRequest = useAsync(() => {
if (documentReady) {
return techDocsApi.getMetadata('mkdocs', { kind, namespace, name });
return techdocsApi.getTechDocsMetadata({ kind, namespace, name });
}
return Promise.resolve({ loading: true });
}, [kind, namespace, name, techDocsApi, documentReady]);
}, [kind, namespace, name, techdocsApi, documentReady]);
const entityMetadataRequest = useAsync(() => {
return techDocsApi.getMetadata('entity', { kind, namespace, name });
}, [kind, namespace, name, techDocsApi]);
return techdocsApi.getEntityMetadata({ kind, namespace, name });
}, [kind, namespace, name, techdocsApi]);
const onReady = () => {
setDocumentReady(true);
@@ -48,7 +48,7 @@ export const TechDocsPage = () => {
<Page themeId="documentation">
<TechDocsPageHeader
metadataRequest={{
mkdocs: mkdocsMetadataRequest,
techdocs: techdocsMetadataRequest,
entity: entityMetadataRequest,
}}
entityId={{
@@ -42,7 +42,7 @@ describe('<TechDocsPageHeader />', () => {
},
},
},
mkdocs: {
techdocs: {
loading: false,
value: {
site_name: 'test-site-name',
@@ -73,7 +73,7 @@ describe('<TechDocsPageHeader />', () => {
entity: {
loading: false,
},
mkdocs: {
techdocs: {
loading: false,
},
}}
@@ -25,7 +25,7 @@ type TechDocsPageHeaderProps = {
entityId: ParsedEntityId;
metadataRequest: {
entity: AsyncState<any>;
mkdocs: AsyncState<any>;
techdocs: AsyncState<any>;
};
};
@@ -33,15 +33,18 @@ export const TechDocsPageHeader = ({
entityId,
metadataRequest,
}: TechDocsPageHeaderProps) => {
const { mkdocs: mkdocsMetadata, entity: entityMetadata } = metadataRequest;
const {
techdocs: techdocsMetadata,
entity: entityMetadata,
} = metadataRequest;
const { value: mkDocsMetadataValues } = mkdocsMetadata;
const { value: techdocsMetadataValues } = techdocsMetadata;
const { value: entityMetadataValues } = entityMetadata;
const { kind, name } = entityId;
const { site_name: siteName, site_description: siteDescription } =
mkDocsMetadataValues || {};
techdocsMetadataValues || {};
const {
locationMetadata,
+1 -1
View File
@@ -32,7 +32,7 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.3.1",
"@backstage/cli": "^0.3.2",
"@backstage/dev-utils": "^0.1.4",
"@backstage/test-utils": "^0.1.3",
"@testing-library/jest-dom": "^5.10.1",
+1 -1
View File
@@ -32,7 +32,7 @@
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.3.1",
"@backstage/cli": "^0.3.2",
"@backstage/dev-utils": "^0.1.4",
"@backstage/test-utils": "^0.1.3",
"@testing-library/jest-dom": "^5.10.1",