refactor: prepare kubernetes backend fan out handler for new endpoint (#11952)

* refactor: prepare kubernetes backend fan out handler for new endpoint

Signed-off-by: Matthew Clarke <mclarke@spotify.com>

* add changeset

Signed-off-by: Matthew Clarke <mclarke@spotify.com>

* PR api report feedback

Signed-off-by: Matthew Clarke <mclarke@spotify.com>

* update api docs

Signed-off-by: Matthew Clarke <mclarke@spotify.com>

* fix api report warnings

Signed-off-by: Matthew Clarke <mclarke@spotify.com>
This commit is contained in:
Matthew Clarke
2022-06-15 15:18:35 -04:00
committed by GitHub
parent 1857c85533
commit 0791af993f
19 changed files with 430 additions and 262 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-kubernetes-backend': minor
'@backstage/plugin-kubernetes-common': minor
---
Refactor `KubernetesObjectsProvider` with new methods, `KubernetesServiceLocator` now takes an `Entity` instead of `serviceId`
+50 -69
View File
@@ -5,18 +5,18 @@
```ts
import { Config } from '@backstage/config';
import { Duration } from 'luxon';
import { Entity } from '@backstage/catalog-model';
import express from 'express';
import type { FetchResponse } from '@backstage/plugin-kubernetes-common';
import type { JsonObject } from '@backstage/types';
import type { KubernetesFetchError } from '@backstage/plugin-kubernetes-common';
import type { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common';
import type { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common';
import { Logger } from 'winston';
import type { ObjectsByEntityResponse } from '@backstage/plugin-kubernetes-common';
import { PodStatus } from '@kubernetes/client-node/dist/top';
// Warning: (ae-missing-release-tag) "AWSClusterDetails" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @alpha (undocumented)
export interface AWSClusterDetails extends ClusterDetails {
// (undocumented)
assumeRole?: string;
@@ -24,14 +24,10 @@ export interface AWSClusterDetails extends ClusterDetails {
externalId?: string;
}
// Warning: (ae-missing-release-tag) "AzureClusterDetails" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @alpha (undocumented)
export interface AzureClusterDetails extends ClusterDetails {}
// Warning: (ae-missing-release-tag) "ClusterDetails" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @alpha (undocumented)
export interface ClusterDetails {
// (undocumented)
authProvider: string;
@@ -51,27 +47,28 @@ export interface ClusterDetails {
url: string;
}
// Warning: (ae-missing-release-tag) "createRouter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public @deprecated
// @alpha @deprecated
export function createRouter(options: RouterOptions): Promise<express.Router>;
// Warning: (ae-missing-release-tag) "CustomResource" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @alpha (undocumented)
export interface CustomResource extends ObjectToFetch {
// (undocumented)
objectType: 'customresources';
}
// Warning: (ae-missing-release-tag) "DEFAULT_OBJECTS" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @alpha (undocumented)
export type CustomResourceMatcher = Omit<ObjectToFetch, 'objectType'>;
// @alpha (undocumented)
export interface CustomResourcesByEntity extends KubernetesObjectsByEntity {
// (undocumented)
customResources: CustomResourceMatcher[];
}
// @alpha (undocumented)
export const DEFAULT_OBJECTS: ObjectToFetch[];
// Warning: (ae-missing-release-tag) "FetchResponseWrapper" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @alpha (undocumented)
export interface FetchResponseWrapper {
// (undocumented)
errors: KubernetesFetchError[];
@@ -79,14 +76,10 @@ export interface FetchResponseWrapper {
responses: FetchResponse[];
}
// Warning: (ae-missing-release-tag) "GKEClusterDetails" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @alpha (undocumented)
export interface GKEClusterDetails extends ClusterDetails {}
// Warning: (ae-missing-release-tag) "KubernetesBuilder" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @alpha (undocumented)
export class KubernetesBuilder {
constructor(env: KubernetesEnvironment);
// (undocumented)
@@ -145,7 +138,7 @@ export class KubernetesBuilder {
setServiceLocator(serviceLocator?: KubernetesServiceLocator): this;
}
// @public
// @alpha
export type KubernetesBuilderReturn = Promise<{
router: express.Router;
clusterSupplier: KubernetesClustersSupplier;
@@ -155,16 +148,12 @@ export type KubernetesBuilderReturn = Promise<{
serviceLocator: KubernetesServiceLocator;
}>;
// Warning: (ae-missing-release-tag) "KubernetesClustersSupplier" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @alpha
export interface KubernetesClustersSupplier {
getClusters(): Promise<ClusterDetails[]>;
}
// Warning: (ae-missing-release-tag) "KubernetesEnvironment" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @alpha (undocumented)
export interface KubernetesEnvironment {
// (undocumented)
config: Config;
@@ -172,9 +161,7 @@ export interface KubernetesEnvironment {
logger: Logger;
}
// Warning: (ae-missing-release-tag) "KubernetesFetcher" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @alpha
export interface KubernetesFetcher {
// (undocumented)
fetchObjectsForService(
@@ -187,19 +174,27 @@ export interface KubernetesFetcher {
): Promise<PodStatus[]>;
}
// Warning: (ae-missing-release-tag) "KubernetesObjectsProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @alpha (undocumented)
export interface KubernetesObjectsByEntity {
// (undocumented)
auth: KubernetesRequestAuth;
// (undocumented)
entity: Entity;
}
// @alpha (undocumented)
export interface KubernetesObjectsProvider {
// (undocumented)
getCustomResourcesByEntity(
customResourcesByEntity: CustomResourcesByEntity,
): Promise<ObjectsByEntityResponse>;
// (undocumented)
getKubernetesObjectsByEntity(
request: ObjectsByEntityRequest,
kubernetesObjectsByEntity: KubernetesObjectsByEntity,
): Promise<ObjectsByEntityResponse>;
}
// Warning: (ae-missing-release-tag) "KubernetesObjectsProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @alpha (undocumented)
export interface KubernetesObjectsProviderOptions {
// (undocumented)
customResources: CustomResource[];
@@ -213,9 +208,7 @@ export interface KubernetesObjectsProviderOptions {
serviceLocator: KubernetesServiceLocator;
}
// Warning: (ae-missing-release-tag) "KubernetesObjectTypes" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @alpha (undocumented)
export type KubernetesObjectTypes =
| 'pods'
| 'services'
@@ -229,17 +222,15 @@ export type KubernetesObjectTypes =
| 'customresources'
| 'statefulsets';
// Warning: (ae-missing-release-tag) "KubernetesServiceLocator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @alpha
export interface KubernetesServiceLocator {
// (undocumented)
getClustersByServiceId(serviceId: string): Promise<ClusterDetails[]>;
getClustersByEntity(entity: Entity): Promise<{
clusters: ClusterDetails[];
}>;
}
// Warning: (ae-missing-release-tag) "ObjectFetchParams" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @alpha (undocumented)
export interface ObjectFetchParams {
// (undocumented)
clusterDetails:
@@ -259,14 +250,10 @@ export interface ObjectFetchParams {
serviceId: string;
}
// Warning: (ae-missing-release-tag) "ObjectsByEntityRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @alpha (undocumented)
export type ObjectsByEntityRequest = KubernetesRequestBody;
// Warning: (ae-missing-release-tag) "ObjectToFetch" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @alpha (undocumented)
export interface ObjectToFetch {
// (undocumented)
apiVersion: string;
@@ -278,9 +265,7 @@ export interface ObjectToFetch {
plural: string;
}
// Warning: (ae-missing-release-tag) "RouterOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @alpha (undocumented)
export interface RouterOptions {
// (undocumented)
clusterSupplier?: KubernetesClustersSupplier;
@@ -290,13 +275,9 @@ export interface RouterOptions {
logger: Logger;
}
// Warning: (ae-missing-release-tag) "ServiceAccountClusterDetails" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @alpha (undocumented)
export interface ServiceAccountClusterDetails extends ClusterDetails {}
// Warning: (ae-missing-release-tag) "ServiceLocatorMethod" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @alpha (undocumented)
export type ServiceLocatorMethod = 'multiTenant' | 'http';
```
@@ -16,20 +16,20 @@
import { KubernetesAuthTranslator } from './types';
import { GKEClusterDetails } from '../types/types';
import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common';
import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common';
export class GoogleKubernetesAuthTranslator
implements KubernetesAuthTranslator
{
async decorateClusterDetailsWithAuth(
clusterDetails: GKEClusterDetails,
requestBody: KubernetesRequestBody,
authConfig: KubernetesRequestAuth,
): Promise<GKEClusterDetails> {
const clusterDetailsWithAuthToken: GKEClusterDetails = Object.assign(
{},
clusterDetails,
);
const authToken: string | undefined = requestBody.auth?.google;
const authToken: string | undefined = authConfig.google;
if (authToken) {
clusterDetailsWithAuthToken.serviceAccountToken = authToken;
@@ -17,7 +17,7 @@
import { KubernetesAuthTranslator } from './types';
import { GoogleKubernetesAuthTranslator } from './GoogleKubernetesAuthTranslator';
import { KubernetesAuthTranslatorGenerator } from './KubernetesAuthTranslatorGenerator';
import { ServiceAccountKubernetesAuthTranslator } from './ServiceAccountKubernetesAuthTranslator';
import { NoopKubernetesAuthTranslator } from './NoopKubernetesAuthTranslator';
import { AwsIamKubernetesAuthTranslator } from './AwsIamKubernetesAuthTranslator';
import { OidcKubernetesAuthTranslator } from './OidcKubernetesAuthTranslator';
import { getVoidLogger } from '@backstage/backend-common';
@@ -42,9 +42,7 @@ describe('getKubernetesAuthTranslatorInstance', () => {
it('can return an auth translator for serviceAccount auth', () => {
const authTranslator: KubernetesAuthTranslator =
sut.getKubernetesAuthTranslatorInstance('serviceAccount', { logger });
expect(
authTranslator instanceof ServiceAccountKubernetesAuthTranslator,
).toBe(true);
expect(authTranslator instanceof NoopKubernetesAuthTranslator).toBe(true);
});
it('can return an auth translator for oidc auth', () => {
@@ -17,7 +17,7 @@
import { Logger } from 'winston';
import { KubernetesAuthTranslator } from './types';
import { GoogleKubernetesAuthTranslator } from './GoogleKubernetesAuthTranslator';
import { ServiceAccountKubernetesAuthTranslator } from './ServiceAccountKubernetesAuthTranslator';
import { NoopKubernetesAuthTranslator } from './NoopKubernetesAuthTranslator';
import { AwsIamKubernetesAuthTranslator } from './AwsIamKubernetesAuthTranslator';
import { GoogleServiceAccountAuthTranslator } from './GoogleServiceAccountAuthProvider';
import { AzureIdentityKubernetesAuthTranslator } from './AzureIdentityKubernetesAuthTranslator';
@@ -41,7 +41,7 @@ export class KubernetesAuthTranslatorGenerator {
return new AzureIdentityKubernetesAuthTranslator(options.logger);
}
case 'serviceAccount': {
return new ServiceAccountKubernetesAuthTranslator();
return new NoopKubernetesAuthTranslator();
}
case 'googleServiceAccount': {
return new GoogleServiceAccountAuthTranslator();
@@ -16,14 +16,10 @@
import { KubernetesAuthTranslator } from './types';
import { ServiceAccountClusterDetails } from '../types/types';
import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common';
export class ServiceAccountKubernetesAuthTranslator
implements KubernetesAuthTranslator
{
export class NoopKubernetesAuthTranslator implements KubernetesAuthTranslator {
async decorateClusterDetailsWithAuth(
clusterDetails: ServiceAccountClusterDetails,
_requestBody: KubernetesRequestBody,
): Promise<ServiceAccountClusterDetails> {
return clusterDetails;
}
@@ -16,15 +16,9 @@
import { OidcKubernetesAuthTranslator } from './OidcKubernetesAuthTranslator';
import { ClusterDetails } from '../types/types';
import { Entity } from '@backstage/catalog-model';
describe('OidcKubernetesAuthTranslator tests', () => {
const at = new OidcKubernetesAuthTranslator();
const entity: Entity = {
apiVersion: 'v1',
kind: 'service',
metadata: { name: 'test' },
};
const baseClusterDetails: ClusterDetails = {
name: 'test',
authProvider: 'oidc',
@@ -38,10 +32,7 @@ describe('OidcKubernetesAuthTranslator tests', () => {
...baseClusterDetails,
},
{
auth: {
oidc: { okta: 'fakeToken' },
},
entity,
oidc: { okta: 'fakeToken' },
},
);
@@ -50,7 +41,7 @@ describe('OidcKubernetesAuthTranslator tests', () => {
it('returns error when oidcTokenProvider is not configured', async () => {
await expect(
at.decorateClusterDetailsWithAuth(baseClusterDetails, { entity }),
at.decorateClusterDetailsWithAuth(baseClusterDetails, {}),
).rejects.toThrow(
'oidc authProvider requires a configured oidcTokenProvider',
);
@@ -60,7 +51,7 @@ describe('OidcKubernetesAuthTranslator tests', () => {
await expect(
at.decorateClusterDetailsWithAuth(
{ oidcTokenProvider: 'okta', ...baseClusterDetails },
{ entity },
{},
),
).rejects.toThrow('Auth token not found under oidc.okta in request body');
});
@@ -16,12 +16,12 @@
import { KubernetesAuthTranslator } from './types';
import { ClusterDetails } from '../types/types';
import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common';
import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common';
export class OidcKubernetesAuthTranslator implements KubernetesAuthTranslator {
async decorateClusterDetailsWithAuth(
clusterDetails: ClusterDetails,
requestBody: KubernetesRequestBody,
authConfig: KubernetesRequestAuth,
): Promise<ClusterDetails> {
const clusterDetailsWithAuthToken: ClusterDetails = Object.assign(
{},
@@ -36,8 +36,7 @@ export class OidcKubernetesAuthTranslator implements KubernetesAuthTranslator {
);
}
const authToken: string | undefined =
requestBody.auth?.oidc?.[oidcTokenProvider];
const authToken: string | undefined = authConfig.oidc?.[oidcTokenProvider];
if (authToken) {
clusterDetailsWithAuthToken.serviceAccountToken = authToken;
@@ -15,11 +15,11 @@
*/
import { ClusterDetails } from '../types/types';
import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common';
import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common';
export interface KubernetesAuthTranslator {
decorateClusterDetailsWithAuth(
clusterDetails: ClusterDetails,
requestBody: KubernetesRequestBody,
authConfig: KubernetesRequestAuth,
): Promise<ClusterDetails>;
}
@@ -15,6 +15,7 @@
*/
import '@backstage/backend-common';
import { Entity } from '@backstage/catalog-model';
import { MultiTenantServiceLocator } from './MultiTenantServiceLocator';
describe('MultiTenantConfigClusterLocator', () => {
@@ -23,9 +24,9 @@ describe('MultiTenantConfigClusterLocator', () => {
getClusters: async () => [],
});
const result = await sut.getClustersByServiceId('ignored');
const result = await sut.getClustersByEntity({} as Entity);
expect(result).toStrictEqual([]);
expect(result).toStrictEqual({ clusters: [] });
});
it('one clusters returns one cluster details', async () => {
@@ -42,16 +43,18 @@ describe('MultiTenantConfigClusterLocator', () => {
},
});
const result = await sut.getClustersByServiceId('ignored');
const result = await sut.getClustersByEntity({} as Entity);
expect(result).toStrictEqual([
{
name: 'cluster1',
serviceAccountToken: '12345',
url: 'http://localhost:8080',
authProvider: 'serviceAccount',
},
]);
expect(result).toStrictEqual({
clusters: [
{
name: 'cluster1',
serviceAccountToken: '12345',
url: 'http://localhost:8080',
authProvider: 'serviceAccount',
},
],
});
});
it('two clusters returns two cluster details', async () => {
@@ -73,20 +76,22 @@ describe('MultiTenantConfigClusterLocator', () => {
},
});
const result = await sut.getClustersByServiceId('ignored');
const result = await sut.getClustersByEntity({} as Entity);
expect(result).toStrictEqual([
{
name: 'cluster1',
serviceAccountToken: 'token',
url: 'http://localhost:8080',
authProvider: 'serviceAccount',
},
{
name: 'cluster2',
url: 'http://localhost:8081',
authProvider: 'google',
},
]);
expect(result).toStrictEqual({
clusters: [
{
name: 'cluster1',
serviceAccountToken: 'token',
url: 'http://localhost:8080',
authProvider: 'serviceAccount',
},
{
name: 'cluster2',
url: 'http://localhost:8081',
authProvider: 'google',
},
],
});
});
});
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import {
ClusterDetails,
KubernetesClustersSupplier,
@@ -30,8 +31,9 @@ export class MultiTenantServiceLocator implements KubernetesServiceLocator {
}
// As this implementation always returns all clusters serviceId is ignored here
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async getClustersByServiceId(_serviceId: string): Promise<ClusterDetails[]> {
return this.clusterSupplier.getClusters();
getClustersByEntity(
_entity: Entity,
): Promise<{ clusters: ClusterDetails[] }> {
return this.clusterSupplier.getClusters().then(clusters => ({ clusters }));
}
}
@@ -15,6 +15,7 @@
*/
import { getVoidLogger } from '@backstage/backend-common';
import { Entity } from '@backstage/catalog-model';
import { Config, ConfigReader } from '@backstage/config';
import { ObjectsByEntityResponse } from '@backstage/plugin-kubernetes-common';
import express from 'express';
@@ -207,8 +208,10 @@ describe('KubernetesBuilder', () => {
};
const serviceLocator: KubernetesServiceLocator = {
getClustersByServiceId(_serviceId: string): Promise<ClusterDetails[]> {
return Promise.resolve([someCluster]);
getClustersByEntity(
_entity: Entity,
): Promise<{ clusters: ClusterDetails[] }> {
return Promise.resolve({ clusters: [someCluster] });
},
};
@@ -244,7 +247,15 @@ describe('KubernetesBuilder', () => {
.build();
app = express().use(router);
const response = await request(app).post('/services/test-service');
const response = await request(app)
.post('/services/test-service')
.send({
entity: {
metadata: {
name: 'thing',
},
},
});
expect(response.body).toEqual(result);
expect(response.status).toEqual(200);
@@ -38,6 +38,10 @@ import {
} from './KubernetesFanOutHandler';
import { KubernetesClientBasedFetcher } from './KubernetesFetcher';
/**
*
* @alpha
*/
export interface KubernetesEnvironment {
logger: Logger;
config: Config;
@@ -46,7 +50,7 @@ export interface KubernetesEnvironment {
/**
* The return type of the `KubernetesBuilder.build` method
*
* @public
* @alpha
*/
export type KubernetesBuilderReturn = Promise<{
router: express.Router;
@@ -57,6 +61,10 @@ export type KubernetesBuilderReturn = Promise<{
serviceLocator: KubernetesServiceLocator;
}>;
/**
*
* @alpha
*/
export class KubernetesBuilder {
private clusterSupplier?: KubernetesClustersSupplier;
private defaultClusterRefreshInterval: Duration = Duration.fromObject({
@@ -227,9 +235,10 @@ export class KubernetesBuilder {
const serviceId = req.params.serviceId;
const requestBody: ObjectsByEntityRequest = req.body;
try {
const response = await objectsProvider.getKubernetesObjectsByEntity(
requestBody,
);
const response = await objectsProvider.getKubernetesObjectsByEntity({
entity: requestBody.entity,
auth: requestBody.auth || {},
});
res.json(response);
} catch (e) {
logger.error(
@@ -22,7 +22,7 @@ import { PodStatus } from '@kubernetes/client-node/dist/top';
const fetchObjectsForService = jest.fn();
const fetchPodMetricsByNamespace = jest.fn();
const getClustersByServiceId = jest.fn();
const getClustersByEntity = jest.fn();
const POD_METRICS_FIXTURE = {
containers: [],
@@ -166,13 +166,15 @@ describe('handleGetKubernetesObjectsForService', () => {
});
it('retrieve objects for one cluster', async () => {
getClustersByServiceId.mockImplementation(() =>
Promise.resolve([
{
name: 'test-cluster',
authProvider: 'serviceAccount',
},
]),
getClustersByEntity.mockImplementation(() =>
Promise.resolve({
clusters: [
{
name: 'test-cluster',
authProvider: 'serviceAccount',
},
],
}),
);
mockFetch(fetchObjectsForService);
@@ -185,7 +187,7 @@ describe('handleGetKubernetesObjectsForService', () => {
fetchPodMetricsByNamespace,
},
serviceLocator: {
getClustersByServiceId,
getClustersByEntity,
},
customResources: [],
});
@@ -207,9 +209,10 @@ describe('handleGetKubernetesObjectsForService', () => {
owner: 'joe',
},
},
auth: {},
});
expect(getClustersByServiceId.mock.calls.length).toBe(1);
expect(getClustersByEntity.mock.calls.length).toBe(1);
expect(fetchObjectsForService.mock.calls.length).toBe(1);
expect(fetchPodMetricsByNamespace.mock.calls.length).toBe(1);
expect(fetchPodMetricsByNamespace.mock.calls[0][1]).toBe(
@@ -265,13 +268,15 @@ describe('handleGetKubernetesObjectsForService', () => {
});
it('dont call top for the same namespace twice', async () => {
getClustersByServiceId.mockImplementation(() =>
Promise.resolve([
{
name: 'test-cluster',
authProvider: 'serviceAccount',
},
]),
getClustersByEntity.mockImplementation(() =>
Promise.resolve({
clusters: [
{
name: 'test-cluster',
authProvider: 'serviceAccount',
},
],
}),
);
fetchObjectsForService.mockImplementation((_: ObjectFetchParams) =>
@@ -314,7 +319,7 @@ describe('handleGetKubernetesObjectsForService', () => {
fetchPodMetricsByNamespace,
},
serviceLocator: {
getClustersByServiceId,
getClustersByEntity,
},
customResources: [],
});
@@ -336,9 +341,10 @@ describe('handleGetKubernetesObjectsForService', () => {
owner: 'joe',
},
},
auth: {},
});
expect(getClustersByServiceId.mock.calls.length).toBe(1);
expect(getClustersByEntity.mock.calls.length).toBe(1);
expect(fetchObjectsForService.mock.calls.length).toBe(1);
expect(fetchPodMetricsByNamespace.mock.calls.length).toBe(2);
expect(fetchPodMetricsByNamespace.mock.calls[0][1]).toBe('ns-a');
@@ -383,18 +389,20 @@ describe('handleGetKubernetesObjectsForService', () => {
});
it('retrieve objects for two clusters', async () => {
getClustersByServiceId.mockImplementation(() =>
Promise.resolve([
{
name: 'test-cluster',
authProvider: 'serviceAccount',
dashboardUrl: 'https://k8s.foo.coom',
},
{
name: 'other-cluster',
authProvider: 'google',
},
]),
getClustersByEntity.mockImplementation(() =>
Promise.resolve({
clusters: [
{
name: 'test-cluster',
authProvider: 'serviceAccount',
dashboardUrl: 'https://k8s.foo.coom',
},
{
name: 'other-cluster',
authProvider: 'google',
},
],
}),
);
mockFetch(fetchObjectsForService);
@@ -407,15 +415,12 @@ describe('handleGetKubernetesObjectsForService', () => {
fetchPodMetricsByNamespace,
},
serviceLocator: {
getClustersByServiceId,
getClustersByEntity,
},
customResources: [],
});
const result = await sut.getKubernetesObjectsByEntity({
auth: {
google: 'google_token_123',
},
entity: {
apiVersion: 'backstage.io/v1beta1',
kind: 'Component',
@@ -432,9 +437,12 @@ describe('handleGetKubernetesObjectsForService', () => {
owner: 'joe',
},
},
auth: {
google: 'google_token_123',
},
});
expect(getClustersByServiceId.mock.calls.length).toBe(1);
expect(getClustersByEntity.mock.calls.length).toBe(1);
expect(fetchObjectsForService.mock.calls.length).toBe(2);
expect(result).toStrictEqual({
items: [
@@ -527,21 +535,23 @@ describe('handleGetKubernetesObjectsForService', () => {
});
});
it('retrieve objects for three clusters, only two have resources and show in ui', async () => {
getClustersByServiceId.mockImplementation(() =>
Promise.resolve([
{
name: 'test-cluster',
authProvider: 'serviceAccount',
},
{
name: 'other-cluster',
authProvider: 'google',
},
{
name: 'empty-cluster',
authProvider: 'google',
},
]),
getClustersByEntity.mockImplementation(() =>
Promise.resolve({
clusters: [
{
name: 'test-cluster',
authProvider: 'serviceAccount',
},
{
name: 'other-cluster',
authProvider: 'google',
},
{
name: 'empty-cluster',
authProvider: 'google',
},
],
}),
);
mockFetch(fetchObjectsForService);
@@ -554,15 +564,12 @@ describe('handleGetKubernetesObjectsForService', () => {
fetchPodMetricsByNamespace,
},
serviceLocator: {
getClustersByServiceId,
getClustersByEntity,
},
customResources: [],
});
const result = await sut.getKubernetesObjectsByEntity({
auth: {
google: 'google_token_123',
},
entity: {
apiVersion: 'backstage.io/v1beta1',
kind: 'Component',
@@ -579,9 +586,12 @@ describe('handleGetKubernetesObjectsForService', () => {
owner: 'joe',
},
},
auth: {
google: 'google_token_123',
},
});
expect(getClustersByServiceId.mock.calls.length).toBe(1);
expect(getClustersByEntity.mock.calls.length).toBe(1);
expect(fetchObjectsForService.mock.calls.length).toBe(3);
expect(result).toStrictEqual({
items: [
@@ -673,25 +683,27 @@ describe('handleGetKubernetesObjectsForService', () => {
});
});
it('retrieve objects for four clusters, two have resources and one error cluster', async () => {
getClustersByServiceId.mockImplementation(() =>
Promise.resolve([
{
name: 'test-cluster',
authProvider: 'serviceAccount',
},
{
name: 'other-cluster',
authProvider: 'google',
},
{
name: 'empty-cluster',
authProvider: 'google',
},
{
name: 'error-cluster',
authProvider: 'google',
},
]),
getClustersByEntity.mockImplementation(() =>
Promise.resolve({
clusters: [
{
name: 'test-cluster',
authProvider: 'serviceAccount',
},
{
name: 'other-cluster',
authProvider: 'google',
},
{
name: 'empty-cluster',
authProvider: 'google',
},
{
name: 'error-cluster',
authProvider: 'google',
},
],
}),
);
mockFetch(fetchObjectsForService);
@@ -704,15 +716,12 @@ describe('handleGetKubernetesObjectsForService', () => {
fetchPodMetricsByNamespace,
},
serviceLocator: {
getClustersByServiceId,
getClustersByEntity,
},
customResources: [],
});
const result = await sut.getKubernetesObjectsByEntity({
auth: {
google: 'google_token_123',
},
entity: {
apiVersion: 'backstage.io/v1beta1',
kind: 'Component',
@@ -729,9 +738,12 @@ describe('handleGetKubernetesObjectsForService', () => {
owner: 'joe',
},
},
auth: {
google: 'google_token_123',
},
});
expect(getClustersByServiceId.mock.calls.length).toBe(1);
expect(getClustersByEntity.mock.calls.length).toBe(1);
expect(fetchObjectsForService.mock.calls.length).toBe(4);
expect(result).toStrictEqual({
items: [
@@ -14,16 +14,20 @@
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import { Logger } from 'winston';
import {
ClusterDetails,
CustomResource,
KubernetesFetcher,
KubernetesObjectsProviderOptions,
KubernetesServiceLocator,
ObjectsByEntityRequest,
FetchResponseWrapper,
ObjectToFetch,
CustomResource,
CustomResourceMatcher,
CustomResourcesByEntity,
KubernetesObjectsByEntity,
} from '../types/types';
import { KubernetesAuthTranslator } from '../kubernetes-auth-translator/types';
import { KubernetesAuthTranslatorGenerator } from '../kubernetes-auth-translator/KubernetesAuthTranslatorGenerator';
@@ -35,6 +39,7 @@ import {
FetchResponse,
ObjectsByEntityResponse,
PodFetchResponse,
KubernetesRequestAuth,
} from '@backstage/plugin-kubernetes-common';
import {
ContainerStatus,
@@ -42,6 +47,10 @@ import {
PodStatus,
} from '@kubernetes/client-node';
/**
*
* @alpha
*/
export const DEFAULT_OBJECTS: ObjectToFetch[] = [
{
group: '',
@@ -178,26 +187,44 @@ export class KubernetesFanOutHandler {
this.authTranslators = {};
}
async getKubernetesObjectsByEntity(
requestBody: KubernetesRequestBody,
): Promise<ObjectsByEntityResponse> {
const entityName =
requestBody.entity?.metadata?.annotations?.[
'backstage.io/kubernetes-id'
] || requestBody.entity?.metadata?.name;
const clusterDetails: ClusterDetails[] =
await this.serviceLocator.getClustersByServiceId(entityName);
// Execute all of these async actions simultaneously/without blocking sequentially as no common object is modified by them
const promises: Promise<ClusterDetails>[] = clusterDetails.map(cd => {
return this.getAuthTranslator(
cd.authProvider,
).decorateClusterDetailsWithAuth(cd, requestBody);
});
const clusterDetailsDecoratedForAuth: ClusterDetails[] = await Promise.all(
promises,
async getCustomResourcesByEntity({
entity,
auth,
customResources,
}: CustomResourcesByEntity): Promise<ObjectsByEntityResponse> {
// Don't fetch the default object types only the provided custom resources
return this.fanOutRequests(
entity,
auth,
new Set<ObjectToFetch>(),
customResources,
);
}
async getKubernetesObjectsByEntity({
entity,
auth,
}: KubernetesObjectsByEntity): Promise<ObjectsByEntityResponse> {
return this.fanOutRequests(
entity,
auth,
this.objectTypesToFetch,
this.customResources,
);
}
private async fanOutRequests(
entity: Entity,
auth: KubernetesRequestAuth,
objectTypesToFetch: Set<ObjectToFetch>,
customResources: CustomResourceMatcher[],
) {
const entityName =
entity.metadata?.annotations?.['backstage.io/kubernetes-id'] ||
entity.metadata?.name;
const clusterDetailsDecoratedForAuth: ClusterDetails[] =
await this.decorateClusterDetailsWithAuth(entity, auth);
this.logger.info(
`entity.metadata.name=${entityName} clusterDetails=[${clusterDetailsDecoratedForAuth
@@ -206,14 +233,12 @@ export class KubernetesFanOutHandler {
);
const labelSelector: string =
requestBody.entity?.metadata?.annotations?.[
entity.metadata?.annotations?.[
'backstage.io/kubernetes-label-selector'
] || `backstage.io/kubernetes-id=${entityName}`;
const namespace =
requestBody.entity?.metadata?.annotations?.[
'backstage.io/kubernetes-namespace'
];
entity.metadata?.annotations?.['backstage.io/kubernetes-namespace'];
return Promise.all(
clusterDetailsDecoratedForAuth.map(clusterDetailsItem => {
@@ -221,9 +246,12 @@ export class KubernetesFanOutHandler {
.fetchObjectsForService({
serviceId: entityName,
clusterDetails: clusterDetailsItem,
objectTypesToFetch: this.objectTypesToFetch,
objectTypesToFetch: objectTypesToFetch,
labelSelector,
customResources: this.customResources,
customResources: customResources.map(c => ({
...c,
objectType: 'customresources',
})),
namespace,
})
.then(result => this.getMetricsForPods(clusterDetailsItem, result))
@@ -232,6 +260,27 @@ export class KubernetesFanOutHandler {
).then(this.toObjectsByEntityResponse);
}
private async decorateClusterDetailsWithAuth(
entity: Entity,
auth: KubernetesRequestAuth,
) {
const clusterDetails: ClusterDetails[] = await (
await this.serviceLocator.getClustersByEntity(entity)
).clusters;
// Execute all of these async actions simultaneously/without blocking sequentially as no common object is modified by them
return await Promise.all(
clusterDetails.map(cd => {
const kubernetesAuthTranslator: KubernetesAuthTranslator =
this.getAuthTranslator(cd.authProvider);
return kubernetesAuthTranslator.decorateClusterDetailsWithAuth(
cd,
auth,
);
}),
);
}
toObjectsByEntityResponse(
clusterObjects: ClusterObjects[],
): ObjectsByEntityResponse {
@@ -20,6 +20,10 @@ import { KubernetesClustersSupplier } from '../types/types';
import express from 'express';
import { KubernetesBuilder } from './KubernetesBuilder';
/**
*
* @alpha
*/
export interface RouterOptions {
logger: Logger;
config: Config;
@@ -38,6 +42,8 @@ export interface RouterOptions {
* config,
* }).build();
* ```
*
* @alpha
*/
export async function createRouter(
options: RouterOptions,
+102 -8
View File
@@ -14,16 +14,22 @@
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import { Logger } from 'winston';
import type { JsonObject } from '@backstage/types';
import type {
FetchResponse,
KubernetesFetchError,
KubernetesRequestAuth,
KubernetesRequestBody,
ObjectsByEntityResponse,
} from '@backstage/plugin-kubernetes-common';
import { PodStatus } from '@kubernetes/client-node/dist/top';
/**
*
* @alpha
*/
export interface ObjectFetchParams {
serviceId: string;
clusterDetails:
@@ -37,8 +43,11 @@ export interface ObjectFetchParams {
namespace?: string;
}
// Fetches information from a kubernetes cluster using the cluster details object
// to target a specific cluster
/**
* Fetches information from a kubernetes cluster using the cluster details object to target a specific cluster
*
* @alpha
*/
export interface KubernetesFetcher {
fetchObjectsForService(
params: ObjectFetchParams,
@@ -49,13 +58,19 @@ export interface KubernetesFetcher {
): Promise<PodStatus[]>;
}
/**
*
* @alpha
*/
export interface FetchResponseWrapper {
errors: KubernetesFetchError[];
responses: FetchResponse[];
}
// TODO fairly sure there's a easier way to do this
/**
*
* @alpha
*/
export interface ObjectToFetch {
objectType: KubernetesObjectTypes;
group: string;
@@ -63,10 +78,24 @@ export interface ObjectToFetch {
plural: string;
}
/**
*
* @alpha
*/
export interface CustomResource extends ObjectToFetch {
objectType: 'customresources';
}
/**
*
* @alpha
*/
export type CustomResourceMatcher = Omit<ObjectToFetch, 'objectType'>;
/**
*
* @alpha
*/
export type KubernetesObjectTypes =
| 'pods'
| 'services'
@@ -80,7 +109,10 @@ export type KubernetesObjectTypes =
| 'customresources'
| 'statefulsets';
// Used to load cluster details from different sources
/**
* Used to load cluster details from different sources
* @alpha
*/
export interface KubernetesClustersSupplier {
/**
* Returns the cached list of clusters.
@@ -91,13 +123,24 @@ export interface KubernetesClustersSupplier {
getClusters(): Promise<ClusterDetails[]>;
}
// Used to locate which cluster(s) a service is running on
/**
* Used to locate which cluster(s) a service is running on
* @alpha
*/
export interface KubernetesServiceLocator {
getClustersByServiceId(serviceId: string): Promise<ClusterDetails[]>;
getClustersByEntity(entity: Entity): Promise<{ clusters: ClusterDetails[] }>;
}
/**
*
* @alpha
*/
export type ServiceLocatorMethod = 'multiTenant' | 'http'; // TODO implement http
/**
*
* @alpha
*/
export interface ClusterDetails {
/**
* Specifies the name of the Kubernetes cluster.
@@ -151,14 +194,37 @@ export interface ClusterDetails {
dashboardParameters?: JsonObject;
}
/**
*
* @alpha
*/
export interface GKEClusterDetails extends ClusterDetails {}
/**
*
* @alpha
*/
export interface AzureClusterDetails extends ClusterDetails {}
/**
*
* @alpha
*/
export interface ServiceAccountClusterDetails extends ClusterDetails {}
/**
*
* @alpha
*/
export interface AWSClusterDetails extends ClusterDetails {
assumeRole?: string;
externalId?: string;
}
/**
*
* @alpha
*/
export interface KubernetesObjectsProviderOptions {
logger: Logger;
fetcher: KubernetesFetcher;
@@ -167,10 +233,38 @@ export interface KubernetesObjectsProviderOptions {
objectTypesToFetch?: ObjectToFetch[];
}
/**
*
* @alpha
*/
export type ObjectsByEntityRequest = KubernetesRequestBody;
/**
*
* @alpha
*/
export interface KubernetesObjectsByEntity {
entity: Entity;
auth: KubernetesRequestAuth;
}
/**
*
* @alpha
*/
export interface CustomResourcesByEntity extends KubernetesObjectsByEntity {
customResources: CustomResourceMatcher[];
}
/**
*
* @alpha
*/
export interface KubernetesObjectsProvider {
getKubernetesObjectsByEntity(
request: ObjectsByEntityRequest,
kubernetesObjectsByEntity: KubernetesObjectsByEntity,
): Promise<ObjectsByEntityResponse>;
getCustomResourcesByEntity(
customResourcesByEntity: CustomResourcesByEntity,
): Promise<ObjectsByEntityResponse>;
}
+13 -6
View File
@@ -190,17 +190,24 @@ export interface KubernetesFetchError {
statusCode?: number;
}
// Warning: (ae-missing-release-tag) "KubernetesRequestAuth" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface KubernetesRequestAuth {
// (undocumented)
google?: string;
// (undocumented)
oidc?: {
[key: string]: string;
};
}
// Warning: (ae-missing-release-tag) "KubernetesRequestBody" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface KubernetesRequestBody {
// (undocumented)
auth?: {
google?: string;
oidc?: {
[key: string]: string;
};
};
auth?: KubernetesRequestAuth;
// (undocumented)
entity: Entity;
}
+8 -6
View File
@@ -29,13 +29,15 @@ import {
} from '@kubernetes/client-node';
import { Entity } from '@backstage/catalog-model';
export interface KubernetesRequestBody {
auth?: {
google?: string;
oidc?: {
[key: string]: string;
};
export interface KubernetesRequestAuth {
google?: string;
oidc?: {
[key: string]: string;
};
}
export interface KubernetesRequestBody {
auth?: KubernetesRequestAuth;
entity: Entity;
}