Merge pull request #24878 from alexef/more-fetch

fix: use fetchApi instead of explicit identityApi token in remaining plugins
This commit is contained in:
Patrik Oldsberg
2024-05-28 15:02:35 +02:00
committed by GitHub
16 changed files with 105 additions and 165 deletions
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/plugin-kubernetes-react': minor
'@backstage/plugin-catalog-import': minor
'@backstage/plugin-kubernetes': patch
'@backstage/plugin-search': patch
---
Migrate from identityApi to fetchApi in frontend plugins.
+2 -2
View File
@@ -13,8 +13,8 @@ import { ConfigApi } from '@backstage/core-plugin-api';
import { Controller } from 'react-hook-form';
import { DiscoveryApi } from '@backstage/core-plugin-api';
import { Entity } from '@backstage/catalog-model';
import { FetchApi } from '@backstage/core-plugin-api';
import { FieldErrors } from 'react-hook-form';
import { IdentityApi } from '@backstage/core-plugin-api';
import { InfoCardVariants } from '@backstage/core-components';
import { JSX as JSX_2 } from 'react';
import { default as React_2 } from 'react';
@@ -102,7 +102,7 @@ export class CatalogImportClient implements CatalogImportApi {
constructor(options: {
discoveryApi: DiscoveryApi;
scmAuthApi: ScmAuthApi;
identityApi: IdentityApi;
fetchApi: FetchApi;
scmIntegrationsApi: ScmIntegrationRegistry;
catalogApi: CatalogApi;
configApi: ConfigApi;
+4 -4
View File
@@ -18,7 +18,7 @@ import {
configApiRef,
createApiFactory,
discoveryApiRef,
identityApiRef,
fetchApiRef,
} from '@backstage/core-plugin-api';
import {
compatWrapper,
@@ -55,7 +55,7 @@ const catalogImportApi = createApiExtension({
deps: {
discoveryApi: discoveryApiRef,
scmAuthApi: scmAuthApiRef,
identityApi: identityApiRef,
fetchApi: fetchApiRef,
scmIntegrationsApi: scmIntegrationsApiRef,
catalogApi: catalogApiRef,
configApi: configApiRef,
@@ -63,7 +63,7 @@ const catalogImportApi = createApiExtension({
factory: ({
discoveryApi,
scmAuthApi,
identityApi,
fetchApi,
scmIntegrationsApi,
catalogApi,
configApi,
@@ -72,7 +72,7 @@ const catalogImportApi = createApiExtension({
discoveryApi,
scmAuthApi,
scmIntegrationsApi,
identityApi,
fetchApi,
catalogApi,
configApi,
}),
@@ -50,7 +50,7 @@ import { ConfigReader, UrlPatternDiscovery } from '@backstage/core-app-api';
import { ScmIntegrations } from '@backstage/integration';
import { ScmAuthApi } from '@backstage/integration-react';
import { CatalogApi } from '@backstage/plugin-catalog-react';
import { setupRequestMockHandlers } from '@backstage/test-utils';
import { MockFetchApi, setupRequestMockHandlers } from '@backstage/test-utils';
import { Octokit } from '@octokit/rest';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
@@ -66,14 +66,7 @@ describe('CatalogImportClient', () => {
const scmAuthApi: jest.Mocked<ScmAuthApi> = {
getCredentials: jest.fn().mockResolvedValue({ token: 'token' }),
};
const identityApi = {
signOut: () => {
return Promise.resolve();
},
getProfileInfo: jest.fn(),
getBackstageIdentity: jest.fn(),
getCredentials: jest.fn().mockResolvedValue({ token: 'token' }),
};
const fetchApi = new MockFetchApi();
const scmIntegrationsApi = ScmIntegrations.fromConfig(
new ConfigReader({
@@ -110,7 +103,7 @@ describe('CatalogImportClient', () => {
discoveryApi,
scmAuthApi,
scmIntegrationsApi,
identityApi,
fetchApi,
catalogApi: catalogApi as Partial<CatalogApi> as CatalogApi,
configApi: new ConfigReader({
app: {
@@ -456,7 +449,7 @@ describe('CatalogImportClient', () => {
discoveryApi,
scmAuthApi,
scmIntegrationsApi,
identityApi,
fetchApi,
catalogApi: catalogApi as Partial<CatalogApi> as CatalogApi,
configApi: new ConfigReader({
catalog: {
@@ -659,7 +652,7 @@ describe('CatalogImportClient', () => {
discoveryApi,
scmAuthApi,
scmIntegrationsApi,
identityApi,
fetchApi,
catalogApi: catalogApi as Partial<CatalogApi> as CatalogApi,
configApi: new ConfigReader({
catalog: {
@@ -745,7 +738,7 @@ describe('CatalogImportClient', () => {
discoveryApi,
scmAuthApi,
scmIntegrationsApi,
identityApi,
fetchApi,
catalogApi: catalogApi as Partial<CatalogApi> as CatalogApi,
configApi: new ConfigReader({
catalog: {
@@ -15,11 +15,7 @@
*/
import { CatalogApi } from '@backstage/catalog-client';
import {
ConfigApi,
DiscoveryApi,
IdentityApi,
} from '@backstage/core-plugin-api';
import { ConfigApi, DiscoveryApi, FetchApi } from '@backstage/core-plugin-api';
import {
GithubIntegrationConfig,
ScmIntegrationRegistry,
@@ -41,7 +37,7 @@ import { CompoundEntityRef } from '@backstage/catalog-model';
*/
export class CatalogImportClient implements CatalogImportApi {
private readonly discoveryApi: DiscoveryApi;
private readonly identityApi: IdentityApi;
private readonly fetchApi: FetchApi;
private readonly scmAuthApi: ScmAuthApi;
private readonly scmIntegrationsApi: ScmIntegrationRegistry;
private readonly catalogApi: CatalogApi;
@@ -50,14 +46,14 @@ export class CatalogImportClient implements CatalogImportApi {
constructor(options: {
discoveryApi: DiscoveryApi;
scmAuthApi: ScmAuthApi;
identityApi: IdentityApi;
fetchApi: FetchApi;
scmIntegrationsApi: ScmIntegrationRegistry;
catalogApi: CatalogApi;
configApi: ConfigApi;
}) {
this.discoveryApi = options.discoveryApi;
this.scmAuthApi = options.scmAuthApi;
this.identityApi = options.identityApi;
this.fetchApi = options.fetchApi;
this.scmIntegrationsApi = options.scmIntegrationsApi;
this.catalogApi = options.catalogApi;
this.configApi = options.configApi;
@@ -206,29 +202,29 @@ the component will become available.\n\nFor more information, read an \
private async analyzeLocation(options: {
repo: string;
}): Promise<AnalyzeLocationResponse> {
const { token } = await this.identityApi.getCredentials();
const response = await fetch(
`${await this.discoveryApi.getBaseUrl('catalog')}/analyze-location`,
{
headers: {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
},
method: 'POST',
body: JSON.stringify({
location: { type: 'url', target: options.repo },
...(this.configApi.getOptionalString(
'catalog.import.entityFilename',
) && {
catalogFilename: this.configApi.getOptionalString(
const response = await this.fetchApi
.fetch(
`${await this.discoveryApi.getBaseUrl('catalog')}/analyze-location`,
{
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
body: JSON.stringify({
location: { type: 'url', target: options.repo },
...(this.configApi.getOptionalString(
'catalog.import.entityFilename',
),
) && {
catalogFilename: this.configApi.getOptionalString(
'catalog.import.entityFilename',
),
}),
}),
}),
},
).catch(e => {
throw new Error(`Failed to generate entity definitions, ${e.message}`);
});
},
)
.catch(e => {
throw new Error(`Failed to generate entity definitions, ${e.message}`);
});
if (!response.ok) {
throw new Error(
`Failed to generate entity definitions. Received http response ${response.status}: ${response.statusText}`,
@@ -25,22 +25,8 @@ import { catalogImportApiRef, CatalogImportClient } from '../../api';
import { DefaultImportPage } from './DefaultImportPage';
describe('<DefaultImportPage />', () => {
const identityApi = {
getUserId: () => {
return 'user';
},
getProfile: () => {
return {};
},
getIdToken: () => {
return Promise.resolve('token');
},
signOut: () => {
return Promise.resolve();
},
getProfileInfo: jest.fn(),
getBackstageIdentity: jest.fn(),
getCredentials: jest.fn(),
const fetchApi = {
fetch: jest.fn(),
};
let apis: TestApiRegistry;
@@ -56,7 +42,7 @@ describe('<DefaultImportPage />', () => {
scmAuthApi: {
getCredentials: async () => ({ token: 'token', headers: {} }),
},
identityApi,
fetchApi,
scmIntegrationsApi: {} as any,
catalogApi: {} as any,
configApi: {} as any,
@@ -16,7 +16,7 @@
import { CatalogClient } from '@backstage/catalog-client';
import { ApiProvider, ConfigReader } from '@backstage/core-app-api';
import { configApiRef } from '@backstage/core-plugin-api';
import { FetchApi, configApiRef } from '@backstage/core-plugin-api';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils';
import { screen } from '@testing-library/react';
@@ -31,22 +31,8 @@ jest.mock('react-router-dom', () => ({
}));
describe('<ImportPage />', () => {
const identityApi = {
getUserId: () => {
return 'user';
},
getProfile: () => {
return {};
},
getIdToken: () => {
return Promise.resolve('token');
},
signOut: () => {
return Promise.resolve();
},
getProfileInfo: jest.fn(),
getBackstageIdentity: jest.fn(),
getCredentials: jest.fn(),
const fetchApi: FetchApi = {
fetch: jest.fn(),
};
let apis: TestApiRegistry;
@@ -59,7 +45,7 @@ describe('<ImportPage />', () => {
catalogImportApiRef,
new CatalogImportClient({
discoveryApi: {} as any,
identityApi,
fetchApi,
scmAuthApi: {} as any,
scmIntegrationsApi: {} as any,
catalogApi: {} as any,
+4 -4
View File
@@ -21,7 +21,7 @@ import {
createRoutableExtension,
createRouteRef,
discoveryApiRef,
identityApiRef,
fetchApiRef,
} from '@backstage/core-plugin-api';
import {
scmAuthApiRef,
@@ -48,7 +48,7 @@ export const catalogImportPlugin = createPlugin({
deps: {
discoveryApi: discoveryApiRef,
scmAuthApi: scmAuthApiRef,
identityApi: identityApiRef,
fetchApi: fetchApiRef,
scmIntegrationsApi: scmIntegrationsApiRef,
catalogApi: catalogApiRef,
configApi: configApiRef,
@@ -56,7 +56,7 @@ export const catalogImportPlugin = createPlugin({
factory: ({
discoveryApi,
scmAuthApi,
identityApi,
fetchApi,
scmIntegrationsApi,
catalogApi,
configApi,
@@ -65,7 +65,7 @@ export const catalogImportPlugin = createPlugin({
discoveryApi,
scmAuthApi,
scmIntegrationsApi,
identityApi,
fetchApi,
catalogApi,
configApi,
}),
+2 -2
View File
@@ -16,10 +16,10 @@ import { DetectedErrorsByCluster } from '@backstage/plugin-kubernetes-common';
import { DiscoveryApi } from '@backstage/core-plugin-api';
import { Entity } from '@backstage/catalog-model';
import { Event as Event_2 } from 'kubernetes-models/v1';
import { FetchApi } from '@backstage/core-plugin-api';
import { GroupedResponses } from '@backstage/plugin-kubernetes-common';
import { IContainer } from 'kubernetes-models/v1';
import { IContainerStatus } from 'kubernetes-models/v1';
import { IdentityApi } from '@backstage/core-plugin-api';
import { IIoK8sApimachineryPkgApisMetaV1ObjectMeta } from '@kubernetes-models/apimachinery/apis/meta/v1/ObjectMeta';
import { IObjectMeta } from '@kubernetes-models/apimachinery/apis/meta/v1/ObjectMeta';
import { JsonObject } from '@backstage/types';
@@ -402,7 +402,7 @@ export const kubernetesAuthProvidersApiRef: ApiRef<KubernetesAuthProvidersApi>;
export class KubernetesBackendClient implements KubernetesApi {
constructor(options: {
discoveryApi: DiscoveryApi;
identityApi: IdentityApi;
fetchApi: FetchApi;
kubernetesAuthProvidersApi: KubernetesAuthProvidersApi;
});
// (undocumented)
@@ -19,7 +19,7 @@ import { KubernetesBackendClient } from './KubernetesBackendClient';
import { rest } from 'msw';
import { UrlPatternDiscovery } from '@backstage/core-app-api';
import { setupServer } from 'msw/node';
import { setupRequestMockHandlers } from '@backstage/test-utils';
import { MockFetchApi, setupRequestMockHandlers } from '@backstage/test-utils';
import {
CustomObjectsByEntityRequest,
KubernetesRequestBody,
@@ -44,6 +44,7 @@ describe('KubernetesBackendClient', () => {
getBackstageIdentity: jest.fn(),
signOut: jest.fn(),
};
const fetchApi = new MockFetchApi({ injectIdentityAuth: { identityApi } });
beforeEach(() => {
jest.resetAllMocks();
@@ -51,7 +52,7 @@ describe('KubernetesBackendClient', () => {
discoveryApi: UrlPatternDiscovery.compile(
'http://localhost:1234/api/{{ pluginId }}',
),
identityApi,
fetchApi,
kubernetesAuthProvidersApi,
});
mockResponse = {
@@ -454,6 +455,7 @@ describe('KubernetesBackendClient', () => {
});
it('hits the /proxy API with serviceAccount as auth provider', async () => {
identityApi.getCredentials.mockResolvedValue({ token: 'idToken' });
worker.use(
rest.get(
'http://localhost:1234/api/kubernetes/clusters',
@@ -21,7 +21,7 @@ import {
WorkloadsByEntityRequest,
CustomObjectsByEntityRequest,
} from '@backstage/plugin-kubernetes-common';
import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
import { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api';
import { stringifyEntityRef } from '@backstage/catalog-model';
import { KubernetesAuthProvidersApi } from '../kubernetes-auth-provider';
import { NotFoundError } from '@backstage/errors';
@@ -29,16 +29,16 @@ import { NotFoundError } from '@backstage/errors';
/** @public */
export class KubernetesBackendClient implements KubernetesApi {
private readonly discoveryApi: DiscoveryApi;
private readonly identityApi: IdentityApi;
private readonly fetchApi: FetchApi;
private readonly kubernetesAuthProvidersApi: KubernetesAuthProvidersApi;
constructor(options: {
discoveryApi: DiscoveryApi;
identityApi: IdentityApi;
fetchApi: FetchApi;
kubernetesAuthProvidersApi: KubernetesAuthProvidersApi;
}) {
this.discoveryApi = options.discoveryApi;
this.identityApi = options.identityApi;
this.fetchApi = options.fetchApi;
this.kubernetesAuthProvidersApi = options.kubernetesAuthProvidersApi;
}
@@ -62,12 +62,10 @@ export class KubernetesBackendClient implements KubernetesApi {
private async postRequired(path: string, requestBody: any): Promise<any> {
const url = `${await this.discoveryApi.getBaseUrl('kubernetes')}${path}`;
const { token: idToken } = await this.identityApi.getCredentials();
const response = await fetch(url, {
const response = await this.fetchApi.fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(idToken && { Authorization: `Bearer ${idToken}` }),
},
body: JSON.stringify(requestBody),
});
@@ -130,14 +128,8 @@ export class KubernetesBackendClient implements KubernetesApi {
}
async getClusters(): Promise<{ name: string; authProvider: string }[]> {
const { token: idToken } = await this.identityApi.getCredentials();
const url = `${await this.discoveryApi.getBaseUrl('kubernetes')}/clusters`;
const response = await fetch(url, {
method: 'GET',
headers: {
...(idToken && { Authorization: `Bearer ${idToken}` }),
},
});
const response = await this.fetchApi.fetch(url);
return (await this.handleResponse(response)).items;
}
@@ -157,15 +149,13 @@ export class KubernetesBackendClient implements KubernetesApi {
const url = `${await this.discoveryApi.getBaseUrl('kubernetes')}/proxy${
options.path
}`;
const identityResponse = await this.identityApi.getCredentials();
const headers = KubernetesBackendClient.getKubernetesHeaders(
options,
kubernetesCredentials?.token,
identityResponse,
authProvider,
oidcTokenProvider,
);
return await fetch(url, { ...options.init, headers });
return await this.fetchApi.fetch(url, { ...options.init, headers });
}
private static getKubernetesHeaders(
@@ -175,7 +165,6 @@ export class KubernetesBackendClient implements KubernetesApi {
init?: RequestInit;
},
k8sToken: string | undefined,
identityResponse: { token?: string },
authProvider: string,
oidcTokenProvider: string | undefined,
) {
@@ -190,9 +179,6 @@ export class KubernetesBackendClient implements KubernetesApi {
...(k8sToken && {
[kubernetesAuthHeader]: k8sToken,
}),
...(identityResponse.token && {
Authorization: `Bearer ${identityResponse.token}`,
}),
};
}
+4 -4
View File
@@ -30,13 +30,13 @@ import {
createPlugin,
createRouteRef,
discoveryApiRef,
identityApiRef,
gitlabAuthApiRef,
googleAuthApiRef,
microsoftAuthApiRef,
oktaAuthApiRef,
oneloginAuthApiRef,
createRoutableExtension,
fetchApiRef,
} from '@backstage/core-plugin-api';
export const rootCatalogKubernetesRouteRef = createRouteRef({
@@ -50,13 +50,13 @@ export const kubernetesPlugin = createPlugin({
api: kubernetesApiRef,
deps: {
discoveryApi: discoveryApiRef,
identityApi: identityApiRef,
fetchApi: fetchApiRef,
kubernetesAuthProvidersApi: kubernetesAuthProvidersApiRef,
},
factory: ({ discoveryApi, identityApi, kubernetesAuthProvidersApi }) =>
factory: ({ discoveryApi, fetchApi, kubernetesAuthProvidersApi }) =>
new KubernetesBackendClient({
discoveryApi,
identityApi,
fetchApi,
kubernetesAuthProvidersApi,
}),
}),
+4 -4
View File
@@ -32,9 +32,9 @@ import {
import {
useApi,
DiscoveryApi,
IdentityApi,
discoveryApiRef,
identityApiRef,
FetchApi,
} from '@backstage/core-plugin-api';
import {
@@ -81,12 +81,12 @@ export const searchApi = createApiExtension({
api: searchApiRef,
deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef },
factory: ({
identityApi,
fetchApi,
discoveryApi,
}: {
identityApi: IdentityApi;
fetchApi: FetchApi;
discoveryApi: DiscoveryApi;
}) => new SearchClient({ discoveryApi, identityApi }),
}) => new SearchClient({ discoveryApi, fetchApi }),
},
});
+19 -30
View File
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import { MockFetchApi } from '@backstage/test-utils';
import { SearchClient } from './apis';
describe('apis', () => {
@@ -26,47 +27,35 @@ describe('apis', () => {
const baseUrl = 'https://base-url.com/';
const getBaseUrl = jest.fn().mockResolvedValue(baseUrl);
const token = 'AUTHTOKEN';
const withToken = jest.fn().mockResolvedValue({ token });
const withoutToken = jest.fn().mockResolvedValue({ token: undefined });
const createIdentityApiMock = (getCredentials: any) => ({
signOut: jest.fn(),
const identityApi = {
getCredentials: jest.fn(),
getProfileInfo: jest.fn(),
getBackstageIdentity: jest.fn(),
getCredentials,
signOut: jest.fn(),
};
const json = jest.fn();
const mockFetch = jest.fn().mockResolvedValue({
ok: true,
json,
});
const fetchApi = new MockFetchApi({
baseImplementation: mockFetch,
injectIdentityAuth: { identityApi },
});
const client = new SearchClient({
discoveryApi: { getBaseUrl },
identityApi: createIdentityApiMock(withoutToken),
});
const json = jest.fn();
const originalFetch = window.fetch;
window.fetch = jest.fn().mockResolvedValue({ json, ok: true });
afterAll(() => {
window.fetch = originalFetch;
fetchApi,
});
it('Fetch is called with expected URL (including stringified Q params)', async () => {
identityApi.getCredentials.mockResolvedValue({});
await client.query(query);
expect(getBaseUrl).toHaveBeenLastCalledWith('search');
expect(fetch).toHaveBeenLastCalledWith(`${baseUrl}/query?term=`, {
headers: {},
});
});
it('Sets Authorization if token is available', async () => {
const authedClient = new SearchClient({
discoveryApi: { getBaseUrl },
identityApi: createIdentityApiMock(withToken),
});
await authedClient.query(query);
expect(getBaseUrl).toHaveBeenLastCalledWith('search');
expect(fetch).toHaveBeenLastCalledWith(`${baseUrl}/query?term=`, {
headers: { Authorization: `Bearer ${token}` },
});
expect(mockFetch).toHaveBeenLastCalledWith(
`${baseUrl}/query?term=`,
undefined,
);
});
it('Resolves JSON from fetch response', async () => {
+5 -11
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
import { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api';
import { ResponseError } from '@backstage/errors';
import { SearchApi } from '@backstage/plugin-search-react';
import { SearchQuery, SearchResultSet } from '@backstage/plugin-search-common';
@@ -23,25 +23,19 @@ import qs from 'qs';
export class SearchClient implements SearchApi {
private readonly discoveryApi: DiscoveryApi;
private readonly identityApi: IdentityApi;
private readonly fetchApi: FetchApi;
constructor(options: {
discoveryApi: DiscoveryApi;
identityApi: IdentityApi;
}) {
constructor(options: { discoveryApi: DiscoveryApi; fetchApi: FetchApi }) {
this.discoveryApi = options.discoveryApi;
this.identityApi = options.identityApi;
this.fetchApi = options.fetchApi;
}
async query(query: SearchQuery): Promise<SearchResultSet> {
const { token } = await this.identityApi.getCredentials();
const queryString = qs.stringify(query);
const url = `${await this.discoveryApi.getBaseUrl(
'search',
)}/query?${queryString}`;
const response = await fetch(url, {
headers: token ? { Authorization: `Bearer ${token}` } : {},
});
const response = await this.fetchApi.fetch(url);
if (!response.ok) {
throw await ResponseError.fromResponse(response);
+4 -4
View File
@@ -23,7 +23,7 @@ import {
createRoutableExtension,
discoveryApiRef,
createComponentExtension,
identityApiRef,
fetchApiRef,
} from '@backstage/core-plugin-api';
export const rootRouteRef = createRouteRef({
@@ -38,9 +38,9 @@ export const searchPlugin = createPlugin({
apis: [
createApiFactory({
api: searchApiRef,
deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef },
factory: ({ discoveryApi, identityApi }) => {
return new SearchClient({ discoveryApi, identityApi });
deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef },
factory: ({ discoveryApi, fetchApi }) => {
return new SearchClient({ discoveryApi, fetchApi });
},
}),
],