Creating a Cluster details object per k8s provider
Signed-off-by: Nicolas Arnold <nic@roadie.io>
This commit is contained in:
@@ -13,9 +13,13 @@ import { Logger as Logger_2 } from 'winston';
|
||||
// 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)
|
||||
export interface ClusterDetails {
|
||||
export interface AWSClusterDetails extends ClusterDetails {
|
||||
// (undocumented)
|
||||
assumeRole?: string;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface ClusterDetails {
|
||||
// (undocumented)
|
||||
authProvider: string;
|
||||
// (undocumented)
|
||||
@@ -57,6 +61,9 @@ export interface FetchResponseWrapper {
|
||||
|
||||
// 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)
|
||||
export interface GKEClusterDetails extends ClusterDetails {}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface KubernetesClustersSupplier {
|
||||
// (undocumented)
|
||||
@@ -109,7 +116,10 @@ export const makeRouter: (
|
||||
// @public (undocumented)
|
||||
export interface ObjectFetchParams {
|
||||
// (undocumented)
|
||||
clusterDetails: ClusterDetails;
|
||||
clusterDetails:
|
||||
| AWSClusterDetails
|
||||
| GKEClusterDetails
|
||||
| ServiceAccountClusterDetails;
|
||||
// (undocumented)
|
||||
customResources: CustomResource[];
|
||||
// (undocumented)
|
||||
@@ -134,6 +144,9 @@ export interface RouterOptions {
|
||||
|
||||
// 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)
|
||||
export interface ServiceAccountClusterDetails extends ClusterDetails {}
|
||||
|
||||
// @public (undocumented)
|
||||
export type ServiceLocatorMethod = 'multiTenant' | 'http';
|
||||
|
||||
|
||||
@@ -35,7 +35,6 @@ describe('ConfigClusterLocator', () => {
|
||||
const config: Config = new ConfigReader({
|
||||
clusters: [
|
||||
{
|
||||
assumeRole: 'SomeRole',
|
||||
name: 'cluster1',
|
||||
url: 'http://localhost:8080',
|
||||
authProvider: 'serviceAccount',
|
||||
@@ -49,7 +48,6 @@ describe('ConfigClusterLocator', () => {
|
||||
|
||||
expect(result).toStrictEqual([
|
||||
{
|
||||
assumeRole: 'SomeRole',
|
||||
name: 'cluster1',
|
||||
serviceAccountToken: undefined,
|
||||
url: 'http://localhost:8080',
|
||||
@@ -63,7 +61,6 @@ describe('ConfigClusterLocator', () => {
|
||||
const config: Config = new ConfigReader({
|
||||
clusters: [
|
||||
{
|
||||
assumeRole: 'SomeRole',
|
||||
name: 'cluster1',
|
||||
serviceAccountToken: 'token',
|
||||
url: 'http://localhost:8080',
|
||||
@@ -71,7 +68,6 @@ describe('ConfigClusterLocator', () => {
|
||||
skipTLSVerify: false,
|
||||
},
|
||||
{
|
||||
assumeRole: undefined,
|
||||
name: 'cluster2',
|
||||
url: 'http://localhost:8081',
|
||||
authProvider: 'google',
|
||||
@@ -86,7 +82,6 @@ describe('ConfigClusterLocator', () => {
|
||||
|
||||
expect(result).toStrictEqual([
|
||||
{
|
||||
assumeRole: 'SomeRole',
|
||||
name: 'cluster1',
|
||||
serviceAccountToken: 'token',
|
||||
url: 'http://localhost:8080',
|
||||
@@ -94,7 +89,6 @@ describe('ConfigClusterLocator', () => {
|
||||
skipTLSVerify: false,
|
||||
},
|
||||
{
|
||||
assumeRole: undefined,
|
||||
name: 'cluster2',
|
||||
serviceAccountToken: undefined,
|
||||
url: 'http://localhost:8081',
|
||||
@@ -103,4 +97,48 @@ describe('ConfigClusterLocator', () => {
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('one aws cluster with assumeRole and one without', async () => {
|
||||
const config: Config = new ConfigReader({
|
||||
clusters: [
|
||||
{
|
||||
name: 'cluster1',
|
||||
serviceAccountToken: 'token',
|
||||
url: 'http://localhost:8080',
|
||||
authProvider: 'aws',
|
||||
skipTLSVerify: false,
|
||||
},
|
||||
{
|
||||
assumeRole: 'SomeRole',
|
||||
name: 'cluster2',
|
||||
url: 'http://localhost:8081',
|
||||
authProvider: 'aws',
|
||||
skipTLSVerify: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const sut = ConfigClusterLocator.fromConfig(config);
|
||||
|
||||
const result = await sut.getClusters();
|
||||
|
||||
expect(result).toStrictEqual([
|
||||
{
|
||||
assumeRole: undefined,
|
||||
name: 'cluster1',
|
||||
serviceAccountToken: 'token',
|
||||
url: 'http://localhost:8080',
|
||||
authProvider: 'aws',
|
||||
skipTLSVerify: false,
|
||||
},
|
||||
{
|
||||
assumeRole: 'SomeRole',
|
||||
name: 'cluster2',
|
||||
serviceAccountToken: undefined,
|
||||
url: 'http://localhost:8081',
|
||||
authProvider: 'aws',
|
||||
skipTLSVerify: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,14 +29,32 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier {
|
||||
// is required if authProvider is serviceAccount
|
||||
return new ConfigClusterLocator(
|
||||
config.getConfigArray('clusters').map(c => {
|
||||
return {
|
||||
const authProvider = c.getString('authProvider');
|
||||
const clusterDetails = {
|
||||
name: c.getString('name'),
|
||||
url: c.getString('url'),
|
||||
serviceAccountToken: c.getOptionalString('serviceAccountToken'),
|
||||
skipTLSVerify: c.getOptionalBoolean('skipTLSVerify') ?? false,
|
||||
authProvider: c.getString('authProvider'),
|
||||
assumeRole: c.getOptionalString('assumeRole'),
|
||||
authProvider: authProvider,
|
||||
};
|
||||
|
||||
switch (authProvider) {
|
||||
case 'google': {
|
||||
return clusterDetails;
|
||||
}
|
||||
case 'aws': {
|
||||
const assumeRole = c.getOptionalString('assumeRole');
|
||||
return { assumeRole, ...clusterDetails };
|
||||
}
|
||||
case 'serviceAccount': {
|
||||
return clusterDetails;
|
||||
}
|
||||
default: {
|
||||
throw new Error(
|
||||
`authProvider "${authProvider}" has no config associated with it`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import * as container from '@google-cloud/container';
|
||||
import { ClusterDetails, KubernetesClustersSupplier } from '../types/types';
|
||||
import { GKEClusterDetails, KubernetesClustersSupplier } from '../types/types';
|
||||
|
||||
type GkeClusterLocatorOptions = {
|
||||
projectId: string;
|
||||
@@ -49,7 +49,7 @@ export class GkeClusterLocator implements KubernetesClustersSupplier {
|
||||
);
|
||||
}
|
||||
|
||||
async getClusters(): Promise<ClusterDetails[]> {
|
||||
async getClusters(): Promise<GKEClusterDetails[]> {
|
||||
const { projectId, region, skipTLSVerify } = this.options;
|
||||
const request = {
|
||||
parent: `projects/${projectId}/locations/${region}`,
|
||||
|
||||
@@ -27,7 +27,6 @@ describe('getCombinedClusterDetails', () => {
|
||||
type: 'config',
|
||||
clusters: [
|
||||
{
|
||||
assumeRole: 'SomeRole',
|
||||
name: 'cluster1',
|
||||
serviceAccountToken: 'token',
|
||||
url: 'http://localhost:8080',
|
||||
@@ -50,7 +49,6 @@ describe('getCombinedClusterDetails', () => {
|
||||
|
||||
expect(result).toStrictEqual([
|
||||
{
|
||||
assumeRole: 'SomeRole',
|
||||
name: 'cluster1',
|
||||
serviceAccountToken: 'token',
|
||||
url: 'http://localhost:8080',
|
||||
@@ -58,7 +56,6 @@ describe('getCombinedClusterDetails', () => {
|
||||
skipTLSVerify: false,
|
||||
},
|
||||
{
|
||||
assumeRole: undefined,
|
||||
name: 'cluster2',
|
||||
serviceAccountToken: undefined,
|
||||
url: 'http://localhost:8081',
|
||||
|
||||
+10
-13
@@ -15,19 +15,17 @@
|
||||
*/
|
||||
import AWS from 'aws-sdk';
|
||||
import { sign } from 'aws4';
|
||||
import { ClusterDetails } from '../types/types';
|
||||
import { AWSClusterDetails } from '../types/types';
|
||||
import { KubernetesAuthTranslator } from './types';
|
||||
|
||||
const base64 = (str: string) =>
|
||||
Buffer.from(str.toString(), 'binary').toString('base64');
|
||||
const prepend = (prep: string) => (str: string) => prep + str;
|
||||
const replace =
|
||||
(search: string | RegExp, substitution: string) => (str: string) =>
|
||||
str.replace(search, substitution);
|
||||
const pipe =
|
||||
(fns: ReadonlyArray<any>) =>
|
||||
(thing: string): string =>
|
||||
fns.reduce((val, fn) => fn(val), thing);
|
||||
const replace = (search: string | RegExp, substitution: string) => (
|
||||
str: string,
|
||||
) => str.replace(search, substitution);
|
||||
const pipe = (fns: ReadonlyArray<any>) => (thing: string): string =>
|
||||
fns.reduce((val, fn) => fn(val), thing);
|
||||
const removePadding = replace(/=+$/, '');
|
||||
const makeUrlSafe = pipe([replace('+', '-'), replace('/', '_')]);
|
||||
|
||||
@@ -38,8 +36,7 @@ type SigningCreds = {
|
||||
};
|
||||
|
||||
export class AwsIamKubernetesAuthTranslator
|
||||
implements KubernetesAuthTranslator
|
||||
{
|
||||
implements KubernetesAuthTranslator {
|
||||
validCredentials(creds: SigningCreds): boolean {
|
||||
if (!creds.accessKeyId || !creds.secretAccessKey || !creds.sessionToken) {
|
||||
return false;
|
||||
@@ -116,9 +113,9 @@ export class AwsIamKubernetesAuthTranslator
|
||||
}
|
||||
|
||||
async decorateClusterDetailsWithAuth(
|
||||
clusterDetails: ClusterDetails,
|
||||
): Promise<ClusterDetails> {
|
||||
const clusterDetailsWithAuthToken: ClusterDetails = Object.assign(
|
||||
clusterDetails: AWSClusterDetails,
|
||||
): Promise<AWSClusterDetails> {
|
||||
const clusterDetailsWithAuthToken: AWSClusterDetails = Object.assign(
|
||||
{},
|
||||
clusterDetails,
|
||||
);
|
||||
|
||||
+5
-6
@@ -15,17 +15,16 @@
|
||||
*/
|
||||
|
||||
import { KubernetesAuthTranslator } from './types';
|
||||
import { ClusterDetails } from '../types/types';
|
||||
import { GKEClusterDetails } from '../types/types';
|
||||
import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common';
|
||||
|
||||
export class GoogleKubernetesAuthTranslator
|
||||
implements KubernetesAuthTranslator
|
||||
{
|
||||
implements KubernetesAuthTranslator {
|
||||
async decorateClusterDetailsWithAuth(
|
||||
clusterDetails: ClusterDetails,
|
||||
clusterDetails: GKEClusterDetails,
|
||||
requestBody: KubernetesRequestBody,
|
||||
): Promise<ClusterDetails> {
|
||||
const clusterDetailsWithAuthToken: ClusterDetails = Object.assign(
|
||||
): Promise<GKEClusterDetails> {
|
||||
const clusterDetailsWithAuthToken: GKEClusterDetails = Object.assign(
|
||||
{},
|
||||
clusterDetails,
|
||||
);
|
||||
|
||||
+9
-6
@@ -24,20 +24,23 @@ describe('getKubernetesAuthTranslatorInstance', () => {
|
||||
const sut = KubernetesAuthTranslatorGenerator;
|
||||
|
||||
it('can return an auth translator for google auth', () => {
|
||||
const authTranslator: KubernetesAuthTranslator =
|
||||
sut.getKubernetesAuthTranslatorInstance('google');
|
||||
const authTranslator: KubernetesAuthTranslator = sut.getKubernetesAuthTranslatorInstance(
|
||||
'google',
|
||||
);
|
||||
expect(authTranslator instanceof GoogleKubernetesAuthTranslator).toBe(true);
|
||||
});
|
||||
|
||||
it('can return an auth translator for aws auth', () => {
|
||||
const authTranslator: KubernetesAuthTranslator =
|
||||
sut.getKubernetesAuthTranslatorInstance('aws');
|
||||
const authTranslator: KubernetesAuthTranslator = sut.getKubernetesAuthTranslatorInstance(
|
||||
'aws',
|
||||
);
|
||||
expect(authTranslator instanceof AwsIamKubernetesAuthTranslator).toBe(true);
|
||||
});
|
||||
|
||||
it('can return an auth translator for serviceAccount auth', () => {
|
||||
const authTranslator: KubernetesAuthTranslator =
|
||||
sut.getKubernetesAuthTranslatorInstance('serviceAccount');
|
||||
const authTranslator: KubernetesAuthTranslator = sut.getKubernetesAuthTranslatorInstance(
|
||||
'serviceAccount',
|
||||
);
|
||||
expect(
|
||||
authTranslator instanceof ServiceAccountKubernetesAuthTranslator,
|
||||
).toBe(true);
|
||||
|
||||
+4
-5
@@ -15,19 +15,18 @@
|
||||
*/
|
||||
|
||||
import { KubernetesAuthTranslator } from './types';
|
||||
import { ClusterDetails } from '../types/types';
|
||||
import { ServiceAccountClusterDetails } from '../types/types';
|
||||
import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common';
|
||||
|
||||
export class ServiceAccountKubernetesAuthTranslator
|
||||
implements KubernetesAuthTranslator
|
||||
{
|
||||
implements KubernetesAuthTranslator {
|
||||
async decorateClusterDetailsWithAuth(
|
||||
clusterDetails: ClusterDetails,
|
||||
clusterDetails: ServiceAccountClusterDetails,
|
||||
// To ignore TS6133 linting error where it detects 'requestBody' is declared but its value is never read.
|
||||
// @ts-ignore-start
|
||||
requestBody: KubernetesRequestBody, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
// @ts-ignore-end
|
||||
): Promise<ClusterDetails> {
|
||||
): Promise<ServiceAccountClusterDetails> {
|
||||
return clusterDetails;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,15 +63,15 @@ export class KubernetesFanOutHandler {
|
||||
'backstage.io/kubernetes-id'
|
||||
] || requestBody.entity?.metadata?.name;
|
||||
|
||||
const clusterDetails: ClusterDetails[] =
|
||||
await this.serviceLocator.getClustersByServiceId(entityName);
|
||||
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 => {
|
||||
const kubernetesAuthTranslator: KubernetesAuthTranslator =
|
||||
KubernetesAuthTranslatorGenerator.getKubernetesAuthTranslatorInstance(
|
||||
cd.authProvider,
|
||||
);
|
||||
const kubernetesAuthTranslator: KubernetesAuthTranslator = KubernetesAuthTranslatorGenerator.getKubernetesAuthTranslatorInstance(
|
||||
cd.authProvider,
|
||||
);
|
||||
return kubernetesAuthTranslator.decorateClusterDetailsWithAuth(
|
||||
cd,
|
||||
requestBody,
|
||||
|
||||
@@ -167,9 +167,10 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher {
|
||||
labelSelector,
|
||||
).then(r => ({ type: type, resources: r }));
|
||||
case 'services':
|
||||
return this.fetchServicesForService(clusterDetails, labelSelector).then(
|
||||
r => ({ type: type, resources: r }),
|
||||
);
|
||||
return this.fetchServicesForService(
|
||||
clusterDetails,
|
||||
labelSelector,
|
||||
).then(r => ({ type: type, resources: r }));
|
||||
case 'horizontalpodautoscalers':
|
||||
return this.fetchHorizontalPodAutoscalersForService(
|
||||
clusterDetails,
|
||||
@@ -191,8 +192,9 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher {
|
||||
customResource: CustomResource,
|
||||
labelSelector: string,
|
||||
): Promise<FetchResponse> {
|
||||
const customObjects =
|
||||
this.kubernetesClientProvider.getCustomObjectsClient(clusterDetails);
|
||||
const customObjects = this.kubernetesClientProvider.getCustomObjectsClient(
|
||||
clusterDetails,
|
||||
);
|
||||
|
||||
return customObjects
|
||||
.listClusterCustomObject(
|
||||
@@ -215,20 +217,18 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher {
|
||||
client: Clients,
|
||||
) => Promise<{ body: { items: Array<T> }; response: http.IncomingMessage }>,
|
||||
): Promise<Array<T>> {
|
||||
const core =
|
||||
this.kubernetesClientProvider.getCoreClientByClusterDetails(
|
||||
clusterDetails,
|
||||
);
|
||||
const apps =
|
||||
this.kubernetesClientProvider.getAppsClientByClusterDetails(
|
||||
clusterDetails,
|
||||
);
|
||||
const autoscaling =
|
||||
this.kubernetesClientProvider.getAutoscalingClientByClusterDetails(
|
||||
clusterDetails,
|
||||
);
|
||||
const networkingBeta1 =
|
||||
this.kubernetesClientProvider.getNetworkingBeta1Client(clusterDetails);
|
||||
const core = this.kubernetesClientProvider.getCoreClientByClusterDetails(
|
||||
clusterDetails,
|
||||
);
|
||||
const apps = this.kubernetesClientProvider.getAppsClientByClusterDetails(
|
||||
clusterDetails,
|
||||
);
|
||||
const autoscaling = this.kubernetesClientProvider.getAutoscalingClientByClusterDetails(
|
||||
clusterDetails,
|
||||
);
|
||||
const networkingBeta1 = this.kubernetesClientProvider.getNetworkingBeta1Client(
|
||||
clusterDetails,
|
||||
);
|
||||
|
||||
this.logger.debug(`calling cluster=${clusterDetails.name}`);
|
||||
return fn({ core, apps, autoscaling, networkingBeta1 }).then(({ body }) => {
|
||||
|
||||
@@ -70,8 +70,9 @@ export const makeRouter = (
|
||||
const serviceId = req.params.serviceId;
|
||||
const requestBody: KubernetesRequestBody = req.body;
|
||||
try {
|
||||
const response =
|
||||
await kubernetesFanOutHandler.getKubernetesObjectsByEntity(requestBody);
|
||||
const response = await kubernetesFanOutHandler.getKubernetesObjectsByEntity(
|
||||
requestBody,
|
||||
);
|
||||
res.json(response);
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
|
||||
@@ -27,7 +27,10 @@ export interface CustomResource {
|
||||
|
||||
export interface ObjectFetchParams {
|
||||
serviceId: string;
|
||||
clusterDetails: ClusterDetails;
|
||||
clusterDetails:
|
||||
| AWSClusterDetails
|
||||
| GKEClusterDetails
|
||||
| ServiceAccountClusterDetails;
|
||||
objectTypesToFetch: Set<KubernetesObjectTypes>;
|
||||
labelSelector: string;
|
||||
customResources: CustomResource[];
|
||||
@@ -76,5 +79,10 @@ export interface ClusterDetails {
|
||||
authProvider: string;
|
||||
serviceAccountToken?: string | undefined;
|
||||
skipTLSVerify?: boolean;
|
||||
}
|
||||
|
||||
export interface GKEClusterDetails extends ClusterDetails {}
|
||||
export interface ServiceAccountClusterDetails extends ClusterDetails {}
|
||||
export interface AWSClusterDetails extends ClusterDetails {
|
||||
assumeRole?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user