Creating a Cluster details object per k8s provider

Signed-off-by: Nicolas Arnold <nic@roadie.io>
This commit is contained in:
Nicolas Arnold
2021-07-14 12:50:24 +01:00
parent f69d408583
commit ec98274a7e
13 changed files with 147 additions and 74 deletions
+15 -2
View File
@@ -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',
@@ -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,
);
@@ -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,
);
@@ -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);
@@ -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;
}