Merge pull request #17121 from RubenV-dev/kubernetes-proxy-client

Add reusable kubernetes-backend proxy client
This commit is contained in:
Jamie Klassen
2023-04-05 12:14:16 -04:00
committed by GitHub
16 changed files with 689 additions and 38 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-kubernetes': patch
---
`KubernetesBackendClient` now requires a `kubernetesAuthProvidersApi` value to be provided. `KubernetesApi` interface now has a proxy method requirement.
+5 -31
View File
@@ -14,42 +14,16 @@ Kubernetes backend plugin's proxy endpoint to allow them to make arbitrary
requests to the [REST
API](https://kubernetes.io/docs/reference/using-api/api-concepts/).
Here is a snippet fetching namespaces from a cluster configured with the
`google` [auth provider](https://backstage.io/docs/features/kubernetes/configuration#clustersauthprovider):
Here is a snippet fetching namespaces using the `KubernetesBackendClient` library
```typescript
import {
discoveryApiRef,
googleAuthApiRef,
useApi,
identityApiRef,
} from '@backstage/core-plugin-api';
import { useApi } from '@backstage/core-plugin-api';
import { kubernetesApiRef } from '@backstage/plugin-kubernetes';
const CLUSTER_NAME = ''; // use a known cluster name
// get a bearer token from Google
const googleAuthApi = useApi(googleAuthApiRef);
const token = await googleAuthApi.getAccessToken(
'https://www.googleapis.com/auth/cloud-platform',
);
// get a backstage ID token
const identityApi = useApi(identityApiRef);
const { token: userToken } = await identityApi.getCredentials();
const discoveryApi = useApi(discoveryApiRef);
const kubernetesBaseUrl = await discoveryApi.getBaseUrl('kubernetes');
const kubernetesProxyEndpoint = `${kubernetesBaseUrl}/proxy`;
// fetch namespaces
await fetch(`${kubernetesProxyEndpoint}/api/v1/namespaces`, {
method: 'GET',
headers: {
'Backstage-Kubernetes-Cluster': CLUSTER_NAME,
'Backstage-Kubernetes-Authorization': `Bearer ${token}`,
Authorization: `Bearer ${userToken}`,
},
});
const kubernetesApi = useApi(kubernetesApiRef);
await kubernetesApi.proxy(CLUSTER_NAME, '/api/v1/namespaces');
```
## How it works
+29
View File
@@ -182,6 +182,10 @@ export class GoogleKubernetesAuthProvider implements KubernetesAuthProvider {
decorateRequestBodyForAuth(
requestBody: KubernetesRequestBody,
): Promise<KubernetesRequestBody>;
// (undocumented)
getCredentials(): Promise<{
token: string;
}>;
}
// Warning: (ae-missing-release-tag) "GroupedResponses" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -257,6 +261,12 @@ export interface KubernetesApi {
getWorkloadsByEntity(
request: WorkloadsByEntityRequest,
): Promise<ObjectsByEntityResponse>;
// (undocumented)
proxy(options: {
clusterName: string;
path: string;
init?: RequestInit;
}): Promise<Response>;
}
// Warning: (ae-missing-release-tag) "kubernetesApiRef" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -279,6 +289,10 @@ export class KubernetesAuthProviders implements KubernetesAuthProvidersApi {
authProvider: string,
requestBody: KubernetesRequestBody,
): Promise<KubernetesRequestBody>;
// (undocumented)
getCredentials(authProvider: string): Promise<{
token: string;
}>;
}
// Warning: (ae-missing-release-tag) "KubernetesAuthProvidersApi" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -290,6 +304,10 @@ export interface KubernetesAuthProvidersApi {
authProvider: string,
requestBody: KubernetesRequestBody,
): Promise<KubernetesRequestBody>;
// (undocumented)
getCredentials(authProvider: string): Promise<{
token: string;
}>;
}
// Warning: (ae-missing-release-tag) "kubernetesAuthProvidersApiRef" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -304,6 +322,7 @@ export class KubernetesBackendClient implements KubernetesApi {
constructor(options: {
discoveryApi: DiscoveryApi;
identityApi: IdentityApi;
kubernetesAuthProvidersApi: KubernetesAuthProvidersApi;
});
// (undocumented)
getClusters(): Promise<
@@ -324,6 +343,12 @@ export class KubernetesBackendClient implements KubernetesApi {
getWorkloadsByEntity(
request: WorkloadsByEntityRequest,
): Promise<ObjectsByEntityResponse>;
// (undocumented)
proxy(options: {
clusterName: string;
path: string;
init?: RequestInit;
}): Promise<Response>;
}
// Warning: (ae-forgotten-export) The symbol "KubernetesContentProps" needs to be exported by the entry point index.d.ts
@@ -414,6 +439,10 @@ export class ServerSideKubernetesAuthProvider
decorateRequestBodyForAuth(
requestBody: KubernetesRequestBody,
): Promise<KubernetesRequestBody>;
// (undocumented)
getCredentials(): Promise<{
token: string;
}>;
}
// Warning: (ae-forgotten-export) The symbol "ServicesAccordionsProps" needs to be exported by the entry point index.d.ts
+10
View File
@@ -106,6 +106,16 @@ class MockKubernetesClient implements KubernetesApi {
async getClusters(): Promise<{ name: string; authProvider: string }[]> {
return [{ name: 'mock-cluster', authProvider: 'serviceAccount' }];
}
async proxy(_options: { clusterName: String; path: String }): Promise<any> {
return {
kind: 'Namespace',
apiVersion: 'v1',
metadata: {
name: 'mock-ns',
},
};
}
}
createDevApp()
+1
View File
@@ -37,6 +37,7 @@
"@backstage/config": "workspace:^",
"@backstage/core-components": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/plugin-catalog-react": "workspace:^",
"@backstage/plugin-kubernetes-common": "workspace:^",
"@backstage/theme": "workspace:^",
@@ -0,0 +1,539 @@
/*
* Copyright 2023 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 { KubernetesAuthProvidersApi } from '../kubernetes-auth-provider';
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 {
CustomObjectsByEntityRequest,
KubernetesRequestBody,
ObjectsByEntityResponse,
WorkloadsByEntityRequest,
} from '@backstage/plugin-kubernetes-common';
import { NotFoundError } from '@backstage/errors';
describe('KubernetesBackendClient', () => {
let backendClient: KubernetesBackendClient;
const kubernetesAuthProvidersApi: jest.Mocked<KubernetesAuthProvidersApi> = {
decorateRequestBodyForAuth: jest.fn(),
getCredentials: jest.fn(),
};
let mockResponse: ObjectsByEntityResponse;
const worker = setupServer();
setupRequestMockHandlers(worker);
const identityApi = {
getCredentials: jest.fn(),
getProfileInfo: jest.fn(),
getBackstageIdentity: jest.fn(),
signOut: jest.fn(),
};
beforeEach(() => {
jest.resetAllMocks();
backendClient = new KubernetesBackendClient({
discoveryApi: UrlPatternDiscovery.compile(
'http://localhost:1234/api/{{ pluginId }}',
),
identityApi,
kubernetesAuthProvidersApi,
});
mockResponse = {
items: [
{
cluster: {
name: 'cluster-a',
},
resources: [{ type: 'pods', resources: [] }],
podMetrics: [
{
pod: {},
cpu: { currentUsage: 8, requestTotal: 2, limitTotal: 1 },
memory: { currentUsage: 8, requestTotal: 2, limitTotal: 1 },
containers: [
{
container: 'test',
cpuUsage: { currentUsage: 8, requestTotal: 2, limitTotal: 1 },
memoryUsage: {
currentUsage: 8,
requestTotal: 2,
limitTotal: 1,
},
},
],
},
],
errors: [],
},
],
};
});
it('hits the /clusters API', async () => {
identityApi.getCredentials.mockResolvedValue({ token: 'idToken' });
worker.use(
rest.get('http://localhost:1234/api/kubernetes/clusters', (_, res, ctx) =>
res(ctx.json({ items: [{ name: 'cluster-a', authProvider: 'aws' }] })),
),
);
const clusters = await backendClient.getClusters();
expect(clusters).toStrictEqual([
{ name: 'cluster-a', authProvider: 'aws' },
]);
});
it('/clusters API throws a 404 Error', async () => {
identityApi.getCredentials.mockResolvedValue({ token: 'idToken' });
worker.use(
rest.get('http://localhost:1234/api/kubernetes/clusters', (_, res, ctx) =>
res(ctx.status(404)),
),
);
await expect(backendClient.getClusters()).rejects.toThrow(
'Could not find the Kubernetes Backend (HTTP 404). Make sure the plugin has been fully installed.',
);
});
it('/clusters API throws a 500 Error', async () => {
identityApi.getCredentials.mockResolvedValue({ token: 'idToken' });
worker.use(
rest.get('http://localhost:1234/api/kubernetes/clusters', (_, res, ctx) =>
res(ctx.status(500)),
),
);
await expect(backendClient.getClusters()).rejects.toThrow(
'Request failed with 500 Internal Server Error, ',
);
});
it('hits the /resources/custom/query API', async () => {
identityApi.getCredentials.mockResolvedValue({ token: 'idToken' });
worker.use(
rest.post(
'http://localhost:1234/api/kubernetes/resources/custom/query',
(_, res, ctx) => res(ctx.json(mockResponse)),
),
);
const request: CustomObjectsByEntityRequest = {
auth: {},
customResources: [
{
group: 'test-group',
apiVersion: 'v1',
plural: 'none',
},
],
entity: {
apiVersion: 'v1',
kind: 'pod',
metadata: {
name: 'test-name',
},
},
};
const customObject: ObjectsByEntityResponse =
await backendClient.getCustomObjectsByEntity(request);
expect(customObject).toStrictEqual(mockResponse);
});
it('/resources/custom/query API throws a 404 error', async () => {
identityApi.getCredentials.mockResolvedValue({ token: 'idToken' });
worker.use(
rest.post(
'http://localhost:1234/api/kubernetes/resources/custom/query',
(_, res, ctx) => res(ctx.status(404)),
),
);
const request: CustomObjectsByEntityRequest = {
auth: {},
customResources: [
{
group: 'test-group',
apiVersion: 'v1',
plural: 'none',
},
],
entity: {
apiVersion: 'v1',
kind: 'pod',
metadata: {
name: 'test-name',
},
},
};
const response = backendClient.getCustomObjectsByEntity(request);
await expect(response).rejects.toThrow(
'Could not find the Kubernetes Backend (HTTP 404). Make sure the plugin has been fully installed.',
);
});
it('/resources/custom/query API throws a 500 error', async () => {
identityApi.getCredentials.mockResolvedValue({ token: 'idToken' });
worker.use(
rest.post(
'http://localhost:1234/api/kubernetes/resources/custom/query',
(_, res, ctx) => res(ctx.status(500)),
),
);
const request: CustomObjectsByEntityRequest = {
auth: {},
customResources: [
{
group: 'test-group',
apiVersion: 'v1',
plural: 'none',
},
],
entity: {
apiVersion: 'v1',
kind: 'pod',
metadata: {
name: 'test-name',
},
},
};
const response = backendClient.getCustomObjectsByEntity(request);
await expect(response).rejects.toThrow(
'Request failed with 500 Internal Server Error, ',
);
});
it('hits the /services/{entityName} API', async () => {
identityApi.getCredentials.mockResolvedValue({ token: 'idToken' });
worker.use(
rest.post(
'http://localhost:1234/api/kubernetes/services/test-name',
(_, res, ctx) => res(ctx.json(mockResponse)),
),
);
const request: KubernetesRequestBody = {
entity: {
apiVersion: 'v1',
kind: 'pod',
metadata: {
name: 'test-name',
},
},
};
const entityObject: ObjectsByEntityResponse =
await backendClient.getObjectsByEntity(request);
expect(entityObject).toStrictEqual(mockResponse);
});
it('services/{entityName} API throws a 404 error', async () => {
identityApi.getCredentials.mockResolvedValue({ token: 'idToken' });
worker.use(
rest.post(
'http://localhost:1234/api/kubernetes/services/test-name',
(_, res, ctx) => res(ctx.status(404)),
),
);
const request: KubernetesRequestBody = {
entity: {
apiVersion: 'v1',
kind: 'pod',
metadata: {
name: 'test-name',
},
},
};
const response = backendClient.getObjectsByEntity(request);
await expect(response).rejects.toThrow(
'Could not find the Kubernetes Backend (HTTP 404). Make sure the plugin has been fully installed.',
);
});
it('services/{entityName} API throws a 500 error', async () => {
identityApi.getCredentials.mockResolvedValue({ token: 'idToken' });
worker.use(
rest.post(
'http://localhost:1234/api/kubernetes/services/test-name',
(_, res, ctx) => res(ctx.status(500)),
),
);
const request: KubernetesRequestBody = {
entity: {
apiVersion: 'v1',
kind: 'pod',
metadata: {
name: 'test-name',
},
},
};
const response = backendClient.getObjectsByEntity(request);
await expect(response).rejects.toThrow(
'Request failed with 500 Internal Server Error, ',
);
});
it('hits the /resources/workloads/query API', async () => {
identityApi.getCredentials.mockResolvedValue({ token: 'idToken' });
worker.use(
rest.post(
'http://localhost:1234/api/kubernetes/resources/workloads/query',
(_, res, ctx) => res(ctx.json(mockResponse)),
),
);
const request: WorkloadsByEntityRequest = {
auth: {},
entity: {
apiVersion: 'v1',
kind: 'pod',
metadata: {
name: 'test-name',
},
},
};
const response: ObjectsByEntityResponse =
await backendClient.getWorkloadsByEntity(request);
expect(response).toStrictEqual(mockResponse);
});
it('/resources/workloads/query API throws a 404 error', async () => {
identityApi.getCredentials.mockResolvedValue({ token: 'idToken' });
worker.use(
rest.post(
'http://localhost:1234/api/kubernetes/resources/workloads/query',
(_, res, ctx) => res(ctx.status(404)),
),
);
const request: WorkloadsByEntityRequest = {
auth: {},
entity: {
apiVersion: 'v1',
kind: 'pod',
metadata: {
name: 'test-name',
},
},
};
const response = backendClient.getWorkloadsByEntity(request);
await expect(response).rejects.toThrow(
'Could not find the Kubernetes Backend (HTTP 404). Make sure the plugin has been fully installed.',
);
});
it('/resources/workloads/query API throws a 500 error', async () => {
identityApi.getCredentials.mockResolvedValue({ token: 'idToken' });
worker.use(
rest.post(
'http://localhost:1234/api/kubernetes/resources/workloads/query',
(_, res, ctx) => res(ctx.status(500)),
),
);
const request: WorkloadsByEntityRequest = {
auth: {},
entity: {
apiVersion: 'v1',
kind: 'pod',
metadata: {
name: 'test-name',
},
},
};
const response = backendClient.getWorkloadsByEntity(request);
await expect(response).rejects.toThrow(
'Request failed with 500 Internal Server Error, ',
);
});
describe('proxy', () => {
beforeEach(() => {
worker.use(
rest.get(
'http://localhost:1234/api/kubernetes/clusters',
(_, res, ctx) =>
res(
ctx.json({ items: [{ name: 'cluster-a', authProvider: 'aws' }] }),
),
),
);
});
it('hits the /proxy API', async () => {
identityApi.getCredentials.mockResolvedValue({ token: 'idToken' });
kubernetesAuthProvidersApi.getCredentials.mockResolvedValue({
token: 'k8-token',
});
const nsResponse = {
kind: 'Namespace',
apiVersion: 'v1',
metadata: {
name: 'new-ns',
},
};
worker.use(
rest.get(
'http://localhost:1234/api/kubernetes/proxy/api/v1/namespaces',
(req, res, ctx) =>
res(
req.headers.get('Backstage-Kubernetes-Authorization') ===
'Bearer k8-token'
? ctx.json(nsResponse)
: ctx.status(403),
),
),
);
const request = {
clusterName: 'cluster-a',
path: '/api/v1/namespaces',
};
const response = await backendClient.proxy(request);
await expect(response.json()).resolves.toStrictEqual(nsResponse);
});
it('hits /proxy api when signed in as a guest', async () => {
// when a user is signed in as a guest the result of the getCredentials() method resolves to the {} value.
identityApi.getCredentials.mockResolvedValue({});
kubernetesAuthProvidersApi.getCredentials.mockResolvedValue({
token: 'k8-token',
});
const nsResponse = {
kind: 'Namespace',
apiVersion: 'v1',
metadata: {
name: 'new-ns',
},
};
worker.use(
rest.get(
'http://localhost:1234/api/kubernetes/proxy/api/v1/namespaces',
(req, res, ctx) =>
res(
req.headers.get('Backstage-Kubernetes-Authorization') ===
'Bearer k8-token'
? ctx.json(nsResponse)
: ctx.status(403),
),
),
);
const request = {
clusterName: 'cluster-a',
path: '/api/v1/namespaces',
};
const response = await backendClient.proxy(request);
await expect(response.json()).resolves.toStrictEqual(nsResponse);
});
it('/proxy API throws a 404 error', async () => {
identityApi.getCredentials.mockResolvedValue({ token: 'idToken' });
kubernetesAuthProvidersApi.getCredentials.mockResolvedValue({
token: 'k8-token',
});
worker.use(
rest.get(
'http://localhost:1234/api/kubernetes/proxy/api/v1/namespaces',
(_, res, ctx) => res(ctx.status(404)),
),
);
const request = {
clusterName: 'cluster-a',
path: '/api/v1/namespaces',
};
const response = await backendClient.proxy(request);
expect(response.status).toEqual(404);
});
it('throws a ERROR_NOT_FOUND if the cluster in the request is not found', async () => {
identityApi.getCredentials.mockResolvedValue({ token: 'idToken' });
const request = {
clusterName: 'cluster-b',
path: '/api/v1/namespaces',
};
await expect(backendClient.proxy(request)).rejects.toThrow(NotFoundError);
});
it('responds with an 403 error when invalid k8 token is provided', async () => {
// when a user is signed in as a guest the result of the getCredentials() method resolves to the {} value.
identityApi.getCredentials.mockResolvedValue({});
kubernetesAuthProvidersApi.getCredentials.mockResolvedValue({
token: 'wrong-token',
});
const nsResponse = {
kind: 'Namespace',
apiVersion: 'v1',
metadata: {
name: 'new-ns',
},
};
worker.use(
rest.get(
'http://localhost:1234/api/kubernetes/proxy/api/v1/namespaces',
(req, res, ctx) =>
res(
req.headers.get('Backstage-Kubernetes-Authorization') ===
'Bearer k8-token'
? ctx.json(nsResponse)
: ctx.status(403),
),
),
);
const request = {
clusterName: 'cluster-a',
path: '/api/v1/namespaces',
};
const response = await backendClient.proxy(request);
expect(response.status).toEqual(403);
});
});
});
@@ -23,17 +23,22 @@ import {
} from '@backstage/plugin-kubernetes-common';
import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
import { stringifyEntityRef } from '@backstage/catalog-model';
import { KubernetesAuthProvidersApi } from '../kubernetes-auth-provider';
import { NotFoundError } from '@backstage/errors';
export class KubernetesBackendClient implements KubernetesApi {
private readonly discoveryApi: DiscoveryApi;
private readonly identityApi: IdentityApi;
private readonly kubernetesAuthProvidersApi: KubernetesAuthProvidersApi;
constructor(options: {
discoveryApi: DiscoveryApi;
identityApi: IdentityApi;
kubernetesAuthProvidersApi: KubernetesAuthProvidersApi;
}) {
this.discoveryApi = options.discoveryApi;
this.identityApi = options.identityApi;
this.kubernetesAuthProvidersApi = options.kubernetesAuthProvidersApi;
}
private async handleResponse(response: Response): Promise<any> {
@@ -69,6 +74,25 @@ export class KubernetesBackendClient implements KubernetesApi {
return this.handleResponse(response);
}
private async getCluster(
clusterName: string,
): Promise<{ name: string; authProvider: string }> {
const cluster = await this.getClusters().then(clusters =>
clusters.find(c => c.name === clusterName),
);
if (!cluster) {
throw new NotFoundError(`Cluster ${clusterName} not found`);
}
return cluster;
}
private async getCredentials(
authProvider: string,
): Promise<{ token: string }> {
return await this.kubernetesAuthProvidersApi.getCredentials(authProvider);
}
async getObjectsByEntity(
requestBody: KubernetesRequestBody,
): Promise<ObjectsByEntityResponse> {
@@ -100,7 +124,6 @@ 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: {
@@ -110,4 +133,27 @@ export class KubernetesBackendClient implements KubernetesApi {
return (await this.handleResponse(response)).items;
}
async proxy(options: {
clusterName: string;
path: string;
init?: RequestInit;
}): Promise<Response> {
const { authProvider } = await this.getCluster(options.clusterName);
const { token: k8sToken } = await this.getCredentials(authProvider);
const url = `${await this.discoveryApi.getBaseUrl('kubernetes')}/proxy${
options.path
}`;
const identityResponse = await this.identityApi.getCredentials();
const headers = {
...options.init?.headers,
[`Backstage-Kubernetes-Cluster`]: options.clusterName,
[`Backstage-Kubernetes-Authorization`]: `Bearer ${k8sToken}`,
...(identityResponse.token && {
Authorization: `Bearer ${identityResponse.token}`,
}),
};
return await fetch(url, { ...options.init, headers });
}
}
+5
View File
@@ -43,4 +43,9 @@ export interface KubernetesApi {
getCustomObjectsByEntity(
request: CustomObjectsByEntityRequest,
): Promise<ObjectsByEntityResponse>;
proxy(options: {
clusterName: string;
path: string;
init?: RequestInit;
}): Promise<Response>;
}
@@ -28,9 +28,7 @@ export class GoogleKubernetesAuthProvider implements KubernetesAuthProvider {
async decorateRequestBodyForAuth(
requestBody: KubernetesRequestBody,
): Promise<KubernetesRequestBody> {
const googleAuthToken: string = await this.authProvider.getAccessToken(
'https://www.googleapis.com/auth/cloud-platform',
);
const googleAuthToken: string = (await this.getCredentials()).token;
if ('auth' in requestBody) {
requestBody.auth!.google = googleAuthToken;
} else {
@@ -38,4 +36,11 @@ export class GoogleKubernetesAuthProvider implements KubernetesAuthProvider {
}
return requestBody;
}
async getCredentials(): Promise<{ token: string }> {
return {
token: await this.authProvider.getAccessToken(
'https://www.googleapis.com/auth/cloud-platform',
),
};
}
}
@@ -91,4 +91,22 @@ export class KubernetesAuthProviders implements KubernetesAuthProvidersApi {
`authProvider "${authProvider}" has no KubernetesAuthProvider defined for it`,
);
}
async getCredentials(authProvider: string): Promise<{ token: string }> {
const kubernetesAuthProvider: KubernetesAuthProvider | undefined =
this.kubernetesAuthProviderMap.get(authProvider);
if (kubernetesAuthProvider) {
return await kubernetesAuthProvider.getCredentials();
}
if (authProvider.startsWith('oidc.')) {
throw new Error(
`KubernetesAuthProviders has no oidcProvider configured for ${authProvider}`,
);
}
throw new Error(
`authProvider "${authProvider}" has no KubernetesAuthProvider defined for it`,
);
}
}
@@ -30,7 +30,7 @@ export class OidcKubernetesAuthProvider implements KubernetesAuthProvider {
async decorateRequestBodyForAuth(
requestBody: KubernetesRequestBody,
): Promise<KubernetesRequestBody> {
const authToken: string = await this.authProvider.getIdToken();
const authToken: string = (await this.getCredentials()).token;
const auth = { ...requestBody.auth };
if (auth.oidc) {
auth.oidc[this.providerName] = authToken;
@@ -40,4 +40,10 @@ export class OidcKubernetesAuthProvider implements KubernetesAuthProvider {
requestBody.auth = auth;
return requestBody;
}
async getCredentials(): Promise<{ token: string }> {
return {
token: await this.authProvider.getIdToken(),
};
}
}
@@ -31,4 +31,8 @@ export class ServerSideKubernetesAuthProvider
// No-op, auth will be taken care of on the server-side
return requestBody;
}
async getCredentials(): Promise<{ token: string }> {
return { token: '' };
}
}
@@ -21,6 +21,7 @@ export interface KubernetesAuthProvider {
decorateRequestBodyForAuth(
requestBody: KubernetesRequestBody,
): Promise<KubernetesRequestBody>;
getCredentials(): Promise<{ token: string }>;
}
export const kubernetesAuthProvidersApiRef =
@@ -33,4 +34,5 @@ export interface KubernetesAuthProvidersApi {
authProvider: string,
requestBody: KubernetesRequestBody,
): Promise<KubernetesRequestBody>;
getCredentials(authProvider: string): Promise<{ token: string }>;
}
+7 -2
View File
@@ -43,9 +43,14 @@ export const kubernetesPlugin = createPlugin({
deps: {
discoveryApi: discoveryApiRef,
identityApi: identityApiRef,
kubernetesAuthProvidersApi: kubernetesAuthProvidersApiRef,
},
factory: ({ discoveryApi, identityApi }) =>
new KubernetesBackendClient({ discoveryApi, identityApi }),
factory: ({ discoveryApi, identityApi, kubernetesAuthProvidersApi }) =>
new KubernetesBackendClient({
discoveryApi,
identityApi,
kubernetesAuthProvidersApi,
}),
}),
createApiFactory({
api: kubernetesAuthProvidersApiRef,
+1
View File
@@ -16,6 +16,7 @@
import '@testing-library/jest-dom';
// eslint-disable-next-line no-restricted-imports
import { TextDecoder, TextEncoder } from 'util';
import 'cross-fetch/polyfill';
// These are missing from jest-node, so not available on global.
Object.defineProperty(global, 'TextEncoder', {
+1
View File
@@ -7155,6 +7155,7 @@ __metadata:
"@backstage/core-components": "workspace:^"
"@backstage/core-plugin-api": "workspace:^"
"@backstage/dev-utils": "workspace:^"
"@backstage/errors": "workspace:^"
"@backstage/plugin-catalog-react": "workspace:^"
"@backstage/plugin-kubernetes-common": "workspace:^"
"@backstage/test-utils": "workspace:^"