Merge branch 'master' of github.com:spotify/backstage into blam/msw
* 'master' of github.com:spotify/backstage: (63 commits) packages/cli: add lint rule to avoid cross-package src imports fix(catalog-backend): make location delete work again Add Other component category (#1292) packages/core-api: add initial definition of IdentityApi additional sqli protection build(deps): bump eslint from 7.1.0 to 7.2.0 (#1285) return 204 when there's no response data build(deps-dev): bump @types/codemirror from 0.0.95 to 0.0.96 (#1284) build(deps-dev): bump @types/morgan from 1.9.0 to 1.9.1 (#1287) build(deps): bump chalk from 4.0.0 to 4.1.0 (#1286) catalog: suffix entiy kind typeswith Entity feat(catalog): new kind LocationRef packages/cli: assume browser build if esm output is included packages/backend-common: import winston as namespace plugins/catalog-backend: typecheck migration files plugins/catalog-backend: move migrations to JS with type annotations packages/cli: fix jest module mapper to only match entire module name github/workflows: use env app config to set e2e test port packages/cli: use app.baseUrl to configure dev server packages/config-loader: more docs! ...
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-auth-backend",
|
||||
"version": "0.1.1-alpha.7",
|
||||
"version": "0.1.1-alpha.8",
|
||||
"main": "dist",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -15,7 +15,7 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.1.1-alpha.7",
|
||||
"@backstage/backend-common": "^0.1.1-alpha.8",
|
||||
"@types/cookie-parser": "^1.4.2",
|
||||
"@types/jwt-decode": "2.2.1",
|
||||
"@types/passport": "^1.0.3",
|
||||
@@ -39,7 +39,7 @@
|
||||
"yn": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.7",
|
||||
"@backstage/cli": "^0.1.1-alpha.8",
|
||||
"@types/body-parser": "^1.19.0",
|
||||
"@types/passport-saml": "^1.1.2",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import express from 'express';
|
||||
import { AuthProviderRouteHandlers } from '../providers/types';
|
||||
import { NotFoundError } from '@backstage/backend-common';
|
||||
|
||||
export type EnvironmentHandlers = {
|
||||
[key: string]: AuthProviderRouteHandlers;
|
||||
};
|
||||
|
||||
export class EnvironmentHandler implements AuthProviderRouteHandlers {
|
||||
constructor(private readonly providers: EnvironmentHandlers) {}
|
||||
|
||||
private getProviderForEnv(req: express.Request): AuthProviderRouteHandlers {
|
||||
const env = req.query.env?.toString();
|
||||
if (!this.providers.hasOwnProperty(env)) {
|
||||
throw new NotFoundError(
|
||||
`No environment for ${env} found in this provider`,
|
||||
);
|
||||
}
|
||||
return this.providers[env];
|
||||
}
|
||||
|
||||
async start(req: express.Request, res: express.Response): Promise<void> {
|
||||
const provider = this.getProviderForEnv(req);
|
||||
provider.start(req, res);
|
||||
}
|
||||
|
||||
async frameHandler(
|
||||
req: express.Request,
|
||||
res: express.Response,
|
||||
): Promise<void> {
|
||||
const provider = this.getProviderForEnv(req);
|
||||
provider.frameHandler(req, res);
|
||||
}
|
||||
|
||||
async refresh(req: express.Request, res: express.Response): Promise<void> {
|
||||
const provider = this.getProviderForEnv(req);
|
||||
if (provider.refresh) {
|
||||
provider.refresh(req, res);
|
||||
}
|
||||
}
|
||||
|
||||
async logout(req: express.Request, res: express.Response): Promise<void> {
|
||||
const provider = this.getProviderForEnv(req);
|
||||
provider.logout(req, res);
|
||||
}
|
||||
}
|
||||
+49
-60
@@ -18,15 +18,12 @@ import express from 'express';
|
||||
import {
|
||||
ensuresXRequestedWith,
|
||||
postMessageResponse,
|
||||
removeRefreshTokenCookie,
|
||||
setRefreshTokenCookie,
|
||||
THOUSAND_DAYS_MS,
|
||||
setNonceCookie,
|
||||
TEN_MINUTES_MS,
|
||||
verifyNonce,
|
||||
OAuthProvider,
|
||||
} from './OAuthProvider';
|
||||
import { AuthResponse, OAuthProviderHandlers } from './types';
|
||||
import { AuthResponse, OAuthProviderHandlers } from '../providers/types';
|
||||
|
||||
describe('OAuthProvider Utils', () => {
|
||||
describe('verifyNonce', () => {
|
||||
@@ -80,52 +77,8 @@ describe('OAuthProvider Utils', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('setNonceCookie', () => {
|
||||
it('should set nonce cookie', () => {
|
||||
const mockResponse = ({
|
||||
cookie: jest.fn().mockReturnThis(),
|
||||
} as unknown) as express.Response;
|
||||
setNonceCookie(mockResponse, 'providera');
|
||||
expect(mockResponse.cookie).toBeCalledTimes(1);
|
||||
expect(mockResponse.cookie).toBeCalledWith(
|
||||
'providera-nonce',
|
||||
expect.any(String),
|
||||
expect.objectContaining({ maxAge: TEN_MINUTES_MS }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setRefreshTokenCookie', () => {
|
||||
it('should set refresh token cookie', () => {
|
||||
const mockResponse = ({
|
||||
cookie: jest.fn().mockReturnThis(),
|
||||
} as unknown) as express.Response;
|
||||
setRefreshTokenCookie(mockResponse, 'providera', 'REFRESH_TOKEN');
|
||||
expect(mockResponse.cookie).toBeCalledTimes(1);
|
||||
expect(mockResponse.cookie).toBeCalledWith(
|
||||
'providera-refresh-token',
|
||||
'REFRESH_TOKEN',
|
||||
expect.objectContaining({ maxAge: THOUSAND_DAYS_MS }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeRefreshTokenCookie', () => {
|
||||
it('should remove refresh token cookie', () => {
|
||||
const mockResponse = ({
|
||||
cookie: jest.fn().mockReturnThis(),
|
||||
} as unknown) as express.Response;
|
||||
removeRefreshTokenCookie(mockResponse, 'providera');
|
||||
expect(mockResponse.cookie).toBeCalledTimes(1);
|
||||
expect(mockResponse.cookie).toBeCalledWith(
|
||||
'providera-refresh-token',
|
||||
'',
|
||||
expect.objectContaining({ maxAge: 0 }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('postMessageResponse', () => {
|
||||
const appOrigin = 'http://localhost:3000';
|
||||
it('should post a message back with payload success', () => {
|
||||
const mockResponse = ({
|
||||
end: jest.fn().mockReturnThis(),
|
||||
@@ -144,7 +97,7 @@ describe('OAuthProvider Utils', () => {
|
||||
const jsonData = JSON.stringify(data);
|
||||
const base64Data = Buffer.from(jsonData, 'utf8').toString('base64');
|
||||
|
||||
postMessageResponse(mockResponse, data);
|
||||
postMessageResponse(mockResponse, appOrigin, data);
|
||||
expect(mockResponse.setHeader).toBeCalledTimes(2);
|
||||
expect(mockResponse.end).toBeCalledTimes(1);
|
||||
expect(mockResponse.end).toBeCalledWith(
|
||||
@@ -165,7 +118,7 @@ describe('OAuthProvider Utils', () => {
|
||||
const jsonData = JSON.stringify(data);
|
||||
const base64Data = Buffer.from(jsonData, 'utf8').toString('base64');
|
||||
|
||||
postMessageResponse(mockResponse, data);
|
||||
postMessageResponse(mockResponse, appOrigin, data);
|
||||
expect(mockResponse.setHeader).toBeCalledTimes(2);
|
||||
expect(mockResponse.end).toBeCalledTimes(1);
|
||||
expect(mockResponse.end).toBeCalledWith(
|
||||
@@ -221,10 +174,19 @@ describe('OAuthProvider', () => {
|
||||
}
|
||||
}
|
||||
const providerInstance = new MyAuthProvider();
|
||||
const providerId = 'test-provider';
|
||||
const oAuthProviderOptions = {
|
||||
providerId: 'test-provider',
|
||||
secure: false,
|
||||
disableRefresh: true,
|
||||
baseUrl: 'http://localhost:7000/auth',
|
||||
appOrigin: 'http://localhost:3000',
|
||||
};
|
||||
|
||||
it('sets the correct headers in start', async () => {
|
||||
const oauthProvider = new OAuthProvider(providerInstance, providerId);
|
||||
const oauthProvider = new OAuthProvider(
|
||||
providerInstance,
|
||||
oAuthProviderOptions,
|
||||
);
|
||||
const mockRequest = ({
|
||||
query: {
|
||||
scope: 'user',
|
||||
@@ -239,6 +201,14 @@ describe('OAuthProvider', () => {
|
||||
} as unknown) as express.Response;
|
||||
|
||||
await oauthProvider.start(mockRequest, mockResponse);
|
||||
// nonce cookie checks
|
||||
expect(mockResponse.cookie).toBeCalledTimes(1);
|
||||
expect(mockResponse.cookie).toBeCalledWith(
|
||||
`${oAuthProviderOptions.providerId}-nonce`,
|
||||
expect.any(String),
|
||||
expect.objectContaining({ maxAge: TEN_MINUTES_MS }),
|
||||
);
|
||||
// redirect checks
|
||||
expect(mockResponse.setHeader).toHaveBeenCalledTimes(2);
|
||||
expect(mockResponse.setHeader).toHaveBeenCalledWith('Location', '/url');
|
||||
expect(mockResponse.setHeader).toHaveBeenCalledWith('Content-Length', '0');
|
||||
@@ -247,7 +217,10 @@ describe('OAuthProvider', () => {
|
||||
});
|
||||
|
||||
it('sets the refresh cookie if refresh is enabled', async () => {
|
||||
const oauthProvider = new OAuthProvider(providerInstance, providerId);
|
||||
const oauthProvider = new OAuthProvider(providerInstance, {
|
||||
...oAuthProviderOptions,
|
||||
disableRefresh: false,
|
||||
});
|
||||
|
||||
const mockRequest = ({
|
||||
cookies: {
|
||||
@@ -269,12 +242,18 @@ describe('OAuthProvider', () => {
|
||||
expect(mockResponse.cookie).toHaveBeenCalledWith(
|
||||
expect.stringContaining('test-provider-refresh-token'),
|
||||
expect.stringContaining('token'),
|
||||
expect.objectContaining({ path: '/auth/test-provider' }),
|
||||
expect.objectContaining({
|
||||
path: '/auth/test-provider',
|
||||
maxAge: THOUSAND_DAYS_MS,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('does no set the refresh cookie if refresh is disabled', async () => {
|
||||
const oauthProvider = new OAuthProvider(providerInstance, providerId, true);
|
||||
it('does not set the refresh cookie if refresh is disabled', async () => {
|
||||
const oauthProvider = new OAuthProvider(providerInstance, {
|
||||
...oAuthProviderOptions,
|
||||
disableRefresh: true,
|
||||
});
|
||||
|
||||
const mockRequest = ({
|
||||
cookies: {
|
||||
@@ -296,7 +275,10 @@ describe('OAuthProvider', () => {
|
||||
});
|
||||
|
||||
it('removes refresh cookie when logging out', async () => {
|
||||
const oauthProvider = new OAuthProvider(providerInstance, providerId);
|
||||
const oauthProvider = new OAuthProvider(providerInstance, {
|
||||
...oAuthProviderOptions,
|
||||
disableRefresh: false,
|
||||
});
|
||||
|
||||
const mockRequest = ({
|
||||
header: () => 'XMLHttpRequest',
|
||||
@@ -317,7 +299,11 @@ describe('OAuthProvider', () => {
|
||||
});
|
||||
|
||||
it('gets new access-token when refreshing', async () => {
|
||||
const oauthProvider = new OAuthProvider(providerInstance, providerId);
|
||||
oAuthProviderOptions.disableRefresh = false;
|
||||
const oauthProvider = new OAuthProvider(providerInstance, {
|
||||
...oAuthProviderOptions,
|
||||
disableRefresh: false,
|
||||
});
|
||||
|
||||
const mockRequest = ({
|
||||
header: () => 'XMLHttpRequest',
|
||||
@@ -341,7 +327,10 @@ describe('OAuthProvider', () => {
|
||||
});
|
||||
|
||||
it('handles refresh without capabilities', async () => {
|
||||
const oauthProvider = new OAuthProvider(providerInstance, providerId, true);
|
||||
const oauthProvider = new OAuthProvider(providerInstance, {
|
||||
...oAuthProviderOptions,
|
||||
disableRefresh: true,
|
||||
});
|
||||
|
||||
const mockRequest = ({
|
||||
header: () => 'XMLHttpRequest',
|
||||
+73
-75
@@ -14,20 +14,29 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import express, { CookieOptions } from 'express';
|
||||
import express from 'express';
|
||||
import crypto from 'crypto';
|
||||
import { URL } from 'url';
|
||||
import {
|
||||
AuthResponse,
|
||||
AuthProviderRouteHandlers,
|
||||
OAuthProviderHandlers,
|
||||
} from './types';
|
||||
} from '../providers/types';
|
||||
import { InputError } from '@backstage/backend-common';
|
||||
|
||||
export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000;
|
||||
export const TEN_MINUTES_MS = 600 * 1000;
|
||||
|
||||
export const verifyNonce = (req: express.Request, provider: string) => {
|
||||
const cookieNonce = req.cookies[`${provider}-nonce`];
|
||||
export type Options = {
|
||||
providerId: string;
|
||||
secure: boolean;
|
||||
disableRefresh?: boolean;
|
||||
baseUrl: string;
|
||||
appOrigin: string;
|
||||
};
|
||||
|
||||
export const verifyNonce = (req: express.Request, providerId: string) => {
|
||||
const cookieNonce = req.cookies[`${providerId}-nonce`];
|
||||
const stateNonce = req.query.state;
|
||||
|
||||
if (!cookieNonce || !stateNonce) {
|
||||
@@ -39,58 +48,9 @@ export const verifyNonce = (req: express.Request, provider: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const setNonceCookie = (res: express.Response, provider: string) => {
|
||||
const nonce = crypto.randomBytes(16).toString('base64');
|
||||
|
||||
const options: CookieOptions = {
|
||||
maxAge: TEN_MINUTES_MS,
|
||||
secure: false,
|
||||
sameSite: 'none',
|
||||
domain: 'localhost',
|
||||
path: `/auth/${provider}/handler`,
|
||||
httpOnly: true,
|
||||
};
|
||||
|
||||
res.cookie(`${provider}-nonce`, nonce, options);
|
||||
|
||||
return nonce;
|
||||
};
|
||||
|
||||
export const setRefreshTokenCookie = (
|
||||
res: express.Response,
|
||||
provider: string,
|
||||
refreshToken: string,
|
||||
) => {
|
||||
const options: CookieOptions = {
|
||||
maxAge: THOUSAND_DAYS_MS,
|
||||
secure: false,
|
||||
sameSite: 'none',
|
||||
domain: 'localhost',
|
||||
path: `/auth/${provider}`,
|
||||
httpOnly: true,
|
||||
};
|
||||
|
||||
res.cookie(`${provider}-refresh-token`, refreshToken, options);
|
||||
};
|
||||
|
||||
export const removeRefreshTokenCookie = (
|
||||
res: express.Response,
|
||||
provider: string,
|
||||
) => {
|
||||
const options: CookieOptions = {
|
||||
maxAge: 0,
|
||||
secure: false,
|
||||
sameSite: 'none',
|
||||
domain: 'localhost',
|
||||
path: `/auth/${provider}`,
|
||||
httpOnly: true,
|
||||
};
|
||||
|
||||
res.cookie(`${provider}-refresh-token`, '', options);
|
||||
};
|
||||
|
||||
export const postMessageResponse = (
|
||||
res: express.Response,
|
||||
appOrigin: string,
|
||||
data: AuthResponse,
|
||||
) => {
|
||||
const jsonData = JSON.stringify(data);
|
||||
@@ -104,7 +64,7 @@ export const postMessageResponse = (
|
||||
<html>
|
||||
<body>
|
||||
<script>
|
||||
(window.opener || window.parent).postMessage(JSON.parse(atob('${base64Data}')), 'http://localhost:3000')
|
||||
(window.opener || window.parent).postMessage(JSON.parse(atob('${base64Data}')), '${appOrigin}')
|
||||
window.close()
|
||||
</script>
|
||||
</body>
|
||||
@@ -122,17 +82,16 @@ export const ensuresXRequestedWith = (req: express.Request) => {
|
||||
};
|
||||
|
||||
export class OAuthProvider implements AuthProviderRouteHandlers {
|
||||
private readonly provider: string;
|
||||
private readonly providerHandlers: OAuthProviderHandlers;
|
||||
private readonly disableRefresh: boolean;
|
||||
private readonly domain: string;
|
||||
private readonly basePath: string;
|
||||
|
||||
constructor(
|
||||
providerHandlers: OAuthProviderHandlers,
|
||||
provider: string,
|
||||
disableRefresh?: boolean,
|
||||
private readonly providerHandlers: OAuthProviderHandlers,
|
||||
private readonly options: Options,
|
||||
) {
|
||||
this.provider = provider;
|
||||
this.providerHandlers = providerHandlers;
|
||||
this.disableRefresh = disableRefresh ?? false;
|
||||
const url = new URL(options.baseUrl);
|
||||
this.domain = url.hostname;
|
||||
this.basePath = url.pathname;
|
||||
}
|
||||
|
||||
async start(req: express.Request, res: express.Response): Promise<any> {
|
||||
@@ -143,8 +102,9 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
|
||||
throw new InputError('missing scope parameter');
|
||||
}
|
||||
|
||||
const nonce = crypto.randomBytes(16).toString('base64');
|
||||
// set a nonce cookie before redirecting to oauth provider
|
||||
const nonce = setNonceCookie(res, this.provider);
|
||||
this.setNonceCookie(res, nonce);
|
||||
|
||||
const options = {
|
||||
scope,
|
||||
@@ -152,6 +112,7 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
|
||||
prompt: 'consent',
|
||||
state: nonce,
|
||||
};
|
||||
|
||||
const { url, status } = await this.providerHandlers.start(req, options);
|
||||
|
||||
res.statusCode = status || 302;
|
||||
@@ -166,11 +127,11 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
|
||||
): Promise<any> {
|
||||
try {
|
||||
// verify nonce cookie and state cookie on callback
|
||||
verifyNonce(req, this.provider);
|
||||
verifyNonce(req, this.options.providerId);
|
||||
|
||||
const { user, info } = await this.providerHandlers.handler(req);
|
||||
|
||||
if (!this.disableRefresh) {
|
||||
if (!this.options.disableRefresh) {
|
||||
// throw error if missing refresh token
|
||||
const { refreshToken } = info;
|
||||
if (!refreshToken) {
|
||||
@@ -178,17 +139,17 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
|
||||
}
|
||||
|
||||
// set new refresh token
|
||||
setRefreshTokenCookie(res, this.provider, refreshToken);
|
||||
this.setRefreshTokenCookie(res, refreshToken);
|
||||
}
|
||||
|
||||
// post message back to popup if successful
|
||||
return postMessageResponse(res, {
|
||||
return postMessageResponse(res, this.options.appOrigin, {
|
||||
type: 'auth-result',
|
||||
payload: user,
|
||||
});
|
||||
} catch (error) {
|
||||
// post error message back to popup if failure
|
||||
return postMessageResponse(res, {
|
||||
return postMessageResponse(res, this.options.appOrigin, {
|
||||
type: 'auth-result',
|
||||
error: {
|
||||
name: error.name,
|
||||
@@ -203,9 +164,9 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
|
||||
return res.status(401).send('Invalid X-Requested-With header');
|
||||
}
|
||||
|
||||
if (!this.disableRefresh) {
|
||||
if (!this.options.disableRefresh) {
|
||||
// remove refresh token cookie before logout
|
||||
removeRefreshTokenCookie(res, this.provider);
|
||||
this.removeRefreshTokenCookie(res);
|
||||
}
|
||||
return res.send('logout!');
|
||||
}
|
||||
@@ -215,14 +176,15 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
|
||||
return res.status(401).send('Invalid X-Requested-With header');
|
||||
}
|
||||
|
||||
if (!this.providerHandlers.refresh || this.disableRefresh) {
|
||||
if (!this.providerHandlers.refresh || this.options.disableRefresh) {
|
||||
return res.send(
|
||||
`Refresh token not supported for provider: ${this.provider}`,
|
||||
`Refresh token not supported for provider: ${this.options.providerId}`,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const refreshToken = req.cookies[`${this.provider}-refresh-token`];
|
||||
const refreshToken =
|
||||
req.cookies[`${this.options.providerId}-refresh-token`];
|
||||
|
||||
// throw error if refresh token is missing in the request
|
||||
if (!refreshToken) {
|
||||
@@ -241,4 +203,40 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
|
||||
return res.status(401).send(`${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private setNonceCookie = (res: express.Response, nonce: string) => {
|
||||
res.cookie(`${this.options.providerId}-nonce`, nonce, {
|
||||
maxAge: TEN_MINUTES_MS,
|
||||
secure: this.options.secure,
|
||||
sameSite: 'none',
|
||||
domain: this.domain,
|
||||
path: `${this.basePath}/${this.options.providerId}/handler`,
|
||||
httpOnly: true,
|
||||
});
|
||||
};
|
||||
|
||||
private setRefreshTokenCookie = (
|
||||
res: express.Response,
|
||||
refreshToken: string,
|
||||
) => {
|
||||
res.cookie(`${this.options.providerId}-refresh-token`, refreshToken, {
|
||||
maxAge: THOUSAND_DAYS_MS,
|
||||
secure: this.options.secure,
|
||||
sameSite: 'none',
|
||||
domain: this.domain,
|
||||
path: `${this.basePath}/${this.options.providerId}`,
|
||||
httpOnly: true,
|
||||
});
|
||||
};
|
||||
|
||||
private removeRefreshTokenCookie = (res: express.Response) => {
|
||||
res.cookie(`${this.options.providerId}-refresh-token`, '', {
|
||||
maxAge: 0,
|
||||
secure: false,
|
||||
sameSite: 'none',
|
||||
domain: `${this.domain}`,
|
||||
path: `${this.basePath}/${this.options.providerId}`,
|
||||
httpOnly: true,
|
||||
});
|
||||
};
|
||||
}
|
||||
+5
-1
@@ -17,7 +17,11 @@
|
||||
import express from 'express';
|
||||
import passport from 'passport';
|
||||
import jwtDecoder from 'jwt-decode';
|
||||
import { RedirectInfo, RefreshTokenResponse, ProfileInfo } from './types';
|
||||
import {
|
||||
RedirectInfo,
|
||||
RefreshTokenResponse,
|
||||
ProfileInfo,
|
||||
} from '../providers/types';
|
||||
|
||||
export const makeProfileInfo = (
|
||||
profile: passport.Profile,
|
||||
@@ -1,43 +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.
|
||||
*/
|
||||
|
||||
export const providers = [
|
||||
{
|
||||
provider: 'google',
|
||||
options: {
|
||||
clientID: process.env.AUTH_GOOGLE_CLIENT_ID!,
|
||||
clientSecret: process.env.AUTH_GOOGLE_CLIENT_SECRET!,
|
||||
callbackURL: 'http://localhost:7000/auth/google/handler/frame',
|
||||
},
|
||||
},
|
||||
{
|
||||
provider: 'github',
|
||||
options: {
|
||||
clientID: process.env.AUTH_GITHUB_CLIENT_ID!,
|
||||
clientSecret: process.env.AUTH_GITHUB_CLIENT_SECRET!,
|
||||
callbackURL: 'http://localhost:7000/auth/github/handler/frame',
|
||||
},
|
||||
disableRefresh: true,
|
||||
},
|
||||
{
|
||||
provider: 'saml',
|
||||
options: {
|
||||
path: '/auth/saml/handler/frame',
|
||||
entryPoint: 'http://localhost:7001/',
|
||||
issuer: 'passport-saml',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -19,6 +19,7 @@ import { createGithubProvider } from './github';
|
||||
import { createGoogleProvider } from './google';
|
||||
import { createSamlProvider } from './saml';
|
||||
import { AuthProviderFactory, AuthProviderConfig } from './types';
|
||||
import { Logger } from 'winston';
|
||||
|
||||
const factories: { [providerId: string]: AuthProviderFactory } = {
|
||||
google: createGoogleProvider,
|
||||
@@ -26,17 +27,18 @@ const factories: { [providerId: string]: AuthProviderFactory } = {
|
||||
saml: createSamlProvider,
|
||||
};
|
||||
|
||||
export function createAuthProvider(providerId: string, config: any) {
|
||||
export const createAuthProviderRouter = (
|
||||
providerId: string,
|
||||
globalConfig: AuthProviderConfig,
|
||||
providerConfig: any, // TODO: make this a config reader object of sorts
|
||||
logger: Logger,
|
||||
) => {
|
||||
const factory = factories[providerId];
|
||||
if (!factory) {
|
||||
throw Error(`No auth provider available for '${providerId}'`);
|
||||
}
|
||||
return factory(config);
|
||||
}
|
||||
|
||||
export const createAuthProviderRouter = (config: AuthProviderConfig) => {
|
||||
const providerId = config.provider;
|
||||
const provider = createAuthProvider(providerId, config);
|
||||
const provider = factory(globalConfig, providerConfig, logger);
|
||||
|
||||
const router = Router();
|
||||
router.get('/start', provider.start.bind(provider));
|
||||
@@ -46,5 +48,6 @@ export const createAuthProviderRouter = (config: AuthProviderConfig) => {
|
||||
if (provider.refresh) {
|
||||
router.get('/refresh', provider.refresh.bind(provider));
|
||||
}
|
||||
|
||||
return router;
|
||||
};
|
||||
|
||||
@@ -19,24 +19,30 @@ import { Strategy as GithubStrategy } from 'passport-github2';
|
||||
import {
|
||||
executeFrameHandlerStrategy,
|
||||
executeRedirectStrategy,
|
||||
} from '../PassportStrategyHelper';
|
||||
} from '../../lib/PassportStrategyHelper';
|
||||
import {
|
||||
OAuthProviderHandlers,
|
||||
AuthProviderConfig,
|
||||
RedirectInfo,
|
||||
AuthInfoBase,
|
||||
AuthInfoPrivate,
|
||||
EnvironmentProviderConfig,
|
||||
OAuthProviderOptions,
|
||||
OAuthProviderConfig,
|
||||
} from '../types';
|
||||
import { OAuthProvider } from '../OAuthProvider';
|
||||
import { OAuthProvider } from '../../lib/OAuthProvider';
|
||||
import {
|
||||
EnvironmentHandlers,
|
||||
EnvironmentHandler,
|
||||
} from '../../lib/EnvironmentHandler';
|
||||
import { Logger } from 'winston';
|
||||
|
||||
export class GithubAuthProvider implements OAuthProviderHandlers {
|
||||
private readonly providerConfig: AuthProviderConfig;
|
||||
private readonly _strategy: GithubStrategy;
|
||||
|
||||
constructor(providerConfig: AuthProviderConfig) {
|
||||
this.providerConfig = providerConfig;
|
||||
constructor(options: OAuthProviderOptions) {
|
||||
this._strategy = new GithubStrategy(
|
||||
{ ...this.providerConfig.options },
|
||||
{ ...options },
|
||||
(accessToken: any, _: any, params: any, profile: any, done: any) => {
|
||||
done(undefined, {
|
||||
profile,
|
||||
@@ -59,8 +65,42 @@ export class GithubAuthProvider implements OAuthProviderHandlers {
|
||||
}
|
||||
}
|
||||
|
||||
export function createGithubProvider(config: AuthProviderConfig) {
|
||||
const provider = new GithubAuthProvider(config);
|
||||
const oauthProvider = new OAuthProvider(provider, config.provider, true);
|
||||
return oauthProvider;
|
||||
export function createGithubProvider(
|
||||
{ baseUrl }: AuthProviderConfig,
|
||||
providerConfig: EnvironmentProviderConfig,
|
||||
logger: Logger,
|
||||
) {
|
||||
const envProviders: EnvironmentHandlers = {};
|
||||
|
||||
for (const [env, envConfig] of Object.entries(providerConfig)) {
|
||||
const config = (envConfig as unknown) as OAuthProviderConfig;
|
||||
const { secure, appOrigin } = config;
|
||||
const callbackURLParam = `?env=${env}`;
|
||||
const opts = {
|
||||
clientID: config.clientId,
|
||||
clientSecret: config.clientSecret,
|
||||
callbackURL: `${baseUrl}/github/handler/frame${callbackURLParam}`,
|
||||
};
|
||||
|
||||
if (!opts.clientID || !opts.clientSecret) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
throw new Error(
|
||||
'Failed to initialize Github auth provider, set AUTH_GITHUB_CLIENT_ID and AUTH_GITHUB_CLIENT_SECRET env vars',
|
||||
);
|
||||
}
|
||||
|
||||
logger.warn(
|
||||
'Github auth provider disabled, set AUTH_GITHUB_CLIENT_ID and AUTH_GITHUB_CLIENT_SECRET env vars to enable',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
envProviders[env] = new OAuthProvider(new GithubAuthProvider(opts), {
|
||||
providerId: 'github',
|
||||
secure,
|
||||
baseUrl,
|
||||
appOrigin,
|
||||
});
|
||||
}
|
||||
return new EnvironmentHandler(envProviders);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
executeRefreshTokenStrategy,
|
||||
makeProfileInfo,
|
||||
executeFetchUserProfileStrategy,
|
||||
} from '../PassportStrategyHelper';
|
||||
} from '../../lib/PassportStrategyHelper';
|
||||
import {
|
||||
OAuthProviderHandlers,
|
||||
AuthInfoBase,
|
||||
@@ -30,19 +30,27 @@ import {
|
||||
RedirectInfo,
|
||||
AuthProviderConfig,
|
||||
AuthInfoWithProfile,
|
||||
EnvironmentProviderConfig,
|
||||
OAuthProviderOptions,
|
||||
OAuthProviderConfig,
|
||||
} from '../types';
|
||||
import { OAuthProvider } from '../OAuthProvider';
|
||||
import { OAuthProvider } from '../../lib/OAuthProvider';
|
||||
import passport from 'passport';
|
||||
import {
|
||||
EnvironmentHandler,
|
||||
EnvironmentHandlers,
|
||||
} from '../../lib/EnvironmentHandler';
|
||||
import { Logger } from 'winston';
|
||||
|
||||
export class GoogleAuthProvider implements OAuthProviderHandlers {
|
||||
private readonly providerConfig: AuthProviderConfig;
|
||||
private readonly _strategy: GoogleStrategy;
|
||||
|
||||
constructor(providerConfig: AuthProviderConfig) {
|
||||
this.providerConfig = providerConfig;
|
||||
constructor(options: OAuthProviderOptions) {
|
||||
// TODO: throw error if env variables not set?
|
||||
this._strategy = new GoogleStrategy(
|
||||
{ ...this.providerConfig.options },
|
||||
// We need passReqToCallback set to false to get params, but there's
|
||||
// no matching type signature for that, so instead behold this beauty
|
||||
{ ...options, passReqToCallback: false as true },
|
||||
(
|
||||
accessToken: any,
|
||||
refreshToken: any,
|
||||
@@ -104,8 +112,42 @@ export class GoogleAuthProvider implements OAuthProviderHandlers {
|
||||
}
|
||||
}
|
||||
|
||||
export function createGoogleProvider(config: AuthProviderConfig) {
|
||||
const provider = new GoogleAuthProvider(config);
|
||||
const oauthProvider = new OAuthProvider(provider, config.provider);
|
||||
return oauthProvider;
|
||||
export function createGoogleProvider(
|
||||
{ baseUrl }: AuthProviderConfig,
|
||||
providerConfig: EnvironmentProviderConfig,
|
||||
logger: Logger,
|
||||
) {
|
||||
const envProviders: EnvironmentHandlers = {};
|
||||
|
||||
for (const [env, envConfig] of Object.entries(providerConfig)) {
|
||||
const config = (envConfig as unknown) as OAuthProviderConfig;
|
||||
const { secure, appOrigin } = config;
|
||||
const callbackURLParam = `?env=${env}`;
|
||||
const opts = {
|
||||
clientID: config.clientId,
|
||||
clientSecret: config.clientSecret,
|
||||
callbackURL: `${baseUrl}/google/handler/frame${callbackURLParam}`,
|
||||
};
|
||||
|
||||
if (!opts.clientID || !opts.clientSecret) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
throw new Error(
|
||||
'Failed to initialize Google auth provider, set AUTH_GOOGLE_CLIENT_ID and AUTH_GOOGLE_CLIENT_SECRET env vars',
|
||||
);
|
||||
}
|
||||
|
||||
logger.warn(
|
||||
'Google auth provider disabled, set AUTH_GOOGLE_CLIENT_ID and AUTH_GOOGLE_CLIENT_SECRET env vars to enable',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
envProviders[env] = new OAuthProvider(new GoogleAuthProvider(opts), {
|
||||
providerId: 'google',
|
||||
secure,
|
||||
baseUrl,
|
||||
appOrigin,
|
||||
});
|
||||
}
|
||||
return new EnvironmentHandler(envProviders);
|
||||
}
|
||||
|
||||
@@ -19,16 +19,26 @@ import { Strategy as SamlStrategy } from 'passport-saml';
|
||||
import {
|
||||
executeFrameHandlerStrategy,
|
||||
executeRedirectStrategy,
|
||||
} from '../PassportStrategyHelper';
|
||||
import { AuthProviderConfig, AuthProviderRouteHandlers } from '../types';
|
||||
import { postMessageResponse } from '../OAuthProvider';
|
||||
} from '../../lib/PassportStrategyHelper';
|
||||
import {
|
||||
AuthProviderConfig,
|
||||
AuthProviderRouteHandlers,
|
||||
EnvironmentProviderConfig,
|
||||
SAMLProviderConfig,
|
||||
} from '../types';
|
||||
import { postMessageResponse } from '../../lib/OAuthProvider';
|
||||
import {
|
||||
EnvironmentHandlers,
|
||||
EnvironmentHandler,
|
||||
} from '../../lib/EnvironmentHandler';
|
||||
import { Logger } from 'winston';
|
||||
|
||||
export class SamlAuthProvider implements AuthProviderRouteHandlers {
|
||||
private readonly strategy: SamlStrategy;
|
||||
|
||||
constructor(providerConfig: AuthProviderConfig) {
|
||||
constructor(options: SAMLProviderOptions) {
|
||||
this.strategy = new SamlStrategy(
|
||||
{ ...providerConfig.options },
|
||||
{ ...options },
|
||||
(profile: any, done: any) => {
|
||||
// TODO: There's plenty more validation and profile handling to do here,
|
||||
// this provider is currently only intended to validate the provider pattern
|
||||
@@ -57,12 +67,12 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers {
|
||||
try {
|
||||
const { user } = await executeFrameHandlerStrategy(req, this.strategy);
|
||||
|
||||
return postMessageResponse(res, {
|
||||
return postMessageResponse(res, 'http://localhost:3000', {
|
||||
type: 'auth-result',
|
||||
payload: user,
|
||||
});
|
||||
} catch (error) {
|
||||
return postMessageResponse(res, {
|
||||
return postMessageResponse(res, 'http://localhost:3000', {
|
||||
type: 'auth-result',
|
||||
error: {
|
||||
name: error.name,
|
||||
@@ -77,6 +87,36 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers {
|
||||
}
|
||||
}
|
||||
|
||||
export function createSamlProvider(config: AuthProviderConfig) {
|
||||
return new SamlAuthProvider(config);
|
||||
type SAMLProviderOptions = {
|
||||
entryPoint: string;
|
||||
issuer: string;
|
||||
path: string;
|
||||
};
|
||||
|
||||
export function createSamlProvider(
|
||||
_authProviderConfig: AuthProviderConfig,
|
||||
providerConfig: EnvironmentProviderConfig,
|
||||
logger: Logger,
|
||||
) {
|
||||
const envProviders: EnvironmentHandlers = {};
|
||||
|
||||
for (const [env, envConfig] of Object.entries(providerConfig)) {
|
||||
const config = (envConfig as unknown) as SAMLProviderConfig;
|
||||
const opts = {
|
||||
entryPoint: config.entryPoint,
|
||||
issuer: config.issuer,
|
||||
path: '/auth/saml/handler/frame',
|
||||
};
|
||||
|
||||
if (!opts.entryPoint || !opts.issuer) {
|
||||
logger.warn(
|
||||
'SAML auth provider disabled, set entryPoint and entryPoint in saml auth config to enable',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
envProviders[env] = new SamlAuthProvider(opts);
|
||||
}
|
||||
|
||||
return new EnvironmentHandler(envProviders);
|
||||
}
|
||||
|
||||
@@ -15,11 +15,32 @@
|
||||
*/
|
||||
|
||||
import express from 'express';
|
||||
import { Logger } from 'winston';
|
||||
|
||||
export type OAuthProviderOptions = {
|
||||
clientID: string;
|
||||
clientSecret: string;
|
||||
callbackURL: string;
|
||||
};
|
||||
|
||||
export type SAMLProviderConfig = {
|
||||
entryPoint: string;
|
||||
issuer: string;
|
||||
};
|
||||
|
||||
export type EnvironmentProviderConfig = {
|
||||
[key: string]: OAuthProviderConfig | SAMLProviderConfig;
|
||||
};
|
||||
|
||||
export type AuthProviderConfig = {
|
||||
provider: string;
|
||||
options: any;
|
||||
disableRefresh?: boolean;
|
||||
baseUrl: string;
|
||||
};
|
||||
|
||||
export type OAuthProviderConfig = {
|
||||
secure: boolean;
|
||||
appOrigin: string; // http://localhost:3000
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
};
|
||||
|
||||
export interface OAuthProviderHandlers {
|
||||
@@ -36,8 +57,14 @@ export interface AuthProviderRouteHandlers {
|
||||
logout(req: express.Request, res: express.Response): Promise<any>;
|
||||
}
|
||||
|
||||
export type SAMLEnvironmentProviderConfig = {
|
||||
[key: string]: SAMLProviderConfig;
|
||||
};
|
||||
|
||||
export type AuthProviderFactory = (
|
||||
config: AuthProviderConfig,
|
||||
globalConfig: AuthProviderConfig,
|
||||
providerConfig: EnvironmentProviderConfig,
|
||||
logger: Logger,
|
||||
) => AuthProviderRouteHandlers;
|
||||
|
||||
export type AuthInfoBase = {
|
||||
|
||||
@@ -19,7 +19,6 @@ import Router from 'express-promise-router';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import bodyParser from 'body-parser';
|
||||
import { Logger } from 'winston';
|
||||
import { providers } from './../providers/config';
|
||||
import { createAuthProviderRouter } from '../providers';
|
||||
|
||||
export interface RouterOptions {
|
||||
@@ -36,13 +35,61 @@ export async function createRouter(
|
||||
router.use(bodyParser.urlencoded({ extended: false }));
|
||||
router.use(bodyParser.json());
|
||||
|
||||
// configure all the providers
|
||||
for (const providerConfig of providers) {
|
||||
const { provider } = providerConfig;
|
||||
const providerRouter = createAuthProviderRouter(providerConfig);
|
||||
logger.info(`Configuring provider, ${provider}`);
|
||||
router.use(`/${provider}`, providerRouter);
|
||||
}
|
||||
// TODO: read from app config
|
||||
const config = {
|
||||
backend: {
|
||||
baseUrl: 'http://localhost:7000',
|
||||
},
|
||||
auth: {
|
||||
providers: {
|
||||
google: {
|
||||
development: {
|
||||
appOrigin: 'http://localhost:3000',
|
||||
secure: false,
|
||||
clientId: process.env.AUTH_GOOGLE_CLIENT_ID!,
|
||||
clientSecret: process.env.AUTH_GOOGLE_CLIENT_SECRET!,
|
||||
},
|
||||
production: {
|
||||
appOrigin: 'http://localhost:3000',
|
||||
secure: false,
|
||||
clientId: '',
|
||||
clientSecret: '',
|
||||
},
|
||||
},
|
||||
github: {
|
||||
development: {
|
||||
appOrigin: 'http://localhost:3000',
|
||||
secure: false,
|
||||
clientId: process.env.AUTH_GITHUB_CLIENT_ID!,
|
||||
clientSecret: process.env.AUTH_GITHUB_CLIENT_SECRET!,
|
||||
},
|
||||
},
|
||||
saml: {
|
||||
development: {
|
||||
entryPoint: 'http://localhost:7001/',
|
||||
issuer: 'passport-saml',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const providerConfigs = config.auth.providers;
|
||||
|
||||
for (const [providerId, providerConfig] of Object.entries(providerConfigs)) {
|
||||
const baseUrl = `${config.backend.baseUrl}/auth`;
|
||||
logger.info(`Configuring provider, ${providerId}`);
|
||||
try {
|
||||
const providerRouter = createAuthProviderRouter(
|
||||
providerId,
|
||||
{ baseUrl },
|
||||
providerConfig,
|
||||
logger,
|
||||
);
|
||||
router.use(`/${providerId}`, providerRouter);
|
||||
} catch (e) {
|
||||
logger.error(e.message);
|
||||
}
|
||||
}
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,32 @@
|
||||
# Catalog Backend
|
||||
|
||||
WORK IN PROGRESS
|
||||
|
||||
This is the backend part of the default catalog plugin.
|
||||
|
||||
It responds to requests from the frontend part, and fulfills them by delegating
|
||||
to your existing catalog related services.
|
||||
It comes with a builtin database backed implementation of the catalog, that can store
|
||||
and serve your catalog for you.
|
||||
|
||||
It can also act as a bridge to your existing catalog solutions, either ingesting their
|
||||
data to store in the database, or by effectively proxying calls to an external catalog
|
||||
service.
|
||||
|
||||
## Getting Started
|
||||
|
||||
After starting the backend, you can issue the `yarn mock-catalog-data` command
|
||||
in this directory to populate the catalog with some mock entities.
|
||||
This backend plugin can be started in a standalone mode from directly in this package
|
||||
with `yarn start`. However, it will have limited functionality and that process is
|
||||
most convenient when developing the catalog backend plugin itself.
|
||||
|
||||
To evaluate the catalog and have a greater amount of functionality available, instead do
|
||||
|
||||
```bash
|
||||
# in one terminal window, run this from from the very root of the Backstage project
|
||||
cd packages/backend
|
||||
yarn start
|
||||
|
||||
# open another terminal window, and run the following from the very root of the Backstage project
|
||||
yarn lerna run mock-catalog-data
|
||||
```
|
||||
|
||||
This will launch the full example backend and populate its catalog with some mock entities.
|
||||
|
||||
## Links
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
apiVersion: backstage.io/v1beta1
|
||||
kind: Location
|
||||
metadata:
|
||||
name: location-1
|
||||
spec:
|
||||
type: github
|
||||
target: https://github.com/spotify/backstage/blob/master/plugins/catalog-backend/examples/example-components.yaml
|
||||
+11
-5
@@ -14,9 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as Knex from 'knex';
|
||||
// @ts-check
|
||||
|
||||
export async function up(knex: Knex): Promise<any> {
|
||||
/**
|
||||
* @param {import('knex')} knex
|
||||
*/
|
||||
exports.up = async function up(knex) {
|
||||
return (
|
||||
knex.schema
|
||||
//
|
||||
@@ -114,9 +117,12 @@ export async function up(knex: Knex): Promise<any> {
|
||||
.comment('The corresponding value to match on');
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export async function down(knex: Knex): Promise<any> {
|
||||
/**
|
||||
* @param {import('knex')} knex
|
||||
*/
|
||||
exports.down = async function down(knex) {
|
||||
return knex.schema
|
||||
.dropTable('entities_search')
|
||||
.alterTable('entities', table => {
|
||||
@@ -124,4 +130,4 @@ export async function down(knex: Knex): Promise<any> {
|
||||
})
|
||||
.dropTable('entities')
|
||||
.dropTable('locations');
|
||||
}
|
||||
};
|
||||
+13
-9
@@ -13,16 +13,17 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import * as Knex from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<any> {
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* @param {import('knex')} knex
|
||||
*/
|
||||
exports.up = async function up(knex) {
|
||||
return knex.schema.createTable('location_update_log', table => {
|
||||
table.uuid('id').primary();
|
||||
table.enum('status', ['success', 'fail']).notNullable();
|
||||
table
|
||||
.dateTime('created_at')
|
||||
.defaultTo(knex.fn.now())
|
||||
.notNullable();
|
||||
table.dateTime('created_at').defaultTo(knex.fn.now()).notNullable();
|
||||
table.string('message');
|
||||
table
|
||||
.uuid('location_id')
|
||||
@@ -32,8 +33,11 @@ export async function up(knex: Knex): Promise<any> {
|
||||
.onDelete('CASCADE');
|
||||
table.string('entity_name').nullable();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export async function down(knex: Knex): Promise<any> {
|
||||
/**
|
||||
* @param {import('knex')} knex
|
||||
*/
|
||||
exports.down = async function down(knex) {
|
||||
return knex.schema.dropTableIfExists('location_update_log');
|
||||
}
|
||||
};
|
||||
+12
-5
@@ -13,9 +13,13 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import * as Knex from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<any> {
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* @param {import('knex')} knex
|
||||
*/
|
||||
exports.up = async function up(knex) {
|
||||
// Need to first order by date of creation
|
||||
const query = knex
|
||||
.select()
|
||||
@@ -28,8 +32,11 @@ export async function up(knex: Knex): Promise<any> {
|
||||
await knex.schema.raw(
|
||||
`CREATE VIEW location_update_log_latest AS ${groupedQuery.toString()};`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export async function down(knex: Knex): Promise<any> {
|
||||
/**
|
||||
* @param {import('knex')} knex
|
||||
*/
|
||||
exports.down = async function down(knex) {
|
||||
return knex.schema.raw(`DROP VIEW location_update_log_latest;`);
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-catalog-backend",
|
||||
"version": "0.1.1-alpha.7",
|
||||
"version": "0.1.1-alpha.8",
|
||||
"main": "dist",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -16,8 +16,8 @@
|
||||
"mock-catalog-data": "./scripts/mock-data"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.1.1-alpha.7",
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.7",
|
||||
"@backstage/backend-common": "^0.1.1-alpha.8",
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.8",
|
||||
"esm": "^3.2.25",
|
||||
"express": "^4.17.1",
|
||||
"express-promise-router": "^3.0.3",
|
||||
@@ -34,7 +34,7 @@
|
||||
"yup": "^0.28.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.7",
|
||||
"@backstage/cli": "^0.1.1-alpha.8",
|
||||
"@types/lodash": "^4.14.151",
|
||||
"@types/node-fetch": "^2.5.7",
|
||||
"@types/supertest": "^2.0.8",
|
||||
@@ -45,7 +45,8 @@
|
||||
"tsc-watch": "^4.2.3"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
"dist",
|
||||
"migrations"
|
||||
],
|
||||
"nodemonConfig": {
|
||||
"watch": "./dist"
|
||||
|
||||
@@ -14,29 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import Knex from 'knex';
|
||||
import path from 'path';
|
||||
import { CommonDatabase } from '../database';
|
||||
import { DatabaseManager } from '../database';
|
||||
import { DatabaseLocationsCatalog } from './DatabaseLocationsCatalog';
|
||||
|
||||
describe('DatabaseLocationsCatalog', () => {
|
||||
let catalog: DatabaseLocationsCatalog;
|
||||
|
||||
beforeEach(async () => {
|
||||
const knex = Knex({
|
||||
client: 'sqlite3',
|
||||
connection: ':memory:',
|
||||
useNullAsDefault: true,
|
||||
});
|
||||
knex.client.pool.on('createSuccess', (_eventId: any, resource: any) => {
|
||||
resource.run('PRAGMA foreign_keys = ON', () => {});
|
||||
});
|
||||
await knex.migrate.latest({
|
||||
directory: path.resolve(__dirname, '../database/migrations'),
|
||||
loadExtensions: ['.ts'],
|
||||
});
|
||||
const db = new CommonDatabase(knex, getVoidLogger());
|
||||
const db = await DatabaseManager.createTestDatabase();
|
||||
catalog = new DatabaseLocationsCatalog(db);
|
||||
});
|
||||
|
||||
|
||||
@@ -14,16 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
ConflictError,
|
||||
getVoidLogger,
|
||||
NotFoundError,
|
||||
} from '@backstage/backend-common';
|
||||
import { ConflictError, NotFoundError } from '@backstage/backend-common';
|
||||
import type { Entity, Location } from '@backstage/catalog-model';
|
||||
import Knex from 'knex';
|
||||
import path from 'path';
|
||||
import { CommonDatabase } from './CommonDatabase';
|
||||
import { DatabaseLocationUpdateLogStatus } from './types';
|
||||
import { DatabaseManager } from './DatabaseManager';
|
||||
import { Database, DatabaseLocationUpdateLogStatus } from './types';
|
||||
import type {
|
||||
DbEntityRequest,
|
||||
DbEntityResponse,
|
||||
@@ -31,22 +25,12 @@ import type {
|
||||
} from './types';
|
||||
|
||||
describe('CommonDatabase', () => {
|
||||
let knex: Knex;
|
||||
let db: Database;
|
||||
let entityRequest: DbEntityRequest;
|
||||
let entityResponse: DbEntityResponse;
|
||||
|
||||
beforeEach(async () => {
|
||||
knex = Knex({
|
||||
client: 'sqlite3',
|
||||
connection: ':memory:',
|
||||
useNullAsDefault: true,
|
||||
});
|
||||
|
||||
await knex.raw('PRAGMA foreign_keys = ON');
|
||||
await knex.migrate.latest({
|
||||
directory: path.resolve(__dirname, 'migrations'),
|
||||
loadExtensions: ['.ts'],
|
||||
});
|
||||
db = await DatabaseManager.createTestDatabase();
|
||||
|
||||
entityRequest = {
|
||||
entity: {
|
||||
@@ -84,7 +68,6 @@ describe('CommonDatabase', () => {
|
||||
});
|
||||
|
||||
it('manages locations', async () => {
|
||||
const db = new CommonDatabase(knex, getVoidLogger());
|
||||
const input: Location = {
|
||||
id: 'dd12620d-0436-422f-93bd-929aa0788123',
|
||||
type: 'a',
|
||||
@@ -115,55 +98,76 @@ describe('CommonDatabase', () => {
|
||||
|
||||
describe('addEntity', () => {
|
||||
it('happy path: adds entity to empty database', async () => {
|
||||
const catalog = new CommonDatabase(knex, getVoidLogger());
|
||||
const added = await catalog.transaction(tx =>
|
||||
catalog.addEntity(tx, entityRequest),
|
||||
);
|
||||
const added = await db.transaction(tx => db.addEntity(tx, entityRequest));
|
||||
expect(added).toStrictEqual(entityResponse);
|
||||
expect(added.entity.metadata.generation).toBe(1);
|
||||
});
|
||||
|
||||
it('rejects adding the same-named entity twice', async () => {
|
||||
const catalog = new CommonDatabase(knex, getVoidLogger());
|
||||
await catalog.transaction(tx => catalog.addEntity(tx, entityRequest));
|
||||
await db.transaction(tx => db.addEntity(tx, entityRequest));
|
||||
await expect(
|
||||
catalog.transaction(tx => catalog.addEntity(tx, entityRequest)),
|
||||
db.transaction(tx => db.addEntity(tx, entityRequest)),
|
||||
).rejects.toThrow(ConflictError);
|
||||
});
|
||||
|
||||
it('rejects adding the almost-same-kind entity twice', async () => {
|
||||
entityRequest.entity.kind = 'some-kind';
|
||||
await db.transaction(tx => db.addEntity(tx, entityRequest));
|
||||
entityRequest.entity.kind = 'SomeKind';
|
||||
await expect(
|
||||
db.transaction(tx => db.addEntity(tx, entityRequest)),
|
||||
).rejects.toThrow(ConflictError);
|
||||
});
|
||||
|
||||
it('rejects adding the almost-same-named entity twice', async () => {
|
||||
entityRequest.entity.metadata.name = 'some-name';
|
||||
await db.transaction(tx => db.addEntity(tx, entityRequest));
|
||||
entityRequest.entity.metadata.name = 'SomeName';
|
||||
await expect(
|
||||
db.transaction(tx => db.addEntity(tx, entityRequest)),
|
||||
).rejects.toThrow(ConflictError);
|
||||
});
|
||||
|
||||
it('rejects adding the almost-same-namespace entity twice', async () => {
|
||||
entityRequest.entity.metadata.namespace = undefined;
|
||||
await db.transaction(tx => db.addEntity(tx, entityRequest));
|
||||
entityRequest.entity.metadata.namespace = '';
|
||||
await expect(
|
||||
db.transaction(tx => db.addEntity(tx, entityRequest)),
|
||||
).rejects.toThrow(ConflictError);
|
||||
});
|
||||
|
||||
it('accepts adding the same-named entity twice if on different namespaces', async () => {
|
||||
const catalog = new CommonDatabase(knex, getVoidLogger());
|
||||
entityRequest.entity.metadata.namespace = 'namespace1';
|
||||
await catalog.transaction(tx => catalog.addEntity(tx, entityRequest));
|
||||
await db.transaction(tx => db.addEntity(tx, entityRequest));
|
||||
entityRequest.entity.metadata.namespace = 'namespace2';
|
||||
await expect(
|
||||
catalog.transaction(tx => catalog.addEntity(tx, entityRequest)),
|
||||
db.transaction(tx => db.addEntity(tx, entityRequest)),
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('locationHistory', () => {
|
||||
it('outputs the history correctly', async () => {
|
||||
const catalog = new CommonDatabase(knex, getVoidLogger());
|
||||
const location: Location = {
|
||||
id: 'dd12620d-0436-422f-93bd-929aa0788123',
|
||||
type: 'a',
|
||||
target: 'b',
|
||||
};
|
||||
await catalog.addLocation(location);
|
||||
await db.addLocation(location);
|
||||
|
||||
await catalog.addLocationUpdateLogEvent(
|
||||
await db.addLocationUpdateLogEvent(
|
||||
'dd12620d-0436-422f-93bd-929aa0788123',
|
||||
DatabaseLocationUpdateLogStatus.SUCCESS,
|
||||
);
|
||||
await catalog.addLocationUpdateLogEvent(
|
||||
await db.addLocationUpdateLogEvent(
|
||||
'dd12620d-0436-422f-93bd-929aa0788123',
|
||||
DatabaseLocationUpdateLogStatus.FAIL,
|
||||
undefined,
|
||||
'Something went wrong',
|
||||
);
|
||||
|
||||
const result = await catalog.locationHistory(
|
||||
const result = await db.locationHistory(
|
||||
'dd12620d-0436-422f-93bd-929aa0788123',
|
||||
);
|
||||
expect(result).toEqual([
|
||||
@@ -189,12 +193,9 @@ describe('CommonDatabase', () => {
|
||||
|
||||
describe('updateEntity', () => {
|
||||
it('can read and no-op-update an entity', async () => {
|
||||
const catalog = new CommonDatabase(knex, getVoidLogger());
|
||||
const added = await catalog.transaction(tx =>
|
||||
catalog.addEntity(tx, entityRequest),
|
||||
);
|
||||
const updated = await catalog.transaction(tx =>
|
||||
catalog.updateEntity(tx, { entity: added.entity }),
|
||||
const added = await db.transaction(tx => db.addEntity(tx, entityRequest));
|
||||
const updated = await db.transaction(tx =>
|
||||
db.updateEntity(tx, { entity: added.entity }),
|
||||
);
|
||||
expect(updated.entity.apiVersion).toEqual(added.entity.apiVersion);
|
||||
expect(updated.entity.kind).toEqual(added.entity.kind);
|
||||
@@ -211,77 +212,55 @@ describe('CommonDatabase', () => {
|
||||
});
|
||||
|
||||
it('can update name if uid matches', async () => {
|
||||
const catalog = new CommonDatabase(knex, getVoidLogger());
|
||||
const added = await catalog.transaction(tx =>
|
||||
catalog.addEntity(tx, entityRequest),
|
||||
);
|
||||
const added = await db.transaction(tx => db.addEntity(tx, entityRequest));
|
||||
added.entity.metadata.name! = 'new!';
|
||||
const updated = await catalog.transaction(tx =>
|
||||
catalog.updateEntity(tx, { entity: added.entity }),
|
||||
const updated = await db.transaction(tx =>
|
||||
db.updateEntity(tx, { entity: added.entity }),
|
||||
);
|
||||
expect(updated.entity.metadata.name).toEqual('new!');
|
||||
});
|
||||
|
||||
it('can update fields if kind, name, and namespace match', async () => {
|
||||
const catalog = new CommonDatabase(knex, getVoidLogger());
|
||||
const added = await catalog.transaction(tx =>
|
||||
catalog.addEntity(tx, entityRequest),
|
||||
);
|
||||
const added = await db.transaction(tx => db.addEntity(tx, entityRequest));
|
||||
added.entity.apiVersion = 'something.new';
|
||||
delete added.entity.metadata.uid;
|
||||
delete added.entity.metadata.generation;
|
||||
const updated = await catalog.transaction(tx =>
|
||||
catalog.updateEntity(tx, { entity: added.entity }),
|
||||
const updated = await db.transaction(tx =>
|
||||
db.updateEntity(tx, { entity: added.entity }),
|
||||
);
|
||||
expect(updated.entity.apiVersion).toEqual('something.new');
|
||||
});
|
||||
|
||||
it('rejects if kind, name, but not namespace match', async () => {
|
||||
const catalog = new CommonDatabase(knex, getVoidLogger());
|
||||
const added = await catalog.transaction(tx =>
|
||||
catalog.addEntity(tx, entityRequest),
|
||||
);
|
||||
const added = await db.transaction(tx => db.addEntity(tx, entityRequest));
|
||||
added.entity.apiVersion = 'something.new';
|
||||
delete added.entity.metadata.uid;
|
||||
delete added.entity.metadata.generation;
|
||||
added.entity.metadata.namespace = 'something.wrong';
|
||||
await expect(
|
||||
catalog.transaction(tx =>
|
||||
catalog.updateEntity(tx, { entity: added.entity }),
|
||||
),
|
||||
db.transaction(tx => db.updateEntity(tx, { entity: added.entity })),
|
||||
).rejects.toThrow(NotFoundError);
|
||||
});
|
||||
|
||||
it('fails to update an entity if etag does not match', async () => {
|
||||
const catalog = new CommonDatabase(knex, getVoidLogger());
|
||||
const added = await catalog.transaction(tx =>
|
||||
catalog.addEntity(tx, entityRequest),
|
||||
);
|
||||
const added = await db.transaction(tx => db.addEntity(tx, entityRequest));
|
||||
added.entity.metadata.etag = 'garbage';
|
||||
await expect(
|
||||
catalog.transaction(tx =>
|
||||
catalog.updateEntity(tx, { entity: added.entity }),
|
||||
),
|
||||
db.transaction(tx => db.updateEntity(tx, { entity: added.entity })),
|
||||
).rejects.toThrow(ConflictError);
|
||||
});
|
||||
|
||||
it('fails to update an entity if generation does not match', async () => {
|
||||
const catalog = new CommonDatabase(knex, getVoidLogger());
|
||||
const added = await catalog.transaction(tx =>
|
||||
catalog.addEntity(tx, entityRequest),
|
||||
);
|
||||
const added = await db.transaction(tx => db.addEntity(tx, entityRequest));
|
||||
added.entity.metadata.generation! += 100;
|
||||
await expect(
|
||||
catalog.transaction(tx =>
|
||||
catalog.updateEntity(tx, { entity: added.entity }),
|
||||
),
|
||||
db.transaction(tx => db.updateEntity(tx, { entity: added.entity })),
|
||||
).rejects.toThrow(ConflictError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('entities', () => {
|
||||
it('can get all entities with empty filters list', async () => {
|
||||
const catalog = new CommonDatabase(knex, getVoidLogger());
|
||||
const e1: Entity = {
|
||||
apiVersion: 'a',
|
||||
kind: 'k1',
|
||||
@@ -293,13 +272,11 @@ describe('CommonDatabase', () => {
|
||||
metadata: { name: 'n' },
|
||||
spec: { c: null },
|
||||
};
|
||||
await catalog.transaction(async tx => {
|
||||
await catalog.addEntity(tx, { entity: e1 });
|
||||
await catalog.addEntity(tx, { entity: e2 });
|
||||
await db.transaction(async tx => {
|
||||
await db.addEntity(tx, { entity: e1 });
|
||||
await db.addEntity(tx, { entity: e2 });
|
||||
});
|
||||
const result = await catalog.transaction(async tx =>
|
||||
catalog.entities(tx, []),
|
||||
);
|
||||
const result = await db.transaction(async tx => db.entities(tx, []));
|
||||
expect(result.length).toEqual(2);
|
||||
expect(result).toEqual(
|
||||
expect.arrayContaining([
|
||||
@@ -316,7 +293,6 @@ describe('CommonDatabase', () => {
|
||||
});
|
||||
|
||||
it('can get all specific entities for matching filters (naive case)', async () => {
|
||||
const catalog = new CommonDatabase(knex, getVoidLogger());
|
||||
const entities: Entity[] = [
|
||||
{ apiVersion: 'a', kind: 'k1', metadata: { name: 'n' } },
|
||||
{
|
||||
@@ -333,15 +309,15 @@ describe('CommonDatabase', () => {
|
||||
},
|
||||
];
|
||||
|
||||
await catalog.transaction(async tx => {
|
||||
await db.transaction(async tx => {
|
||||
for (const entity of entities) {
|
||||
await catalog.addEntity(tx, { entity });
|
||||
await db.addEntity(tx, { entity });
|
||||
}
|
||||
});
|
||||
|
||||
await expect(
|
||||
catalog.transaction(async tx =>
|
||||
catalog.entities(tx, [
|
||||
db.transaction(async tx =>
|
||||
db.entities(tx, [
|
||||
{ key: 'kind', values: ['k2'] },
|
||||
{ key: 'spec.c', values: ['some'] },
|
||||
]),
|
||||
@@ -355,7 +331,6 @@ describe('CommonDatabase', () => {
|
||||
});
|
||||
|
||||
it('can get all specific entities for matching filters with nulls (both missing and literal null value)', async () => {
|
||||
const catalog = new CommonDatabase(knex, getVoidLogger());
|
||||
const entities: Entity[] = [
|
||||
{ apiVersion: 'a', kind: 'k1', metadata: { name: 'n' } },
|
||||
{
|
||||
@@ -372,14 +347,14 @@ describe('CommonDatabase', () => {
|
||||
},
|
||||
];
|
||||
|
||||
await catalog.transaction(async tx => {
|
||||
await db.transaction(async tx => {
|
||||
for (const entity of entities) {
|
||||
await catalog.addEntity(tx, { entity });
|
||||
await db.addEntity(tx, { entity });
|
||||
}
|
||||
});
|
||||
|
||||
const rows = await catalog.transaction(async tx =>
|
||||
catalog.entities(tx, [
|
||||
const rows = await db.transaction(async tx =>
|
||||
db.entities(tx, [
|
||||
{ key: 'apiVersion', values: ['a'] },
|
||||
{ key: 'spec.c', values: [null, 'some'] },
|
||||
]),
|
||||
|
||||
@@ -43,7 +43,6 @@ function getStrippedMetadata(metadata: EntityMeta): EntityMeta {
|
||||
delete output.uid;
|
||||
delete output.etag;
|
||||
delete output.generation;
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
@@ -70,7 +69,7 @@ function toEntityRow(
|
||||
generation: entity.metadata.generation!,
|
||||
api_version: entity.apiVersion,
|
||||
kind: entity.kind,
|
||||
name: entity.metadata.name || null,
|
||||
name: entity.metadata.name,
|
||||
namespace: entity.metadata.namespace || null,
|
||||
metadata: serializeMetadata(entity.metadata),
|
||||
spec: serializeSpec(entity.spec),
|
||||
@@ -124,6 +123,7 @@ function generateEtag(): string {
|
||||
export class CommonDatabase implements Database {
|
||||
constructor(
|
||||
private readonly database: Knex,
|
||||
private readonly normalize: (value: string) => string,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
@@ -158,6 +158,8 @@ export class CommonDatabase implements Database {
|
||||
throw new InputError('May not specify generation for new entities');
|
||||
}
|
||||
|
||||
await this.ensureNoSimilarNames(tx, request.entity);
|
||||
|
||||
const newEntity = lodash.cloneDeep(request.entity);
|
||||
newEntity.metadata = {
|
||||
...newEntity.metadata,
|
||||
@@ -255,6 +257,8 @@ export class CommonDatabase implements Database {
|
||||
}
|
||||
}
|
||||
|
||||
await this.ensureNoSimilarNames(tx, newEntity);
|
||||
|
||||
// Store the updated entity; select on the old etag to ensure that we do
|
||||
// not lose to another writer
|
||||
const newRow = toEntityRow(request.locationId, newEntity);
|
||||
@@ -278,23 +282,50 @@ export class CommonDatabase implements Database {
|
||||
const tx = txOpaque as Knex.Transaction<any, any>;
|
||||
|
||||
let builder = tx<DbEntitiesRow>('entities');
|
||||
for (const [index, filter] of (filters ?? []).entries()) {
|
||||
for (const [indexU, filter] of (filters ?? []).entries()) {
|
||||
const index = Number(indexU);
|
||||
const key = filter.key.replace('*', '%');
|
||||
const keyOp = filter.key.includes('*') ? 'like' : '=';
|
||||
|
||||
let matchNulls = false;
|
||||
const matchIn: string[] = [];
|
||||
const matchLike: string[] = [];
|
||||
|
||||
for (const value of filter.values) {
|
||||
if (!value) {
|
||||
matchNulls = true;
|
||||
} else if (value.includes('*')) {
|
||||
matchLike.push(value.replace('*', '%'));
|
||||
} else {
|
||||
matchIn.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
builder = builder
|
||||
.leftOuterJoin(`entities_search as t${index}`, function join() {
|
||||
this.on('entities.id', '=', `t${index}.entity_id`).onIn(
|
||||
`t${index}.value`,
|
||||
filter.values.filter(x => x),
|
||||
);
|
||||
if (filter.values.some(x => !x)) {
|
||||
this.orOnNull(`t${index}.value`);
|
||||
}
|
||||
.leftOuterJoin(`entities_search as t${index}`, function joins() {
|
||||
this.on('entities.id', '=', `t${index}.entity_id`);
|
||||
this.andOn(`t${index}.key`, keyOp, tx.raw('?', [key]));
|
||||
})
|
||||
.where(`t${index}.key`, '=', filter.key);
|
||||
.where(function rules() {
|
||||
if (matchIn.length) {
|
||||
this.orWhereIn(`t${index}.value`, matchIn);
|
||||
}
|
||||
if (matchLike.length) {
|
||||
for (const x of matchLike) {
|
||||
this.orWhere(`t${index}.value`, 'like', tx.raw('?', [x]));
|
||||
}
|
||||
}
|
||||
if (matchNulls) {
|
||||
this.orWhereNull(`t${index}.value`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const rows = await builder
|
||||
.orderBy('namespace', 'name')
|
||||
.select('entities.*')
|
||||
.orderBy('kind', 'asc')
|
||||
.orderBy('namespace', 'asc')
|
||||
.orderBy('name', 'asc')
|
||||
.groupBy('id');
|
||||
|
||||
return rows.map(row => toEntityResponse(row));
|
||||
@@ -359,6 +390,10 @@ export class CommonDatabase implements Database {
|
||||
async removeLocation(txOpaque: unknown, id: string): Promise<void> {
|
||||
const tx = txOpaque as Knex.Transaction<any, any>;
|
||||
|
||||
await tx<DbEntitiesRow>('entities')
|
||||
.where({ location_id: id })
|
||||
.update({ location_id: null });
|
||||
|
||||
const result = await tx<DbLocationsRow>('locations').where({ id }).del();
|
||||
|
||||
if (!result) {
|
||||
@@ -447,4 +482,46 @@ export class CommonDatabase implements Database {
|
||||
// we got around to writing the entries
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureNoSimilarNames(
|
||||
tx: Knex.Transaction<any, any>,
|
||||
data: Entity,
|
||||
): Promise<void> {
|
||||
const newKind = data.kind;
|
||||
const newName = data.metadata.name;
|
||||
const newNamespace = data.metadata.namespace;
|
||||
const newKindNorm = this.normalize(newKind);
|
||||
const newNameNorm = this.normalize(newName);
|
||||
const newNamespaceNorm = this.normalize(newNamespace || '');
|
||||
|
||||
for (const item of await this.entities(tx)) {
|
||||
if (data.metadata.uid === item.entity.metadata.uid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const oldKind = item.entity.kind;
|
||||
const oldName = item.entity.metadata.name;
|
||||
const oldNamespace = item.entity.metadata.namespace;
|
||||
const oldKindNorm = this.normalize(oldKind);
|
||||
const oldNameNorm = this.normalize(oldName);
|
||||
const oldNamespaceNorm = this.normalize(oldNamespace || '');
|
||||
|
||||
if (
|
||||
oldKindNorm === newKindNorm &&
|
||||
oldNameNorm === newNameNorm &&
|
||||
oldNamespaceNorm === newNamespaceNorm
|
||||
) {
|
||||
// Only throw if things were actually different - for completely equal
|
||||
// things, we let the database handle the conflict
|
||||
if (
|
||||
oldKind !== newKind ||
|
||||
oldName !== newName ||
|
||||
oldNamespace !== newNamespace
|
||||
) {
|
||||
const message = `Kind, namespace, name are too similar to an existing entity`;
|
||||
throw new ConflictError(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,26 +14,43 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { makeValidator } from '@backstage/catalog-model';
|
||||
import Knex from 'knex';
|
||||
import path from 'path';
|
||||
import { Logger } from 'winston';
|
||||
import { CommonDatabase } from './CommonDatabase';
|
||||
import { Database } from './types';
|
||||
|
||||
const migrationsDir = path.resolve(
|
||||
require.resolve('@backstage/plugin-catalog-backend/package.json'),
|
||||
'../migrations',
|
||||
);
|
||||
|
||||
export type CreateDatabaseOptions = {
|
||||
logger: Logger;
|
||||
fieldNormalizer: (value: string) => string;
|
||||
};
|
||||
|
||||
const defaultOptions: CreateDatabaseOptions = {
|
||||
logger: getVoidLogger(),
|
||||
fieldNormalizer: makeValidator().normalizeEntityName,
|
||||
};
|
||||
|
||||
export class DatabaseManager {
|
||||
public static async createDatabase(
|
||||
knex: Knex,
|
||||
logger: Logger,
|
||||
options: Partial<CreateDatabaseOptions> = {},
|
||||
): Promise<Database> {
|
||||
await knex.migrate.latest({
|
||||
directory: path.resolve(__dirname, 'migrations'),
|
||||
loadExtensions: ['.js'],
|
||||
directory: migrationsDir,
|
||||
});
|
||||
return new CommonDatabase(knex, logger);
|
||||
const { logger, fieldNormalizer } = { ...defaultOptions, ...options };
|
||||
return new CommonDatabase(knex, fieldNormalizer, logger);
|
||||
}
|
||||
|
||||
public static async createInMemoryDatabase(
|
||||
logger: Logger,
|
||||
options: Partial<CreateDatabaseOptions> = {},
|
||||
): Promise<Database> {
|
||||
const knex = Knex({
|
||||
client: 'sqlite3',
|
||||
@@ -43,6 +60,22 @@ export class DatabaseManager {
|
||||
knex.client.pool.on('createSuccess', (_eventId: any, resource: any) => {
|
||||
resource.run('PRAGMA foreign_keys = ON', () => {});
|
||||
});
|
||||
return DatabaseManager.createDatabase(knex, logger);
|
||||
return DatabaseManager.createDatabase(knex, options);
|
||||
}
|
||||
|
||||
public static async createTestDatabase(): Promise<Database> {
|
||||
const knex = Knex({
|
||||
client: 'sqlite3',
|
||||
connection: ':memory:',
|
||||
useNullAsDefault: true,
|
||||
});
|
||||
knex.client.pool.on('createSuccess', (_eventId: any, resource: any) => {
|
||||
resource.run('PRAGMA foreign_keys = ON', () => {});
|
||||
});
|
||||
await knex.migrate.latest({
|
||||
directory: migrationsDir,
|
||||
});
|
||||
const { logger, fieldNormalizer } = defaultOptions;
|
||||
return new CommonDatabase(knex, fieldNormalizer, logger);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import { AnnotateLocationEntityProcessor } from './processors/AnnotateLocationEn
|
||||
import { EntityPolicyProcessor } from './processors/EntityPolicyProcessor';
|
||||
import { FileReaderProcessor } from './processors/FileReaderProcessor';
|
||||
import { GithubReaderProcessor } from './processors/GithubReaderProcessor';
|
||||
import { LocationRefProcessor } from './processors/LocationEntityProcessor';
|
||||
import * as result from './processors/results';
|
||||
import {
|
||||
LocationProcessor,
|
||||
@@ -57,6 +58,7 @@ export class LocationReaders implements LocationReader {
|
||||
new GithubReaderProcessor(),
|
||||
new YamlProcessor(),
|
||||
new EntityPolicyProcessor(entityPolicy),
|
||||
new LocationRefProcessor(),
|
||||
new AnnotateLocationEntityProcessor(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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, LocationEntity, LocationSpec } from '@backstage/catalog-model';
|
||||
import * as result from './results';
|
||||
import { LocationProcessor, LocationProcessorEmit } from './types';
|
||||
|
||||
export class LocationRefProcessor implements LocationProcessor {
|
||||
async processEntity(
|
||||
entity: Entity,
|
||||
_location: LocationSpec,
|
||||
emit: LocationProcessorEmit,
|
||||
): Promise<Entity> {
|
||||
if (entity.kind === 'Location') {
|
||||
const location = entity as LocationEntity;
|
||||
if (location.spec.target) {
|
||||
emit(
|
||||
result.location(
|
||||
{ type: location.spec.type, target: location.spec.target },
|
||||
false,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (location.spec.targets) {
|
||||
for (const target of location.spec.targets) {
|
||||
emit(result.location({ type: location.spec.type, target }, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
@@ -110,7 +110,7 @@ export async function createRouter(
|
||||
.delete('/locations/:id', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
await locationsCatalog.removeLocation(id);
|
||||
res.status(200).send();
|
||||
res.status(204).send();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ export async function startStandaloneServer(
|
||||
const logger = options.logger.child({ service: 'catalog-backend' });
|
||||
|
||||
logger.debug('Creating application...');
|
||||
const db = await DatabaseManager.createInMemoryDatabase(logger);
|
||||
const db = await DatabaseManager.createInMemoryDatabase({ logger });
|
||||
const entitiesCatalog = new DatabaseEntitiesCatalog(db);
|
||||
const locationsCatalog = new DatabaseLocationsCatalog(db);
|
||||
const locationReader = new LocationReaders();
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"target": "es2019",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"allowJs": true,
|
||||
"lib": ["es2019"],
|
||||
"types": ["node", "jest"]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-catalog",
|
||||
"version": "0.1.1-alpha.7",
|
||||
"version": "0.1.1-alpha.8",
|
||||
"main": "dist/index.esm.js",
|
||||
"main:src": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
@@ -22,25 +22,25 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.7",
|
||||
"@backstage/core": "^0.1.1-alpha.7",
|
||||
"@backstage/plugin-scaffolder": "^0.1.1-alpha.7",
|
||||
"@backstage/plugin-sentry": "^0.1.1-alpha.7",
|
||||
"@backstage/theme": "^0.1.1-alpha.7",
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.8",
|
||||
"@backstage/core": "^0.1.1-alpha.8",
|
||||
"@backstage/plugin-scaffolder": "^0.1.1-alpha.8",
|
||||
"@backstage/plugin-sentry": "^0.1.1-alpha.8",
|
||||
"@backstage/theme": "^0.1.1-alpha.8",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
"node-cache": "^5.1.1",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-router": "^5.2.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-use": "^14.2.0"
|
||||
"react-router": "^6.0.0-alpha.5",
|
||||
"react-router-dom": "^6.0.0-alpha.5",
|
||||
"react-use": "^14.2.0",
|
||||
"swr": "^0.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.7",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.7",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.7",
|
||||
"@backstage/cli": "^0.1.1-alpha.8",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.8",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.8",
|
||||
"@testing-library/jest-dom": "^5.7.0",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/react-hooks": "^3.3.0",
|
||||
|
||||
@@ -19,14 +19,9 @@ import {
|
||||
Location,
|
||||
LOCATION_ANNOTATION,
|
||||
} from '@backstage/catalog-model';
|
||||
import Cache from 'node-cache';
|
||||
import { CatalogApi, EntityCompoundName } from './types';
|
||||
|
||||
export class CatalogClient implements CatalogApi {
|
||||
// TODO(blam): This cache is just temporary until we have GraphQL.
|
||||
// And client side caching using things like React Apollo or Relay.
|
||||
// There's a lot of loading states that cause flickering around the app which aren't needed.
|
||||
private cache: Cache;
|
||||
private apiOrigin: string;
|
||||
private basePath: string;
|
||||
|
||||
@@ -39,7 +34,6 @@ export class CatalogClient implements CatalogApi {
|
||||
}) {
|
||||
this.apiOrigin = apiOrigin;
|
||||
this.basePath = basePath;
|
||||
this.cache = new Cache({ stdTTL: 10 });
|
||||
}
|
||||
|
||||
private async getRequired(path: string): Promise<any> {
|
||||
@@ -79,11 +73,6 @@ export class CatalogClient implements CatalogApi {
|
||||
async getEntities(
|
||||
filter?: Record<string, string | string[]>,
|
||||
): Promise<Entity[]> {
|
||||
const cachedValue = this.cache.get<Entity[]>(
|
||||
`get:${JSON.stringify(filter)}`,
|
||||
);
|
||||
if (cachedValue) return cachedValue;
|
||||
|
||||
let path = `/entities`;
|
||||
if (filter) {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
@@ -35,7 +35,6 @@ import Star from '@material-ui/icons/Star';
|
||||
import StarOutline from '@material-ui/icons/StarBorder';
|
||||
import React, { FC, useCallback, useState } from 'react';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { useAsync } from 'react-use';
|
||||
import { catalogApiRef } from '../..';
|
||||
import { defaultFilter, entityFilters, filterGroups } from '../../data/filters';
|
||||
import { findLocationForEntityMeta } from '../../data/utils';
|
||||
@@ -45,6 +44,7 @@ import {
|
||||
CatalogFilterItem,
|
||||
} from '../CatalogFilter/CatalogFilter';
|
||||
import { CatalogTable } from '../CatalogTable/CatalogTable';
|
||||
import useStaleWhileRevalidate from 'swr';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
contentWrapper: {
|
||||
@@ -61,20 +61,21 @@ const useStyles = makeStyles(theme => ({
|
||||
|
||||
export const CatalogPage: FC<{}> = () => {
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const {
|
||||
starredEntities,
|
||||
toggleStarredEntity,
|
||||
isStarredEntity,
|
||||
} = useStarredEntities();
|
||||
const { toggleStarredEntity, isStarredEntity } = useStarredEntities();
|
||||
|
||||
const [selectedFilter, setSelectedFilter] = useState<CatalogFilterItem>(
|
||||
defaultFilter,
|
||||
);
|
||||
|
||||
const { value, error, loading } = useAsync(async () => {
|
||||
const filter = entityFilters[selectedFilter.id];
|
||||
const all = await catalogApi.getEntities();
|
||||
return all.filter(e => filter(e, { isStarred: isStarredEntity(e) }));
|
||||
}, [selectedFilter.id, starredEntities.size]);
|
||||
const { data: entities, error } = useStaleWhileRevalidate(
|
||||
['catalog/all', entityFilters[selectedFilter.id]],
|
||||
async () => catalogApi.getEntities(),
|
||||
);
|
||||
|
||||
const data =
|
||||
entities?.filter(e =>
|
||||
entityFilters[selectedFilter.id](e, { isStarred: isStarredEntity(e) }),
|
||||
) ?? [];
|
||||
|
||||
const onFilterSelected = useCallback(
|
||||
selected => setSelectedFilter(selected),
|
||||
@@ -147,6 +148,10 @@ export const CatalogPage: FC<{}> = () => {
|
||||
id: 'documentation',
|
||||
label: 'Documentation',
|
||||
},
|
||||
{
|
||||
id: 'other',
|
||||
label: 'Other',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -195,8 +200,8 @@ export const CatalogPage: FC<{}> = () => {
|
||||
</div>
|
||||
<CatalogTable
|
||||
titlePreamble={selectedFilter.label}
|
||||
entities={value || []}
|
||||
loading={loading}
|
||||
entities={data || []}
|
||||
loading={!data && !error}
|
||||
error={error}
|
||||
actions={actions}
|
||||
/>
|
||||
|
||||
@@ -14,32 +14,38 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
const actual = jest.requireActual('react-router-dom');
|
||||
const mockNavigate = jest.fn();
|
||||
return {
|
||||
...actual,
|
||||
useNavigate: jest.fn(() => mockNavigate),
|
||||
useParams: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { render, wait } from '@testing-library/react';
|
||||
import * as React from 'react';
|
||||
import { CatalogApi, catalogApiRef } from '../../api/types';
|
||||
import { EntityPage } from './EntityPage';
|
||||
|
||||
const getTestProps = (name: string) => {
|
||||
return {
|
||||
match: {
|
||||
params: {
|
||||
optionalNamespaceAndName: name,
|
||||
kind: 'Component',
|
||||
},
|
||||
},
|
||||
history: {
|
||||
push: jest.fn(),
|
||||
},
|
||||
};
|
||||
};
|
||||
const {
|
||||
useParams,
|
||||
useNavigate,
|
||||
}: { useParams: jest.Mock; useNavigate: () => jest.Mock } = jest.requireMock(
|
||||
'react-router-dom',
|
||||
);
|
||||
|
||||
const errorApi = { post: () => {} };
|
||||
|
||||
describe('EntityPage', () => {
|
||||
it('should redirect to catalog page when name is not provided', async () => {
|
||||
const props = getTestProps('');
|
||||
useParams.mockReturnValue({
|
||||
kind: 'Component',
|
||||
optionalNamespaceAndName: '',
|
||||
});
|
||||
|
||||
render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider
|
||||
@@ -53,13 +59,11 @@ describe('EntityPage', () => {
|
||||
],
|
||||
])}
|
||||
>
|
||||
<EntityPage {...props} />
|
||||
<EntityPage />
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
|
||||
await wait(() =>
|
||||
expect(props.history.push).toHaveBeenCalledWith('/catalog'),
|
||||
);
|
||||
await wait(() => expect(useNavigate()).toHaveBeenCalledWith('/catalog'));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -34,21 +34,9 @@ import { catalogApiRef } from '../..';
|
||||
import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu';
|
||||
import { EntityMetadataCard } from '../EntityMetadataCard/EntityMetadataCard';
|
||||
import { UnregisterEntityDialog } from '../UnregisterEntityDialog/UnregisterEntityDialog';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
|
||||
const REDIRECT_DELAY = 1000;
|
||||
|
||||
type Props = {
|
||||
match: {
|
||||
params: {
|
||||
optionalNamespaceAndName: string;
|
||||
kind: string;
|
||||
};
|
||||
};
|
||||
history: {
|
||||
push: (url: string) => void;
|
||||
};
|
||||
};
|
||||
|
||||
function headerProps(
|
||||
kind: string,
|
||||
namespace: string | undefined,
|
||||
@@ -68,8 +56,12 @@ function headerProps(
|
||||
};
|
||||
}
|
||||
|
||||
export const EntityPage: FC<Props> = ({ match, history }) => {
|
||||
const { optionalNamespaceAndName, kind } = match.params;
|
||||
export const EntityPage: FC<{}> = () => {
|
||||
const { optionalNamespaceAndName, kind } = useParams() as {
|
||||
optionalNamespaceAndName: string;
|
||||
kind: string;
|
||||
};
|
||||
const navigate = useNavigate();
|
||||
const [name, namespace] = optionalNamespaceAndName.split(':').reverse();
|
||||
|
||||
const errorApi = useApi(errorApiRef);
|
||||
@@ -85,19 +77,19 @@ export const EntityPage: FC<Props> = ({ match, history }) => {
|
||||
if (!error && !loading && !entity) {
|
||||
errorApi.post(new Error('Entity not found!'));
|
||||
setTimeout(() => {
|
||||
history.push('/');
|
||||
navigate('/');
|
||||
}, REDIRECT_DELAY);
|
||||
}
|
||||
}, [errorApi, history, error, loading, entity]);
|
||||
}, [errorApi, navigate, error, loading, entity]);
|
||||
|
||||
if (!name) {
|
||||
history.push('/catalog');
|
||||
navigate('/catalog');
|
||||
return null;
|
||||
}
|
||||
|
||||
const cleanUpAfterRemoval = async () => {
|
||||
setConfirmationDialogOpen(false);
|
||||
history.push('/');
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
const showRemovalDialog = () => setConfirmationDialogOpen(true);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-circleci",
|
||||
"version": "0.1.1-alpha.7",
|
||||
"version": "0.1.1-alpha.8",
|
||||
"main": "dist/index.esm.js",
|
||||
"main:src": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
@@ -31,8 +31,8 @@
|
||||
"postpack": "backstage-cli postpack"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core": "^0.1.1-alpha.7",
|
||||
"@backstage/theme": "^0.1.1-alpha.7",
|
||||
"@backstage/core": "^0.1.1-alpha.8",
|
||||
"@backstage/theme": "^0.1.1-alpha.8",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
@@ -42,13 +42,13 @@
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-lazylog": "^4.5.2",
|
||||
"react-router": "^5.1.2",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-router": "^6.0.0-alpha.5",
|
||||
"react-router-dom": "^6.0.0-alpha.5",
|
||||
"react-use": "^14.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.7",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.7",
|
||||
"@backstage/cli": "^0.1.1-alpha.8",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.8",
|
||||
"@testing-library/jest-dom": "^5.7.0",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^10.2.4",
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Switch, Route, MemoryRouter } from 'react-router';
|
||||
import { Route, MemoryRouter, Routes } from 'react-router';
|
||||
import { BuildsPage, Builds } from '../pages/BuildsPage';
|
||||
import { DetailedViewPage, BuildWithSteps } from '../pages/BuildWithStepsPage';
|
||||
import { AppStateProvider } from '../state';
|
||||
@@ -24,14 +24,13 @@ export const App = () => {
|
||||
return (
|
||||
<AppStateProvider>
|
||||
<>
|
||||
<Switch>
|
||||
<Route path="/circleci" exact component={BuildsPage} />
|
||||
<Routes>
|
||||
<Route path="/circleci" element={<BuildsPage />} />
|
||||
<Route
|
||||
path="/circleci/build/:buildId"
|
||||
exact
|
||||
component={DetailedViewPage}
|
||||
element={<DetailedViewPage />}
|
||||
/>
|
||||
</Switch>
|
||||
</Routes>
|
||||
<Settings />
|
||||
</>
|
||||
</AppStateProvider>
|
||||
@@ -45,14 +44,10 @@ export const CircleCIWidget = () => (
|
||||
<MemoryRouter initialEntries={['/circleci']}>
|
||||
<AppStateProvider>
|
||||
<>
|
||||
<Switch>
|
||||
<Route path="/circleci" exact component={Builds} />
|
||||
<Route
|
||||
path="/circleci/build/:buildId"
|
||||
exact
|
||||
component={BuildWithSteps}
|
||||
/>
|
||||
</Switch>
|
||||
<Routes>
|
||||
<Route path="/circleci" element={<Builds />} />
|
||||
<Route path="/circleci/build/:buildId" element={<BuildWithSteps />} />
|
||||
</Routes>
|
||||
<Settings />
|
||||
</>
|
||||
</AppStateProvider>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-explore",
|
||||
"version": "0.1.1-alpha.7",
|
||||
"version": "0.1.1-alpha.8",
|
||||
"main": "dist/index.esm.js",
|
||||
"main:src": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
@@ -22,8 +22,8 @@
|
||||
"start": "backstage-cli plugin:serve"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core": "^0.1.1-alpha.7",
|
||||
"@backstage/theme": "^0.1.1-alpha.7",
|
||||
"@backstage/core": "^0.1.1-alpha.8",
|
||||
"@backstage/theme": "^0.1.1-alpha.8",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
@@ -33,9 +33,9 @@
|
||||
"react-use": "^14.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.7",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.7",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.7",
|
||||
"@backstage/cli": "^0.1.1-alpha.8",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.8",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.8",
|
||||
"@testing-library/jest-dom": "^5.7.0",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^10.2.4",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-gitops-profiles",
|
||||
"version": "0.1.1-alpha.7",
|
||||
"version": "0.1.1-alpha.8",
|
||||
"main": "dist/index.esm.js",
|
||||
"main:src": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
@@ -22,19 +22,19 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core": "^0.1.1-alpha.7",
|
||||
"@backstage/theme": "^0.1.1-alpha.7",
|
||||
"@backstage/core": "^0.1.1-alpha.8",
|
||||
"@backstage/theme": "^0.1.1-alpha.8",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-use": "^14.2.0",
|
||||
"react-router-dom": "^5.2.0"
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-use": "^14.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.7",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.7",
|
||||
"@backstage/cli": "^0.1.1-alpha.8",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.8",
|
||||
"@testing-library/jest-dom": "^5.7.0",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^10.2.4",
|
||||
|
||||
@@ -33,8 +33,7 @@ import { gitOpsApiRef, Status } from '../../api';
|
||||
import { transformRunStatus } from '../ProfileCatalog';
|
||||
|
||||
const ClusterPage: FC<{}> = () => {
|
||||
const params = useParams<{ owner: string; repo: string }>();
|
||||
|
||||
const params = useParams() as { owner: string; repo: string };
|
||||
const [loginInfo] = useLocalStorage<{
|
||||
token: string;
|
||||
username: string;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/plugin-graphiql",
|
||||
"description": "Backstage plugin for browsing GraphQL APIs",
|
||||
"version": "0.1.1-alpha.7",
|
||||
"version": "0.1.1-alpha.8",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
@@ -32,8 +32,8 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core": "^0.1.1-alpha.7",
|
||||
"@backstage/theme": "^0.1.1-alpha.7",
|
||||
"@backstage/core": "^0.1.1-alpha.8",
|
||||
"@backstage/theme": "^0.1.1-alpha.8",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
@@ -44,18 +44,18 @@
|
||||
"react-use": "^14.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.7",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.7",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.7",
|
||||
"@backstage/cli": "^0.1.1-alpha.8",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.8",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.8",
|
||||
"@testing-library/jest-dom": "^5.7.0",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^10.2.4",
|
||||
"@types/codemirror": "^0.0.95",
|
||||
"@types/codemirror": "^0.0.96",
|
||||
"@types/jest": "^25.2.2",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/testing-library__jest-dom": "^5.0.4",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"react-router-dom": "^5.2.0"
|
||||
"react-router-dom": "6.0.0-alpha.5"
|
||||
},
|
||||
"files": [
|
||||
"dist/**/*.{js,d.ts}"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-home-page",
|
||||
"version": "0.1.1-alpha.7",
|
||||
"version": "0.1.1-alpha.8",
|
||||
"main": "dist/index.esm.js",
|
||||
"main:src": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
@@ -22,8 +22,8 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core": "^0.1.1-alpha.7",
|
||||
"@backstage/theme": "^0.1.1-alpha.7",
|
||||
"@backstage/core": "^0.1.1-alpha.8",
|
||||
"@backstage/theme": "^0.1.1-alpha.8",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
@@ -32,8 +32,8 @@
|
||||
"react-use": "^14.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.7",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.7",
|
||||
"@backstage/cli": "^0.1.1-alpha.8",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.8",
|
||||
"@testing-library/jest-dom": "^5.7.0",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^10.2.4",
|
||||
@@ -41,7 +41,7 @@
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/testing-library__jest-dom": "^5.0.4",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"react-router-dom": "^5.2.0"
|
||||
"react-router-dom": "6.0.0-alpha.5"
|
||||
},
|
||||
"files": [
|
||||
"dist/**/*.{js,d.ts}"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-identity-backend",
|
||||
"version": "0.1.1-alpha.7",
|
||||
"version": "0.1.1-alpha.8",
|
||||
"main": "dist",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -15,7 +15,7 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.1.1-alpha.7",
|
||||
"@backstage/backend-common": "^0.1.1-alpha.8",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.17.1",
|
||||
@@ -27,7 +27,7 @@
|
||||
"yn": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.7",
|
||||
"@backstage/cli": "^0.1.1-alpha.8",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"tsc-watch": "^4.2.3"
|
||||
},
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "@backstage/plugin-lighthouse",
|
||||
"version": "0.1.1-alpha.7",
|
||||
"version": "0.1.1-alpha.8",
|
||||
"main": "dist/index.esm.js",
|
||||
"main:src": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.esm.js",
|
||||
@@ -22,21 +22,21 @@
|
||||
"start": "backstage-cli plugin:serve"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core": "^0.1.1-alpha.7",
|
||||
"@backstage/theme": "^0.1.1-alpha.7",
|
||||
"@backstage/core": "^0.1.1-alpha.8",
|
||||
"@backstage/theme": "^0.1.1-alpha.8",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-markdown": "^4.3.1",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-router-dom": "6.0.0-alpha.5",
|
||||
"react-use": "^14.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.7",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.7",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.7",
|
||||
"@backstage/cli": "^0.1.1-alpha.8",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.8",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.8",
|
||||
"@testing-library/jest-dom": "^5.7.0",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^10.2.4",
|
||||
|
||||
@@ -16,13 +16,10 @@
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
const actual = jest.requireActual('react-router-dom');
|
||||
const mocks = {
|
||||
replace: jest.fn(),
|
||||
push: jest.fn(),
|
||||
};
|
||||
const mockNavigation = jest.fn();
|
||||
return {
|
||||
...actual,
|
||||
useHistory: jest.fn(() => mocks),
|
||||
useNavigate: jest.fn(() => mockNavigation),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -41,7 +38,7 @@ import AuditList from '.';
|
||||
|
||||
import * as data from '../../__fixtures__/website-list-response.json';
|
||||
|
||||
const { useHistory } = jest.requireMock('react-router-dom');
|
||||
const { useNavigate } = jest.requireMock('react-router-dom');
|
||||
const websiteListResponse = data as WebsiteListResponse;
|
||||
|
||||
describe('AuditList', () => {
|
||||
@@ -145,7 +142,8 @@ describe('AuditList', () => {
|
||||
);
|
||||
const element = await rendered.findByLabelText(/Go to page 1/);
|
||||
fireEvent.click(element);
|
||||
expect(useHistory().replace).toHaveBeenCalledWith(`/lighthouse?page=1`);
|
||||
|
||||
expect(useNavigate()).toHaveBeenCalledWith(`/lighthouse?page=1`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import React, { useState, useMemo, FC, ReactNode } from 'react';
|
||||
import { useLocalStorage, useAsync } from 'react-use';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Grid, Button } from '@material-ui/core';
|
||||
import Alert from '@material-ui/lab/Alert';
|
||||
import Pagination from '@material-ui/lab/Pagination';
|
||||
@@ -65,7 +65,7 @@ const AuditList: FC<{}> = () => {
|
||||
return 0;
|
||||
}, [value?.total, value?.limit]);
|
||||
|
||||
const history = useHistory();
|
||||
const navigate = useNavigate();
|
||||
|
||||
let content: ReactNode = null;
|
||||
if (value) {
|
||||
@@ -77,7 +77,7 @@ const AuditList: FC<{}> = () => {
|
||||
page={page}
|
||||
count={pageCount}
|
||||
onChange={(_event: Event, newPage: number) => {
|
||||
history.replace(`/lighthouse?page=${newPage}`);
|
||||
navigate(`/lighthouse?page=${newPage}`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -18,10 +18,10 @@ import { Link, useParams } from 'react-router-dom';
|
||||
import { useAsync } from 'react-use';
|
||||
import {
|
||||
makeStyles,
|
||||
Button,
|
||||
Grid,
|
||||
List,
|
||||
ListItem,
|
||||
Button,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
} from '@material-ui/core';
|
||||
@@ -68,7 +68,7 @@ const AuditLinkList: FC<AuditLinkListProps> = ({
|
||||
component="nav"
|
||||
aria-label="lighthouse audit history"
|
||||
>
|
||||
{audits.map((audit) => (
|
||||
{audits.map(audit => (
|
||||
<ListItem
|
||||
key={audit.id}
|
||||
selected={audit.id === selectedId}
|
||||
@@ -88,7 +88,7 @@ const AuditLinkList: FC<AuditLinkListProps> = ({
|
||||
|
||||
const AuditView: FC<{ audit?: Audit }> = ({ audit }: { audit?: Audit }) => {
|
||||
const classes = useStyles();
|
||||
const params = useParams<{ id: string }>();
|
||||
const params = useParams() as { id: string };
|
||||
const { url: lighthouseUrl } = useApi(lighthouseApiRef);
|
||||
|
||||
if (audit?.status === 'RUNNING') return <Progress />;
|
||||
@@ -114,7 +114,7 @@ const AuditView: FC<{ audit?: Audit }> = ({ audit }: { audit?: Audit }) => {
|
||||
|
||||
const ConnectedAuditView: FC<{}> = () => {
|
||||
const lighthouseApi = useApi(lighthouseApiRef);
|
||||
const params = useParams<{ id: string }>();
|
||||
const params = useParams() as { id: string };
|
||||
const classes = useStyles();
|
||||
|
||||
const { loading, error, value: nextValue } = useAsync<Website>(
|
||||
@@ -136,7 +136,7 @@ const ConnectedAuditView: FC<{}> = () => {
|
||||
<AuditLinkList audits={value?.audits} selectedId={params.id} />
|
||||
</Grid>
|
||||
<Grid item xs={9}>
|
||||
<AuditView audit={value?.audits.find((a) => a.id === params.id)} />
|
||||
<AuditView audit={value?.audits.find(a => a.id === params.id)} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
|
||||
@@ -16,13 +16,10 @@
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
const actual = jest.requireActual('react-router-dom');
|
||||
const mocks = {
|
||||
replace: jest.fn(),
|
||||
push: jest.fn(),
|
||||
};
|
||||
const mockNavigate = jest.fn();
|
||||
return {
|
||||
...actual,
|
||||
useHistory: jest.fn(() => mocks),
|
||||
useNavigate: jest.fn(() => mockNavigate),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -41,7 +38,7 @@ import { lighthouseApiRef, LighthouseRestApi, Audit } from '../../api';
|
||||
import CreateAudit from '.';
|
||||
import * as data from '../../__fixtures__/create-audit-response.json';
|
||||
|
||||
const { useHistory }: { useHistory: jest.Mock } = jest.requireMock(
|
||||
const { useNavigate }: { useNavigate: jest.Mock } = jest.requireMock(
|
||||
'react-router-dom',
|
||||
);
|
||||
const createAuditResponse = data as Audit;
|
||||
@@ -115,7 +112,7 @@ describe('CreateAudit', () => {
|
||||
|
||||
describe('when the audit is successfully created', () => {
|
||||
it('triggers a location change to the table', async () => {
|
||||
useHistory().push.mockClear();
|
||||
useNavigate.mockClear();
|
||||
mockFetch.mockResponseOnce(JSON.stringify(createAuditResponse));
|
||||
|
||||
const rendered = render(
|
||||
@@ -140,7 +137,7 @@ describe('CreateAudit', () => {
|
||||
|
||||
await wait(() => expect(rendered.getByLabelText(/URL/)).toBeEnabled());
|
||||
|
||||
expect(useHistory().push).toHaveBeenCalledWith('/lighthouse');
|
||||
expect(useNavigate()).toHaveBeenCalledWith('/lighthouse');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React, { useState, useCallback, FC } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
makeStyles,
|
||||
Grid,
|
||||
@@ -40,7 +40,7 @@ import { lighthouseApiRef } from '../../api';
|
||||
import { useQuery } from '../../utils';
|
||||
import LighthouseSupportButton from '../SupportButton';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
const useStyles = makeStyles(theme => ({
|
||||
input: {
|
||||
minWidth: 300,
|
||||
},
|
||||
@@ -58,7 +58,7 @@ const CreateAudit: FC<{}> = () => {
|
||||
const lighthouseApi = useApi(lighthouseApiRef);
|
||||
const classes = useStyles();
|
||||
const query = useQuery();
|
||||
const history = useHistory();
|
||||
const navigate = useNavigate();
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [url, setUrl] = useState<string>(query.get('url') || '');
|
||||
const [emulatedFormFactor, setEmulatedFormFactor] = useState('mobile');
|
||||
@@ -78,7 +78,7 @@ const CreateAudit: FC<{}> = () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
history.push('/lighthouse');
|
||||
navigate('/lighthouse');
|
||||
} catch (err) {
|
||||
errorApi.post(err);
|
||||
} finally {
|
||||
@@ -90,7 +90,7 @@ const CreateAudit: FC<{}> = () => {
|
||||
lighthouseApi,
|
||||
setSubmitting,
|
||||
errorApi,
|
||||
history,
|
||||
navigate,
|
||||
]);
|
||||
|
||||
return (
|
||||
@@ -113,7 +113,7 @@ const CreateAudit: FC<{}> = () => {
|
||||
<Grid item xs={12} sm={6}>
|
||||
<InfoCard>
|
||||
<form
|
||||
onSubmit={(ev) => {
|
||||
onSubmit={ev => {
|
||||
ev.preventDefault();
|
||||
triggerAudit();
|
||||
}}
|
||||
@@ -128,7 +128,7 @@ const CreateAudit: FC<{}> = () => {
|
||||
helperText="The target URL for Lighthouse to use."
|
||||
required
|
||||
disabled={submitting}
|
||||
onChange={(ev) => setUrl(ev.target.value)}
|
||||
onChange={ev => setUrl(ev.target.value)}
|
||||
value={url}
|
||||
inputProps={{ 'aria-label': 'URL' }}
|
||||
/>
|
||||
@@ -142,7 +142,7 @@ const CreateAudit: FC<{}> = () => {
|
||||
select
|
||||
required
|
||||
disabled={submitting}
|
||||
onChange={(ev) => setEmulatedFormFactor(ev.target.value)}
|
||||
onChange={ev => setEmulatedFormFactor(ev.target.value)}
|
||||
value={emulatedFormFactor}
|
||||
inputProps={{ 'aria-label': 'Emulated form factor' }}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-register-component",
|
||||
"version": "0.1.1-alpha.7",
|
||||
"version": "0.1.1-alpha.8",
|
||||
"main": "dist/index.esm.js",
|
||||
"main:src": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
@@ -22,23 +22,23 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.7",
|
||||
"@backstage/core": "^0.1.1-alpha.7",
|
||||
"@backstage/plugin-catalog": "^0.1.1-alpha.7",
|
||||
"@backstage/theme": "^0.1.1-alpha.7",
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.8",
|
||||
"@backstage/core": "^0.1.1-alpha.8",
|
||||
"@backstage/plugin-catalog": "^0.1.1-alpha.8",
|
||||
"@backstage/theme": "^0.1.1-alpha.8",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-hook-form": "^5.7.2",
|
||||
"react-router": "^5.2.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-router": "^6.0.0-alpha.5",
|
||||
"react-router-dom": "^6.0.0-alpha.5",
|
||||
"react-use": "^14.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.7",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.7",
|
||||
"@backstage/cli": "^0.1.1-alpha.8",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.8",
|
||||
"@testing-library/jest-dom": "^5.7.0",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^10.2.4",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-scaffolder-backend",
|
||||
"version": "0.1.1-alpha.7",
|
||||
"version": "0.1.1-alpha.8",
|
||||
"main": "dist",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -14,7 +14,7 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.1.1-alpha.7",
|
||||
"@backstage/backend-common": "^0.1.1-alpha.8",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"dockerode": "^3.2.0",
|
||||
@@ -27,7 +27,7 @@
|
||||
"winston": "^3.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.7",
|
||||
"@backstage/cli": "^0.1.1-alpha.8",
|
||||
"@types/fs-extra": "^9.0.1",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"supertest": "^4.0.2"
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { StorageBase, createStorage } from '.';
|
||||
import winston from 'winston';
|
||||
import * as winston from 'winston';
|
||||
|
||||
describe('Storage Interface Test', () => {
|
||||
const mockStore = new (class MockStorage implements StorageBase {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-scaffolder",
|
||||
"version": "0.1.1-alpha.7",
|
||||
"version": "0.1.1-alpha.8",
|
||||
"main": "dist/index.esm.js",
|
||||
"main:src": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
@@ -22,19 +22,19 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core": "^0.1.1-alpha.7",
|
||||
"@backstage/theme": "^0.1.1-alpha.7",
|
||||
"@backstage/core": "^0.1.1-alpha.8",
|
||||
"@backstage/theme": "^0.1.1-alpha.8",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-router-dom": "6.0.0-alpha.5",
|
||||
"react-use": "^14.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.7",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.7",
|
||||
"@backstage/cli": "^0.1.1-alpha.8",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.8",
|
||||
"@testing-library/jest-dom": "^5.7.0",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^10.2.4",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-sentry-backend",
|
||||
"version": "0.1.1-alpha.7",
|
||||
"version": "0.1.1-alpha.8",
|
||||
"main": "dist",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -15,7 +15,7 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.1.1-alpha.7",
|
||||
"@backstage/backend-common": "^0.1.1-alpha.8",
|
||||
"axios": "^0.19.2",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
@@ -28,7 +28,7 @@
|
||||
"yn": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.7",
|
||||
"@backstage/cli": "^0.1.1-alpha.8",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"tsc-watch": "^4.2.3"
|
||||
},
|
||||
|
||||
@@ -13,4 +13,5 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './service/router';
|
||||
|
||||
@@ -22,13 +22,19 @@ export async function createRouter(logger: Logger): Promise<express.Router> {
|
||||
const router = Router();
|
||||
const SENTRY_TOKEN = process.env.SENTRY_TOKEN;
|
||||
if (!SENTRY_TOKEN) {
|
||||
throw new Error(
|
||||
'Sentry token must be provided in SENTRY_TOKEN environment variable to start the API.',
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
throw new Error(
|
||||
'Sentry token must be provided in SENTRY_TOKEN environment variable to start the API.',
|
||||
);
|
||||
}
|
||||
logger.warn(
|
||||
'Failed to initialize Sentry backend, set SENTRY_TOKEN environment variable to start the API.',
|
||||
);
|
||||
}
|
||||
const sentryForwarder = getSentryApiForwarder(SENTRY_TOKEN, logger);
|
||||
} else {
|
||||
const sentryForwarder = getSentryApiForwarder(SENTRY_TOKEN, logger);
|
||||
|
||||
router.use(sentryForwarder);
|
||||
router.use(sentryForwarder);
|
||||
}
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-sentry",
|
||||
"version": "0.1.1-alpha.7",
|
||||
"version": "0.1.1-alpha.8",
|
||||
"main": "dist/index.esm.js",
|
||||
"main:src": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
@@ -22,8 +22,8 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core": "^0.1.1-alpha.7",
|
||||
"@backstage/theme": "^0.1.1-alpha.7",
|
||||
"@backstage/core": "^0.1.1-alpha.8",
|
||||
"@backstage/theme": "^0.1.1-alpha.8",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
@@ -34,8 +34,8 @@
|
||||
"timeago.js": "^4.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.7",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.7",
|
||||
"@backstage/cli": "^0.1.1-alpha.8",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.8",
|
||||
"@testing-library/jest-dom": "^5.7.0",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^10.2.4",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-tech-radar",
|
||||
"version": "0.1.1-alpha.7",
|
||||
"version": "0.1.1-alpha.8",
|
||||
"main": "dist/index.esm.js",
|
||||
"main:src": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
@@ -22,9 +22,9 @@
|
||||
"start": "backstage-cli plugin:serve"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core": "^0.1.1-alpha.7",
|
||||
"@backstage/core": "^0.1.1-alpha.8",
|
||||
"@backstage/test-utils-core": "^0.1.1-alpha.7",
|
||||
"@backstage/theme": "^0.1.1-alpha.7",
|
||||
"@backstage/theme": "^0.1.1-alpha.8",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
@@ -37,8 +37,8 @@
|
||||
"react-use": "^14.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.7",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.7",
|
||||
"@backstage/cli": "^0.1.1-alpha.8",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.8",
|
||||
"@testing-library/jest-dom": "^5.7.0",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^10.2.4",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-welcome",
|
||||
"version": "0.1.1-alpha.7",
|
||||
"version": "0.1.1-alpha.8",
|
||||
"main": "dist/index.esm.js",
|
||||
"main:src": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
@@ -22,19 +22,19 @@
|
||||
"start": "backstage-cli plugin:serve"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core": "^0.1.1-alpha.7",
|
||||
"@backstage/theme": "^0.1.1-alpha.7",
|
||||
"@backstage/core": "^0.1.1-alpha.8",
|
||||
"@backstage/theme": "^0.1.1-alpha.8",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-router-dom": "6.0.0-alpha.5",
|
||||
"react-use": "^14.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.7",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.7",
|
||||
"@backstage/cli": "^0.1.1-alpha.8",
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.8",
|
||||
"@testing-library/jest-dom": "^5.7.0",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^10.2.4",
|
||||
|
||||
Reference in New Issue
Block a user