Merge pull request #24006 from backstage/blam/k8s-cluster-fix
kubernetes: fixing credentials propagation for cluster suppliers
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-kubernetes-backend': patch
|
||||
'@backstage/plugin-kubernetes-node': patch
|
||||
---
|
||||
|
||||
Fixing issue where `BackstageCredentials` were not properly forwarded for all calls
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-kubernetes-backend': minor
|
||||
---
|
||||
|
||||
**BREAKING**: `KubernetesProxy` now requires the `DiscoveryService` to be passed to the constuctor
|
||||
@@ -26,6 +26,7 @@ import { KubernetesServiceLocator as KubernetesServiceLocator_2 } from '@backsta
|
||||
import { Logger } from 'winston';
|
||||
import { ObjectToFetch as ObjectToFetch_2 } from '@backstage/plugin-kubernetes-node';
|
||||
import { PermissionEvaluator } from '@backstage/plugin-permission-common';
|
||||
import { PermissionsService } from '@backstage/backend-plugin-api';
|
||||
import { PluginEndpointDiscovery } from '@backstage/backend-common';
|
||||
import { RequestHandler } from 'http-proxy-middleware';
|
||||
import { TokenCredential } from '@azure/identity';
|
||||
@@ -190,6 +191,8 @@ export class KubernetesBuilder {
|
||||
protected buildProxy(
|
||||
logger: Logger,
|
||||
clusterSupplier: KubernetesClustersSupplier_2,
|
||||
discovery: DiscoveryService,
|
||||
httpAuth: HttpAuthService,
|
||||
): KubernetesProxy;
|
||||
// (undocumented)
|
||||
protected buildRouter(
|
||||
@@ -239,6 +242,8 @@ export class KubernetesBuilder {
|
||||
protected getProxy(
|
||||
logger: Logger,
|
||||
clusterSupplier: KubernetesClustersSupplier_2,
|
||||
discovery: DiscoveryService,
|
||||
httpAuth: HttpAuthService,
|
||||
): KubernetesProxy;
|
||||
// (undocumented)
|
||||
protected getServiceLocator(): KubernetesServiceLocator_2;
|
||||
@@ -337,7 +342,7 @@ export class KubernetesProxy {
|
||||
|
||||
// @public
|
||||
export type KubernetesProxyCreateRequestHandlerOptions = {
|
||||
permissionApi: PermissionEvaluator;
|
||||
permissionApi: PermissionsService;
|
||||
};
|
||||
|
||||
// @public
|
||||
@@ -345,6 +350,8 @@ export type KubernetesProxyOptions = {
|
||||
logger: Logger;
|
||||
clusterSupplier: KubernetesClustersSupplier;
|
||||
authStrategy: AuthenticationStrategy;
|
||||
discovery: DiscoveryService;
|
||||
httpAuth?: HttpAuthService;
|
||||
};
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
|
||||
@@ -63,10 +63,13 @@ export const addResourceRoutesToRouter = (
|
||||
|
||||
router.post('/resources/workloads/query', async (req, res) => {
|
||||
const entity = await getEntityByReq(req);
|
||||
const response = await objectsProvider.getKubernetesObjectsByEntity({
|
||||
entity,
|
||||
auth: req.body.auth,
|
||||
});
|
||||
const response = await objectsProvider.getKubernetesObjectsByEntity(
|
||||
{
|
||||
entity,
|
||||
auth: req.body.auth,
|
||||
},
|
||||
{ credentials: await httpAuth.credentials(req) },
|
||||
);
|
||||
res.json(response);
|
||||
});
|
||||
|
||||
@@ -81,11 +84,14 @@ export const addResourceRoutesToRouter = (
|
||||
throw new InputError('at least 1 customResource is required');
|
||||
}
|
||||
|
||||
const response = await objectsProvider.getCustomResourcesByEntity({
|
||||
entity,
|
||||
customResources: req.body.customResources,
|
||||
auth: req.body.auth,
|
||||
});
|
||||
const response = await objectsProvider.getCustomResourcesByEntity(
|
||||
{
|
||||
entity,
|
||||
customResources: req.body.customResources,
|
||||
auth: req.body.auth,
|
||||
},
|
||||
{ credentials: await httpAuth.credentials(req) },
|
||||
);
|
||||
res.json(response);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -33,7 +33,7 @@ export class CatalogRelationServiceLocator implements KubernetesServiceLocator {
|
||||
// As this implementation always returns all clusters serviceId is ignored here
|
||||
getClustersByEntity(
|
||||
entity: Entity,
|
||||
_requestContext: ServiceLocatorRequestContext,
|
||||
requestContext: ServiceLocatorRequestContext,
|
||||
): Promise<{ clusters: ClusterDetails[] }> {
|
||||
if (
|
||||
entity.relations &&
|
||||
@@ -41,13 +41,15 @@ export class CatalogRelationServiceLocator implements KubernetesServiceLocator {
|
||||
r => r.type === 'dependsOn' && r.targetRef.includes('resource:'),
|
||||
)
|
||||
) {
|
||||
return this.clusterSupplier.getClusters().then(clusters => {
|
||||
return {
|
||||
clusters: clusters.filter(c =>
|
||||
this.doesEntityDependOnCluster(entity, c),
|
||||
),
|
||||
};
|
||||
});
|
||||
return this.clusterSupplier
|
||||
.getClusters({ credentials: requestContext.credentials })
|
||||
.then(clusters => {
|
||||
return {
|
||||
clusters: clusters.filter(c =>
|
||||
this.doesEntityDependOnCluster(entity, c),
|
||||
),
|
||||
};
|
||||
});
|
||||
}
|
||||
return Promise.resolve({ clusters: [] });
|
||||
}
|
||||
|
||||
@@ -34,8 +34,10 @@ export class MultiTenantServiceLocator implements KubernetesServiceLocator {
|
||||
// As this implementation always returns all clusters serviceId is ignored here
|
||||
getClustersByEntity(
|
||||
_entity: Entity,
|
||||
_requestContext: ServiceLocatorRequestContext,
|
||||
requestContext: ServiceLocatorRequestContext,
|
||||
): Promise<{ clusters: ClusterDetails[] }> {
|
||||
return this.clusterSupplier.getClusters().then(clusters => ({ clusters }));
|
||||
return this.clusterSupplier
|
||||
.getClusters({ credentials: requestContext.credentials })
|
||||
.then(clusters => ({ clusters }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,21 +34,25 @@ export class SingleTenantServiceLocator implements KubernetesServiceLocator {
|
||||
// As this implementation always returns all clusters serviceId is ignored here
|
||||
getClustersByEntity(
|
||||
_entity: Entity,
|
||||
_requestContext: ServiceLocatorRequestContext,
|
||||
requestContext: ServiceLocatorRequestContext,
|
||||
): Promise<{ clusters: ClusterDetails[] }> {
|
||||
return this.clusterSupplier.getClusters().then(clusters => {
|
||||
if (_entity.metadata?.annotations?.['backstage.io/kubernetes-cluster']) {
|
||||
return {
|
||||
clusters: clusters.filter(
|
||||
c =>
|
||||
c.name ===
|
||||
_entity.metadata?.annotations?.[
|
||||
'backstage.io/kubernetes-cluster'
|
||||
],
|
||||
),
|
||||
};
|
||||
}
|
||||
return { clusters };
|
||||
});
|
||||
return this.clusterSupplier
|
||||
.getClusters({ credentials: requestContext.credentials })
|
||||
.then(clusters => {
|
||||
if (
|
||||
_entity.metadata?.annotations?.['backstage.io/kubernetes-cluster']
|
||||
) {
|
||||
return {
|
||||
clusters: clusters.filter(
|
||||
c =>
|
||||
c.name ===
|
||||
_entity.metadata?.annotations?.[
|
||||
'backstage.io/kubernetes-cluster'
|
||||
],
|
||||
),
|
||||
};
|
||||
}
|
||||
return { clusters };
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,7 +157,12 @@ export class KubernetesBuilder {
|
||||
|
||||
const authStrategyMap = this.getAuthStrategyMap();
|
||||
|
||||
const proxy = this.getProxy(logger, clusterSupplier);
|
||||
const proxy = this.getProxy(
|
||||
logger,
|
||||
clusterSupplier,
|
||||
this.env.discovery,
|
||||
httpAuth,
|
||||
);
|
||||
|
||||
const serviceLocator = this.getServiceLocator();
|
||||
|
||||
@@ -350,6 +355,8 @@ export class KubernetesBuilder {
|
||||
protected buildProxy(
|
||||
logger: Logger,
|
||||
clusterSupplier: KubernetesClustersSupplier,
|
||||
discovery: DiscoveryService,
|
||||
httpAuth: HttpAuthService,
|
||||
): KubernetesProxy {
|
||||
const authStrategyMap = this.getAuthStrategyMap();
|
||||
const authStrategy = new DispatchStrategy({
|
||||
@@ -359,6 +366,8 @@ export class KubernetesBuilder {
|
||||
logger,
|
||||
clusterSupplier,
|
||||
authStrategy,
|
||||
discovery,
|
||||
httpAuth,
|
||||
});
|
||||
return this.proxy;
|
||||
}
|
||||
@@ -386,10 +395,13 @@ export class KubernetesBuilder {
|
||||
const serviceId = req.params.serviceId;
|
||||
const requestBody: ObjectsByEntityRequest = req.body;
|
||||
try {
|
||||
const response = await objectsProvider.getKubernetesObjectsByEntity({
|
||||
entity: requestBody.entity,
|
||||
auth: requestBody.auth || {},
|
||||
});
|
||||
const response = await objectsProvider.getKubernetesObjectsByEntity(
|
||||
{
|
||||
entity: requestBody.entity,
|
||||
auth: requestBody.auth || {},
|
||||
},
|
||||
{ credentials: await httpAuth.credentials(req) },
|
||||
);
|
||||
res.json(response);
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
@@ -530,8 +542,13 @@ export class KubernetesBuilder {
|
||||
protected getProxy(
|
||||
logger: Logger,
|
||||
clusterSupplier: KubernetesClustersSupplier,
|
||||
discovery: DiscoveryService,
|
||||
httpAuth: HttpAuthService,
|
||||
) {
|
||||
return this.proxy ?? this.buildProxy(logger, clusterSupplier);
|
||||
return (
|
||||
this.proxy ??
|
||||
this.buildProxy(logger, clusterSupplier, discovery, httpAuth)
|
||||
);
|
||||
}
|
||||
|
||||
protected getAuthStrategyMap() {
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
} from '@backstage/plugin-kubernetes-common';
|
||||
import { Config, ConfigReader } from '@backstage/config';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { BackstageCredentials } from '@backstage/backend-plugin-api';
|
||||
|
||||
describe('KubernetesFanOutHandler', () => {
|
||||
const fetchObjectsForService = jest.fn();
|
||||
@@ -62,6 +63,14 @@ describe('KubernetesFanOutHandler', () => {
|
||||
pod: {},
|
||||
};
|
||||
|
||||
const mockCredentials: BackstageCredentials = {
|
||||
$$type: '@backstage/BackstageCredentials',
|
||||
principal: {
|
||||
userEntityRef: 'user:default/guest',
|
||||
type: 'user',
|
||||
},
|
||||
};
|
||||
|
||||
const entity = {
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'Component',
|
||||
@@ -372,10 +381,13 @@ describe('KubernetesFanOutHandler', () => {
|
||||
|
||||
sut = getKubernetesFanOutHandler([]);
|
||||
|
||||
const result = await sut.getKubernetesObjectsByEntity({
|
||||
entity,
|
||||
auth: {},
|
||||
});
|
||||
const result = await sut.getKubernetesObjectsByEntity(
|
||||
{
|
||||
entity,
|
||||
auth: {},
|
||||
},
|
||||
{ credentials: mockCredentials },
|
||||
);
|
||||
|
||||
expect(getClustersByEntity).toHaveBeenCalledTimes(1);
|
||||
expect(fetchObjectsForService).toHaveBeenCalledTimes(1);
|
||||
@@ -410,10 +422,13 @@ describe('KubernetesFanOutHandler', () => {
|
||||
|
||||
sut = getKubernetesFanOutHandler([]);
|
||||
|
||||
await sut.getKubernetesObjectsByEntity({
|
||||
entity,
|
||||
auth: {},
|
||||
});
|
||||
await sut.getKubernetesObjectsByEntity(
|
||||
{
|
||||
entity,
|
||||
auth: {},
|
||||
},
|
||||
{ credentials: mockCredentials },
|
||||
);
|
||||
|
||||
expect(fetchObjectsForService).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
@@ -442,10 +457,13 @@ describe('KubernetesFanOutHandler', () => {
|
||||
|
||||
sut = getKubernetesFanOutHandler([]);
|
||||
|
||||
await sut.getKubernetesObjectsByEntity({
|
||||
entity,
|
||||
auth: {},
|
||||
});
|
||||
await sut.getKubernetesObjectsByEntity(
|
||||
{
|
||||
entity,
|
||||
auth: {},
|
||||
},
|
||||
{ credentials: mockCredentials },
|
||||
);
|
||||
|
||||
expect(fetchObjectsForService).toHaveBeenCalledTimes(2);
|
||||
expect(fetchObjectsForService).toHaveBeenCalledWith(
|
||||
@@ -497,10 +515,13 @@ describe('KubernetesFanOutHandler', () => {
|
||||
},
|
||||
]);
|
||||
|
||||
await sut.getKubernetesObjectsByEntity({
|
||||
entity,
|
||||
auth: {},
|
||||
});
|
||||
await sut.getKubernetesObjectsByEntity(
|
||||
{
|
||||
entity,
|
||||
auth: {},
|
||||
},
|
||||
{ credentials: mockCredentials },
|
||||
);
|
||||
|
||||
expect(fetchObjectsForService).toHaveBeenCalledTimes(2);
|
||||
expect(fetchObjectsForService).toHaveBeenCalledWith(
|
||||
@@ -559,10 +580,13 @@ describe('KubernetesFanOutHandler', () => {
|
||||
},
|
||||
]);
|
||||
|
||||
await sut.getKubernetesObjectsByEntity({
|
||||
entity,
|
||||
auth: {},
|
||||
});
|
||||
await sut.getKubernetesObjectsByEntity(
|
||||
{
|
||||
entity,
|
||||
auth: {},
|
||||
},
|
||||
{ credentials: mockCredentials },
|
||||
);
|
||||
|
||||
expect(fetchObjectsForService).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@@ -600,10 +624,13 @@ describe('KubernetesFanOutHandler', () => {
|
||||
},
|
||||
]);
|
||||
|
||||
await sut.getKubernetesObjectsByEntity({
|
||||
entity,
|
||||
auth: {},
|
||||
});
|
||||
await sut.getKubernetesObjectsByEntity(
|
||||
{
|
||||
entity,
|
||||
auth: {},
|
||||
},
|
||||
{ credentials: mockCredentials },
|
||||
);
|
||||
|
||||
expect(fetchObjectsForService).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@@ -665,10 +692,13 @@ describe('KubernetesFanOutHandler', () => {
|
||||
|
||||
sut = getKubernetesFanOutHandler([]);
|
||||
|
||||
const result = await sut.getKubernetesObjectsByEntity({
|
||||
entity,
|
||||
auth: {},
|
||||
});
|
||||
const result = await sut.getKubernetesObjectsByEntity(
|
||||
{
|
||||
entity,
|
||||
auth: {},
|
||||
},
|
||||
{ credentials: mockCredentials },
|
||||
);
|
||||
|
||||
expect(getClustersByEntity).toHaveBeenCalledTimes(1);
|
||||
expect(fetchObjectsForService).toHaveBeenCalledTimes(1);
|
||||
@@ -744,10 +774,13 @@ describe('KubernetesFanOutHandler', () => {
|
||||
|
||||
sut = getKubernetesFanOutHandler([]);
|
||||
|
||||
const result = await sut.getKubernetesObjectsByEntity({
|
||||
entity,
|
||||
auth: {},
|
||||
});
|
||||
const result = await sut.getKubernetesObjectsByEntity(
|
||||
{
|
||||
entity,
|
||||
auth: {},
|
||||
},
|
||||
{ credentials: mockCredentials },
|
||||
);
|
||||
|
||||
expect(getClustersByEntity).toHaveBeenCalledTimes(1);
|
||||
expect(fetchObjectsForService).toHaveBeenCalledTimes(1);
|
||||
@@ -779,12 +812,15 @@ describe('KubernetesFanOutHandler', () => {
|
||||
|
||||
sut = getKubernetesFanOutHandler([]);
|
||||
|
||||
const result = await sut.getKubernetesObjectsByEntity({
|
||||
entity,
|
||||
auth: {
|
||||
google: 'google_token_123',
|
||||
const result = await sut.getKubernetesObjectsByEntity(
|
||||
{
|
||||
entity,
|
||||
auth: {
|
||||
google: 'google_token_123',
|
||||
},
|
||||
},
|
||||
});
|
||||
{ credentials: mockCredentials },
|
||||
);
|
||||
|
||||
expect(getClustersByEntity).toHaveBeenCalledTimes(1);
|
||||
expect(fetchObjectsForService).toHaveBeenCalledTimes(2);
|
||||
@@ -836,12 +872,15 @@ describe('KubernetesFanOutHandler', () => {
|
||||
|
||||
sut = getKubernetesFanOutHandler([]);
|
||||
|
||||
const result = await sut.getKubernetesObjectsByEntity({
|
||||
entity,
|
||||
auth: {
|
||||
google: 'google_token_123',
|
||||
const result = await sut.getKubernetesObjectsByEntity(
|
||||
{
|
||||
entity,
|
||||
auth: {
|
||||
google: 'google_token_123',
|
||||
},
|
||||
},
|
||||
});
|
||||
{ credentials: mockCredentials },
|
||||
);
|
||||
|
||||
expect(getClustersByEntity).toHaveBeenCalledTimes(1);
|
||||
expect(fetchObjectsForService).toHaveBeenCalledTimes(3);
|
||||
@@ -897,12 +936,15 @@ describe('KubernetesFanOutHandler', () => {
|
||||
|
||||
sut = getKubernetesFanOutHandler([]);
|
||||
|
||||
const result = await sut.getKubernetesObjectsByEntity({
|
||||
entity,
|
||||
auth: {
|
||||
google: 'google_token_123',
|
||||
const result = await sut.getKubernetesObjectsByEntity(
|
||||
{
|
||||
entity,
|
||||
auth: {
|
||||
google: 'google_token_123',
|
||||
},
|
||||
},
|
||||
});
|
||||
{ credentials: mockCredentials },
|
||||
);
|
||||
|
||||
expect(getClustersByEntity).toHaveBeenCalledTimes(1);
|
||||
expect(fetchObjectsForService).toHaveBeenCalledTimes(4);
|
||||
@@ -1015,12 +1057,15 @@ describe('KubernetesFanOutHandler', () => {
|
||||
|
||||
sut = getKubernetesFanOutHandler([]);
|
||||
|
||||
const result = await sut.getKubernetesObjectsByEntity({
|
||||
entity,
|
||||
auth: {
|
||||
google: 'google_token_123',
|
||||
const result = await sut.getKubernetesObjectsByEntity(
|
||||
{
|
||||
entity,
|
||||
auth: {
|
||||
google: 'google_token_123',
|
||||
},
|
||||
},
|
||||
});
|
||||
{ credentials: mockCredentials },
|
||||
);
|
||||
|
||||
expect(getClustersByEntity).toHaveBeenCalledTimes(1);
|
||||
expect(fetchObjectsForService).toHaveBeenCalledTimes(2);
|
||||
@@ -1069,10 +1114,13 @@ describe('KubernetesFanOutHandler', () => {
|
||||
|
||||
sut = getKubernetesFanOutHandler([]);
|
||||
|
||||
const result = sut.getKubernetesObjectsByEntity({
|
||||
entity,
|
||||
auth: {},
|
||||
});
|
||||
const result = sut.getKubernetesObjectsByEntity(
|
||||
{
|
||||
entity,
|
||||
auth: {},
|
||||
},
|
||||
{ credentials: mockCredentials },
|
||||
);
|
||||
await expect(result).rejects.toThrow(nonFetchError);
|
||||
});
|
||||
|
||||
@@ -1155,10 +1203,13 @@ describe('KubernetesFanOutHandler', () => {
|
||||
});
|
||||
|
||||
const result =
|
||||
await kubernetesFanOutHandler.getKubernetesObjectsByEntity({
|
||||
entity,
|
||||
auth: {},
|
||||
});
|
||||
await kubernetesFanOutHandler.getKubernetesObjectsByEntity(
|
||||
{
|
||||
entity,
|
||||
auth: {},
|
||||
},
|
||||
{ credentials: mockCredentials },
|
||||
);
|
||||
|
||||
const expected: ObjectsByEntityResponse = {
|
||||
items: [
|
||||
@@ -1200,17 +1251,20 @@ describe('KubernetesFanOutHandler', () => {
|
||||
|
||||
sut = getKubernetesFanOutHandler([]);
|
||||
|
||||
await sut.getCustomResourcesByEntity({
|
||||
entity,
|
||||
auth: {},
|
||||
customResources: [
|
||||
{
|
||||
group: 'parameter-crd.example.com',
|
||||
apiVersion: 'v1alpha1',
|
||||
plural: 'parameter-crd',
|
||||
},
|
||||
],
|
||||
});
|
||||
await sut.getCustomResourcesByEntity(
|
||||
{
|
||||
entity,
|
||||
auth: {},
|
||||
customResources: [
|
||||
{
|
||||
group: 'parameter-crd.example.com',
|
||||
apiVersion: 'v1alpha1',
|
||||
plural: 'parameter-crd',
|
||||
},
|
||||
],
|
||||
},
|
||||
{ credentials: mockCredentials },
|
||||
);
|
||||
|
||||
expect(fetchObjectsForService).toHaveBeenCalledTimes(1);
|
||||
expect(fetchObjectsForService).toHaveBeenCalledWith(
|
||||
@@ -1262,17 +1316,20 @@ describe('KubernetesFanOutHandler', () => {
|
||||
},
|
||||
]);
|
||||
|
||||
await sut.getCustomResourcesByEntity({
|
||||
entity,
|
||||
auth: {},
|
||||
customResources: [
|
||||
{
|
||||
group: 'parameter-crd.example.com',
|
||||
apiVersion: 'v1alpha1',
|
||||
plural: 'parameter-crd',
|
||||
},
|
||||
],
|
||||
});
|
||||
await sut.getCustomResourcesByEntity(
|
||||
{
|
||||
entity,
|
||||
auth: {},
|
||||
customResources: [
|
||||
{
|
||||
group: 'parameter-crd.example.com',
|
||||
apiVersion: 'v1alpha1',
|
||||
plural: 'parameter-crd',
|
||||
},
|
||||
],
|
||||
},
|
||||
{ credentials: mockCredentials },
|
||||
);
|
||||
|
||||
expect(fetchObjectsForService).toHaveBeenCalledTimes(2);
|
||||
expect(fetchObjectsForService).toHaveBeenCalledWith(
|
||||
@@ -1329,17 +1386,20 @@ describe('KubernetesFanOutHandler', () => {
|
||||
|
||||
sut = getKubernetesFanOutHandler([]);
|
||||
|
||||
await sut.getCustomResourcesByEntity({
|
||||
entity,
|
||||
auth: {},
|
||||
customResources: [
|
||||
{
|
||||
group: 'parameter-crd.example.com',
|
||||
apiVersion: 'v1alpha1',
|
||||
plural: 'parameter-crd',
|
||||
},
|
||||
],
|
||||
});
|
||||
await sut.getCustomResourcesByEntity(
|
||||
{
|
||||
entity,
|
||||
auth: {},
|
||||
customResources: [
|
||||
{
|
||||
group: 'parameter-crd.example.com',
|
||||
apiVersion: 'v1alpha1',
|
||||
plural: 'parameter-crd',
|
||||
},
|
||||
],
|
||||
},
|
||||
{ credentials: mockCredentials },
|
||||
);
|
||||
|
||||
expect(fetchObjectsForService).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
||||
@@ -48,7 +48,9 @@ import {
|
||||
CustomResourcesByEntity,
|
||||
KubernetesCredential,
|
||||
KubernetesObjectsByEntity,
|
||||
KubernetesObjectsProvider,
|
||||
} from '@backstage/plugin-kubernetes-node';
|
||||
import { BackstageCredentials } from '@backstage/backend-plugin-api';
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -190,7 +192,7 @@ const toClientSafePodMetrics = (
|
||||
|
||||
type responseWithMetrics = [FetchResponseWrapper, PodStatusFetchResponse[]];
|
||||
|
||||
export class KubernetesFanOutHandler {
|
||||
export class KubernetesFanOutHandler implements KubernetesObjectsProvider {
|
||||
private readonly logger: Logger;
|
||||
private readonly fetcher: KubernetesFetcher;
|
||||
private readonly serviceLocator: KubernetesServiceLocator;
|
||||
@@ -214,30 +216,38 @@ export class KubernetesFanOutHandler {
|
||||
this.authStrategy = authStrategy;
|
||||
}
|
||||
|
||||
async getCustomResourcesByEntity({
|
||||
entity,
|
||||
auth,
|
||||
customResources,
|
||||
}: CustomResourcesByEntity): Promise<ObjectsByEntityResponse> {
|
||||
async getCustomResourcesByEntity(
|
||||
{ entity, auth, customResources }: CustomResourcesByEntity,
|
||||
options: { credentials: BackstageCredentials },
|
||||
): Promise<ObjectsByEntityResponse> {
|
||||
// Don't fetch the default object types only the provided custom resources
|
||||
return this.fanOutRequests(
|
||||
entity,
|
||||
auth,
|
||||
{ credentials: options.credentials },
|
||||
new Set<ObjectToFetch>(),
|
||||
customResources,
|
||||
);
|
||||
}
|
||||
|
||||
async getKubernetesObjectsByEntity({
|
||||
entity,
|
||||
auth,
|
||||
}: KubernetesObjectsByEntity): Promise<ObjectsByEntityResponse> {
|
||||
return this.fanOutRequests(entity, auth, this.objectTypesToFetch);
|
||||
async getKubernetesObjectsByEntity(
|
||||
{ entity, auth }: KubernetesObjectsByEntity,
|
||||
options: { credentials: BackstageCredentials },
|
||||
): Promise<ObjectsByEntityResponse> {
|
||||
return this.fanOutRequests(
|
||||
entity,
|
||||
auth,
|
||||
{
|
||||
credentials: options.credentials,
|
||||
},
|
||||
this.objectTypesToFetch,
|
||||
);
|
||||
}
|
||||
|
||||
private async fanOutRequests(
|
||||
entity: Entity,
|
||||
auth: KubernetesRequestAuth,
|
||||
options: { credentials: BackstageCredentials },
|
||||
objectTypesToFetch: Set<ObjectToFetch>,
|
||||
customResources?: CustomResourceMatcher[],
|
||||
) {
|
||||
@@ -248,6 +258,7 @@ export class KubernetesFanOutHandler {
|
||||
const { clusters } = await this.serviceLocator.getClustersByEntity(entity, {
|
||||
objectTypesToFetch: objectTypesToFetch,
|
||||
customResources: customResources ?? [],
|
||||
credentials: options.credentials,
|
||||
});
|
||||
|
||||
this.logger.info(
|
||||
|
||||
@@ -55,6 +55,10 @@ import {
|
||||
} from './KubernetesProxy';
|
||||
|
||||
import type { Request } from 'express';
|
||||
import {
|
||||
BackstageCredentials,
|
||||
DiscoveryService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
|
||||
const mockCertDir = createMockDirectory({
|
||||
content: {
|
||||
@@ -69,7 +73,10 @@ describe('KubernetesProxy', () => {
|
||||
const logger = getVoidLogger();
|
||||
|
||||
const clusterSupplier: jest.Mocked<KubernetesClustersSupplier> = {
|
||||
getClusters: jest.fn<Promise<ClusterDetails[]>, []>(),
|
||||
getClusters: jest.fn<
|
||||
Promise<ClusterDetails[]>,
|
||||
[{ credentials: BackstageCredentials }]
|
||||
>(),
|
||||
};
|
||||
|
||||
const permissionApi: jest.Mocked<PermissionEvaluator> = {
|
||||
@@ -77,6 +84,11 @@ describe('KubernetesProxy', () => {
|
||||
authorizeConditional: jest.fn(),
|
||||
};
|
||||
|
||||
const mockDisocveryApi: jest.Mocked<DiscoveryService> = {
|
||||
getBaseUrl: jest.fn(),
|
||||
getExternalBaseUrl: jest.fn(),
|
||||
};
|
||||
|
||||
setupRequestMockHandlers(worker);
|
||||
|
||||
const buildMockRequest = (clusterName: any, path: string): Request => {
|
||||
@@ -146,7 +158,12 @@ describe('KubernetesProxy', () => {
|
||||
validateCluster: jest.fn(),
|
||||
presentAuthMetadata: jest.fn(),
|
||||
};
|
||||
proxy = new KubernetesProxy({ logger, clusterSupplier, authStrategy });
|
||||
proxy = new KubernetesProxy({
|
||||
logger,
|
||||
clusterSupplier,
|
||||
authStrategy,
|
||||
discovery: mockDisocveryApi,
|
||||
});
|
||||
permissionApi.authorize.mockResolvedValue([
|
||||
{ result: AuthorizeResult.ALLOW },
|
||||
]);
|
||||
@@ -535,6 +552,7 @@ describe('KubernetesProxy', () => {
|
||||
logger: getVoidLogger(),
|
||||
clusterSupplier: clusterSupplier,
|
||||
authStrategy: strategy,
|
||||
discovery: mockDisocveryApi,
|
||||
});
|
||||
|
||||
worker.use(
|
||||
@@ -656,6 +674,7 @@ describe('KubernetesProxy', () => {
|
||||
logger: getVoidLogger(),
|
||||
clusterSupplier: new LocalKubectlProxyClusterLocator(),
|
||||
authStrategy: new AnonymousStrategy(),
|
||||
discovery: mockDisocveryApi,
|
||||
});
|
||||
|
||||
worker.use(
|
||||
|
||||
@@ -20,16 +20,12 @@ import {
|
||||
NotFoundError,
|
||||
serializeError,
|
||||
} from '@backstage/errors';
|
||||
import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node';
|
||||
import {
|
||||
ANNOTATION_KUBERNETES_AUTH_PROVIDER,
|
||||
KubernetesRequestAuth,
|
||||
kubernetesProxyPermission,
|
||||
} from '@backstage/plugin-kubernetes-common';
|
||||
import {
|
||||
AuthorizeResult,
|
||||
PermissionEvaluator,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
import { AuthorizeResult } from '@backstage/plugin-permission-common';
|
||||
import {
|
||||
Cluster,
|
||||
KubeConfig,
|
||||
@@ -45,6 +41,12 @@ import { ClusterDetails, KubernetesClustersSupplier } from '../types/types';
|
||||
|
||||
import type { Request } from 'express';
|
||||
import { IncomingHttpHeaders } from 'http';
|
||||
import {
|
||||
DiscoveryService,
|
||||
HttpAuthService,
|
||||
PermissionsService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { createLegacyAuthAdapters } from '@backstage/backend-common';
|
||||
|
||||
export const APPLICATION_JSON: string = 'application/json';
|
||||
|
||||
@@ -69,7 +71,7 @@ export const HEADER_KUBERNETES_AUTH: string =
|
||||
* @public
|
||||
*/
|
||||
export type KubernetesProxyCreateRequestHandlerOptions = {
|
||||
permissionApi: PermissionEvaluator;
|
||||
permissionApi: PermissionsService;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -81,6 +83,8 @@ export type KubernetesProxyOptions = {
|
||||
logger: Logger;
|
||||
clusterSupplier: KubernetesClustersSupplier;
|
||||
authStrategy: AuthenticationStrategy;
|
||||
discovery: DiscoveryService;
|
||||
httpAuth?: HttpAuthService;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -93,11 +97,19 @@ export class KubernetesProxy {
|
||||
private readonly logger: Logger;
|
||||
private readonly clusterSupplier: KubernetesClustersSupplier;
|
||||
private readonly authStrategy: AuthenticationStrategy;
|
||||
private readonly httpAuth: HttpAuthService;
|
||||
|
||||
constructor(options: KubernetesProxyOptions) {
|
||||
this.logger = options.logger;
|
||||
this.clusterSupplier = options.clusterSupplier;
|
||||
this.authStrategy = options.authStrategy;
|
||||
|
||||
const legacy = createLegacyAuthAdapters({
|
||||
discovery: options.discovery,
|
||||
httpAuth: options.httpAuth,
|
||||
});
|
||||
|
||||
this.httpAuth = legacy.httpAuth;
|
||||
}
|
||||
|
||||
public createRequestHandler(
|
||||
@@ -108,9 +120,7 @@ export class KubernetesProxy {
|
||||
const authorizeResponse = await permissionApi.authorize(
|
||||
[{ permission: kubernetesProxyPermission }],
|
||||
{
|
||||
token: getBearerTokenFromAuthorizationHeader(
|
||||
req.header('authorization'),
|
||||
),
|
||||
credentials: await this.httpAuth.credentials(req),
|
||||
},
|
||||
);
|
||||
const auth = authorizeResponse[0];
|
||||
@@ -220,7 +230,9 @@ export class KubernetesProxy {
|
||||
|
||||
private async getClusterForRequest(req: Request): Promise<ClusterDetails> {
|
||||
const clusterName = req.headers[HEADER_KUBERNETES_CLUSTER.toLowerCase()];
|
||||
const clusters = await this.clusterSupplier.getClusters();
|
||||
const clusters = await this.clusterSupplier.getClusters({
|
||||
credentials: await this.httpAuth.credentials(req),
|
||||
});
|
||||
|
||||
if (!clusters || clusters.length <= 0) {
|
||||
throw new NotFoundError(`No Clusters configured`);
|
||||
|
||||
@@ -88,7 +88,7 @@ export const kubernetesAuthStrategyExtensionPoint: ExtensionPoint<KubernetesAuth
|
||||
|
||||
// @public
|
||||
export interface KubernetesClustersSupplier {
|
||||
getClusters(options?: {
|
||||
getClusters(options: {
|
||||
credentials: BackstageCredentials;
|
||||
}): Promise<ClusterDetails[]>;
|
||||
}
|
||||
@@ -154,10 +154,16 @@ export interface KubernetesObjectsProvider {
|
||||
// (undocumented)
|
||||
getCustomResourcesByEntity(
|
||||
customResourcesByEntity: CustomResourcesByEntity,
|
||||
options: {
|
||||
credentials: BackstageCredentials;
|
||||
},
|
||||
): Promise<ObjectsByEntityResponse>;
|
||||
// (undocumented)
|
||||
getKubernetesObjectsByEntity(
|
||||
kubernetesObjectsByEntity: KubernetesObjectsByEntity,
|
||||
options: {
|
||||
credentials: BackstageCredentials;
|
||||
},
|
||||
): Promise<ObjectsByEntityResponse>;
|
||||
}
|
||||
|
||||
@@ -269,6 +275,8 @@ export type PinnipedParameters = {
|
||||
|
||||
// @public (undocumented)
|
||||
export interface ServiceLocatorRequestContext {
|
||||
// (undocumented)
|
||||
credentials: BackstageCredentials;
|
||||
// (undocumented)
|
||||
customResources: CustomResourceMatcher[];
|
||||
// (undocumented)
|
||||
|
||||
@@ -32,9 +32,15 @@ import { JsonObject } from '@backstage/types';
|
||||
export interface KubernetesObjectsProvider {
|
||||
getKubernetesObjectsByEntity(
|
||||
kubernetesObjectsByEntity: KubernetesObjectsByEntity,
|
||||
options: {
|
||||
credentials: BackstageCredentials;
|
||||
},
|
||||
): Promise<ObjectsByEntityResponse>;
|
||||
getCustomResourcesByEntity(
|
||||
customResourcesByEntity: CustomResourcesByEntity,
|
||||
options: {
|
||||
credentials: BackstageCredentials;
|
||||
},
|
||||
): Promise<ObjectsByEntityResponse>;
|
||||
}
|
||||
|
||||
@@ -135,7 +141,7 @@ export interface KubernetesClustersSupplier {
|
||||
* Implementations _should_ cache the clusters and refresh them periodically,
|
||||
* as getClusters is called whenever the list of clusters is needed.
|
||||
*/
|
||||
getClusters(options?: {
|
||||
getClusters(options: {
|
||||
credentials: BackstageCredentials;
|
||||
}): Promise<ClusterDetails[]>;
|
||||
}
|
||||
@@ -248,6 +254,7 @@ export interface KubernetesFetcher {
|
||||
export interface ServiceLocatorRequestContext {
|
||||
objectTypesToFetch: Set<ObjectToFetch>;
|
||||
customResources: CustomResourceMatcher[];
|
||||
credentials: BackstageCredentials;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user