Merge pull request #3914 from erikxiv/feat/api-auth-client
Add identity token to api requests
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
---
|
||||
'@backstage/catalog-client': patch
|
||||
'@backstage/plugin-auth-backend': patch
|
||||
'@backstage/plugin-catalog': patch
|
||||
'@backstage/plugin-catalog-import': minor
|
||||
'@backstage/plugin-fossa': patch
|
||||
'@backstage/plugin-kubernetes': patch
|
||||
'@backstage/plugin-rollbar': minor
|
||||
'@backstage/plugin-scaffolder': minor
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
Include Backstage identity token in requests to backend plugins.
|
||||
@@ -21,6 +21,7 @@ import { CatalogClient } from './CatalogClient';
|
||||
import { CatalogListResponse, DiscoveryApi } from './types';
|
||||
|
||||
const server = setupServer();
|
||||
const token = 'fake-token';
|
||||
const mockBaseUrl = 'http://backstage:9191/i-am-a-mock-base';
|
||||
const discoveryApi: DiscoveryApi = {
|
||||
async getBaseUrl(_pluginId) {
|
||||
@@ -71,7 +72,7 @@ describe('CatalogClient', () => {
|
||||
});
|
||||
|
||||
it('should entities from correct endpoint', async () => {
|
||||
const response = await client.getEntities();
|
||||
const response = await client.getEntities({}, { token });
|
||||
expect(response).toEqual(defaultResponse);
|
||||
});
|
||||
|
||||
@@ -85,13 +86,16 @@ describe('CatalogClient', () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const response = await client.getEntities({
|
||||
filter: {
|
||||
a: '1',
|
||||
b: ['2', '3'],
|
||||
ö: '=',
|
||||
const response = await client.getEntities(
|
||||
{
|
||||
filter: {
|
||||
a: '1',
|
||||
b: ['2', '3'],
|
||||
ö: '=',
|
||||
},
|
||||
},
|
||||
});
|
||||
{ token },
|
||||
);
|
||||
|
||||
expect(response.items).toEqual([]);
|
||||
});
|
||||
@@ -106,11 +110,61 @@ describe('CatalogClient', () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const response = await client.getEntities({
|
||||
fields: ['a.b', 'ö'],
|
||||
});
|
||||
const response = await client.getEntities(
|
||||
{
|
||||
fields: ['a.b', 'ö'],
|
||||
},
|
||||
{ token },
|
||||
);
|
||||
|
||||
expect(response.items).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLocationById', () => {
|
||||
const defaultResponse = {
|
||||
data: {
|
||||
id: '42',
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
server.use(
|
||||
rest.get(`${mockBaseUrl}/locations/42`, (_, res, ctx) => {
|
||||
return res(ctx.json(defaultResponse));
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should locations from correct endpoint', async () => {
|
||||
const response = await client.getLocationById('42', { token });
|
||||
expect(response).toEqual(defaultResponse);
|
||||
});
|
||||
|
||||
it('forwards authorization token', async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
server.use(
|
||||
rest.get(`${mockBaseUrl}/locations/42`, (req, res, ctx) => {
|
||||
expect(req.headers.get('authorization')).toBe(`Bearer ${token}`);
|
||||
return res(ctx.json(defaultResponse));
|
||||
}),
|
||||
);
|
||||
|
||||
await client.getLocationById('42', { token });
|
||||
});
|
||||
|
||||
it('skips authorization header if token is omitted', async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
server.use(
|
||||
rest.get(`${mockBaseUrl}/locations/42`, (req, res, ctx) => {
|
||||
expect(req.headers.get('authorization')).toBeNull();
|
||||
return res(ctx.json(defaultResponse));
|
||||
}),
|
||||
);
|
||||
|
||||
await client.getLocationById('42');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,6 +24,7 @@ import fetch from 'cross-fetch';
|
||||
import {
|
||||
AddLocationRequest,
|
||||
AddLocationResponse,
|
||||
CatalogRequestOptions,
|
||||
CatalogApi,
|
||||
CatalogEntitiesRequest,
|
||||
CatalogListResponse,
|
||||
@@ -37,12 +38,16 @@ export class CatalogClient implements CatalogApi {
|
||||
this.discoveryApi = options.discoveryApi;
|
||||
}
|
||||
|
||||
async getLocationById(id: String): Promise<Location | undefined> {
|
||||
return await this.getOptional(`/locations/${id}`);
|
||||
async getLocationById(
|
||||
id: String,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<Location | undefined> {
|
||||
return await this.getOptional(`/locations/${id}`, options);
|
||||
}
|
||||
|
||||
async getEntities(
|
||||
request?: CatalogEntitiesRequest,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<CatalogListResponse<Entity>> {
|
||||
const { filter = {}, fields = [] } = request ?? {};
|
||||
const params: string[] = [];
|
||||
@@ -62,21 +67,28 @@ export class CatalogClient implements CatalogApi {
|
||||
}
|
||||
|
||||
const query = params.length ? `?${params.join('&')}` : '';
|
||||
const entities: Entity[] = await this.getRequired(`/entities${query}`);
|
||||
const entities: Entity[] = await this.getRequired(
|
||||
`/entities${query}`,
|
||||
options,
|
||||
);
|
||||
return { items: entities };
|
||||
}
|
||||
|
||||
async getEntityByName(compoundName: EntityName): Promise<Entity | undefined> {
|
||||
async getEntityByName(
|
||||
compoundName: EntityName,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<Entity | undefined> {
|
||||
const { kind, namespace = 'default', name } = compoundName;
|
||||
return this.getOptional(`/entities/by-name/${kind}/${namespace}/${name}`);
|
||||
return this.getOptional(
|
||||
`/entities/by-name/${kind}/${namespace}/${name}`,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
async addLocation({
|
||||
type = 'url',
|
||||
target,
|
||||
dryRun,
|
||||
presence,
|
||||
}: AddLocationRequest): Promise<AddLocationResponse> {
|
||||
async addLocation(
|
||||
{ type = 'url', target, dryRun, presence }: AddLocationRequest,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<AddLocationResponse> {
|
||||
const response = await fetch(
|
||||
`${await this.discoveryApi.getBaseUrl('catalog')}/locations${
|
||||
dryRun ? '?dryRun=true' : ''
|
||||
@@ -84,6 +96,7 @@ export class CatalogClient implements CatalogApi {
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(options?.token && { Authorization: `Bearer ${options?.token}` }),
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ type, target, presence }),
|
||||
@@ -111,18 +124,30 @@ export class CatalogClient implements CatalogApi {
|
||||
};
|
||||
}
|
||||
|
||||
async getLocationByEntity(entity: Entity): Promise<Location | undefined> {
|
||||
async getLocationByEntity(
|
||||
entity: Entity,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<Location | undefined> {
|
||||
const locationCompound = entity.metadata.annotations?.[LOCATION_ANNOTATION];
|
||||
const all: { data: Location }[] = await this.getRequired('/locations');
|
||||
const all: { data: Location }[] = await this.getRequired(
|
||||
'/locations',
|
||||
options,
|
||||
);
|
||||
return all
|
||||
.map(r => r.data)
|
||||
.find(l => locationCompound === `${l.type}:${l.target}`);
|
||||
}
|
||||
|
||||
async removeEntityByUid(uid: string): Promise<void> {
|
||||
async removeEntityByUid(
|
||||
uid: string,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<void> {
|
||||
const response = await fetch(
|
||||
`${await this.discoveryApi.getBaseUrl('catalog')}/entities/by-uid/${uid}`,
|
||||
{
|
||||
headers: options?.token
|
||||
? { Authorization: `Bearer ${options.token}` }
|
||||
: {},
|
||||
method: 'DELETE',
|
||||
},
|
||||
);
|
||||
@@ -139,9 +164,16 @@ export class CatalogClient implements CatalogApi {
|
||||
// Private methods
|
||||
//
|
||||
|
||||
private async getRequired(path: string): Promise<any> {
|
||||
private async getRequired(
|
||||
path: string,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<any> {
|
||||
const url = `${await this.discoveryApi.getBaseUrl('catalog')}${path}`;
|
||||
const response = await fetch(url);
|
||||
const response = await fetch(url, {
|
||||
headers: options?.token
|
||||
? { Authorization: `Bearer ${options.token}` }
|
||||
: {},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const payload = await response.text();
|
||||
@@ -152,9 +184,16 @@ export class CatalogClient implements CatalogApi {
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
private async getOptional(path: string): Promise<any | undefined> {
|
||||
private async getOptional(
|
||||
path: string,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<any | undefined> {
|
||||
const url = `${await this.discoveryApi.getBaseUrl('catalog')}${path}`;
|
||||
const response = await fetch(url);
|
||||
const response = await fetch(url, {
|
||||
headers: options?.token
|
||||
? { Authorization: `Bearer ${options.token}` }
|
||||
: {},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
|
||||
@@ -15,4 +15,10 @@
|
||||
*/
|
||||
|
||||
export { CatalogClient } from './CatalogClient';
|
||||
export type { CatalogApi } from './types';
|
||||
export type {
|
||||
AddLocationRequest,
|
||||
AddLocationResponse,
|
||||
CatalogApi,
|
||||
CatalogEntitiesRequest,
|
||||
CatalogListResponse,
|
||||
} from './types';
|
||||
|
||||
@@ -25,15 +25,35 @@ export type CatalogListResponse<T> = {
|
||||
items: T[];
|
||||
};
|
||||
|
||||
export type CatalogRequestOptions = {
|
||||
token?: string;
|
||||
};
|
||||
|
||||
export interface CatalogApi {
|
||||
getLocationById(id: String): Promise<Location | undefined>;
|
||||
getEntityByName(name: EntityName): Promise<Entity | undefined>;
|
||||
getLocationById(
|
||||
id: String,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<Location | undefined>;
|
||||
getEntityByName(
|
||||
name: EntityName,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<Entity | undefined>;
|
||||
getEntities(
|
||||
request?: CatalogEntitiesRequest,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<CatalogListResponse<Entity>>;
|
||||
addLocation(location: AddLocationRequest): Promise<AddLocationResponse>;
|
||||
getLocationByEntity(entity: Entity): Promise<Location | undefined>;
|
||||
removeEntityByUid(uid: string): Promise<void>;
|
||||
addLocation(
|
||||
location: AddLocationRequest,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<AddLocationResponse>;
|
||||
getLocationByEntity(
|
||||
entity: Entity,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<Location | undefined>;
|
||||
removeEntityByUid(
|
||||
uid: string,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
export type AddLocationRequest = {
|
||||
|
||||
@@ -38,11 +38,14 @@ describe('CatalogIdentityClient', () => {
|
||||
|
||||
client.findUser({ annotations: { key: 'value' } });
|
||||
|
||||
expect(catalogApi.getEntities).toBeCalledWith({
|
||||
filter: {
|
||||
kind: 'user',
|
||||
'metadata.annotations.key': 'value',
|
||||
expect(catalogApi.getEntities).toBeCalledWith(
|
||||
{
|
||||
filter: {
|
||||
kind: 'user',
|
||||
'metadata.annotations.key': 'value',
|
||||
},
|
||||
},
|
||||
});
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,7 +37,10 @@ export class CatalogIdentityClient {
|
||||
*
|
||||
* Throws a NotFoundError or ConflictError if 0 or multiple users are found.
|
||||
*/
|
||||
async findUser(query: UserQuery): Promise<UserEntity> {
|
||||
async findUser(
|
||||
query: UserQuery,
|
||||
options?: { token?: string },
|
||||
): Promise<UserEntity> {
|
||||
const filter: Record<string, string> = {
|
||||
kind: 'user',
|
||||
};
|
||||
@@ -45,7 +48,7 @@ export class CatalogIdentityClient {
|
||||
filter[`metadata.annotations.${key}`] = value;
|
||||
}
|
||||
|
||||
const { items } = await this.catalogApi.getEntities({ filter });
|
||||
const { items } = await this.catalogApi.getEntities({ filter }, options);
|
||||
|
||||
if (items.length !== 1) {
|
||||
if (items.length > 1) {
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
PassportDoneCallback,
|
||||
} from '../../lib/passport';
|
||||
import { AuthProviderFactory, RedirectInfo } from '../types';
|
||||
import { TokenIssuer } from '../../identity';
|
||||
|
||||
type PrivateInfo = {
|
||||
refreshToken: string;
|
||||
@@ -46,16 +47,19 @@ type PrivateInfo = {
|
||||
export type GoogleAuthProviderOptions = OAuthProviderOptions & {
|
||||
logger: Logger;
|
||||
identityClient: CatalogIdentityClient;
|
||||
tokenIssuer: TokenIssuer;
|
||||
};
|
||||
|
||||
export class GoogleAuthProvider implements OAuthHandlers {
|
||||
private readonly _strategy: GoogleStrategy;
|
||||
private readonly logger: Logger;
|
||||
private readonly identityClient: CatalogIdentityClient;
|
||||
private readonly tokenIssuer: TokenIssuer;
|
||||
|
||||
constructor(options: GoogleAuthProviderOptions) {
|
||||
this.logger = options.logger;
|
||||
this.identityClient = options.identityClient;
|
||||
this.tokenIssuer = options.tokenIssuer;
|
||||
// TODO: throw error if env variables not set?
|
||||
this._strategy = new GoogleStrategy(
|
||||
{
|
||||
@@ -150,11 +154,17 @@ export class GoogleAuthProvider implements OAuthHandlers {
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await this.identityClient.findUser({
|
||||
annotations: {
|
||||
'google.com/email': profile.email,
|
||||
},
|
||||
const token = await this.tokenIssuer.issueToken({
|
||||
claims: { sub: 'backstage.io/auth-backend' },
|
||||
});
|
||||
const user = await this.identityClient.findUser(
|
||||
{
|
||||
annotations: {
|
||||
'google.com/email': profile.email,
|
||||
},
|
||||
},
|
||||
{ token },
|
||||
);
|
||||
|
||||
return {
|
||||
...response,
|
||||
@@ -192,6 +202,7 @@ export const createGoogleProvider: AuthProviderFactory = ({
|
||||
clientSecret,
|
||||
callbackUrl,
|
||||
logger,
|
||||
tokenIssuer,
|
||||
identityClient: new CatalogIdentityClient({ catalogApi }),
|
||||
});
|
||||
|
||||
|
||||
@@ -72,6 +72,20 @@ describe('CatalogImportClient', () => {
|
||||
const githubAuthApi: jest.Mocked<OAuthApi> = {
|
||||
getAccessToken: jest.fn(),
|
||||
};
|
||||
const identityApi = {
|
||||
getUserId: () => {
|
||||
return 'user';
|
||||
},
|
||||
getProfile: () => {
|
||||
return {};
|
||||
},
|
||||
getIdToken: () => {
|
||||
return Promise.resolve('token');
|
||||
},
|
||||
signOut: () => {
|
||||
return Promise.resolve();
|
||||
},
|
||||
};
|
||||
|
||||
const configApi = new ConfigReader({});
|
||||
|
||||
@@ -92,6 +106,7 @@ describe('CatalogImportClient', () => {
|
||||
discoveryApi,
|
||||
githubAuthApi,
|
||||
configApi,
|
||||
identityApi,
|
||||
catalogApi,
|
||||
});
|
||||
|
||||
|
||||
@@ -16,7 +16,12 @@
|
||||
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
import { EntityName } from '@backstage/catalog-model';
|
||||
import { ConfigApi, DiscoveryApi, OAuthApi } from '@backstage/core';
|
||||
import {
|
||||
ConfigApi,
|
||||
DiscoveryApi,
|
||||
IdentityApi,
|
||||
OAuthApi,
|
||||
} from '@backstage/core';
|
||||
import { GitHubIntegrationConfig } from '@backstage/integration';
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import { PartialEntity } from '../types';
|
||||
@@ -25,6 +30,7 @@ import { getGithubIntegrationConfig } from './GitHub';
|
||||
|
||||
export class CatalogImportClient implements CatalogImportApi {
|
||||
private readonly discoveryApi: DiscoveryApi;
|
||||
private readonly identityApi: IdentityApi;
|
||||
private readonly githubAuthApi: OAuthApi;
|
||||
private readonly configApi: ConfigApi;
|
||||
private readonly catalogApi: CatalogApi;
|
||||
@@ -32,11 +38,13 @@ export class CatalogImportClient implements CatalogImportApi {
|
||||
constructor(options: {
|
||||
discoveryApi: DiscoveryApi;
|
||||
githubAuthApi: OAuthApi;
|
||||
identityApi: IdentityApi;
|
||||
configApi: ConfigApi;
|
||||
catalogApi: CatalogApi;
|
||||
}) {
|
||||
this.discoveryApi = options.discoveryApi;
|
||||
this.githubAuthApi = options.githubAuthApi;
|
||||
this.identityApi = options.identityApi;
|
||||
this.configApi = options.configApi;
|
||||
this.catalogApi = options.catalogApi;
|
||||
}
|
||||
@@ -124,11 +132,13 @@ export class CatalogImportClient implements CatalogImportApi {
|
||||
}: {
|
||||
repo: string;
|
||||
}): Promise<PartialEntity[]> {
|
||||
const idToken = await this.identityApi.getIdToken();
|
||||
const response = await fetch(
|
||||
`${await this.discoveryApi.getBaseUrl('catalog')}/analyze-location`,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(idToken && { Authorization: `Bearer ${idToken}` }),
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -29,6 +29,21 @@ import { catalogImportApiRef, CatalogImportClient } from '../api';
|
||||
import { ImportComponentPage } from './ImportComponentPage';
|
||||
|
||||
describe('<ImportComponentPage />', () => {
|
||||
const identityApi = {
|
||||
getUserId: () => {
|
||||
return 'user';
|
||||
},
|
||||
getProfile: () => {
|
||||
return {};
|
||||
},
|
||||
getIdToken: () => {
|
||||
return Promise.resolve('token');
|
||||
},
|
||||
signOut: () => {
|
||||
return Promise.resolve();
|
||||
},
|
||||
};
|
||||
|
||||
let apis: ApiRegistry;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -41,7 +56,10 @@ describe('<ImportComponentPage />', () => {
|
||||
catalogImportApiRef,
|
||||
new CatalogImportClient({
|
||||
discoveryApi: {} as any,
|
||||
githubAuthApi: {} as any,
|
||||
githubAuthApi: {
|
||||
getAccessToken: async () => 'token',
|
||||
},
|
||||
identityApi,
|
||||
configApi: {} as any,
|
||||
catalogApi: {} as any,
|
||||
}),
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
identityApiRef,
|
||||
configApiRef,
|
||||
createApiFactory,
|
||||
createPlugin,
|
||||
@@ -39,14 +40,22 @@ export const catalogImportPlugin = createPlugin({
|
||||
deps: {
|
||||
discoveryApi: discoveryApiRef,
|
||||
githubAuthApi: githubAuthApiRef,
|
||||
identityApi: identityApiRef,
|
||||
configApi: configApiRef,
|
||||
catalogApi: catalogApiRef,
|
||||
},
|
||||
factory: ({ discoveryApi, githubAuthApi, configApi, catalogApi }) =>
|
||||
factory: ({
|
||||
discoveryApi,
|
||||
githubAuthApi,
|
||||
identityApi,
|
||||
configApi,
|
||||
catalogApi,
|
||||
}) =>
|
||||
new CatalogImportClient({
|
||||
discoveryApi,
|
||||
githubAuthApi,
|
||||
configApi,
|
||||
identityApi,
|
||||
catalogApi,
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { CatalogClient } from '@backstage/catalog-client';
|
||||
import { DiscoveryApi, IdentityApi } from '@backstage/core';
|
||||
import { CatalogClientWrapper } from './CatalogClientWrapper';
|
||||
|
||||
jest.mock('@backstage/catalog-client');
|
||||
const MockedCatalogClient = CatalogClient as jest.Mock<CatalogClient>;
|
||||
|
||||
const mockBaseUrl = 'http://backstage:9191/i-am-a-mock-base';
|
||||
const discoveryApi: DiscoveryApi = {
|
||||
async getBaseUrl(_pluginId) {
|
||||
return mockBaseUrl;
|
||||
},
|
||||
};
|
||||
const identityApi: IdentityApi = {
|
||||
getUserId() {
|
||||
return 'jane-fonda';
|
||||
},
|
||||
getProfile() {
|
||||
return { email: 'jane-fonda@spotify.com' };
|
||||
},
|
||||
async getIdToken() {
|
||||
return Promise.resolve('fake-id-token');
|
||||
},
|
||||
async signOut() {
|
||||
return Promise.resolve();
|
||||
},
|
||||
};
|
||||
const guestIdentityApi: IdentityApi = {
|
||||
getUserId() {
|
||||
return 'guest';
|
||||
},
|
||||
getProfile() {
|
||||
return {};
|
||||
},
|
||||
async getIdToken() {
|
||||
return Promise.resolve(undefined);
|
||||
},
|
||||
async signOut() {
|
||||
return Promise.resolve();
|
||||
},
|
||||
};
|
||||
|
||||
describe('CatalogClientWrapper', () => {
|
||||
beforeEach(() => {
|
||||
MockedCatalogClient.mockClear();
|
||||
});
|
||||
|
||||
describe('getEntities', () => {
|
||||
it('injects authorization token', async () => {
|
||||
const client = new CatalogClientWrapper({
|
||||
client: new MockedCatalogClient({ discoveryApi }),
|
||||
identityApi,
|
||||
});
|
||||
await client.getEntities();
|
||||
const getEntities = MockedCatalogClient.mock.instances[0].getEntities;
|
||||
expect(getEntities).toHaveBeenCalledWith(undefined, {
|
||||
token: 'fake-id-token',
|
||||
});
|
||||
expect(getEntities).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLocationById', () => {
|
||||
it('omits authorization token when guest', async () => {
|
||||
const client = new CatalogClientWrapper({
|
||||
client: new MockedCatalogClient({ discoveryApi }),
|
||||
identityApi: guestIdentityApi,
|
||||
});
|
||||
await client.getLocationById('42');
|
||||
const getLocationById =
|
||||
MockedCatalogClient.mock.instances[0].getLocationById;
|
||||
expect(getLocationById).toHaveBeenCalledWith('42', {});
|
||||
expect(getLocationById).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEntityByName', () => {
|
||||
const name = {
|
||||
kind: 'kind',
|
||||
namespace: 'namespace',
|
||||
name: 'name',
|
||||
};
|
||||
it('injects authorization token', async () => {
|
||||
const client = new CatalogClientWrapper({
|
||||
client: new MockedCatalogClient({ discoveryApi }),
|
||||
identityApi,
|
||||
});
|
||||
await client.getEntityByName(name);
|
||||
const getEntityByName =
|
||||
MockedCatalogClient.mock.instances[0].getEntityByName;
|
||||
expect(getEntityByName).toHaveBeenCalledWith(name, {
|
||||
token: 'fake-id-token',
|
||||
});
|
||||
expect(getEntityByName).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addLocation', () => {
|
||||
const location = { target: 'target' };
|
||||
it('injects authorization token', async () => {
|
||||
const client = new CatalogClientWrapper({
|
||||
client: new MockedCatalogClient({ discoveryApi }),
|
||||
identityApi,
|
||||
});
|
||||
await client.addLocation(location);
|
||||
const addLocation = MockedCatalogClient.mock.instances[0].addLocation;
|
||||
expect(addLocation).toHaveBeenCalledWith(location, {
|
||||
token: 'fake-id-token',
|
||||
});
|
||||
expect(addLocation).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLocationByEntity', () => {
|
||||
const entity = {
|
||||
apiVersion: 'apiVersion',
|
||||
kind: 'kind',
|
||||
metadata: {
|
||||
name: 'name',
|
||||
},
|
||||
};
|
||||
it('injects authorization token', async () => {
|
||||
const client = new CatalogClientWrapper({
|
||||
client: new MockedCatalogClient({ discoveryApi }),
|
||||
identityApi,
|
||||
});
|
||||
await client.getLocationByEntity(entity);
|
||||
const getLocationByEntity =
|
||||
MockedCatalogClient.mock.instances[0].getLocationByEntity;
|
||||
expect(getLocationByEntity).toHaveBeenCalledWith(entity, {
|
||||
token: 'fake-id-token',
|
||||
});
|
||||
expect(getLocationByEntity).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeEntityByUid', () => {
|
||||
it('injects authorization token', async () => {
|
||||
const uid = 'uid';
|
||||
const client = new CatalogClientWrapper({
|
||||
client: new MockedCatalogClient({ discoveryApi }),
|
||||
identityApi,
|
||||
});
|
||||
await client.removeEntityByUid(uid);
|
||||
const removeEntityByUid =
|
||||
MockedCatalogClient.mock.instances[0].removeEntityByUid;
|
||||
expect(removeEntityByUid).toHaveBeenCalledWith(uid, {
|
||||
token: 'fake-id-token',
|
||||
});
|
||||
expect(removeEntityByUid).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity, EntityName, Location } from '@backstage/catalog-model';
|
||||
import {
|
||||
AddLocationRequest,
|
||||
AddLocationResponse,
|
||||
CatalogApi,
|
||||
CatalogEntitiesRequest,
|
||||
CatalogListResponse,
|
||||
CatalogClient,
|
||||
} from '@backstage/catalog-client';
|
||||
import { IdentityApi } from '@backstage/core';
|
||||
|
||||
type CatalogRequestOptions = {
|
||||
token?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* CatalogClient wrapper that injects identity token for all requests
|
||||
*/
|
||||
export class CatalogClientWrapper implements CatalogApi {
|
||||
private readonly identityApi: IdentityApi;
|
||||
private readonly client: CatalogClient;
|
||||
|
||||
constructor(options: { client: CatalogClient; identityApi: IdentityApi }) {
|
||||
this.client = options.client;
|
||||
this.identityApi = options.identityApi;
|
||||
}
|
||||
|
||||
async getLocationById(
|
||||
id: String,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<Location | undefined> {
|
||||
return await this.client.getLocationById(id, {
|
||||
token: options?.token ?? (await this.identityApi.getIdToken()),
|
||||
});
|
||||
}
|
||||
|
||||
async getEntities(
|
||||
request?: CatalogEntitiesRequest,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<CatalogListResponse<Entity>> {
|
||||
return await this.client.getEntities(request, {
|
||||
token: options?.token ?? (await this.identityApi.getIdToken()),
|
||||
});
|
||||
}
|
||||
|
||||
async getEntityByName(
|
||||
compoundName: EntityName,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<Entity | undefined> {
|
||||
return await this.client.getEntityByName(compoundName, {
|
||||
token: options?.token ?? (await this.identityApi.getIdToken()),
|
||||
});
|
||||
}
|
||||
|
||||
async addLocation(
|
||||
request: AddLocationRequest,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<AddLocationResponse> {
|
||||
return await this.client.addLocation(request, {
|
||||
token: options?.token ?? (await this.identityApi.getIdToken()),
|
||||
});
|
||||
}
|
||||
|
||||
async getLocationByEntity(
|
||||
entity: Entity,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<Location | undefined> {
|
||||
return await this.client.getLocationByEntity(entity, {
|
||||
token: options?.token ?? (await this.identityApi.getIdToken()),
|
||||
});
|
||||
}
|
||||
|
||||
async removeEntityByUid(
|
||||
uid: string,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<void> {
|
||||
return await this.client.removeEntityByUid(uid, {
|
||||
token: options?.token ?? (await this.identityApi.getIdToken()),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -20,20 +20,26 @@ import {
|
||||
createPlugin,
|
||||
discoveryApiRef,
|
||||
createRoutableExtension,
|
||||
identityApiRef,
|
||||
} from '@backstage/core';
|
||||
import {
|
||||
catalogApiRef,
|
||||
catalogRouteRef,
|
||||
entityRouteRef,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { CatalogClientWrapper } from './CatalogClientWrapper';
|
||||
|
||||
export const catalogPlugin = createPlugin({
|
||||
id: 'catalog',
|
||||
apis: [
|
||||
createApiFactory({
|
||||
api: catalogApiRef,
|
||||
deps: { discoveryApi: discoveryApiRef },
|
||||
factory: ({ discoveryApi }) => new CatalogClient({ discoveryApi }),
|
||||
deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef },
|
||||
factory: ({ discoveryApi, identityApi }) =>
|
||||
new CatalogClientWrapper({
|
||||
client: new CatalogClient({ discoveryApi }),
|
||||
identityApi,
|
||||
}),
|
||||
}),
|
||||
],
|
||||
routes: {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { UrlPatternDiscovery } from '@backstage/core';
|
||||
import { UrlPatternDiscovery, IdentityApi } from '@backstage/core';
|
||||
import { msw } from '@backstage/test-utils';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
@@ -22,6 +22,21 @@ import { FindingSummary, FossaApi, FossaClient } from './index';
|
||||
|
||||
const server = setupServer();
|
||||
|
||||
const identityApi: IdentityApi = {
|
||||
getUserId() {
|
||||
return 'jane-fonda';
|
||||
},
|
||||
getProfile() {
|
||||
return { email: 'jane-fonda@spotify.com' };
|
||||
},
|
||||
async getIdToken() {
|
||||
return Promise.resolve('fake-id-token');
|
||||
},
|
||||
async signOut() {
|
||||
return Promise.resolve();
|
||||
},
|
||||
};
|
||||
|
||||
describe('FossaClient', () => {
|
||||
msw.setupDefaultHandlers(server);
|
||||
|
||||
@@ -30,7 +45,11 @@ describe('FossaClient', () => {
|
||||
let client: FossaApi;
|
||||
|
||||
beforeEach(() => {
|
||||
client = new FossaClient({ discoveryApi, organizationId: '8736' });
|
||||
client = new FossaClient({
|
||||
discoveryApi,
|
||||
identityApi,
|
||||
organizationId: '8736',
|
||||
});
|
||||
});
|
||||
|
||||
it('should report finding summary', async () => {
|
||||
@@ -137,7 +156,7 @@ describe('FossaClient', () => {
|
||||
});
|
||||
|
||||
it('should skip organizationId', async () => {
|
||||
client = new FossaClient({ discoveryApi });
|
||||
client = new FossaClient({ discoveryApi, identityApi });
|
||||
|
||||
server.use(
|
||||
rest.get(`${mockBaseUrl}/fossa/projects`, (req, res, ctx) => {
|
||||
|
||||
@@ -14,28 +14,35 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { DiscoveryApi } from '@backstage/core';
|
||||
import { DiscoveryApi, IdentityApi } from '@backstage/core';
|
||||
import fetch from 'cross-fetch';
|
||||
import { FindingSummary, FossaApi } from './FossaApi';
|
||||
|
||||
export class FossaClient implements FossaApi {
|
||||
discoveryApi: DiscoveryApi;
|
||||
identityApi: IdentityApi;
|
||||
organizationId?: string;
|
||||
|
||||
constructor({
|
||||
discoveryApi,
|
||||
identityApi,
|
||||
organizationId,
|
||||
}: {
|
||||
discoveryApi: DiscoveryApi;
|
||||
identityApi: IdentityApi;
|
||||
organizationId?: string;
|
||||
}) {
|
||||
this.discoveryApi = discoveryApi;
|
||||
this.identityApi = identityApi;
|
||||
this.organizationId = organizationId;
|
||||
}
|
||||
|
||||
private async callApi(path: string): Promise<any> {
|
||||
const apiUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/fossa`;
|
||||
const response = await fetch(`${apiUrl}/${path}`);
|
||||
const idToken = await this.identityApi.getIdToken();
|
||||
const response = await fetch(`${apiUrl}/${path}`, {
|
||||
headers: idToken ? { Authorization: `Bearer ${idToken}` } : {},
|
||||
});
|
||||
if (response.status === 200) {
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
createApiFactory,
|
||||
createPlugin,
|
||||
discoveryApiRef,
|
||||
identityApiRef,
|
||||
} from '@backstage/core';
|
||||
import { fossaApiRef, FossaClient } from './api';
|
||||
|
||||
@@ -27,10 +28,15 @@ export const fossaPlugin = createPlugin({
|
||||
apis: [
|
||||
createApiFactory({
|
||||
api: fossaApiRef,
|
||||
deps: { configApi: configApiRef, discoveryApi: discoveryApiRef },
|
||||
factory: ({ configApi, discoveryApi }) =>
|
||||
deps: {
|
||||
configApi: configApiRef,
|
||||
discoveryApi: discoveryApiRef,
|
||||
identityApi: identityApiRef,
|
||||
},
|
||||
factory: ({ configApi, discoveryApi, identityApi }) =>
|
||||
new FossaClient({
|
||||
discoveryApi,
|
||||
identityApi,
|
||||
organizationId: configApi.getOptionalString('fossa.organizationId'),
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { DiscoveryApi } from '@backstage/core';
|
||||
import { DiscoveryApi, IdentityApi } from '@backstage/core';
|
||||
import { KubernetesApi } from './types';
|
||||
import {
|
||||
KubernetesRequestBody,
|
||||
@@ -23,9 +23,14 @@ import {
|
||||
|
||||
export class KubernetesBackendClient implements KubernetesApi {
|
||||
private readonly discoveryApi: DiscoveryApi;
|
||||
private readonly identityApi: IdentityApi;
|
||||
|
||||
constructor(options: { discoveryApi: DiscoveryApi }) {
|
||||
constructor(options: {
|
||||
discoveryApi: DiscoveryApi;
|
||||
identityApi: IdentityApi;
|
||||
}) {
|
||||
this.discoveryApi = options.discoveryApi;
|
||||
this.identityApi = options.identityApi;
|
||||
}
|
||||
|
||||
private async getRequired(
|
||||
@@ -33,10 +38,12 @@ export class KubernetesBackendClient implements KubernetesApi {
|
||||
requestBody: KubernetesRequestBody,
|
||||
): Promise<any> {
|
||||
const url = `${await this.discoveryApi.getBaseUrl('kubernetes')}${path}`;
|
||||
const idToken = await this.identityApi.getIdToken();
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(idToken && { Authorization: `Bearer ${idToken}` }),
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
createPlugin,
|
||||
createRouteRef,
|
||||
discoveryApiRef,
|
||||
identityApiRef,
|
||||
googleAuthApiRef,
|
||||
createRoutableExtension,
|
||||
} from '@backstage/core';
|
||||
@@ -36,9 +37,9 @@ export const kubernetesPlugin = createPlugin({
|
||||
apis: [
|
||||
createApiFactory({
|
||||
api: kubernetesApiRef,
|
||||
deps: { discoveryApi: discoveryApiRef },
|
||||
factory: ({ discoveryApi }) =>
|
||||
new KubernetesBackendClient({ discoveryApi }),
|
||||
deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef },
|
||||
factory: ({ discoveryApi, identityApi }) =>
|
||||
new KubernetesBackendClient({ discoveryApi, identityApi }),
|
||||
}),
|
||||
createApiFactory({
|
||||
api: kubernetesAuthProvidersApiRef,
|
||||
|
||||
@@ -20,13 +20,18 @@ import {
|
||||
RollbarProject,
|
||||
RollbarTopActiveItem,
|
||||
} from './types';
|
||||
import { DiscoveryApi } from '@backstage/core';
|
||||
import { DiscoveryApi, IdentityApi } from '@backstage/core';
|
||||
|
||||
export class RollbarClient implements RollbarApi {
|
||||
private readonly discoveryApi: DiscoveryApi;
|
||||
private readonly identityApi: IdentityApi;
|
||||
|
||||
constructor(options: { discoveryApi: DiscoveryApi }) {
|
||||
constructor(options: {
|
||||
discoveryApi: DiscoveryApi;
|
||||
identityApi: IdentityApi;
|
||||
}) {
|
||||
this.discoveryApi = options.discoveryApi;
|
||||
this.identityApi = options.identityApi;
|
||||
}
|
||||
|
||||
async getAllProjects(): Promise<RollbarProject[]> {
|
||||
@@ -53,7 +58,10 @@ export class RollbarClient implements RollbarApi {
|
||||
|
||||
private async get(path: string): Promise<any> {
|
||||
const url = `${await this.discoveryApi.getBaseUrl('rollbar')}${path}`;
|
||||
const response = await fetch(url);
|
||||
const idToken = await this.identityApi.getIdToken();
|
||||
const response = await fetch(url, {
|
||||
headers: idToken ? { Authorization: `Bearer ${idToken}` } : {},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const payload = await response.text();
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
createRoutableExtension,
|
||||
createRouteRef,
|
||||
discoveryApiRef,
|
||||
identityApiRef,
|
||||
} from '@backstage/core';
|
||||
import { rollbarApiRef } from './api/RollbarApi';
|
||||
import { RollbarClient } from './api/RollbarClient';
|
||||
@@ -34,8 +35,9 @@ export const rollbarPlugin = createPlugin({
|
||||
apis: [
|
||||
createApiFactory({
|
||||
api: rollbarApiRef,
|
||||
deps: { discoveryApi: discoveryApiRef },
|
||||
factory: ({ discoveryApi }) => new RollbarClient({ discoveryApi }),
|
||||
deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef },
|
||||
factory: ({ discoveryApi, identityApi }) =>
|
||||
new RollbarClient({ discoveryApi, identityApi }),
|
||||
}),
|
||||
],
|
||||
routes: {
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.5.2",
|
||||
"@backstage/catalog-client": "^0.3.5",
|
||||
"@backstage/catalog-model": "^0.7.1",
|
||||
"@backstage/config": "^0.1.2",
|
||||
"@backstage/integration": "^0.3.2",
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import fetch from 'cross-fetch';
|
||||
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
|
||||
import { CatalogClient } from '@backstage/catalog-client';
|
||||
import {
|
||||
ConflictError,
|
||||
NotFoundError,
|
||||
@@ -26,10 +26,12 @@ import {
|
||||
* A catalog client tailored for reading out entity data from the catalog.
|
||||
*/
|
||||
export class CatalogEntityClient {
|
||||
private readonly discovery: PluginEndpointDiscovery;
|
||||
private readonly catalogClient: CatalogClient;
|
||||
|
||||
constructor(options: { discovery: PluginEndpointDiscovery }) {
|
||||
this.discovery = options.discovery;
|
||||
this.catalogClient = new CatalogClient({
|
||||
discoveryApi: options.discovery,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,25 +39,19 @@ export class CatalogEntityClient {
|
||||
*
|
||||
* Throws a NotFoundError or ConflictError if 0 or multiple templates are found.
|
||||
*/
|
||||
async findTemplate(templateName: string): Promise<TemplateEntityV1alpha1> {
|
||||
const conditions = [
|
||||
'kind=template',
|
||||
`metadata.name=${encodeURIComponent(templateName)}`,
|
||||
];
|
||||
|
||||
const baseUrl = await this.discovery.getBaseUrl('catalog');
|
||||
const response = await fetch(
|
||||
`${baseUrl}/entities?filter=${conditions.join(',')}`,
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(
|
||||
`Request failed with ${response.status} ${response.statusText}, ${text}`,
|
||||
);
|
||||
}
|
||||
|
||||
const templates: TemplateEntityV1alpha1[] = await response.json();
|
||||
async findTemplate(
|
||||
templateName: string,
|
||||
options?: { token?: string },
|
||||
): Promise<TemplateEntityV1alpha1> {
|
||||
const { items: templates } = (await this.catalogClient.getEntities(
|
||||
{
|
||||
filter: {
|
||||
kind: 'template',
|
||||
'metadata.name': templateName,
|
||||
},
|
||||
},
|
||||
options,
|
||||
)) as { items: TemplateEntityV1alpha1[] };
|
||||
|
||||
if (templates.length !== 1) {
|
||||
if (templates.length > 1) {
|
||||
|
||||
@@ -133,7 +133,10 @@ export async function createRouter(
|
||||
},
|
||||
};
|
||||
|
||||
const template = await entityClient.findTemplate(templateName);
|
||||
// Forward authorization from client
|
||||
const template = await entityClient.findTemplate(templateName, {
|
||||
token: getBearerToken(req.headers.authorization),
|
||||
});
|
||||
|
||||
const validationResult: ValidatorResult = validate(
|
||||
values,
|
||||
@@ -295,3 +298,7 @@ export async function createRouter(
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
function getBearerToken(header?: string): string | undefined {
|
||||
return header?.match(/Bearer\s+(\S+)/i)?.[1];
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import { createDevApp } from '@backstage/dev-utils';
|
||||
import { discoveryApiRef } from '@backstage/core';
|
||||
import { discoveryApiRef, identityApiRef } from '@backstage/core';
|
||||
import { CatalogClient } from '@backstage/catalog-client';
|
||||
import { catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
import { TemplateIndexPage, TemplatePage } from '../src/plugin';
|
||||
@@ -30,8 +30,9 @@ createDevApp()
|
||||
})
|
||||
.registerApi({
|
||||
api: scaffolderApiRef,
|
||||
deps: { discoveryApi: discoveryApiRef },
|
||||
factory: ({ discoveryApi }) => new ScaffolderApi({ discoveryApi }),
|
||||
deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef },
|
||||
factory: ({ discoveryApi, identityApi }) =>
|
||||
new ScaffolderApi({ discoveryApi, identityApi }),
|
||||
})
|
||||
.addPage({
|
||||
path: '/create',
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createApiRef, DiscoveryApi } from '@backstage/core';
|
||||
import { createApiRef, DiscoveryApi, IdentityApi } from '@backstage/core';
|
||||
|
||||
export const scaffolderApiRef = createApiRef<ScaffolderApi>({
|
||||
id: 'plugin.scaffolder.service',
|
||||
@@ -23,9 +23,14 @@ export const scaffolderApiRef = createApiRef<ScaffolderApi>({
|
||||
|
||||
export class ScaffolderApi {
|
||||
private readonly discoveryApi: DiscoveryApi;
|
||||
private readonly identityApi: IdentityApi;
|
||||
|
||||
constructor(options: { discoveryApi: DiscoveryApi }) {
|
||||
constructor(options: {
|
||||
discoveryApi: DiscoveryApi;
|
||||
identityApi: IdentityApi;
|
||||
}) {
|
||||
this.discoveryApi = options.discoveryApi;
|
||||
this.identityApi = options.identityApi;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,11 +41,13 @@ export class ScaffolderApi {
|
||||
* @param values Parameters for the template, e.g. name, description
|
||||
*/
|
||||
async scaffold(templateName: string, values: Record<string, any>) {
|
||||
const token = await this.identityApi.getIdToken();
|
||||
const url = `${await this.discoveryApi.getBaseUrl('scaffolder')}/v1/jobs`;
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { Authorization: `Bearer ${token}` }),
|
||||
},
|
||||
body: JSON.stringify({ templateName, values: { ...values } }),
|
||||
});
|
||||
@@ -56,8 +63,11 @@ export class ScaffolderApi {
|
||||
}
|
||||
|
||||
async getJob(jobId: string) {
|
||||
const token = await this.identityApi.getIdToken();
|
||||
const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder');
|
||||
const url = `${baseUrl}/v1/job/${encodeURIComponent(jobId)}`;
|
||||
return fetch(url).then(x => x.json());
|
||||
return fetch(url, {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
}).then(x => x.json());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
createPlugin,
|
||||
createApiFactory,
|
||||
discoveryApiRef,
|
||||
identityApiRef,
|
||||
createRoutableExtension,
|
||||
} from '@backstage/core';
|
||||
import { ScaffolderPage as ScaffolderPageComponent } from './components/ScaffolderPage';
|
||||
@@ -30,8 +31,9 @@ export const scaffolderPlugin = createPlugin({
|
||||
apis: [
|
||||
createApiFactory({
|
||||
api: scaffolderApiRef,
|
||||
deps: { discoveryApi: discoveryApiRef },
|
||||
factory: ({ discoveryApi }) => new ScaffolderApi({ discoveryApi }),
|
||||
deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef },
|
||||
factory: ({ discoveryApi, identityApi }) =>
|
||||
new ScaffolderApi({ discoveryApi, identityApi }),
|
||||
}),
|
||||
],
|
||||
register({ router }) {
|
||||
|
||||
Reference in New Issue
Block a user