Merge branch 'master' of https://github.com/backstage/backstage into pr-draft

This commit is contained in:
Lykke Axlin
2021-10-12 15:23:12 +02:00
342 changed files with 4728 additions and 2321 deletions
+20 -1
View File
@@ -15,29 +15,34 @@
*/
import { ApiEntity, Entity } from '@backstage/catalog-model';
import { Content, Header, Page } from '@backstage/core-components';
import { createDevApp } from '@backstage/dev-utils';
import { CatalogEntityPage } from '@backstage/plugin-catalog';
import { catalogApiRef, EntityProvider } from '@backstage/plugin-catalog-react';
import React from 'react';
import {
apiDocsConfigRef,
apiDocsPlugin,
ApiExplorerPage,
defaultDefinitionWidgets,
EntityApiDefinitionCard,
} from '../src';
import asyncapiApiEntity from './asyncapi-example-api.yaml';
import graphqlApiEntity from './graphql-example-api.yaml';
import invalidLanguageApiEntity from './invalid-language-example-api.yaml';
import openapiApiEntity from './openapi-example-api.yaml';
import otherApiEntity from './other-example-api.yaml';
import { Content, Header, Page } from '@backstage/core-components';
const mockEntities = [
openapiApiEntity,
asyncapiApiEntity,
graphqlApiEntity,
invalidLanguageApiEntity,
otherApiEntity,
] as unknown as Entity[];
createDevApp()
.registerPlugin(apiDocsPlugin)
.registerApi({
api: catalogApiRef,
deps: {},
@@ -65,6 +70,7 @@ createDevApp()
};
},
})
.addPage({ element: <CatalogEntityPage /> })
.addPage({ title: 'API Explorer', element: <ApiExplorerPage /> })
.addPage({
title: 'OpenAPI',
@@ -105,6 +111,19 @@ createDevApp()
</Page>
),
})
.addPage({
title: 'Invalid Language',
element: (
<Page themeId="home">
<Header title="Invalid Language" />
<Content>
<EntityProvider entity={invalidLanguageApiEntity as any as Entity}>
<EntityApiDefinitionCard />
</EntityProvider>
</Content>
</Page>
),
})
.addPage({
title: 'Other',
element: (
@@ -0,0 +1,48 @@
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
name: hello-world
description: Hello World example for gRPC
spec:
type: my-invalid-language
lifecycle: deprecated
owner: team-c
definition: |
// Copyright 2015 gRPC authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";
option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";
option objc_class_prefix = "HLW";
package helloworld;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
+1 -2
View File
@@ -37,7 +37,6 @@
"@backstage/plugin-catalog": "^0.7.0",
"@backstage/plugin-catalog-react": "^0.5.2",
"@backstage/theme": "^0.2.10",
"@material-icons/font": "^1.0.2",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.57",
@@ -50,7 +49,7 @@
"react-router": "6.0.0-beta.0",
"react-router-dom": "6.0.0-beta.0",
"react-use": "^17.2.4",
"swagger-ui-react": "^3.37.2"
"swagger-ui-react": "^4.0.0-rc.3"
},
"devDependencies": {
"@backstage/cli": "^0.7.15",
+24 -6
View File
@@ -3,6 +3,8 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
/// <reference types="node" />
import { CatalogApi } from '@backstage/catalog-client';
import { Config } from '@backstage/config';
import { Entity } from '@backstage/catalog-model';
@@ -32,7 +34,6 @@ export type AuthProviderFactoryOptions = {
tokenIssuer: TokenIssuer;
discovery: PluginEndpointDiscovery;
catalogApi: CatalogApi;
identityResolver?: ExperimentalIdentityResolver;
};
// Warning: (tsdoc-escape-greater-than) The ">" character should be escaped using a backslash to avoid confusion with an HTML tag
@@ -74,6 +75,16 @@ export type AuthResponse<ProviderInfo> = {
backstageIdentity?: BackstageIdentity;
};
// Warning: (ae-missing-release-tag) "AwsAlbProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type AwsAlbProviderOptions = {
authHandler?: AuthHandler<AwsAlbResult>;
signIn: {
resolver: SignInResolver<AwsAlbResult>;
};
};
// Warning: (ae-missing-release-tag) "BackstageIdentity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -135,6 +146,13 @@ export const bitbucketUserIdSignInResolver: SignInResolver<BitbucketOAuthResult>
// @public (undocumented)
export const bitbucketUsernameSignInResolver: SignInResolver<BitbucketOAuthResult>;
// Warning: (ae-missing-release-tag) "createAwsAlbProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const createAwsAlbProvider: (
options?: AwsAlbProviderOptions | undefined,
) => AuthProviderFactory;
// Warning: (ae-missing-release-tag) "createBitbucketProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -526,9 +544,9 @@ export type WebMessageResponse =
//
// src/identity/types.d.ts:25:5 - (ae-forgotten-export) The symbol "TokenParams" needs to be exported by the entry point index.d.ts
// src/identity/types.d.ts:31:9 - (ae-forgotten-export) The symbol "AnyJWK" needs to be exported by the entry point index.d.ts
// src/providers/bitbucket/provider.d.ts:61:5 - (ae-forgotten-export) The symbol "AuthHandler" needs to be exported by the entry point index.d.ts
// src/providers/bitbucket/provider.d.ts:69:9 - (ae-forgotten-export) The symbol "SignInResolver" needs to be exported by the entry point index.d.ts
// src/providers/types.d.ts:109:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts
// src/providers/types.d.ts:115:5 - (ae-forgotten-export) The symbol "ExperimentalIdentityResolver" needs to be exported by the entry point index.d.ts
// src/providers/types.d.ts:132:8 - (tsdoc-missing-deprecation-message) The @deprecated block must include a deprecation message, e.g. describing the recommended alternative
// src/providers/aws-alb/provider.d.ts:77:5 - (ae-forgotten-export) The symbol "AuthHandler" needs to be exported by the entry point index.d.ts
// src/providers/aws-alb/provider.d.ts:77:5 - (ae-forgotten-export) The symbol "AwsAlbResult" needs to be exported by the entry point index.d.ts
// src/providers/aws-alb/provider.d.ts:85:9 - (ae-forgotten-export) The symbol "SignInResolver" needs to be exported by the entry point index.d.ts
// src/providers/types.d.ts:99:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts
// src/providers/types.d.ts:121:8 - (tsdoc-missing-deprecation-message) The @deprecated block must include a deprecation message, e.g. describing the recommended alternative
```
+2
View File
@@ -50,6 +50,8 @@ export interface Config {
issuer: string;
cert: string;
privateKey?: string;
authnContext?: string[];
identifierFormat?: string;
decryptionPvk?: string;
signatureAlgorithm?: 'sha256' | 'sha512';
digestAlgorithm?: string;
@@ -17,8 +17,14 @@ import { getVoidLogger } from '@backstage/backend-common';
import express from 'express';
import { JWT } from 'jose';
import { AwsAlbAuthProvider } from './provider';
import { AuthResponse } from '../types';
import {
ALB_ACCESSTOKEN_HEADER,
ALB_JWT_HEADER,
AwsAlbAuthProvider,
} from './provider';
import { TokenIssuer } from '../../identity/types';
import { CatalogIdentityClient } from '../../lib/catalog';
import { makeProfileInfo } from '../../lib/passport';
const jwtMock = JWT as jest.Mocked<any>;
@@ -29,9 +35,21 @@ yOlxJ2VW88mLAQGJ7HPAvOdylxZsItMnzCuqNzZvie8m/NJsOjhDncVkrw==
-----END PUBLIC KEY-----
`;
};
const mockJwt =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IktFWV9JRCIsImlzcyI6IklTU1VFUl9VUkwifQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlVzZXIgTmFtZSIsImlhdCI6MTUxNjIzOTAyMn0.uMCSBGhij1xn5pnot8XgD-huQuTIBOFGs6kkW_p_X94';
const mockAccessToken = 'ACCESS_TOKEN';
const mockClaims = {
sub: '1234567890',
name: 'User Name',
family_name: 'Name',
given_name: 'User',
picture: 'PICTURE_URL',
email: 'user.name@email.test',
exp: 1632833763,
iss: 'ISSUER_URL',
};
jest.mock('jose');
jest.mock('cross-fetch', () => ({
__esModule: true,
default: async () => {
@@ -43,53 +61,48 @@ jest.mock('cross-fetch', () => ({
},
}));
const identityResolutionCallbackMock = async (): Promise<AuthResponse<any>> => {
return {
backstageIdentity: {
id: 'foo',
idToken: '',
},
profile: {
displayName: 'Foo Bar',
},
providerInfo: {},
};
};
const identityResolutionCallbackRejectedMock = async (): Promise<
AuthResponse<any>
> => {
throw new Error('failed');
};
beforeEach(() => {
jest.clearAllMocks();
});
describe('AwsALBAuthProvider', () => {
const catalogApi = {
addLocation: jest.fn(),
removeLocationById: jest.fn(),
getEntities: jest.fn(),
getOriginLocationByEntity: jest.fn(),
getLocationByEntity: jest.fn(),
getLocationById: jest.fn(),
removeEntityByUid: jest.fn(),
getEntityByName: jest.fn(),
refreshEntity: jest.fn(),
getEntityAncestors: jest.fn(),
describe('AwsAlbAuthProvider', () => {
const tokenIssuer: TokenIssuer = {
listPublicKeys: jest.fn(),
async issueToken(params) {
return `token-for-${params.claims.sub}`;
},
};
const catalogIdentityClient: CatalogIdentityClient = {
findUser: jest.fn(),
} as unknown as CatalogIdentityClient;
const mockRequest = {
header: jest.fn(() => {
return 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImZvbyIsImlzcyI6ImZvbyJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.T2BNS4G-6RoiFnXc8Q8TiwdWzTpNitY8jcsGM3N3-Yo';
}),
} as unknown as express.Request;
const mockRequestWithoutJwt = {
header: jest.fn(() => {
header: jest.fn(name => {
if (name === ALB_JWT_HEADER) {
return mockJwt;
} else if (name === ALB_ACCESSTOKEN_HEADER) {
return mockAccessToken;
}
return undefined;
}),
} as unknown as express.Request;
const mockRequestWithoutJwt = {
header: jest.fn(name => {
if (name === ALB_ACCESSTOKEN_HEADER) {
return mockAccessToken;
}
return undefined;
}),
} as unknown as express.Request;
const mockRequestWithoutAccessToken = {
header: jest.fn(name => {
if (name === ALB_JWT_HEADER) {
return mockJwt;
}
return undefined;
}),
} as unknown as express.Request;
const mockResponse = {
end: jest.fn(),
header: () => jest.fn(),
@@ -97,38 +110,78 @@ describe('AwsALBAuthProvider', () => {
status: jest.fn(),
} as unknown as express.Response;
describe('should transform to type OAuthResponse', () => {
describe('should transform to type AwsAlbResponse', () => {
it('when JWT is valid and identity is resolved successfully', async () => {
const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, {
region: 'us-west-2',
identityResolutionCallback: identityResolutionCallbackMock,
issuer: 'foo',
const provider = new AwsAlbAuthProvider({
region: 'eu-west-1',
issuer: 'ISSUER_URL',
logger: getVoidLogger(),
catalogIdentityClient,
tokenIssuer,
authHandler: async ({ fullProfile }) => ({
profile: makeProfileInfo(fullProfile),
}),
signInResolver: async () => {
return { id: 'user.name', token: 'TOKEN' };
},
});
jwtMock.verify.mockImplementationOnce(() => ({
sub: 'foo',
}));
jwtMock.verify.mockReturnValueOnce(mockClaims);
await provider.refresh(mockRequest, mockResponse);
expect(mockResponse.json).toHaveBeenCalledWith({
backstageIdentity: {
id: 'foo',
idToken: '',
id: 'user.name',
token: 'TOKEN',
},
profile: {
displayName: 'Foo Bar',
displayName: 'User Name',
email: 'user.name@email.test',
picture: 'PICTURE_URL',
},
providerInfo: {
accessToken: mockAccessToken,
expiresInSeconds: mockClaims.exp,
},
providerInfo: {},
});
});
});
describe('should fail when', () => {
it('Access token is missing', async () => {
const provider = new AwsAlbAuthProvider({
region: 'eu-west-1',
issuer: 'ISSUER_URL',
logger: getVoidLogger(),
catalogIdentityClient,
tokenIssuer,
authHandler: async ({ fullProfile }) => ({
profile: makeProfileInfo(fullProfile),
}),
signInResolver: async () => {
return { id: 'user.name', token: 'TOKEN' };
},
});
await provider.refresh(mockRequestWithoutAccessToken, mockResponse);
expect(mockResponse.status).toHaveBeenCalledWith(401);
});
it('JWT is missing', async () => {
const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, {
region: 'us-west-2',
identityResolutionCallback: identityResolutionCallbackMock,
issuer: 'foo',
const provider = new AwsAlbAuthProvider({
region: 'eu-west-1',
issuer: 'ISSUER_URL',
logger: getVoidLogger(),
catalogIdentityClient,
tokenIssuer,
authHandler: async ({ fullProfile }) => ({
profile: makeProfileInfo(fullProfile),
}),
signInResolver: async () => {
return { id: 'user.name', token: 'TOKEN' };
},
});
await provider.refresh(mockRequestWithoutJwt, mockResponse);
@@ -137,10 +190,18 @@ describe('AwsALBAuthProvider', () => {
});
it('JWT is invalid', async () => {
const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, {
region: 'us-west-2',
identityResolutionCallback: identityResolutionCallbackMock,
issuer: 'foo',
const provider = new AwsAlbAuthProvider({
region: 'eu-west-1',
issuer: 'ISSUER_URL',
logger: getVoidLogger(),
catalogIdentityClient,
tokenIssuer,
authHandler: async ({ fullProfile }) => ({
profile: makeProfileInfo(fullProfile),
}),
signInResolver: async () => {
return { id: 'user.name', token: 'TOKEN' };
},
});
jwtMock.verify.mockImplementationOnce(() => {
@@ -152,11 +213,19 @@ describe('AwsALBAuthProvider', () => {
expect(mockResponse.status).toHaveBeenCalledWith(401);
});
it('issuer is invalid', async () => {
const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, {
region: 'us-west-2',
identityResolutionCallback: identityResolutionCallbackMock,
issuer: 'foobar',
it('issuer is missing', async () => {
const provider = new AwsAlbAuthProvider({
region: 'eu-west-1',
issuer: 'ISSUER_URL',
logger: getVoidLogger(),
catalogIdentityClient,
tokenIssuer,
authHandler: async ({ fullProfile }) => ({
profile: makeProfileInfo(fullProfile),
}),
signInResolver: async () => {
return { id: 'user.name', token: 'TOKEN' };
},
});
jwtMock.verify.mockReturnValueOnce({});
@@ -165,14 +234,68 @@ describe('AwsALBAuthProvider', () => {
expect(mockResponse.status).toHaveBeenCalledWith(401);
});
it('identity resolution callback rejects', async () => {
const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, {
region: 'us-west-2',
identityResolutionCallback: identityResolutionCallbackRejectedMock,
issuer: 'foo',
it('issuer is invalid', async () => {
const provider = new AwsAlbAuthProvider({
region: 'eu-west-1',
issuer: 'ISSUER_URL',
logger: getVoidLogger(),
catalogIdentityClient,
tokenIssuer,
authHandler: async ({ fullProfile }) => ({
profile: makeProfileInfo(fullProfile),
}),
signInResolver: async () => {
return { id: 'user.name', token: 'TOKEN' };
},
});
jwtMock.verify.mockReturnValueOnce({});
jwtMock.verify.mockReturnValueOnce({
iss: 'INVALID_ISSUE_URL',
});
await provider.refresh(mockRequest, mockResponse);
expect(mockResponse.status).toHaveBeenCalledWith(401);
});
it('SignInResolver rejects', async () => {
const provider = new AwsAlbAuthProvider({
region: 'eu-west-1',
issuer: 'ISSUER_URL',
logger: getVoidLogger(),
catalogIdentityClient,
tokenIssuer,
authHandler: async ({ fullProfile }) => ({
profile: makeProfileInfo(fullProfile),
}),
signInResolver: async () => {
throw new Error();
},
});
jwtMock.verify.mockReturnValueOnce(mockClaims);
await provider.refresh(mockRequest, mockResponse);
expect(mockResponse.status).toHaveBeenCalledWith(401);
expect(mockResponse.end).toHaveBeenCalledTimes(1);
});
it('AuthHandler rejects', async () => {
const provider = new AwsAlbAuthProvider({
region: 'eu-west-1',
issuer: 'ISSUER_URL',
logger: getVoidLogger(),
catalogIdentityClient,
tokenIssuer,
authHandler: async () => {
throw new Error();
},
signInResolver: async () => {
return { id: 'user.name', token: 'TOKEN' };
},
});
jwtMock.verify.mockReturnValueOnce(mockClaims);
await provider.refresh(mockRequest, mockResponse);
@@ -14,9 +14,11 @@
* limitations under the License.
*/
import {
AuthProviderFactoryOptions,
AuthHandler,
AuthProviderFactory,
AuthProviderRouteHandlers,
ExperimentalIdentityResolver,
AuthResponse,
SignInResolver,
} from '../types';
import express from 'express';
import fetch from 'cross-fetch';
@@ -25,66 +27,101 @@ import { KeyObject } from 'crypto';
import { Logger } from 'winston';
import NodeCache from 'node-cache';
import { JWT } from 'jose';
import { CatalogApi } from '@backstage/catalog-client';
import { TokenIssuer } from '../../identity/types';
import { CatalogIdentityClient } from '../../lib/catalog';
import { Profile as PassportProfile } from 'passport';
import { makeProfileInfo } from '../../lib/passport';
import { AuthenticationError } from '@backstage/errors';
const ALB_JWT_HEADER = 'x-amzn-oidc-data';
/**
* A callback function that receives a verified JWT and returns a UserEntity
* @param {payload} The verified JWT payload
*/
type AwsAlbAuthProviderOptions = {
export const ALB_JWT_HEADER = 'x-amzn-oidc-data';
export const ALB_ACCESSTOKEN_HEADER = 'x-amzn-oidc-accesstoken';
type Options = {
region: string;
issuer?: string;
identityResolutionCallback: ExperimentalIdentityResolver;
logger: Logger;
authHandler: AuthHandler<AwsAlbResult>;
signInResolver: SignInResolver<AwsAlbResult>;
tokenIssuer: TokenIssuer;
catalogIdentityClient: CatalogIdentityClient;
};
export const getJWTHeaders = (input: string) => {
export const getJWTHeaders = (input: string): AwsAlbHeaders => {
const encoded = input.split('.')[0];
return JSON.parse(Buffer.from(encoded, 'base64').toString('utf8'));
};
export class AwsAlbAuthProvider implements AuthProviderRouteHandlers {
private logger: Logger;
private readonly catalogClient: CatalogApi;
private options: AwsAlbAuthProviderOptions;
private readonly keyCache: NodeCache;
export type AwsAlbHeaders = {
alg: string;
kid: string;
signer: string;
iss: string;
client: string;
exp: number;
};
constructor(
logger: Logger,
catalogClient: CatalogApi,
options: AwsAlbAuthProviderOptions,
) {
this.logger = logger;
this.catalogClient = catalogClient;
this.options = options;
export type AwsAlbClaims = {
sub: string;
name: string;
family_name: string;
given_name: string;
picture: string;
email: string;
exp: number;
iss: string;
};
export type AwsAlbResult = {
fullProfile: PassportProfile;
expiresInSeconds?: number;
accessToken: string;
};
export type AwsAlbProviderInfo = {
/**
* An access token issued for the signed in user.
*/
accessToken: string;
/**
* Expiry of the access token in seconds.
*/
expiresInSeconds?: number;
};
export type AwsAlbResponse = AuthResponse<AwsAlbProviderInfo>;
export class AwsAlbAuthProvider implements AuthProviderRouteHandlers {
private readonly region: string;
private readonly issuer?: string;
private readonly tokenIssuer: TokenIssuer;
private readonly catalogIdentityClient: CatalogIdentityClient;
private readonly logger: Logger;
private readonly keyCache: NodeCache;
private readonly authHandler: AuthHandler<AwsAlbResult>;
private readonly signInResolver: SignInResolver<AwsAlbResult>;
constructor(options: Options) {
this.region = options.region;
this.issuer = options.issuer;
this.authHandler = options.authHandler;
this.signInResolver = options.signInResolver;
this.tokenIssuer = options.tokenIssuer;
this.catalogIdentityClient = options.catalogIdentityClient;
this.logger = options.logger;
this.keyCache = new NodeCache({ stdTTL: 3600 });
}
frameHandler(): Promise<void> {
return Promise.resolve(undefined);
}
async refresh(req: express.Request, res: express.Response): Promise<void> {
const jwt = req.header(ALB_JWT_HEADER);
if (jwt !== undefined) {
try {
const headers = getJWTHeaders(jwt);
const key = await this.getKey(headers.kid);
const payload = JWT.verify(jwt, key);
if (this.options.issuer && headers.iss !== this.options.issuer) {
throw new Error('issuer mismatch on JWT');
}
const resolvedEntity = await this.options.identityResolutionCallback(
payload,
this.catalogClient,
);
res.json(resolvedEntity);
} catch (e) {
this.logger.error('exception occurred during JWT processing', e);
res.status(401);
res.end();
}
} else {
try {
const result = await this.getResult(req);
const response = await this.handleResult(result);
res.json(response);
} catch (e) {
this.logger.error('Exception occurred during AWS ALB token refresh', e);
res.status(401);
res.end();
}
@@ -94,13 +131,85 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers {
return Promise.resolve(undefined);
}
private async getResult(req: express.Request): Promise<AwsAlbResult> {
const jwt = req.header(ALB_JWT_HEADER);
const accessToken = req.header(ALB_ACCESSTOKEN_HEADER);
if (jwt === undefined) {
throw new AuthenticationError(
`Missing ALB OIDC header: ${ALB_JWT_HEADER}`,
);
}
if (accessToken === undefined) {
throw new AuthenticationError(
`Missing ALB OIDC header: ${ALB_ACCESSTOKEN_HEADER}`,
);
}
try {
const headers = getJWTHeaders(jwt);
const key = await this.getKey(headers.kid);
const claims = JWT.verify(jwt, key) as AwsAlbClaims;
if (this.issuer && claims.iss !== this.issuer) {
throw new AuthenticationError('Issuer mismatch on JWT token');
}
const fullProfile: PassportProfile = {
provider: 'unknown',
id: claims.sub,
displayName: claims.name,
username: claims.email.split('@')[0].toLowerCase(),
name: {
familyName: claims.family_name,
givenName: claims.given_name,
},
emails: [{ value: claims.email.toLowerCase() }],
photos: [{ value: claims.picture }],
};
return {
fullProfile,
expiresInSeconds: claims.exp,
accessToken,
};
} catch (e) {
throw new Error(`Exception occurred during JWT processing: ${e}`);
}
}
private async handleResult(result: AwsAlbResult): Promise<AwsAlbResponse> {
const { profile } = await this.authHandler(result);
const backstageIdentity = await this.signInResolver(
{
result,
profile,
},
{
tokenIssuer: this.tokenIssuer,
catalogIdentityClient: this.catalogIdentityClient,
logger: this.logger,
},
);
return {
providerInfo: {
accessToken: result.accessToken,
expiresInSeconds: result.expiresInSeconds,
},
backstageIdentity,
profile,
};
}
async getKey(keyId: string): Promise<KeyObject> {
const optionalCacheKey = this.keyCache.get<KeyObject>(keyId);
if (optionalCacheKey) {
return crypto.createPublicKey(optionalCacheKey);
}
const keyText: string = await fetch(
`https://public-keys.auth.elb.${this.options.region}.amazonaws.com/${keyId}`,
`https://public-keys.auth.elb.${this.region}.amazonaws.com/${keyId}`,
).then(response => response.text());
const keyValue = crypto.createPublicKey(keyText);
this.keyCache.set(keyId, keyValue.export({ format: 'pem', type: 'spki' }));
@@ -108,26 +217,58 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers {
}
}
export type AwsAlbProviderOptions = {};
export type AwsAlbProviderOptions = {
/**
* The profile transformation function used to verify and convert the auth response
* into the profile that will be presented to the user.
*/
authHandler?: AuthHandler<AwsAlbResult>;
export const createAwsAlbProvider = (_options?: AwsAlbProviderOptions) => {
return ({
logger,
catalogApi,
config,
identityResolver,
}: AuthProviderFactoryOptions) => {
const region = config.getString('region');
const issuer = config.getOptionalString('iss');
if (identityResolver !== undefined) {
return new AwsAlbAuthProvider(logger, catalogApi, {
region,
issuer,
identityResolutionCallback: identityResolver,
});
}
throw new Error(
'Identity resolver is required to use this authentication provider',
);
/**
* Configure sign-in for this provider, without it the provider can not be used to sign users in.
*/
signIn: {
/**
* Maps an auth result to a Backstage identity for the user.
*/
resolver: SignInResolver<AwsAlbResult>;
};
};
export const createAwsAlbProvider = (
options?: AwsAlbProviderOptions,
): AuthProviderFactory => {
return ({ config, tokenIssuer, catalogApi, logger }) => {
const region = config.getString('region');
const issuer = config.getOptionalString('iss');
if (options?.signIn.resolver === undefined) {
throw new Error(
'SignInResolver is required to use this authentication provider',
);
}
const catalogIdentityClient = new CatalogIdentityClient({
catalogApi,
tokenIssuer,
});
const authHandler: AuthHandler<AwsAlbResult> = options?.authHandler
? options.authHandler
: async ({ fullProfile }) => ({
profile: makeProfileInfo(fullProfile),
});
const signInResolver = options?.signIn.resolver;
return new AwsAlbAuthProvider({
region,
issuer,
signInResolver,
authHandler,
tokenIssuer,
catalogIdentityClient,
logger,
});
};
};
@@ -21,6 +21,7 @@ export * from './microsoft';
export * from './oauth2';
export * from './okta';
export * from './bitbucket';
export * from './aws-alb';
export { factories as defaultAuthProviderFactories } from './factories';
@@ -127,6 +127,8 @@ export const createSamlProvider = (
issuer: config.getString('issuer'),
cert: config.getString('cert'),
privateCert: config.getOptionalString('privateKey'),
authnContext: config.getOptionalStringArray('authnContext'),
identifierFormat: config.getOptionalString('identifierFormat'),
decryptionPvk: config.getOptionalString('decryptionPvk'),
signatureAlgorithm: config.getOptionalString('signatureAlgorithm') as
| SignatureAlgorithm
@@ -119,19 +119,6 @@ export interface AuthProviderRouteHandlers {
logout?(req: express.Request, res: express.Response): Promise<void>;
}
/**
* EXPERIMENTAL - this will almost certainly break in a future release.
*
* Used to resolve an identity from auth information in some auth providers.
*/
export type ExperimentalIdentityResolver = (
/**
* An object containing information specific to the auth provider.
*/
payload: object,
catalogApi: CatalogApi,
) => Promise<AuthResponse<any>>;
export type AuthProviderFactoryOptions = {
providerId: string;
globalConfig: AuthProviderConfig;
@@ -140,7 +127,6 @@ export type AuthProviderFactoryOptions = {
tokenIssuer: TokenIssuer;
discovery: PluginEndpointDiscovery;
catalogApi: CatalogApi;
identityResolver?: ExperimentalIdentityResolver;
};
export type AuthProviderFactory = (
+1 -1
View File
@@ -47,7 +47,7 @@ export function createRouter(options: RouterOptions): Promise<express.Router>;
export type RepoBuild = {
id?: number;
title: string;
link: string;
link?: string;
status?: BuildStatus;
result?: BuildResult;
queueTime?: Date;
@@ -94,4 +94,100 @@ describe('AzureDevOpsApi', () => {
expect(repoBuildFromBuild(inputBuild)).toEqual(outputRepoBuild);
});
});
describe('repoBuildFromBuild with undefined status', () => {
it('should return BuildStatus of None for status', () => {
const inputLinks: any = {
web: {
href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1',
},
};
const inputBuild: Build = {
id: 1,
buildNumber: 'Build-1',
status: undefined,
result: BuildResult.Succeeded,
queueTime: new Date('2020-09-12T06:10:23.9325232Z'),
sourceBranch: 'refs/heads/develop',
sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c',
definition: undefined,
_links: inputLinks,
};
const outputRepoBuild: RepoBuild = {
id: 1,
title: 'Build-1',
link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1',
status: BuildStatus.None,
result: BuildResult.Succeeded,
queueTime: new Date('2020-09-12T06:10:23.9325232Z'),
source: 'refs/heads/develop (f4f78b31)',
};
expect(repoBuildFromBuild(inputBuild)).toEqual(outputRepoBuild);
});
});
describe('repoBuildFromBuild with undefined result', () => {
it('should return BuildResult of None for result', () => {
const inputLinks: any = {
web: {
href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1',
},
};
const inputBuild: Build = {
id: 1,
buildNumber: 'Build-1',
status: BuildStatus.InProgress,
result: undefined,
queueTime: new Date('2020-09-12T06:10:23.9325232Z'),
sourceBranch: 'refs/heads/develop',
sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c',
definition: undefined,
_links: inputLinks,
};
const outputRepoBuild: RepoBuild = {
id: 1,
title: 'Build-1',
link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1',
status: BuildStatus.InProgress,
result: BuildResult.None,
queueTime: new Date('2020-09-12T06:10:23.9325232Z'),
source: 'refs/heads/develop (f4f78b31)',
};
expect(repoBuildFromBuild(inputBuild)).toEqual(outputRepoBuild);
});
});
describe('repoBuildFromBuild with undefined link', () => {
it('should return empty string for link', () => {
const inputBuild: Build = {
id: 1,
buildNumber: 'Build-1',
status: BuildStatus.InProgress,
result: undefined,
queueTime: new Date('2020-09-12T06:10:23.9325232Z'),
sourceBranch: 'refs/heads/develop',
sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c',
definition: undefined,
_links: undefined,
};
const outputRepoBuild: RepoBuild = {
id: 1,
title: 'Build-1',
link: '',
status: BuildStatus.InProgress,
result: BuildResult.None,
queueTime: new Date('2020-09-12T06:10:23.9325232Z'),
source: 'refs/heads/develop (f4f78b31)',
};
expect(repoBuildFromBuild(inputBuild)).toEqual(outputRepoBuild);
});
});
});
@@ -17,7 +17,11 @@
import { Logger } from 'winston';
import { WebApi } from 'azure-devops-node-api';
import { RepoBuild } from './types';
import { Build } from 'azure-devops-node-api/interfaces/BuildInterfaces';
import {
Build,
BuildResult,
BuildStatus,
} from 'azure-devops-node-api/interfaces/BuildInterfaces';
export class AzureDevOpsApi {
constructor(
@@ -97,9 +101,9 @@ export function repoBuildFromBuild(build: Build) {
title: [build.definition?.name, build.buildNumber]
.filter(Boolean)
.join(' - '),
link: build._links?.web.href,
status: build.status,
result: build.result,
link: build._links?.web.href ? build._links?.web.href : '',
status: build.status ? build.status : BuildStatus.None,
result: build.result ? build.result : BuildResult.None,
queueTime: build.queueTime,
source: `${build.sourceBranch} (${build.sourceVersion?.substr(0, 8)})`,
};
@@ -22,7 +22,7 @@ import {
export type RepoBuild = {
id?: number;
title: string;
link: string;
link?: string;
status?: BuildStatus;
result?: BuildResult;
queueTime?: Date;
+34 -34
View File
@@ -435,7 +435,7 @@ export class CodeOwnersProcessor implements CatalogProcessor {
// Warning: (ae-missing-release-tag) "CommonDatabase" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
// @public @deprecated
export class CommonDatabase implements Database {
constructor(database: Knex, logger: Logger_2);
// (undocumented)
@@ -499,7 +499,7 @@ export class CommonDatabase implements Database {
// Warning: (ae-missing-release-tag) "CreateDatabaseOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @public @deprecated (undocumented)
export type CreateDatabaseOptions = {
logger: Logger_2;
};
@@ -528,7 +528,7 @@ export function createRouter(options: RouterOptions): Promise<express.Router>;
// Warning: (ae-missing-release-tag) "Database" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
// @public @deprecated
export type Database = {
transaction<T>(fn: (tx: Transaction) => Promise<T>): Promise<T>;
addEntities(
@@ -596,7 +596,7 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog {
// Warning: (ae-missing-release-tag) "DatabaseLocationsCatalog" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @public @deprecated (undocumented)
export class DatabaseLocationsCatalog implements LocationsCatalog {
constructor(database: Database);
// (undocumented)
@@ -624,7 +624,7 @@ export class DatabaseLocationsCatalog implements LocationsCatalog {
// Warning: (ae-missing-release-tag) "DatabaseLocationUpdateLogEvent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @public @deprecated (undocumented)
export type DatabaseLocationUpdateLogEvent = {
id: string;
status: DatabaseLocationUpdateLogStatus;
@@ -646,7 +646,7 @@ export enum DatabaseLocationUpdateLogStatus {
// Warning: (ae-missing-release-tag) "DatabaseManager" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @public @deprecated (undocumented)
export class DatabaseManager {
// (undocumented)
static createDatabase(
@@ -665,7 +665,7 @@ export class DatabaseManager {
// Warning: (ae-missing-release-tag) "DbEntitiesRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @public @deprecated (undocumented)
export type DbEntitiesRequest = {
filter?: EntityFilter;
pagination?: EntityPagination;
@@ -673,7 +673,7 @@ export type DbEntitiesRequest = {
// Warning: (ae-missing-release-tag) "DbEntitiesResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @public @deprecated (undocumented)
export type DbEntitiesResponse = {
entities: DbEntityResponse[];
pageInfo: DbPageInfo;
@@ -681,7 +681,7 @@ export type DbEntitiesResponse = {
// Warning: (ae-missing-release-tag) "DbEntityRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @public @deprecated (undocumented)
export type DbEntityRequest = {
locationId?: string;
entity: Entity;
@@ -690,7 +690,7 @@ export type DbEntityRequest = {
// Warning: (ae-missing-release-tag) "DbEntityResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @public @deprecated (undocumented)
export type DbEntityResponse = {
locationId?: string;
entity: Entity;
@@ -698,7 +698,7 @@ export type DbEntityResponse = {
// Warning: (ae-missing-release-tag) "DbLocationsRow" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @public @deprecated (undocumented)
export type DbLocationsRow = {
id: string;
type: string;
@@ -707,7 +707,7 @@ export type DbLocationsRow = {
// Warning: (ae-missing-release-tag) "DbLocationsRowWithStatus" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @public @deprecated (undocumented)
export type DbLocationsRowWithStatus = DbLocationsRow & {
status: string | null;
timestamp: string | null;
@@ -716,7 +716,7 @@ export type DbLocationsRowWithStatus = DbLocationsRow & {
// Warning: (ae-missing-release-tag) "DbPageInfo" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @public @deprecated (undocumented)
export type DbPageInfo =
| {
hasNextPage: false;
@@ -812,7 +812,7 @@ export function durationText(startTimestamp: [number, number]): string;
export type EntitiesCatalog = {
entities(request?: EntitiesRequest): Promise<EntitiesResponse>;
removeEntityByUid(uid: string): Promise<void>;
batchAddOrUpdateEntities(
batchAddOrUpdateEntities?(
requests: EntityUpsertRequest[],
options?: {
locationId?: string;
@@ -943,7 +943,7 @@ export type EntityProviderMutation =
// Warning: (ae-missing-release-tag) "EntityUpsertRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @public @deprecated (undocumented)
export type EntityUpsertRequest = {
entity: Entity;
relations: EntityRelationSpec[];
@@ -951,7 +951,7 @@ export type EntityUpsertRequest = {
// Warning: (ae-missing-release-tag) "EntityUpsertResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @public @deprecated (undocumented)
export type EntityUpsertResponse = {
entityId: string;
entity?: Entity;
@@ -1157,7 +1157,7 @@ export class LocationReaders implements LocationReader {
// Warning: (ae-missing-release-tag) "LocationResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @public @deprecated (undocumented)
export type LocationResponse = {
data: Location_2;
currentStatus: LocationUpdateStatus;
@@ -1165,7 +1165,7 @@ export type LocationResponse = {
// Warning: (ae-missing-release-tag) "LocationsCatalog" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @public @deprecated (undocumented)
export type LocationsCatalog = {
addLocation(location: Location_2): Promise<Location_2>;
removeLocation(id: string): Promise<void>;
@@ -1220,7 +1220,7 @@ export interface LocationStore {
// Warning: (ae-missing-release-tag) "LocationUpdateLogEvent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @public @deprecated (undocumented)
export type LocationUpdateLogEvent = {
id: string;
status: 'fail' | 'success';
@@ -1232,7 +1232,7 @@ export type LocationUpdateLogEvent = {
// Warning: (ae-missing-release-tag) "LocationUpdateStatus" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @public @deprecated (undocumented)
export type LocationUpdateStatus = {
timestamp: string | null;
status: string | null;
@@ -1487,7 +1487,7 @@ export class StaticLocationProcessor implements StaticLocationProcessor {
// Warning: (ae-missing-release-tag) "Transaction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
// @public @deprecated
export type Transaction = {
rollback(): Promise<unknown>;
};
@@ -1512,20 +1512,20 @@ export class UrlReaderProcessor implements CatalogProcessor {
// Warnings were encountered during analysis:
//
// src/catalog/types.d.ts:52:8 - (tsdoc-param-tag-with-invalid-name) The @param block should be followed by a valid parameter name: The identifier cannot non-word characters
// src/catalog/types.d.ts:53:8 - (tsdoc-param-tag-with-invalid-name) The @param block should be followed by a valid parameter name: The identifier cannot non-word characters
// src/catalog/types.d.ts:54:8 - (tsdoc-param-tag-with-invalid-name) The @param block should be followed by a valid parameter name: The identifier cannot non-word characters
// src/database/types.d.ts:125:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/database/types.d.ts:131:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/database/types.d.ts:132:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/database/types.d.ts:146:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/database/types.d.ts:147:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/database/types.d.ts:148:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/database/types.d.ts:150:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/database/types.d.ts:163:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/database/types.d.ts:164:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/database/types.d.ts:165:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/catalog/types.d.ts:97:8 - (tsdoc-param-tag-with-invalid-name) The @param block should be followed by a valid parameter name: The identifier cannot non-word characters
// src/catalog/types.d.ts:98:8 - (tsdoc-param-tag-with-invalid-name) The @param block should be followed by a valid parameter name: The identifier cannot non-word characters
// src/catalog/types.d.ts:99:8 - (tsdoc-param-tag-with-invalid-name) The @param block should be followed by a valid parameter name: The identifier cannot non-word characters
// src/ingestion/processors/GithubMultiOrgReaderProcessor.d.ts:23:9 - (ae-forgotten-export) The symbol "GithubMultiOrgConfig" needs to be exported by the entry point index.d.ts
// src/ingestion/types.d.ts:8:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/legacy/database/types.d.ts:98:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/legacy/database/types.d.ts:104:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/legacy/database/types.d.ts:105:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/legacy/database/types.d.ts:119:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/legacy/database/types.d.ts:120:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/legacy/database/types.d.ts:121:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/legacy/database/types.d.ts:123:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/legacy/database/types.d.ts:136:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/legacy/database/types.d.ts:137:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/legacy/database/types.d.ts:138:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/legacy/ingestion/types.d.ts:19:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
```
+3 -5
View File
@@ -14,7 +14,6 @@
* limitations under the License.
*/
export { DatabaseLocationsCatalog } from './DatabaseLocationsCatalog';
export type {
EntitiesCatalog,
EntitiesRequest,
@@ -22,9 +21,8 @@ export type {
EntityAncestryResponse,
EntityUpsertRequest,
EntityUpsertResponse,
LocationResponse,
LocationsCatalog,
LocationUpdateLogEvent,
LocationUpdateStatus,
PageInfo,
EntitiesSearchFilter,
EntityFilter,
EntityPagination,
} from './types';
+50 -47
View File
@@ -14,12 +14,52 @@
* limitations under the License.
*/
import { Entity, EntityRelationSpec, Location } from '@backstage/catalog-model';
import { EntityFilter, EntityPagination } from '../database/types';
import { Entity, EntityRelationSpec } from '@backstage/catalog-model';
//
// Entities
//
/**
* A filter expression for entities.
*
* Any (at least one) of the outer sets must match, within which all of the
* individual filters must match.
*/
export type EntityFilter = {
anyOf: { allOf: EntitiesSearchFilter[] }[];
};
/**
* A pagination rule for entities.
*/
export type EntityPagination = {
limit?: number;
offset?: number;
after?: string;
};
/**
* Matches rows in the entities_search table.
*/
export type EntitiesSearchFilter = {
/**
* The key to match on.
*
* Matches are always case insensitive.
*/
key: string;
/**
* Match on plain equality of values.
*
* If undefined, this factor is not taken into account. Otherwise, match on
* values that are equal to any of the given array items. Matches are always
* case insensitive.
*/
matchValueIn?: string[];
/**
* Match on existence of key.
*/
matchValueExists?: boolean;
};
export type PageInfo =
| {
@@ -41,11 +81,13 @@ export type EntitiesResponse = {
pageInfo: PageInfo;
};
/** @deprecated This was part of the legacy catalog engine */
export type EntityUpsertRequest = {
entity: Entity;
relations: EntityRelationSpec[];
};
/** @deprecated This was part of the legacy catalog engine */
export type EntityUpsertResponse = {
entityId: string;
entity?: Entity;
@@ -79,12 +121,14 @@ export type EntitiesCatalog = {
/**
* Writes a number of entities efficiently to storage.
*
* @deprecated This method was part of the legacy catalog engine an will be removed.
*
* @param requests - The entities and their relations
* @param options.locationId - The location that they all belong to (default none)
* @param options.dryRun - Whether to throw away the results (default false)
* @param options.outputEntities - Whether to return the resulting entities (default false)
*/
batchAddOrUpdateEntities(
batchAddOrUpdateEntities?(
requests: EntityUpsertRequest[],
options?: {
locationId?: string;
@@ -100,44 +144,3 @@ export type EntitiesCatalog = {
*/
entityAncestry(entityRef: string): Promise<EntityAncestryResponse>;
};
//
// Locations
//
export type LocationUpdateStatus = {
timestamp: string | null;
status: string | null;
message: string | null;
};
export type LocationUpdateLogEvent = {
id: string;
status: 'fail' | 'success';
location_id: string;
entity_name: string;
created_at?: string;
message?: string;
};
export type LocationResponse = {
data: Location;
currentStatus: LocationUpdateStatus;
};
export type LocationsCatalog = {
addLocation(location: Location): Promise<Location>;
removeLocation(id: string): Promise<void>;
locations(): Promise<LocationResponse[]>;
location(id: string): Promise<LocationResponse>;
locationHistory(id: string): Promise<LocationUpdateLogEvent[]>;
logUpdateSuccess(
locationId: string,
entityName?: string | string[],
): Promise<void>;
logUpdateFailure(
locationId: string,
error?: Error,
entityName?: string,
): Promise<void>;
};
@@ -21,14 +21,14 @@ import { Knex } from 'knex';
import * as uuid from 'uuid';
import { Logger } from 'winston';
import { DateTime } from 'luxon';
import { DatabaseManager } from './DatabaseManager';
import { applyDatabaseMigrations } from './migrations';
import { DefaultProcessingDatabase } from './DefaultProcessingDatabase';
import {
DbRefreshStateReferencesRow,
DbRefreshStateRow,
DbRelationsRow,
} from './tables';
import { createRandomRefreshInterval } from '../refresh';
import { createRandomRefreshInterval } from '../processing/refresh';
import { timestampToDateTime } from './conversion';
import { generateStableHash } from './util';
@@ -43,7 +43,7 @@ describe('Default Processing Database', () => {
logger: Logger = defaultLogger,
) {
const knex = await databases.init(databaseId);
await DatabaseManager.createDatabase(knex);
await applyDatabaseMigrations(knex);
return {
knex,
db: new DefaultProcessingDatabase({
@@ -20,17 +20,8 @@ import { Knex } from 'knex';
import lodash from 'lodash';
import { v4 as uuid } from 'uuid';
import type { Logger } from 'winston';
import { Transaction } from '../../database';
import { DeferredEntity } from '../processing/types';
import { RefreshIntervalFunction } from '../refresh';
import { rethrowError, timestampToDateTime } from './conversion';
import { initDatabaseMetrics } from './metrics';
import {
DbRefreshStateReferencesRow,
DbRefreshStateRow,
DbRelationsRow,
} from './tables';
import {
Transaction,
GetProcessableEntitiesResult,
ProcessingDatabase,
RefreshStateItem,
@@ -41,6 +32,16 @@ import {
ListAncestorsResult,
UpdateEntityCacheOptions,
} from './types';
import { DeferredEntity } from '../processing/types';
import { RefreshIntervalFunction } from '../processing/refresh';
import { rethrowError, timestampToDateTime } from './conversion';
import { initDatabaseMetrics } from './metrics';
import {
DbRefreshStateReferencesRow,
DbRefreshStateRow,
DbRelationsRow,
} from './tables';
import { generateStableHash } from './util';
// The number of items that are sent per batch to the database layer, when
@@ -15,9 +15,8 @@
*/
import { Knex } from 'knex';
import { DbLocationsRow } from '../../database/types';
import { createGaugeMetric } from '../metrics';
import { DbRefreshStateRow, DbRelationsRow } from './tables';
import { createGaugeMetric } from '../util/metrics';
import { DbRefreshStateRow, DbRelationsRow, DbLocationsRow } from './tables';
export function initDatabaseMetrics(knex: Knex) {
const seen = new Set<string>();
@@ -0,0 +1,29 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { resolvePackagePath } from '@backstage/backend-common';
import { Knex } from 'knex';
export async function applyDatabaseMigrations(knex: Knex): Promise<void> {
const migrationsDir = resolvePackagePath(
'@backstage/plugin-catalog-backend',
'migrations',
);
await knex.migrate.latest({
directory: migrationsDir,
});
}
@@ -14,6 +14,15 @@
* limitations under the License.
*/
export type DbPageInfo =
| {
hasNextPage: false;
}
| {
hasNextPage: true;
endCursor: string;
};
export type DbLocationsRow = {
id: string;
type: string;
+120 -222
View File
@@ -1,5 +1,5 @@
/*
* Copyright 2020 The Backstage Authors
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,135 +14,10 @@
* limitations under the License.
*/
import type {
Entity,
EntityName,
EntityRelationSpec,
Location,
} from '@backstage/catalog-model';
export type DbEntitiesRow = {
id: string;
location_id: string | null;
etag: string;
generation: number;
full_name: string;
data: string;
};
export type DbEntityRequest = {
locationId?: string;
entity: Entity;
relations: EntityRelationSpec[];
};
export type DbEntitiesRequest = {
filter?: EntityFilter;
pagination?: EntityPagination;
};
export type DbEntitiesResponse = {
entities: DbEntityResponse[];
pageInfo: DbPageInfo;
};
export type DbPageInfo =
| {
hasNextPage: false;
}
| {
hasNextPage: true;
endCursor: string;
};
export type DbEntityResponse = {
locationId?: string;
entity: Entity;
};
export type DbEntitiesRelationsRow = {
originating_entity_id: string;
source_full_name: string;
type: string;
target_full_name: string;
};
export type DbEntitiesSearchRow = {
entity_id: string;
key: string;
value: string | null;
};
export type DbLocationsRow = {
id: string;
type: string;
target: string;
};
export type DbLocationsRowWithStatus = DbLocationsRow & {
status: string | null;
timestamp: string | null;
message: string | null;
};
export enum DatabaseLocationUpdateLogStatus {
FAIL = 'fail',
SUCCESS = 'success',
}
export type DatabaseLocationUpdateLogEvent = {
id: string;
status: DatabaseLocationUpdateLogStatus;
location_id: string;
entity_name: string;
created_at?: string;
message?: string;
};
/**
* Matches rows in the entities_search table.
*/
export type EntitiesSearchFilter = {
/**
* The key to match on.
*
* Matches are always case insensitive.
*/
key: string;
/**
* Match on plain equality of values.
*
* If undefined, this factor is not taken into account. Otherwise, match on
* values that are equal to any of the given array items. Matches are always
* case insensitive.
*/
matchValueIn?: string[];
/**
* Match on existence of key.
*/
matchValueExists?: boolean;
};
/**
* A filter expression for entities.
*
* Any (at least one) of the outer sets must match, within which all of the
* individual filters must match.
*/
export type EntityFilter = {
anyOf: { allOf: EntitiesSearchFilter[] }[];
};
/**
* A pagination rule for entities.
*/
export type EntityPagination = {
limit?: number;
offset?: number;
after?: string;
};
import { Entity, EntityRelationSpec } from '@backstage/catalog-model';
import { JsonObject } from '@backstage/config';
import { DateTime } from 'luxon';
import { DeferredEntity } from '../processing/types';
/**
* An abstraction for transactions of the underlying database technology.
@@ -151,103 +26,126 @@ export type Transaction = {
rollback(): Promise<unknown>;
};
/**
* An abstraction on top of the underlying database, wrapping the basic CRUD
* needs.
*/
export type Database = {
/**
* Runs a transaction.
*
* The callback is expected to make calls back into this class. When it
* completes, the transaction is closed.
*
* @param fn The callback that implements the transaction
*/
export type AddUnprocessedEntitiesResult = {};
export type UpdateProcessedEntityOptions = {
id: string;
processedEntity: Entity;
resultHash: string;
errors?: string;
relations: EntityRelationSpec[];
deferredEntities: DeferredEntity[];
locationKey?: string;
};
export type UpdateEntityCacheOptions = {
id: string;
state?: JsonObject;
};
export type UpdateProcessedEntityErrorsOptions = {
id: string;
errors?: string;
resultHash: string;
};
export type RefreshStateItem = {
id: string;
entityRef: string;
unprocessedEntity: Entity;
processedEntity?: Entity;
resultHash: string;
nextUpdateAt: DateTime;
lastDiscoveryAt: DateTime; // remove?
state?: JsonObject;
errors?: string;
locationKey?: string;
};
export type GetProcessableEntitiesResult = {
items: RefreshStateItem[];
};
export type ReplaceUnprocessedEntitiesOptions =
| {
sourceKey: string;
items: DeferredEntity[];
type: 'full';
}
| {
sourceKey: string;
added: DeferredEntity[];
removed: DeferredEntity[];
type: 'delta';
};
export type RefreshOptions = {
entityRef: string;
};
export type ListAncestorsOptions = {
entityRef: string;
};
export type ListAncestorsResult = {
entityRefs: string[];
};
export interface ProcessingDatabase {
transaction<T>(fn: (tx: Transaction) => Promise<T>): Promise<T>;
/**
* Adds a set of new entities to the catalog.
*
* @param tx An ongoing transaction
* @param request The entities being added
* Add unprocessed entities to the front of the processing queue using a mutation.
*/
addEntities(
tx: Transaction,
request: DbEntityRequest[],
): Promise<DbEntityResponse[]>;
/**
* Updates an existing entity in the catalog.
*
* The given entity must contain an uid to identify an already stored entity
* in the catalog. If it is missing or if no matching entity is found, the
* operation fails.
*
* If matchingEtag or matchingGeneration are given, they are taken into
* account. Attempts to update a matching entity, but where the etag and/or
* generation are not equal to the passed values, will fail.
*
* @param tx An ongoing transaction
* @param request The entity being updated
* @param matchingEtag If specified, reject with ConflictError if not
* matching the entry in the database
* @param matchingGeneration If specified, reject with ConflictError if not
* matching the entry in the database
* @returns The updated entity
*/
updateEntity(
tx: Transaction,
request: DbEntityRequest,
matchingEtag?: string,
matchingGeneration?: number,
): Promise<DbEntityResponse>;
entities(
tx: Transaction,
request?: DbEntitiesRequest,
): Promise<DbEntitiesResponse>;
entityByName(
tx: Transaction,
name: EntityName,
): Promise<DbEntityResponse | undefined>;
entityByUid(
tx: Transaction,
uid: string,
): Promise<DbEntityResponse | undefined>;
removeEntityByUid(tx: Transaction, uid: string): Promise<void>;
/**
* Remove current relations for the entity and replace them with the new
* relations array.
*
* @param tx An ongoing transaction
* @param entityUid The entity uid
* @param relations The relationships to be set
*/
setRelations(
tx: Transaction,
entityUid: string,
relations: EntityRelationSpec[],
replaceUnprocessedEntities(
txOpaque: Transaction,
options: ReplaceUnprocessedEntitiesOptions,
): Promise<void>;
addLocation(tx: Transaction, location: Location): Promise<DbLocationsRow>;
getProcessableEntities(
txOpaque: Transaction,
request: { processBatchSize: number },
): Promise<GetProcessableEntitiesResult>;
removeLocation(tx: Transaction, id: string): Promise<void>;
location(id: string): Promise<DbLocationsRowWithStatus>;
locations(): Promise<DbLocationsRowWithStatus[]>;
locationHistory(id: string): Promise<DatabaseLocationUpdateLogEvent[]>;
addLocationUpdateLogEvent(
locationId: string,
status: DatabaseLocationUpdateLogStatus,
entityName?: string | string[],
message?: string,
/**
* Updates a processed entity.
*
* Any deferred entities are added at the front of the processing queue for
* immediate processing, meaning this should only be called when the entity has changes.
*/
updateProcessedEntity(
txOpaque: Transaction,
options: UpdateProcessedEntityOptions,
): Promise<void>;
};
/**
* Updates the cache associated with an entity.
*/
updateEntityCache(
txOpaque: Transaction,
options: UpdateEntityCacheOptions,
): Promise<void>;
/**
* Updates only the errors of a processed entity
*/
updateProcessedEntityErrors(
txOpaque: Transaction,
options: UpdateProcessedEntityErrorsOptions,
): Promise<void>;
/**
* Schedules a refresh of a given entityRef.
*/
refresh(txOpaque: Transaction, options: RefreshOptions): Promise<void>;
/**
* Lists all ancestors of a given entityRef.
*
* The returned list is ordered from the most immediate ancestor to the most distant one.
*/
listAncestors(
txOpaque: Transaction,
options: ListAncestorsOptions,
): Promise<ListAncestorsResult>;
}
+3 -2
View File
@@ -21,9 +21,10 @@
*/
export * from './catalog';
export * from './database';
export * from './ingestion';
export * from './legacy';
export * from './search';
export * from './util';
export * from './next';
export * from './processing';
export * from './providers';
export * from './service';
@@ -37,13 +37,8 @@ AWSMock.mock(
Buffer.from(
require('fs').readFileSync(
path.resolve(
'src',
'ingestion',
'processors',
'__fixtures__',
'fileReaderProcessor',
'awsS3',
'awsS3-mock-object.txt',
__dirname,
'__fixtures__/fileReaderProcessor/awsS3/awsS3-mock-object.txt',
),
),
),
@@ -23,13 +23,7 @@ import {
import path from 'path';
describe('FileReaderProcessor', () => {
const fixturesRoot = path.join(
'src',
'ingestion',
'processors',
'__fixtures__',
'fileReaderProcessor',
);
const fixturesRoot = path.join(__dirname, '__fixtures__/fileReaderProcessor');
it('should load from file', async () => {
const processor = new FileReaderProcessor();
@@ -16,7 +16,7 @@
import { getVoidLogger } from '@backstage/backend-common';
import { Entity, LOCATION_ANNOTATION } from '@backstage/catalog-model';
import { Database, DatabaseManager, Transaction } from '../../database';
import { Database, DatabaseManager, Transaction } from '../database';
import { basicEntityFilter } from '../../service/request';
import { DatabaseEntitiesCatalog } from './DatabaseEntitiesCatalog';
import { EntityUpsertRequest } from '../../catalog/types';
@@ -26,8 +26,8 @@ import { ConflictError } from '@backstage/errors';
import { chunk, groupBy } from 'lodash';
import limiterFactory from 'p-limit';
import { Logger } from 'winston';
import type { Database, DbEntityResponse, Transaction } from '../../database';
import { DbEntitiesRequest } from '../../database/types';
import type { Database, DbEntityResponse, Transaction } from '../database';
import { DbEntitiesRequest } from '../database/types';
import { basicEntityFilter } from '../../service/request';
import { durationText } from '../../util/timing';
import type {
@@ -22,6 +22,7 @@ import {
} from '../database/types';
import { LocationResponse, LocationsCatalog } from './types';
/** @deprecated This was part of the legacy catalog engine */
export class DatabaseLocationsCatalog implements LocationsCatalog {
constructor(private readonly database: Database) {}
@@ -15,3 +15,10 @@
*/
export { DatabaseEntitiesCatalog } from './DatabaseEntitiesCatalog';
export { DatabaseLocationsCatalog } from './DatabaseLocationsCatalog';
export type {
LocationResponse,
LocationsCatalog,
LocationUpdateLogEvent,
LocationUpdateStatus,
} from './types';
@@ -0,0 +1,62 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Location } from '@backstage/catalog-model';
//
// Locations
//
/** @deprecated This was part of the legacy catalog engine */
export type LocationUpdateStatus = {
timestamp: string | null;
status: string | null;
message: string | null;
};
/** @deprecated This was part of the legacy catalog engine */
export type LocationUpdateLogEvent = {
id: string;
status: 'fail' | 'success';
location_id: string;
entity_name: string;
created_at?: string;
message?: string;
};
/** @deprecated This was part of the legacy catalog engine */
export type LocationResponse = {
data: Location;
currentStatus: LocationUpdateStatus;
};
/** @deprecated This was part of the legacy catalog engine */
export type LocationsCatalog = {
addLocation(location: Location): Promise<Location>;
removeLocation(id: string): Promise<void>;
locations(): Promise<LocationResponse[]>;
location(id: string): Promise<LocationResponse>;
locationHistory(id: string): Promise<LocationUpdateLogEvent[]>;
logUpdateSuccess(
locationId: string,
entityName?: string | string[],
): Promise<void>;
logUpdateFailure(
locationId: string,
error?: Error,
entityName?: string,
): Promise<void>;
};
@@ -16,7 +16,7 @@
import { Entity, Location, parseEntityRef } from '@backstage/catalog-model';
import { ConflictError } from '@backstage/errors';
import { basicEntityFilter } from '../service/request';
import { basicEntityFilter } from '../../service/request';
import { DatabaseManager } from './DatabaseManager';
import type {
DbEntityRequest,
@@ -44,9 +44,9 @@ import {
DbLocationsRow,
DbLocationsRowWithStatus,
DbPageInfo,
EntityPagination,
Transaction,
} from './types';
import { EntityPagination } from '../../catalog/types';
// The number of items that are sent per batch to the database layer, when
// doing .batchInsert calls to knex. This needs to be low enough to not cause
@@ -55,7 +55,8 @@ import {
const BATCH_SIZE = 50;
/**
* The core database implementation.
* The core database implementation..
* @deprecated This was part of the legacy catalog engin
*/
export class CommonDatabase implements Database {
constructor(
@@ -26,6 +26,7 @@ const migrationsDir = resolvePackagePath(
'migrations',
);
/** @deprecated This was part of the legacy catalog engine */
export type CreateDatabaseOptions = {
logger: Logger;
};
@@ -34,6 +35,7 @@ const defaultOptions: CreateDatabaseOptions = {
logger: getVoidLogger(),
};
/** @deprecated This was part of the legacy catalog engine */
export class DatabaseManager {
public static async createDatabase(
knex: Knex,
@@ -21,9 +21,6 @@ export type {
Database,
DbEntityRequest,
DbEntityResponse,
EntitiesSearchFilter,
EntityFilter,
EntityPagination,
Transaction,
DbEntitiesRequest,
DbEntitiesResponse,
@@ -0,0 +1,223 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type {
Entity,
EntityName,
EntityRelationSpec,
Location,
} from '@backstage/catalog-model';
import { EntityFilter, EntityPagination } from '../../catalog/types';
/** @deprecated This was part of the legacy catalog engine */
export type DbEntitiesRow = {
id: string;
location_id: string | null;
etag: string;
generation: number;
full_name: string;
data: string;
};
/** @deprecated This was part of the legacy catalog engine */
export type DbEntityRequest = {
locationId?: string;
entity: Entity;
relations: EntityRelationSpec[];
};
/** @deprecated This was part of the legacy catalog engine */
export type DbEntitiesRequest = {
filter?: EntityFilter;
pagination?: EntityPagination;
};
/** @deprecated This was part of the legacy catalog engine */
export type DbEntitiesResponse = {
entities: DbEntityResponse[];
pageInfo: DbPageInfo;
};
/** @deprecated This was part of the legacy catalog engine */
export type DbPageInfo =
| {
hasNextPage: false;
}
| {
hasNextPage: true;
endCursor: string;
};
/** @deprecated This was part of the legacy catalog engine */
export type DbEntityResponse = {
locationId?: string;
entity: Entity;
};
/** @deprecated This was part of the legacy catalog engine */
export type DbEntitiesRelationsRow = {
originating_entity_id: string;
source_full_name: string;
type: string;
target_full_name: string;
};
/** @deprecated This was part of the legacy catalog engine */
export type DbEntitiesSearchRow = {
entity_id: string;
key: string;
value: string | null;
};
/** @deprecated This was part of the legacy catalog engine */
export type DbLocationsRow = {
id: string;
type: string;
target: string;
};
/** @deprecated This was part of the legacy catalog engine */
export type DbLocationsRowWithStatus = DbLocationsRow & {
status: string | null;
timestamp: string | null;
message: string | null;
};
export enum DatabaseLocationUpdateLogStatus {
FAIL = 'fail',
SUCCESS = 'success',
}
/** @deprecated This was part of the legacy catalog engine */
export type DatabaseLocationUpdateLogEvent = {
id: string;
status: DatabaseLocationUpdateLogStatus;
location_id: string;
entity_name: string;
created_at?: string;
message?: string;
};
/**
* An abstraction for transactions of the underlying database technology.
*
* @deprecated This was part of the legacy catalog engine
*/
export type Transaction = {
rollback(): Promise<unknown>;
};
/**
* An abstraction on top of the underlying database, wrapping the basic CRUD
* needs.
* @deprecated This was part of the legacy catalog engine
*/
export type Database = {
/**
* Runs a transaction.
*
* The callback is expected to make calls back into this class. When it
* completes, the transaction is closed.
*
* @param fn The callback that implements the transaction
*/
transaction<T>(fn: (tx: Transaction) => Promise<T>): Promise<T>;
/**
* Adds a set of new entities to the catalog.
*
* @param tx An ongoing transaction
* @param request The entities being added
*/
addEntities(
tx: Transaction,
request: DbEntityRequest[],
): Promise<DbEntityResponse[]>;
/**
* Updates an existing entity in the catalog.
*
* The given entity must contain an uid to identify an already stored entity
* in the catalog. If it is missing or if no matching entity is found, the
* operation fails.
*
* If matchingEtag or matchingGeneration are given, they are taken into
* account. Attempts to update a matching entity, but where the etag and/or
* generation are not equal to the passed values, will fail.
*
* @param tx An ongoing transaction
* @param request The entity being updated
* @param matchingEtag If specified, reject with ConflictError if not
* matching the entry in the database
* @param matchingGeneration If specified, reject with ConflictError if not
* matching the entry in the database
* @returns The updated entity
*/
updateEntity(
tx: Transaction,
request: DbEntityRequest,
matchingEtag?: string,
matchingGeneration?: number,
): Promise<DbEntityResponse>;
entities(
tx: Transaction,
request?: DbEntitiesRequest,
): Promise<DbEntitiesResponse>;
entityByName(
tx: Transaction,
name: EntityName,
): Promise<DbEntityResponse | undefined>;
entityByUid(
tx: Transaction,
uid: string,
): Promise<DbEntityResponse | undefined>;
removeEntityByUid(tx: Transaction, uid: string): Promise<void>;
/**
* Remove current relations for the entity and replace them with the new
* relations array.
*
* @param tx An ongoing transaction
* @param entityUid The entity uid
* @param relations The relationships to be set
*/
setRelations(
tx: Transaction,
entityUid: string,
relations: EntityRelationSpec[],
): Promise<void>;
addLocation(tx: Transaction, location: Location): Promise<DbLocationsRow>;
removeLocation(tx: Transaction, id: string): Promise<void>;
location(id: string): Promise<DbLocationsRowWithStatus>;
locations(): Promise<DbLocationsRowWithStatus[]>;
locationHistory(id: string): Promise<DatabaseLocationUpdateLogEvent[]>;
addLocationUpdateLogEvent(
locationId: string,
status: DatabaseLocationUpdateLogStatus,
entityName?: string | string[],
message?: string,
): Promise<void>;
};
@@ -17,3 +17,4 @@
export * from './catalog';
export * from './ingestion';
export * from './service';
export * from './database';
@@ -16,14 +16,15 @@
import { getVoidLogger } from '@backstage/backend-common';
import { Entity, Location, LocationSpec } from '@backstage/catalog-model';
import { EntitiesCatalog, LocationsCatalog } from '../../catalog';
import { LocationUpdateStatus } from '../../catalog/types';
import { DatabaseLocationUpdateLogStatus } from '../../database/types';
import { EntitiesCatalog } from '../../catalog';
import { LocationsCatalog } from '../catalog';
import { LocationUpdateStatus } from '../catalog/types';
import { DatabaseLocationUpdateLogStatus } from '../database/types';
import { HigherOrderOperations } from './HigherOrderOperations';
import { LocationReader } from './types';
describe('HigherOrderOperations', () => {
let entitiesCatalog: jest.Mocked<EntitiesCatalog>;
let entitiesCatalog: jest.Mocked<Required<EntitiesCatalog>>;
let locationsCatalog: jest.Mocked<LocationsCatalog>;
let locationReader: jest.Mocked<LocationReader>;
let higherOrderOperation: HigherOrderOperations;
@@ -21,7 +21,8 @@ import {
} from '@backstage/catalog-model';
import { v4 as uuidv4 } from 'uuid';
import { Logger } from 'winston';
import { EntitiesCatalog, LocationsCatalog } from '../../catalog';
import { EntitiesCatalog } from '../../catalog';
import { LocationsCatalog } from '../catalog';
import { durationText } from '../../util';
import {
AddLocationResult,
@@ -95,14 +96,12 @@ export class HigherOrderOperations implements HigherOrderOperation {
return { location, entities: [] };
}
const writtenEntities = await this.entitiesCatalog.batchAddOrUpdateEntities(
readerOutput.entities,
{
locationId: dryRun ? undefined : location.id,
dryRun,
outputEntities: true,
},
);
const writtenEntities = await this.entitiesCatalog
.batchAddOrUpdateEntities!(readerOutput.entities, {
locationId: dryRun ? undefined : location.id,
dryRun,
outputEntities: true,
});
const entities = writtenEntities.map(e => e.entity!);
@@ -186,7 +185,7 @@ export class HigherOrderOperations implements HigherOrderOperation {
startTimestamp = process.hrtime();
try {
await this.entitiesCatalog.batchAddOrUpdateEntities(
await this.entitiesCatalog.batchAddOrUpdateEntities!(
readerOutput.entities,
{ locationId: location.id },
);
@@ -19,11 +19,11 @@ import { Entity } from '@backstage/catalog-model';
import { ConfigReader } from '@backstage/config';
import { Knex } from 'knex';
import yaml from 'yaml';
import { DatabaseManager } from '../../database';
import { DatabaseManager } from '../database';
import { CatalogProcessorParser } from '../../ingestion';
import * as result from '../../ingestion/processors/results';
import { CatalogBuilder } from './CatalogBuilder';
import { CatalogEnvironment } from '../../next';
import { CatalogEnvironment } from '../../service';
const dummyEntity = {
apiVersion: 'backstage.io/v1alpha1',
@@ -26,13 +26,13 @@ import {
} from '@backstage/catalog-model';
import { ScmIntegrations } from '@backstage/integration';
import lodash from 'lodash';
import { EntitiesCatalog } from '../../catalog';
import {
DatabaseEntitiesCatalog,
DatabaseLocationsCatalog,
EntitiesCatalog,
LocationsCatalog,
} from '../../catalog';
import { DatabaseEntitiesCatalog } from '../catalog';
import { DatabaseManager } from '../../database';
} from '../catalog';
import { DatabaseManager } from '../database';
import {
AnnotateLocationEntityProcessor,
BitbucketDiscoveryProcessor,
@@ -64,7 +64,7 @@ import {
} from '../../ingestion/processors/PlaceholderProcessor';
import { defaultEntityDataParser } from '../../ingestion/processors/util/parse';
import { LocationAnalyzer } from '../../ingestion/types';
import { CatalogEnvironment, NextCatalogBuilder } from '../../next';
import { CatalogEnvironment, NextCatalogBuilder } from '../../service';
/**
* A builder that helps wire up all of the component parts of the catalog.
@@ -20,15 +20,15 @@ import { NotFoundError } from '@backstage/errors';
import type { Entity, LocationSpec } from '@backstage/catalog-model';
import express from 'express';
import request from 'supertest';
import { EntitiesCatalog, LocationsCatalog } from '../../catalog';
import { LocationResponse } from '../../catalog/types';
import { EntitiesCatalog } from '../../catalog';
import { LocationResponse, LocationsCatalog } from '../catalog/types';
import { HigherOrderOperation } from '../ingestion/types';
import { createRouter } from './router';
import { basicEntityFilter } from '../../service/request';
import { RefreshService } from '../../next';
import { RefreshService } from '../../service';
describe('createRouter readonly disabled', () => {
let entitiesCatalog: jest.Mocked<EntitiesCatalog>;
let entitiesCatalog: jest.Mocked<Required<EntitiesCatalog>>;
let locationsCatalog: jest.Mocked<LocationsCatalog>;
let higherOrderOperation: jest.Mocked<HigherOrderOperation>;
let app: express.Express;
@@ -26,14 +26,15 @@ import express from 'express';
import Router from 'express-promise-router';
import { Logger } from 'winston';
import yn from 'yn';
import { EntitiesCatalog, LocationsCatalog } from '../../catalog';
import { EntitiesCatalog } from '../../catalog';
import { LocationsCatalog } from '../catalog';
import { LocationAnalyzer } from '../../ingestion/types';
import { HigherOrderOperation } from '../ingestion/types';
import {
RefreshService,
LocationService,
RefreshOptions,
} from '../../next/types';
} from '../../service/types';
import {
basicEntityFilter,
parseEntityFilterParams,
@@ -124,7 +125,7 @@ export async function createRouter(
disallowReadonlyMode(readonlyEnabled);
const body = await requireRequestBody(req);
const [result] = await entitiesCatalog.batchAddOrUpdateEntities([
const [result] = await entitiesCatalog.batchAddOrUpdateEntities!([
{ entity: body as Entity, relations: [] },
]);
const response = await entitiesCatalog.entities({
@@ -1,39 +0,0 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Context, ContextKey } from './types';
/**
* A Context implementation that holds a single value, optionally extending an existing context.
*/
export class ContextWithValue implements Context {
static create(parent: Context, key: ContextKey<unknown>, value: unknown) {
return new ContextWithValue(parent, key, value);
}
private constructor(
private readonly parent: Context,
private readonly key: ContextKey<unknown>,
private readonly value: unknown,
) {}
getContextValue<T>(key: ContextKey<T>): T {
if (this.key === key) {
return this.value as T;
}
return this.parent.getContextValue(key);
}
}
@@ -1,37 +0,0 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { TransactionValue } from './TransactionValue';
import { Knex } from 'knex';
import { BackgroundContext } from './BackgroundContext';
describe('TransactionValue Context', () => {
it('should be able to store tx values and retrieve them from a context', () => {
const tx = {} as Knex.Transaction;
const ctx = new BackgroundContext();
const nextCtx = TransactionValue.in(ctx, tx);
expect(TransactionValue.from(nextCtx)).toBe(tx);
});
it('should throw when there is no tx value in the context', () => {
const ctx = new BackgroundContext();
expect(() => TransactionValue.from(ctx)).toThrow(
/No transaction available in context/,
);
});
});
@@ -1,40 +0,0 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Context, ContextKey } from './types';
import { Knex } from 'knex';
import { ContextWithValue } from './ContextWithValue';
const transactionContextKey = new ContextKey<Knex.Transaction | undefined>(
undefined,
);
/**
* TransactionValue handles the wrapping of a knex transaction in a Context.
*/
export class TransactionValue {
static in(parent: Context, tx: Knex.Transaction) {
return ContextWithValue.create(parent, transactionContextKey, tx);
}
static from(context: Context): Knex.Transaction {
const transaction = context.getContextValue(transactionContextKey);
if (!transaction) {
throw new Error(`No transaction available in context`);
}
return transaction;
}
}
@@ -1,99 +0,0 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { getVoidLogger, resolvePackagePath } from '@backstage/backend-common';
import knexFactory, { Knex } from 'knex';
import { v4 as uuid } from 'uuid';
import { Logger } from 'winston';
import { CommonDatabase } from '../../database/CommonDatabase';
import { Database } from '../../database/types';
export type CreateDatabaseOptions = {
logger: Logger;
};
const defaultOptions: CreateDatabaseOptions = {
logger: getVoidLogger(),
};
export class DatabaseManager {
public static async createDatabase(
knex: Knex,
options: Partial<CreateDatabaseOptions> = {},
): Promise<Database> {
const migrationsDir = resolvePackagePath(
'@backstage/plugin-catalog-backend',
'migrations',
);
await knex.migrate.latest({
directory: migrationsDir,
});
const { logger } = { ...defaultOptions, ...options };
return new CommonDatabase(knex, logger);
}
public static async createInMemoryDatabase(): Promise<Database> {
const knex = await this.createInMemoryDatabaseConnection();
return await this.createDatabase(knex);
}
public static async createInMemoryDatabaseConnection(): Promise<Knex> {
const knex = knexFactory({
client: 'sqlite3',
connection: ':memory:',
useNullAsDefault: true,
});
knex.client.pool.on('createSuccess', (_eventId: any, resource: any) => {
resource.run('PRAGMA foreign_keys = ON', () => {});
});
return knex;
}
public static async createTestDatabase(): Promise<Database> {
const knex = await this.createTestDatabaseConnection();
return await this.createDatabase(knex);
}
public static async createTestDatabaseConnection(): Promise<Knex> {
const config: Knex.Config<any> = {
client: 'sqlite3',
connection: ':memory:',
useNullAsDefault: true,
};
let knex = knexFactory(config);
if (typeof config.connection !== 'string') {
const tempDbName = `d${uuid().replace(/-/g, '')}`;
await knex.raw(`CREATE DATABASE ${tempDbName};`);
knex = knexFactory({
...config,
connection: {
...config.connection,
database: tempDbName,
},
});
}
knex.client.pool.on('createSuccess', (_eventId: any, resource: any) => {
resource.run('PRAGMA foreign_keys = ON', () => {});
});
return knex;
}
}
@@ -1,145 +0,0 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Entity, EntityRelationSpec } from '@backstage/catalog-model';
import { JsonObject } from '@backstage/config';
import { DateTime } from 'luxon';
import { Transaction } from '../../database/types';
import { DeferredEntity } from '../processing/types';
export type AddUnprocessedEntitiesResult = {};
export type UpdateProcessedEntityOptions = {
id: string;
processedEntity: Entity;
resultHash: string;
errors?: string;
relations: EntityRelationSpec[];
deferredEntities: DeferredEntity[];
locationKey?: string;
};
export type UpdateEntityCacheOptions = {
id: string;
state?: JsonObject;
};
export type UpdateProcessedEntityErrorsOptions = {
id: string;
errors?: string;
resultHash: string;
};
export type RefreshStateItem = {
id: string;
entityRef: string;
unprocessedEntity: Entity;
processedEntity?: Entity;
resultHash: string;
nextUpdateAt: DateTime;
lastDiscoveryAt: DateTime; // remove?
state?: JsonObject;
errors?: string;
locationKey?: string;
};
export type GetProcessableEntitiesResult = {
items: RefreshStateItem[];
};
export type ReplaceUnprocessedEntitiesOptions =
| {
sourceKey: string;
items: DeferredEntity[];
type: 'full';
}
| {
sourceKey: string;
added: DeferredEntity[];
removed: DeferredEntity[];
type: 'delta';
};
export type RefreshOptions = {
entityRef: string;
};
export type ListAncestorsOptions = {
entityRef: string;
};
export type ListAncestorsResult = {
entityRefs: string[];
};
export interface ProcessingDatabase {
transaction<T>(fn: (tx: Transaction) => Promise<T>): Promise<T>;
/**
* Add unprocessed entities to the front of the processing queue using a mutation.
*/
replaceUnprocessedEntities(
txOpaque: Transaction,
options: ReplaceUnprocessedEntitiesOptions,
): Promise<void>;
getProcessableEntities(
txOpaque: Transaction,
request: { processBatchSize: number },
): Promise<GetProcessableEntitiesResult>;
/**
* Updates a processed entity.
*
* Any deferred entities are added at the front of the processing queue for
* immediate processing, meaning this should only be called when the entity has changes.
*/
updateProcessedEntity(
txOpaque: Transaction,
options: UpdateProcessedEntityOptions,
): Promise<void>;
/**
* Updates the cache associated with an entity.
*/
updateEntityCache(
txOpaque: Transaction,
options: UpdateEntityCacheOptions,
): Promise<void>;
/**
* Updates only the errors of a processed entity
*/
updateProcessedEntityErrors(
txOpaque: Transaction,
options: UpdateProcessedEntityErrorsOptions,
): Promise<void>;
/**
* Schedules a refresh of a given entityRef.
*/
refresh(txOpaque: Transaction, options: RefreshOptions): Promise<void>;
/**
* Lists all ancestors of a given entityRef.
*
* The returned list is ordered from the most immediate ancestor to the most distant one.
*/
listAncestors(
txOpaque: Transaction,
options: ListAncestorsOptions,
): Promise<ListAncestorsResult>;
}
@@ -18,10 +18,10 @@ import { getVoidLogger } from '@backstage/backend-common';
import { Hash } from 'crypto';
import { DateTime } from 'luxon';
import waitForExpect from 'wait-for-expect';
import { DefaultProcessingDatabase } from './database/DefaultProcessingDatabase';
import { DefaultProcessingDatabase } from '../database/DefaultProcessingDatabase';
import { DefaultCatalogProcessingEngine } from './DefaultCatalogProcessingEngine';
import { CatalogProcessingOrchestrator } from './processing/types';
import { Stitcher } from './stitching/Stitcher';
import { CatalogProcessingOrchestrator } from './types';
import { Stitcher } from '../stitching/Stitcher';
describe('DefaultCatalogProcessingEngine', () => {
const db = {
@@ -23,20 +23,20 @@ import { serializeError } from '@backstage/errors';
import { Hash } from 'crypto';
import stableStringify from 'fast-json-stable-stringify';
import { Logger } from 'winston';
import { ProcessingDatabase, RefreshStateItem } from './database/types';
import { createCounterMetric, createSummaryMetric } from './metrics';
import {
CatalogProcessingOrchestrator,
EntityProcessingResult,
} from './processing/types';
import { Stitcher } from './stitching/Stitcher';
import { startTaskPipeline } from './TaskPipeline';
import { ProcessingDatabase, RefreshStateItem } from '../database/types';
import { createCounterMetric, createSummaryMetric } from '../util/metrics';
import {
CatalogProcessingEngine,
CatalogProcessingOrchestrator,
EntityProcessingResult,
} from '../processing/types';
import { Stitcher } from '../stitching/Stitcher';
import { startTaskPipeline } from './TaskPipeline';
import {
EntityProvider,
EntityProviderConnection,
EntityProviderMutation,
} from './types';
} from '../providers/types';
const CACHE_TTL = 5;
@@ -30,10 +30,10 @@ import {
CatalogProcessorEmit,
CatalogProcessorParser,
results,
} from '../../ingestion';
import { CatalogRulesEnforcer } from '../../ingestion/CatalogRules';
} from '../ingestion';
import { CatalogRulesEnforcer } from '../ingestion/CatalogRules';
import { DefaultCatalogProcessingOrchestrator } from './DefaultCatalogProcessingOrchestrator';
import { defaultEntityDataParser } from '../../ingestion/processors/util/parse';
import { defaultEntityDataParser } from '../ingestion/processors/util/parse';
import { ConfigReader } from '@backstage/config';
class FooBarProcessor implements CatalogProcessor {
@@ -31,8 +31,8 @@ import { Logger } from 'winston';
import {
CatalogProcessor,
CatalogProcessorParser,
} from '../../ingestion/processors';
import * as results from '../../ingestion/processors/results';
} from '../ingestion/processors';
import * as results from '../ingestion/processors/results';
import {
CatalogProcessingOrchestrator,
EntityProcessingRequest,
@@ -48,7 +48,7 @@ import {
validateEntityEnvelope,
isObject,
} from './util';
import { CatalogRulesEnforcer } from '../../ingestion/CatalogRules';
import { CatalogRulesEnforcer } from '../ingestion/CatalogRules';
import { ProcessorCacheManager } from './ProcessorCacheManager';
type Context = {
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { CatalogProcessor } from '../../ingestion/processors';
import { CatalogProcessor } from '../ingestion/processors';
import { ProcessorCacheManager } from './ProcessorCacheManager';
class MyProcessor implements CatalogProcessor {
@@ -15,8 +15,8 @@
*/
import { JsonObject, JsonValue } from '@backstage/config';
import { CatalogProcessor } from '../../ingestion/processors';
import { CatalogProcessorCache } from '../../ingestion/processors/types';
import { CatalogProcessor } from '../ingestion/processors';
import { CatalogProcessorCache } from '../ingestion/processors/types';
import { isObject } from './util';
class SingleProcessorSubCache implements CatalogProcessorCache {
@@ -22,8 +22,8 @@ import {
stringifyLocationReference,
} from '@backstage/catalog-model';
import { Logger } from 'winston';
import { CatalogProcessorResult } from '../../ingestion';
import { locationSpecToLocationEntity } from '../util';
import { CatalogProcessorResult } from '../ingestion';
import { locationSpecToLocationEntity } from '../util/conversion';
import { DeferredEntity } from './types';
import {
getEntityLocationRef,
@@ -16,8 +16,12 @@
export type {
CatalogProcessingOrchestrator,
CatalogProcessingEngine,
EntityProcessingRequest,
EntityProcessingResult,
DeferredEntity,
} from './types';
export { DefaultCatalogProcessingOrchestrator } from './DefaultCatalogProcessingOrchestrator';
export { createRandomRefreshInterval } from './refresh';
export type { RefreshIntervalFunction } from './refresh';
@@ -44,3 +44,8 @@ export type DeferredEntity = {
entity: Entity;
locationKey?: string;
};
export interface CatalogProcessingEngine {
start(): Promise<void>;
stop(): Promise<void>;
}
@@ -14,7 +14,6 @@
* limitations under the License.
*/
import { resolvePackagePath } from '@backstage/backend-common';
import { ConfigReader } from '@backstage/config';
import path from 'path';
import { ConfigLocationEntityProvider } from './ConfigLocationEntityProvider';
@@ -44,10 +43,7 @@ describe('ConfigLocationEntityProvider', () => {
{
entity: expect.objectContaining({
spec: {
target: path.join(
resolvePackagePath('@backstage/plugin-catalog-backend'),
'./lols.yaml',
),
target: path.join(process.cwd(), 'lols.yaml'),
type: 'file',
},
}),
@@ -16,9 +16,9 @@
import { Config } from '@backstage/config';
import path from 'path';
import { getEntityLocationRef } from './processing/util';
import { getEntityLocationRef } from '../processing/util';
import { EntityProvider, EntityProviderConnection } from './types';
import { locationSpecToLocationEntity } from './util';
import { locationSpecToLocationEntity } from '../util/conversion';
export class ConfigLocationEntityProvider implements EntityProvider {
constructor(private readonly config: Config) {}
@@ -15,7 +15,7 @@
*/
import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils';
import { v4 as uuid } from 'uuid';
import { DatabaseManager } from './database/DatabaseManager';
import { applyDatabaseMigrations } from '../database/migrations';
import { DefaultLocationStore } from './DefaultLocationStore';
describe('DefaultLocationStore', () => {
@@ -25,7 +25,7 @@ describe('DefaultLocationStore', () => {
async function createLocationStore(databaseId: TestDatabaseId) {
const knex = await databases.init(databaseId);
await DatabaseManager.createDatabase(knex);
await applyDatabaseMigrations(knex);
const connection = { applyMutation: jest.fn() };
const store = new DefaultLocationStore(knex);
await store.connect(connection);
@@ -18,14 +18,11 @@ import { Location, LocationSpec } from '@backstage/catalog-model';
import { ConflictError, NotFoundError } from '@backstage/errors';
import { Knex } from 'knex';
import { v4 as uuid } from 'uuid';
import { DbLocationsRow } from './database/tables';
import { getEntityLocationRef } from './processing/util';
import {
EntityProvider,
EntityProviderConnection,
LocationStore,
} from './types';
import { locationSpecToLocationEntity } from './util';
import { DbLocationsRow } from '../database/tables';
import { getEntityLocationRef } from '../processing/util';
import { EntityProvider, EntityProviderConnection } from './types';
import { locationSpecToLocationEntity } from '../util/conversion';
import { LocationStore } from '../service';
export class DefaultLocationStore implements LocationStore, EntityProvider {
private _connection: EntityProviderConnection | undefined;
@@ -14,10 +14,8 @@
* limitations under the License.
*/
export class ContextKey<T> {
constructor(readonly defaultValue: T) {}
}
export interface Context {
getContextValue<T>(key: ContextKey<T>): T;
}
export type {
EntityProvider,
EntityProviderConnection,
EntityProviderMutation,
} from './types';
@@ -0,0 +1,30 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { DeferredEntity } from '../processing';
export type EntityProviderMutation =
| { type: 'full'; entities: DeferredEntity[] }
| { type: 'delta'; added: DeferredEntity[]; removed: DeferredEntity[] };
export interface EntityProviderConnection {
applyMutation(mutation: EntityProviderMutation): Promise<void>;
}
export interface EntityProvider {
getProviderName(): string;
connect(connection: EntityProviderConnection): Promise<void>;
}
@@ -15,7 +15,7 @@
*/
import { DefaultLocationService } from './DefaultLocationService';
import { CatalogProcessingOrchestrator } from './processing/types';
import { CatalogProcessingOrchestrator } from '../processing/types';
import { LocationStore } from './types';
describe('DefaultLocationServiceTest', () => {
@@ -23,9 +23,9 @@ import {
import {
CatalogProcessingOrchestrator,
DeferredEntity,
} from './processing/types';
} from '../processing/types';
import { LocationService, LocationStore } from './types';
import { locationSpecToMetadataName } from './util';
import { locationSpecToMetadataName } from '../util/conversion';
export class DefaultLocationService implements LocationService {
constructor(
@@ -19,16 +19,16 @@ import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils';
import { createHash } from 'crypto';
import { Knex } from 'knex';
import { Logger } from 'winston';
import { DatabaseManager } from './database/DatabaseManager';
import { DefaultProcessingDatabase } from './database/DefaultProcessingDatabase';
import { applyDatabaseMigrations } from '../database/migrations';
import { DefaultProcessingDatabase } from '../database/DefaultProcessingDatabase';
import {
DbRefreshStateReferencesRow,
DbRefreshStateRow,
} from './database/tables';
import { ProcessingDatabase } from './database/types';
import { DefaultCatalogProcessingEngine } from './DefaultCatalogProcessingEngine';
import { EntityProcessingRequest } from './processing/types';
import { Stitcher } from './stitching/Stitcher';
} from '../database/tables';
import { ProcessingDatabase } from '../database/types';
import { DefaultCatalogProcessingEngine } from '../processing/DefaultCatalogProcessingEngine';
import { EntityProcessingRequest } from '../processing/types';
import { Stitcher } from '../stitching/Stitcher';
import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
import { v4 as uuid } from 'uuid';
import { DefaultRefreshService } from './DefaultRefreshService';
@@ -44,7 +44,7 @@ describe('Refresh integration', () => {
logger: Logger = defaultLogger,
) {
const knex = await databases.init(databaseId);
await DatabaseManager.createDatabase(knex);
await applyDatabaseMigrations(knex);
return {
knex,
db: new DefaultProcessingDatabase({
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { DefaultProcessingDatabase } from './database/DefaultProcessingDatabase';
import { DefaultProcessingDatabase } from '../database/DefaultProcessingDatabase';
import { RefreshOptions, RefreshService } from './types';
export class DefaultRefreshService implements RefreshService {
@@ -14,11 +14,7 @@
* limitations under the License.
*/
import {
PluginDatabaseManager,
resolvePackagePath,
UrlReader,
} from '@backstage/backend-common';
import { PluginDatabaseManager, UrlReader } from '@backstage/backend-common';
import {
DefaultNamespaceEntityPolicy,
EntityPolicies,
@@ -33,12 +29,13 @@ import { ScmIntegrations } from '@backstage/integration';
import { createHash } from 'crypto';
import { Router } from 'express';
import lodash from 'lodash';
import { EntitiesCatalog } from '../catalog';
import {
DatabaseLocationsCatalog,
EntitiesCatalog,
LocationsCatalog,
} from '../catalog';
import { CommonDatabase } from '../database/CommonDatabase';
CommonDatabase,
} from '../legacy';
import {
AnnotateLocationEntityProcessor,
BitbucketDiscoveryProcessor,
@@ -62,28 +59,27 @@ import {
} from '../ingestion/processors/PlaceholderProcessor';
import { defaultEntityDataParser } from '../ingestion/processors/util/parse';
import { LocationAnalyzer } from '../ingestion/types';
import {
CatalogProcessingEngine,
EntityProvider,
LocationService,
} from '../next/types';
import { ConfigLocationEntityProvider } from './ConfigLocationEntityProvider';
import { DefaultProcessingDatabase } from './database/DefaultProcessingDatabase';
import { DefaultCatalogProcessingEngine } from './DefaultCatalogProcessingEngine';
import { EntityProvider } from '../providers/types';
import { CatalogProcessingEngine } from '../processing/types';
import { ConfigLocationEntityProvider } from '../providers/ConfigLocationEntityProvider';
import { DefaultProcessingDatabase } from '../database/DefaultProcessingDatabase';
import { applyDatabaseMigrations } from '../database/migrations';
import { DefaultCatalogProcessingEngine } from '../processing/DefaultCatalogProcessingEngine';
import { DefaultLocationService } from './DefaultLocationService';
import { DefaultLocationStore } from './DefaultLocationStore';
import { DefaultLocationStore } from '../providers/DefaultLocationStore';
import { NextEntitiesCatalog } from './NextEntitiesCatalog';
import { DefaultCatalogProcessingOrchestrator } from './processing/DefaultCatalogProcessingOrchestrator';
import { Stitcher } from './stitching/Stitcher';
import { DefaultCatalogProcessingOrchestrator } from '../processing/DefaultCatalogProcessingOrchestrator';
import { Stitcher } from '../stitching/Stitcher';
import {
createRandomRefreshInterval,
RefreshIntervalFunction,
} from './refresh';
} from '../processing/refresh';
import { createNextRouter } from './NextRouter';
import { DefaultRefreshService } from './DefaultRefreshService';
import { DefaultCatalogRulesEnforcer } from '../ingestion/CatalogRules';
import { Config } from '@backstage/config';
import { Logger } from 'winston';
import { LocationService } from './types';
export type CatalogEnvironment = {
logger: Logger;
@@ -289,6 +285,7 @@ export class NextCatalogBuilder {
*/
async build(): Promise<{
entitiesCatalog: EntitiesCatalog;
/** @deprecated This will be removed */
locationsCatalog: LocationsCatalog;
locationAnalyzer: LocationAnalyzer;
processingEngine: CatalogProcessingEngine;
@@ -302,12 +299,7 @@ export class NextCatalogBuilder {
const parser = this.parser || defaultEntityDataParser;
const dbClient = await database.getClient();
await dbClient.migrate.latest({
directory: resolvePackagePath(
'@backstage/plugin-catalog-backend',
'migrations',
),
});
await applyDatabaseMigrations(dbClient);
const db = new CommonDatabase(dbClient, logger);
@@ -18,12 +18,12 @@ import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils';
import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
import { Knex } from 'knex';
import { v4 as uuid } from 'uuid';
import { DatabaseManager } from './database/DatabaseManager';
import { applyDatabaseMigrations } from '../database/migrations';
import {
DbFinalEntitiesRow,
DbRefreshStateReferencesRow,
DbRefreshStateRow,
} from './database/tables';
} from '../database/tables';
import { NextEntitiesCatalog } from './NextEntitiesCatalog';
describe('NextEntitiesCatalog', () => {
@@ -33,7 +33,7 @@ describe('NextEntitiesCatalog', () => {
async function createDatabase(databaseId: TestDatabaseId) {
const knex = await databases.init(databaseId);
await DatabaseManager.createDatabase(knex);
await applyDatabaseMigrations(knex);
return { knex };
}
@@ -22,14 +22,15 @@ import {
EntitiesRequest,
EntitiesResponse,
EntityAncestryResponse,
EntityPagination,
} from '../catalog/types';
import { DbPageInfo, EntityPagination } from '../database/types';
import {
DbFinalEntitiesRow,
DbRefreshStateReferencesRow,
DbRefreshStateRow,
DbSearchRow,
} from './database/tables';
DbPageInfo,
} from '../database/tables';
function parsePagination(input?: EntityPagination): {
limit?: number;
@@ -22,7 +22,7 @@ import express from 'express';
import request from 'supertest';
import { EntitiesCatalog } from '../catalog';
import { LocationService, RefreshService } from './types';
import { basicEntityFilter } from '../service/request';
import { basicEntityFilter } from './request';
import { createNextRouter } from './NextRouter';
describe('createNextRouter readonly disabled', () => {
@@ -14,21 +14,13 @@
* limitations under the License.
*/
export type { CatalogEnvironment } from './NextCatalogBuilder';
export { NextCatalogBuilder } from './NextCatalogBuilder';
export type {
LocationService,
RefreshService,
RefreshOptions,
LocationStore,
} from './types';
export { createNextRouter } from './NextRouter';
export type { NextRouterOptions } from './NextRouter';
export * from './processing';
export { createRandomRefreshInterval } from './refresh';
export type { RefreshIntervalFunction } from './refresh';
export * from './stitching';
export type {
EntityProvider,
EntityProviderConnection,
EntityProviderMutation,
CatalogProcessingEngine,
LocationService,
LocationStore,
RefreshOptions,
RefreshService,
} from './types';
export type { CatalogEnvironment } from './NextCatalogBuilder';
export { NextCatalogBuilder } from './NextCatalogBuilder';
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { EntitiesSearchFilter, EntityFilter } from '../../database';
import { EntitiesSearchFilter, EntityFilter } from '../../catalog';
/**
* Forms a full EntityFilter based on a single key-value(s) object.
@@ -15,7 +15,7 @@
*/
import { InputError } from '@backstage/errors';
import { EntitiesSearchFilter, EntityFilter } from '../../database';
import { EntitiesSearchFilter, EntityFilter } from '../../catalog';
import { parseStringsParam } from './common';
/**
@@ -15,7 +15,7 @@
*/
import { InputError } from '@backstage/errors';
import { EntityPagination } from '../../database';
import { EntityPagination } from '../../catalog';
import { parseIntegerParam, parseStringParam } from './common';
/**
@@ -22,7 +22,7 @@ import {
} from '@backstage/backend-common';
import { Server } from 'http';
import { Logger } from 'winston';
import { DatabaseManager } from '../database';
import { DatabaseManager } from '../legacy/database';
import { CatalogBuilder } from '../legacy/service/CatalogBuilder';
import { createRouter } from '../legacy/service';
@@ -14,8 +14,7 @@
* limitations under the License.
*/
import { Entity, Location, LocationSpec } from '@backstage/catalog-model';
import { DeferredEntity } from './processing/types';
import { Entity, LocationSpec, Location } from '@backstage/catalog-model';
export interface LocationService {
createLocation(
@@ -27,18 +26,6 @@ export interface LocationService {
deleteLocation(id: string): Promise<void>;
}
export interface LocationStore {
createLocation(spec: LocationSpec): Promise<Location>;
listLocations(): Promise<Location[]>;
getLocation(id: string): Promise<Location>;
deleteLocation(id: string): Promise<void>;
}
export interface CatalogProcessingEngine {
start(): Promise<void>;
stop(): Promise<void>;
}
/**
* Options for requesting a refresh of entities in the catalog.
*
@@ -61,15 +48,9 @@ export interface RefreshService {
refresh(options: RefreshOptions): Promise<void>;
}
export type EntityProviderMutation =
| { type: 'full'; entities: DeferredEntity[] }
| { type: 'delta'; added: DeferredEntity[]; removed: DeferredEntity[] };
export interface EntityProviderConnection {
applyMutation(mutation: EntityProviderMutation): Promise<void>;
}
export interface EntityProvider {
getProviderName(): string;
connect(connection: EntityProviderConnection): Promise<void>;
export interface LocationStore {
createLocation(spec: LocationSpec): Promise<Location>;
listLocations(): Promise<Location[]>;
getLocation(id: string): Promise<Location>;
deleteLocation(id: string): Promise<void>;
}
@@ -17,7 +17,7 @@
import { getVoidLogger } from '@backstage/backend-common';
import { TestDatabases } from '@backstage/backend-test-utils';
import { Entity } from '@backstage/catalog-model';
import { DatabaseManager } from '../database/DatabaseManager';
import { applyDatabaseMigrations } from '../database/migrations';
import {
DbFinalEntitiesRow,
DbRefreshStateReferencesRow,
@@ -37,7 +37,7 @@ describe('Stitcher', () => {
'runs the happy path for %p',
async databaseId => {
const db = await databases.init(databaseId);
await DatabaseManager.createDatabase(db);
await applyDatabaseMigrations(db);
const stitcher = new Stitcher(db, logger);
let entities: DbFinalEntitiesRow[];
+14 -4
View File
@@ -93,13 +93,23 @@ export const EntityCatalogGraphCard: ({
}) => JSX.Element;
// @public
export type EntityEdge = DependencyGraphTypes.DependencyEdge<{
export type EntityEdge = DependencyGraphTypes.DependencyEdge<EntityEdgeData>;
// Warning: (ae-missing-release-tag) "EntityEdgeData" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export type EntityEdgeData = {
relations: string[];
label: 'visible';
}>;
};
// @public
export type EntityNode = DependencyGraphTypes.DependencyNode<{
export type EntityNode = DependencyGraphTypes.DependencyNode<EntityNodeData>;
// Warning: (ae-missing-release-tag) "EntityNodeData" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export type EntityNodeData = {
name: string;
kind?: string;
title?: string;
@@ -107,7 +117,7 @@ export type EntityNode = DependencyGraphTypes.DependencyNode<{
focused?: boolean;
color?: 'primary' | 'secondary' | 'default';
onClick?: MouseEventHandler<unknown>;
}>;
};
// @public
export const EntityRelationsGraph: ({
@@ -31,12 +31,6 @@ describe('<CustomLabel />', () => {
relations: [RELATION_PARENT_OF],
from: 'from-id',
to: 'to-id',
id: 'id',
x: 111,
y: 222,
width: 100,
height: 25,
points: [],
}}
/>
</svg>,
@@ -54,12 +48,6 @@ describe('<CustomLabel />', () => {
relations: [RELATION_PARENT_OF, RELATION_CHILD_OF],
from: 'from-id',
to: 'to-id',
id: 'id',
x: 111,
y: 222,
width: 100,
height: 25,
points: [],
}}
/>
</svg>,
@@ -17,7 +17,7 @@ import { DependencyGraphTypes } from '@backstage/core-components';
import { BackstageTheme } from '@backstage/theme';
import makeStyles from '@material-ui/core/styles/makeStyles';
import React from 'react';
import { GraphEdge } from './types';
import { EntityEdgeData } from './types';
import classNames from 'classnames';
const useStyles = makeStyles((theme: BackstageTheme) => ({
@@ -31,7 +31,7 @@ const useStyles = makeStyles((theme: BackstageTheme) => ({
export function CustomLabel({
edge: { relations },
}: DependencyGraphTypes.RenderLabelProps<GraphEdge>) {
}: DependencyGraphTypes.RenderLabelProps<EntityEdgeData>) {
const classes = useStyles();
return (
<text className={classes.text} textAnchor="middle">
@@ -36,10 +36,6 @@ describe('<CustomNode />', () => {
name: 'name',
namespace: 'namespace',
id: 'kind:namespace/name',
x: 111,
y: 222,
width: 100,
height: 25,
color: 'primary',
}}
/>
@@ -59,10 +55,6 @@ describe('<CustomNode />', () => {
name: 'name',
namespace: 'default',
id: 'kind:default/name',
x: 111,
y: 222,
width: 100,
height: 25,
}}
/>
</svg>,
@@ -83,10 +75,6 @@ describe('<CustomNode />', () => {
namespace: 'namespace',
onClick,
id: 'kind:namespace/name',
x: 111,
y: 222,
width: 100,
height: 25,
}}
/>
</svg>,
@@ -108,10 +96,6 @@ describe('<CustomNode />', () => {
namespace: 'namespace',
title: 'Custom Title',
id: 'kind:namespace/name',
x: 111,
y: 222,
width: 100,
height: 25,
}}
/>
</svg>,

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