Add CachedEntityLoader for techdocs
Signed-off-by: Joe Porpeglia <josephp@spotify.com>
This commit is contained in:
committed by
Joe Porpeglia
parent
b76fec242d
commit
7dcc6b1fe6
@@ -37,6 +37,7 @@
|
||||
"@backstage/config": "^0.1.13-next.0",
|
||||
"@backstage/errors": "^0.2.0",
|
||||
"@backstage/integration": "^0.7.2-next.0",
|
||||
"@backstage/plugin-auth-backend": "^0.6.2",
|
||||
"@backstage/search-common": "^0.2.1",
|
||||
"@backstage/techdocs-common": "^0.11.4-next.0",
|
||||
"@types/express": "^4.17.6",
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* Copyright 2022 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 { CachedEntityLoader } from './CachedEntityLoader';
|
||||
import { CatalogClient } from '@backstage/catalog-client';
|
||||
import { CacheClient } from '@backstage/backend-common';
|
||||
import { ResponseError } from '@backstage/errors';
|
||||
import {
|
||||
BackstageIdentityResponse,
|
||||
IdentityClient,
|
||||
} from '@backstage/plugin-auth-backend';
|
||||
import { EntityName } from '@backstage/catalog-model';
|
||||
import { Response } from 'cross-fetch';
|
||||
|
||||
describe('CachedEntityLoader', () => {
|
||||
const catalog: jest.Mocked<CatalogClient> = {
|
||||
getEntityByName: jest.fn(),
|
||||
} as any;
|
||||
|
||||
const cache: jest.Mocked<CacheClient> = {
|
||||
get: jest.fn(),
|
||||
set: jest.fn(),
|
||||
} as any;
|
||||
|
||||
const identity: jest.Mocked<IdentityClient> = {
|
||||
authenticate: jest.fn(),
|
||||
} as any;
|
||||
|
||||
const entityName: EntityName = {
|
||||
kind: 'component',
|
||||
namespace: 'default',
|
||||
name: 'test',
|
||||
};
|
||||
|
||||
const entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'test',
|
||||
namespace: 'default',
|
||||
},
|
||||
};
|
||||
|
||||
const token = 'test-token';
|
||||
|
||||
const identityResponse: BackstageIdentityResponse = {
|
||||
id: '',
|
||||
token,
|
||||
identity: {
|
||||
type: 'user',
|
||||
userEntityRef: 'user:default/test-user',
|
||||
ownershipEntityRefs: [],
|
||||
},
|
||||
};
|
||||
|
||||
const loader = new CachedEntityLoader({ catalog, cache, identity });
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('writes entities to cache', async () => {
|
||||
identity.authenticate.mockResolvedValue(identityResponse);
|
||||
cache.get.mockResolvedValue(undefined);
|
||||
catalog.getEntityByName.mockResolvedValue(entity);
|
||||
|
||||
const result = await loader.load(entityName, token);
|
||||
|
||||
expect(result).toEqual(entity);
|
||||
expect(cache.set).toBeCalledWith(
|
||||
'component:default/test:user:default/test-user',
|
||||
entity,
|
||||
{ ttl: 5000 },
|
||||
);
|
||||
});
|
||||
|
||||
it('returns entities from cache', async () => {
|
||||
identity.authenticate.mockResolvedValue(identityResponse);
|
||||
cache.get.mockResolvedValue(entity);
|
||||
|
||||
const result = await loader.load(entityName, token);
|
||||
|
||||
expect(result).toEqual(entity);
|
||||
expect(catalog.getEntityByName).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('does not cache missing entites', async () => {
|
||||
identity.authenticate.mockResolvedValue(identityResponse);
|
||||
cache.get.mockResolvedValue(undefined);
|
||||
catalog.getEntityByName.mockResolvedValue(undefined);
|
||||
|
||||
const result = await loader.load(entityName, token);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(cache.set).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('transforms 403 responses from catalog to undefined', async () => {
|
||||
identity.authenticate.mockResolvedValue(identityResponse);
|
||||
cache.get.mockResolvedValue(undefined);
|
||||
catalog.getEntityByName.mockRejectedValue(
|
||||
await ResponseError.fromResponse(new Response(null, { status: 403 })),
|
||||
);
|
||||
|
||||
const result = await loader.load(entityName, token);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('uses entity ref as cache key for anonymous users', async () => {
|
||||
cache.get.mockResolvedValue(undefined);
|
||||
catalog.getEntityByName.mockResolvedValue(entity);
|
||||
|
||||
const result = await loader.load(entityName, undefined);
|
||||
|
||||
expect(result).toEqual(entity);
|
||||
expect(cache.set).toBeCalledWith('component:default/test', entity, {
|
||||
ttl: 5000,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2022 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 { CatalogClient } from '@backstage/catalog-client';
|
||||
import { CacheClient } from '@backstage/backend-common';
|
||||
import {
|
||||
Entity,
|
||||
EntityName,
|
||||
stringifyEntityRef,
|
||||
} from '@backstage/catalog-model';
|
||||
import { IdentityClient } from '@backstage/plugin-auth-backend';
|
||||
import { ResponseError } from '@backstage/errors';
|
||||
|
||||
export type CachedEntityLoaderOptions = {
|
||||
catalog: CatalogClient;
|
||||
cache: CacheClient;
|
||||
identity: IdentityClient;
|
||||
};
|
||||
|
||||
export class CachedEntityLoader {
|
||||
private catalog: CatalogClient;
|
||||
private cache: CacheClient;
|
||||
private identity: IdentityClient;
|
||||
|
||||
constructor({ catalog, cache, identity }: CachedEntityLoaderOptions) {
|
||||
this.catalog = catalog;
|
||||
this.cache = cache;
|
||||
this.identity = identity;
|
||||
}
|
||||
|
||||
async load(
|
||||
entityName: EntityName,
|
||||
token: string | undefined,
|
||||
): Promise<Entity | undefined> {
|
||||
const cacheKey = await this.getCacheKey(entityName, token);
|
||||
let result = (await this.cache.get(cacheKey)) as Entity | undefined;
|
||||
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
result = await this.catalog.getEntityByName(entityName, { token });
|
||||
} catch (err) {
|
||||
if (err instanceof ResponseError && err.response.status === 403) {
|
||||
result = undefined;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
if (result) {
|
||||
await this.cache.set(cacheKey, result, { ttl: 5000 });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async getCacheKey(
|
||||
entityName: EntityName,
|
||||
token: string | undefined,
|
||||
): Promise<string> {
|
||||
const entityRef = stringifyEntityRef(entityName);
|
||||
|
||||
if (!token) {
|
||||
return entityRef;
|
||||
}
|
||||
|
||||
const response = await this.identity.authenticate(token);
|
||||
|
||||
return `${entityRef}:${response.identity.userEntityRef}`;
|
||||
}
|
||||
}
|
||||
@@ -2310,6 +2310,54 @@
|
||||
"@material-ui/icons" "^4.9.1"
|
||||
react-router-dom "6.0.0-beta.0"
|
||||
|
||||
"@backstage/backend-common@^0.10.3":
|
||||
version "0.10.3"
|
||||
resolved "https://registry.npmjs.org/@backstage/backend-common/-/backend-common-0.10.3.tgz#32ced91c2585d20d3597a4854004e35c10411a68"
|
||||
integrity sha512-bfAlBzkskmd9o4MTzkh+gN2uQVIXoQUGLJyPRvunRURg5Ki3UY0IJWb812K3vz/JEnUzB1j3zZbqxpwoTup6ow==
|
||||
dependencies:
|
||||
"@backstage/cli-common" "^0.1.6"
|
||||
"@backstage/config" "^0.1.12"
|
||||
"@backstage/config-loader" "^0.9.2"
|
||||
"@backstage/errors" "^0.2.0"
|
||||
"@backstage/integration" "^0.7.1"
|
||||
"@backstage/types" "^0.1.1"
|
||||
"@google-cloud/storage" "^5.8.0"
|
||||
"@manypkg/get-packages" "^1.1.3"
|
||||
"@octokit/rest" "^18.5.3"
|
||||
"@types/cors" "^2.8.6"
|
||||
"@types/dockerode" "^3.3.0"
|
||||
"@types/express" "^4.17.6"
|
||||
archiver "^5.0.2"
|
||||
aws-sdk "^2.840.0"
|
||||
compression "^1.7.4"
|
||||
concat-stream "^2.0.0"
|
||||
cors "^2.8.5"
|
||||
dockerode "^3.3.1"
|
||||
express "^4.17.1"
|
||||
express-promise-router "^4.1.0"
|
||||
fs-extra "9.1.0"
|
||||
git-url-parse "^11.6.0"
|
||||
helmet "^4.0.0"
|
||||
isomorphic-git "^1.8.0"
|
||||
jose "^1.27.1"
|
||||
keyv "^4.0.3"
|
||||
keyv-memcache "^1.2.5"
|
||||
knex "^0.95.1"
|
||||
lodash "^4.17.21"
|
||||
logform "^2.3.2"
|
||||
minimatch "^3.0.4"
|
||||
minimist "^1.2.5"
|
||||
morgan "^1.10.0"
|
||||
node-abort-controller "^3.0.1"
|
||||
node-fetch "^2.6.1"
|
||||
raw-body "^2.4.1"
|
||||
selfsigned "^1.10.7"
|
||||
stoppable "^1.1.0"
|
||||
tar "^6.1.2"
|
||||
unzipper "^0.10.11"
|
||||
winston "^3.2.1"
|
||||
yn "^4.0.0"
|
||||
|
||||
"@backstage/catalog-client@^0.5.4":
|
||||
version "0.5.4"
|
||||
resolved "https://registry.npmjs.org/@backstage/catalog-client/-/catalog-client-0.5.4.tgz#6162bf460ac0f041d4f0f906172733e7a59df6f9"
|
||||
@@ -2335,6 +2383,27 @@
|
||||
uuid "^8.0.0"
|
||||
yup "^0.32.9"
|
||||
|
||||
"@backstage/config-loader@^0.9.2":
|
||||
version "0.9.2"
|
||||
resolved "https://registry.npmjs.org/@backstage/config-loader/-/config-loader-0.9.2.tgz#6e3f0ddcad4a80ead0567442735e2be233979523"
|
||||
integrity sha512-VNdqKFgeHmzzLkhb2M3TYp4D01aVcBCD1+pCv79sEDoJdcTFy7o1LOmneccn+m6d2poy6in/9zr6VsHgZT0uRA==
|
||||
dependencies:
|
||||
"@backstage/cli-common" "^0.1.6"
|
||||
"@backstage/config" "^0.1.12"
|
||||
"@backstage/errors" "^0.2.0"
|
||||
"@backstage/types" "^0.1.1"
|
||||
"@types/json-schema" "^7.0.6"
|
||||
ajv "^7.0.3"
|
||||
chokidar "^3.5.2"
|
||||
fs-extra "9.1.0"
|
||||
json-schema "^0.4.0"
|
||||
json-schema-merge-allof "^0.8.1"
|
||||
json-schema-traverse "^1.0.0"
|
||||
node-fetch "^2.6.1"
|
||||
typescript-json-schema "^0.52.0"
|
||||
yaml "^1.9.2"
|
||||
yup "^0.32.9"
|
||||
|
||||
"@backstage/config@^0.1.11", "@backstage/config@^0.1.12":
|
||||
version "0.1.12"
|
||||
resolved "https://registry.npmjs.org/@backstage/config/-/config-0.1.12.tgz#7b55dd852c7bddb0b9ef55eb58241f6a1d8a5047"
|
||||
@@ -2481,6 +2550,53 @@
|
||||
lodash "^4.17.21"
|
||||
luxon "^2.0.2"
|
||||
|
||||
"@backstage/plugin-auth-backend@^0.6.2":
|
||||
version "0.6.2"
|
||||
resolved "https://registry.npmjs.org/@backstage/plugin-auth-backend/-/plugin-auth-backend-0.6.2.tgz#0180744ac44e7a77e6bbdf237187211dae7060fa"
|
||||
integrity sha512-Wt8ajzLLiUsgYQuwzekLFeDDytP2uQnwu8N/hFhkMx/9xpEWMTUHCfyg3bFJJ28c5ZMHoiKgyNcC1+2LbLI+2w==
|
||||
dependencies:
|
||||
"@backstage/backend-common" "^0.10.3"
|
||||
"@backstage/catalog-client" "^0.5.4"
|
||||
"@backstage/catalog-model" "^0.9.9"
|
||||
"@backstage/config" "^0.1.12"
|
||||
"@backstage/errors" "^0.2.0"
|
||||
"@backstage/types" "^0.1.1"
|
||||
"@google-cloud/firestore" "^4.15.1"
|
||||
"@types/express" "^4.17.6"
|
||||
"@types/passport" "^1.0.3"
|
||||
compression "^1.7.4"
|
||||
cookie-parser "^1.4.5"
|
||||
cors "^2.8.5"
|
||||
express "^4.17.1"
|
||||
express-promise-router "^4.1.0"
|
||||
express-session "^1.17.1"
|
||||
fs-extra "9.1.0"
|
||||
google-auth-library "^7.6.1"
|
||||
helmet "^4.0.0"
|
||||
jose "^1.27.1"
|
||||
jwt-decode "^3.1.0"
|
||||
knex "^0.95.1"
|
||||
lodash "^4.17.21"
|
||||
luxon "^2.0.2"
|
||||
minimatch "^3.0.3"
|
||||
morgan "^1.10.0"
|
||||
node-cache "^5.1.2"
|
||||
node-fetch "^2.6.1"
|
||||
openid-client "^4.2.1"
|
||||
passport "^0.4.1"
|
||||
passport-bitbucket-oauth2 "^0.1.2"
|
||||
passport-github2 "^0.1.12"
|
||||
passport-gitlab2 "^5.0.0"
|
||||
passport-google-oauth20 "^2.0.0"
|
||||
passport-microsoft "^0.1.0"
|
||||
passport-oauth2 "^1.5.0"
|
||||
passport-okta-oauth "^0.0.1"
|
||||
passport-onelogin-oauth "^0.0.1"
|
||||
passport-saml "^3.1.2"
|
||||
uuid "^8.0.0"
|
||||
winston "^3.2.1"
|
||||
yn "^4.0.0"
|
||||
|
||||
"@backstage/plugin-catalog-react@^0.6.11", "@backstage/plugin-catalog-react@^0.6.5":
|
||||
version "0.6.11"
|
||||
resolved "https://registry.npmjs.org/@backstage/plugin-catalog-react/-/plugin-catalog-react-0.6.11.tgz#51cc77237c48a645c889103cddb85dabb2159724"
|
||||
|
||||
Reference in New Issue
Block a user