strategies validate metadata

And migrate authProvider into metadata. Also thread a DispatchStrategy into
ConfigClusterLocator instead of a map of validator functions.

Signed-off-by: Jamie Klassen <jklassen@vmware.com>
This commit is contained in:
Jamie Klassen
2023-09-06 19:55:46 -04:00
parent 54c957c1bc
commit ce6905f86d
33 changed files with 498 additions and 309 deletions
@@ -20,10 +20,10 @@ describe('AksStrategy', () => {
const strategy = new AksStrategy();
const details = await strategy.decorateClusterDetailsWithAuth(
{ name: '', authProvider: 'aks', url: '' },
{ name: '', url: '', authMetadata: { authProvider: 'aks' } },
{ aks: 'aksToken' },
);
expect(details.authMetadata!.serviceAccountToken).toBe('aksToken');
expect(details.authMetadata.serviceAccountToken).toBe('aksToken');
});
});
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ClusterDetails } from '../types/types';
import { AuthMetadata, ClusterDetails } from '../types/types';
import { AuthenticationStrategy } from './types';
import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common';
@@ -22,7 +22,7 @@ import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common';
* @public
*/
export class AksStrategy implements AuthenticationStrategy {
async decorateClusterDetailsWithAuth(
public async decorateClusterDetailsWithAuth(
clusterDetails: ClusterDetails,
auth: KubernetesRequestAuth,
): Promise<ClusterDetails> {
@@ -36,4 +36,5 @@ export class AksStrategy implements AuthenticationStrategy {
}),
};
}
public validate(_: AuthMetadata) {}
}
@@ -63,9 +63,9 @@ describe('AwsIamStrategy tests', () => {
const authPromise = strategy.decorateClusterDetailsWithAuth({
name: 'test-cluster',
url: '',
authProvider: 'aws',
authMetadata: { authProvider: 'aws' },
});
expect((await authPromise).authMetadata!.serviceAccountToken).toEqual(
expect((await authPromise).authMetadata.serviceAccountToken).toEqual(
'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_',
);
});
@@ -76,10 +76,12 @@ describe('AwsIamStrategy tests', () => {
const authPromise = strategy.decorateClusterDetailsWithAuth({
name: 'test-cluster',
url: '',
authProvider: 'aws',
authMetadata: { [ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE]: 'SomeRole' },
authMetadata: {
authProvider: 'aws',
[ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE]: 'SomeRole',
},
});
expect((await authPromise).authMetadata!.serviceAccountToken).toEqual(
expect((await authPromise).authMetadata.serviceAccountToken).toEqual(
'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_',
);
expect(fromTemporaryCredentials).toHaveBeenCalledWith({
@@ -102,13 +104,13 @@ describe('AwsIamStrategy tests', () => {
const authPromise = strategy.decorateClusterDetailsWithAuth({
name: 'test-cluster',
url: '',
authProvider: 'aws',
authMetadata: {
authProvider: 'aws',
[ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE]: 'SomeRole',
[ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID]: 'external-id',
},
});
expect((await authPromise).authMetadata!.serviceAccountToken).toEqual(
expect((await authPromise).authMetadata.serviceAccountToken).toEqual(
'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_',
);
expect(fromTemporaryCredentials).toHaveBeenCalledWith({
@@ -137,7 +139,7 @@ describe('AwsIamStrategy tests', () => {
strategy.decorateClusterDetailsWithAuth({
name: 'test-cluster',
url: '',
authProvider: 'aws',
authMetadata: { authProvider: 'aws' },
}),
).rejects.toThrow('no way');
});
@@ -25,7 +25,7 @@ import {
ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE,
ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID,
} from '@backstage/plugin-kubernetes-common';
import { ClusterDetails } from '../types/types';
import { AuthMetadata, ClusterDetails } from '../types/types';
import { AuthenticationStrategy } from './types';
/**
@@ -46,10 +46,32 @@ const defaultRegion = 'us-east-1';
*/
export class AwsIamStrategy implements AuthenticationStrategy {
private readonly credsManager: AwsCredentialsManager;
constructor(opts: { config: Config }) {
this.credsManager = DefaultAwsCredentialsManager.fromConfig(opts.config);
}
public async decorateClusterDetailsWithAuth(
clusterDetails: ClusterDetails,
): Promise<ClusterDetails> {
const clusterDetailsWithAuthToken: ClusterDetails = Object.assign(
{},
clusterDetails,
);
clusterDetailsWithAuthToken.authMetadata = {
serviceAccountToken: await this.getBearerToken(
clusterDetails.name,
clusterDetails.authMetadata[ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE],
clusterDetails.authMetadata[ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID],
),
...clusterDetailsWithAuthToken.authMetadata,
};
return clusterDetailsWithAuthToken;
}
public validate(_: AuthMetadata) {}
private async getBearerToken(
clusterName: string,
assumeRole?: string,
@@ -110,23 +132,4 @@ export class AwsIamStrategy implements AuthenticationStrategy {
return `k8s-aws-v1.${Buffer.from(url).toString('base64url')}`;
}
async decorateClusterDetailsWithAuth(
clusterDetails: ClusterDetails,
): Promise<ClusterDetails> {
const clusterDetailsWithAuthToken: ClusterDetails = Object.assign(
{},
clusterDetails,
);
clusterDetailsWithAuthToken.authMetadata = {
serviceAccountToken: await this.getBearerToken(
clusterDetails.name,
clusterDetails.authMetadata?.[ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE],
clusterDetails.authMetadata?.[ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID],
),
...clusterDetailsWithAuthToken.authMetadata,
};
return clusterDetailsWithAuthToken;
}
}
@@ -41,9 +41,9 @@ class StaticTokenCredential implements TokenCredential {
describe('AzureIdentityStrategy tests', () => {
const cd = {
authProvider: 'Azure',
name: 'My Cluster',
url: 'mycluster.privatelink.westeurope.azmk8s.io',
authMetadata: { authProvider: 'azure' },
};
afterEach(() => {
@@ -57,7 +57,7 @@ describe('AzureIdentityStrategy tests', () => {
);
const response = await strategy.decorateClusterDetailsWithAuth(cd);
expect(response.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_1');
expect(response.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_1');
});
it('should re-use token before expiry', async () => {
@@ -67,10 +67,10 @@ describe('AzureIdentityStrategy tests', () => {
);
const response = await strategy.decorateClusterDetailsWithAuth(cd);
expect(response.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_1');
expect(response.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_1');
const response2 = await strategy.decorateClusterDetailsWithAuth(cd);
expect(response2.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_1');
expect(response2.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_1');
});
it('should issue new token 15 minutes befory expiry', async () => {
@@ -82,12 +82,12 @@ describe('AzureIdentityStrategy tests', () => {
);
const response = await strategy.decorateClusterDetailsWithAuth(cd);
expect(response.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_1');
expect(response.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_1');
jest.setSystemTime(Date.now() + 2 * 60 * 1000); // advance time by 2mins
const response2 = await strategy.decorateClusterDetailsWithAuth(cd);
expect(response2.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_2');
expect(response2.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_2');
});
it('should re-use existing token if there is afailure', async () => {
@@ -99,20 +99,20 @@ describe('AzureIdentityStrategy tests', () => {
);
const response = await strategy.decorateClusterDetailsWithAuth(cd);
expect(response.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_1');
expect(response.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_1');
jest.setSystemTime(Date.now() + 2 * 60 * 1000); // advance time by 2min
const response2 = await strategy.decorateClusterDetailsWithAuth(cd);
expect(response2.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_2');
expect(response2.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_2');
jest.setSystemTime(Date.now() + 2 * 60 * 1000); // advance time by 2min
const response3 = await strategy.decorateClusterDetailsWithAuth(cd);
expect(response3.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_2');
expect(response3.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_2');
const response4 = await strategy.decorateClusterDetailsWithAuth(cd);
expect(response4.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_4');
expect(response4.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_4');
});
it('should throw if existing token expired and failed to fetch a new one', async () => {
@@ -124,12 +124,12 @@ describe('AzureIdentityStrategy tests', () => {
);
const response = await strategy.decorateClusterDetailsWithAuth(cd);
expect(response.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_1');
expect(response.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_1');
jest.setSystemTime(Date.now() + 2 * 60 * 1000); // advance time by 2min
const response2 = await strategy.decorateClusterDetailsWithAuth(cd);
expect(response2.authMetadata!.serviceAccountToken).toEqual('MY_TOKEN_2');
expect(response2.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_2');
jest.setSystemTime(Date.now() + 17 * 60 * 1000); // advance time by 17min
@@ -16,7 +16,7 @@
import { Logger } from 'winston';
import { AuthenticationStrategy } from './types';
import { ClusterDetails } from '../types/types';
import { AuthMetadata, ClusterDetails } from '../types/types';
import {
AccessToken,
DefaultAzureCredential,
@@ -38,7 +38,7 @@ export class AzureIdentityStrategy implements AuthenticationStrategy {
private readonly tokenCredential: TokenCredential = new DefaultAzureCredential(),
) {}
async decorateClusterDetailsWithAuth(
public async decorateClusterDetailsWithAuth(
clusterDetails: ClusterDetails,
): Promise<ClusterDetails> {
const clusterDetailsWithAuthToken: ClusterDetails = Object.assign(
@@ -53,6 +53,8 @@ export class AzureIdentityStrategy implements AuthenticationStrategy {
return clusterDetailsWithAuthToken;
}
public validate(_: AuthMetadata) {}
private async getToken(): Promise<string> {
if (!this.tokenRequiresRefresh()) {
return this.accessToken.token;
@@ -25,7 +25,10 @@ describe('decorateClusterDetailsWithAuth', () => {
const authObject: KubernetesRequestAuth = {};
beforeEach(() => {
mockStrategy = { decorateClusterDetailsWithAuth: jest.fn() };
mockStrategy = {
decorateClusterDetailsWithAuth: jest.fn(),
validate: jest.fn(),
};
strategy = new DispatchStrategy({
authStrategyMap: { google: mockStrategy },
});
@@ -35,8 +38,10 @@ describe('decorateClusterDetailsWithAuth', () => {
const expectedClusterDetails: ClusterDetails = {
url: 'notanything.com',
name: 'randomName',
authProvider: 'google',
authMetadata: { serviceAccountToken: 'added by mock strategy' },
authMetadata: {
authProvider: 'google',
serviceAccountToken: 'added by mock strategy',
},
};
mockStrategy.decorateClusterDetailsWithAuth.mockResolvedValue(
@@ -44,12 +49,20 @@ describe('decorateClusterDetailsWithAuth', () => {
);
const returnedValue = await strategy.decorateClusterDetailsWithAuth(
{ name: 'googleCluster', url: 'anything.com', authProvider: 'google' },
{
name: 'googleCluster',
url: 'anything.com',
authMetadata: { authProvider: 'google' },
},
authObject,
);
expect(mockStrategy.decorateClusterDetailsWithAuth).toHaveBeenCalledWith(
{ name: 'googleCluster', url: 'anything.com', authProvider: 'google' },
{
name: 'googleCluster',
url: 'anything.com',
authMetadata: { authProvider: 'google' },
},
authObject,
);
expect(returnedValue).toBe(expectedClusterDetails);
@@ -61,7 +74,7 @@ describe('decorateClusterDetailsWithAuth', () => {
{
name: 'test-cluster',
url: 'anything.com',
authProvider: 'linode',
authMetadata: { authProvider: 'linode' },
},
authObject,
),
@@ -15,7 +15,7 @@
*/
import { AuthenticationStrategy } from './types';
import { ClusterDetails } from '../types';
import { AuthMetadata, ClusterDetails } from '../types';
import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common';
/**
@@ -42,13 +42,25 @@ export class DispatchStrategy implements AuthenticationStrategy {
clusterDetails: ClusterDetails,
auth: KubernetesRequestAuth,
) {
if (this.strategyMap[clusterDetails.authProvider]) {
return this.strategyMap[
clusterDetails.authProvider
].decorateClusterDetailsWithAuth(clusterDetails, auth);
const authProvider = clusterDetails.authMetadata.authProvider;
if (this.strategyMap[authProvider]) {
return this.strategyMap[authProvider].decorateClusterDetailsWithAuth(
clusterDetails,
auth,
);
}
throw new Error(
`authProvider "${clusterDetails.authProvider}" has no AuthenticationStrategy associated with it`,
`authProvider "${authProvider}" has no AuthenticationStrategy associated with it`,
);
}
public validate(authMetadata: AuthMetadata) {
const strategy = this.strategyMap[authMetadata.authProvider];
if (!strategy) {
throw new Error(
`authProvider "${authMetadata.authProvider}" has no config associated with it`,
);
}
strategy.validate(authMetadata);
}
}
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { AuthenticationStrategy } from './types';
import { ClusterDetails } from '../types/types';
import { AuthMetadata, ClusterDetails } from '../types/types';
import * as container from '@google-cloud/container';
/**
@@ -22,7 +22,7 @@ import * as container from '@google-cloud/container';
* @public
*/
export class GoogleServiceAccountStrategy implements AuthenticationStrategy {
async decorateClusterDetailsWithAuth(
public async decorateClusterDetailsWithAuth(
clusterDetails: ClusterDetails,
): Promise<ClusterDetails> {
const clusterDetailsWithAuthToken: ClusterDetails = Object.assign(
@@ -44,4 +44,6 @@ export class GoogleServiceAccountStrategy implements AuthenticationStrategy {
}
return clusterDetailsWithAuthToken;
}
public validate(_: AuthMetadata) {}
}
@@ -15,7 +15,7 @@
*/
import { AuthenticationStrategy } from './types';
import { ClusterDetails } from '../types/types';
import { AuthMetadata, ClusterDetails } from '../types/types';
import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common';
/**
@@ -23,7 +23,7 @@ import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common';
* @public
*/
export class GoogleStrategy implements AuthenticationStrategy {
async decorateClusterDetailsWithAuth(
public async decorateClusterDetailsWithAuth(
clusterDetails: ClusterDetails,
authConfig: KubernetesRequestAuth,
): Promise<ClusterDetails> {
@@ -45,4 +45,5 @@ export class GoogleStrategy implements AuthenticationStrategy {
}
return clusterDetailsWithAuthToken;
}
public validate(_: AuthMetadata) {}
}
@@ -15,16 +15,18 @@
*/
import { AuthenticationStrategy } from './types';
import { ClusterDetails } from '../types/types';
import { AuthMetadata, ClusterDetails } from '../types/types';
/**
*
* @public
*/
export class NoopStrategy implements AuthenticationStrategy {
async decorateClusterDetailsWithAuth(
public async decorateClusterDetailsWithAuth(
clusterDetails: ClusterDetails,
): Promise<ClusterDetails> {
return clusterDetails;
}
public validate(_: AuthMetadata) {}
}
@@ -16,33 +16,38 @@
import { ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER } from '@backstage/plugin-kubernetes-common';
import { OidcStrategy } from './OidcStrategy';
import { ClusterDetails } from '../types/types';
describe('OidcStrategy tests', () => {
const strategy = new OidcStrategy();
const baseClusterDetails: ClusterDetails = {
name: 'test',
authProvider: 'oidc',
url: '',
};
it('returns cluster details with auth token', async () => {
const details = await strategy.decorateClusterDetailsWithAuth(
{
authMetadata: { [ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: 'okta' },
...baseClusterDetails,
name: 'test',
url: '',
authMetadata: {
authProvider: 'oidc',
[ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: 'okta',
},
},
{
oidc: { okta: 'fakeToken' },
},
);
expect(details.authMetadata!.serviceAccountToken).toBe('fakeToken');
expect(details.authMetadata.serviceAccountToken).toBe('fakeToken');
});
it('returns error when oidcTokenProvider is not configured', async () => {
await expect(
strategy.decorateClusterDetailsWithAuth(baseClusterDetails, {}),
strategy.decorateClusterDetailsWithAuth(
{
name: 'test',
url: '',
authMetadata: { authProvider: 'oidc' },
},
{},
),
).rejects.toThrow(
'oidc authProvider requires a configured oidcTokenProvider',
);
@@ -52,8 +57,12 @@ describe('OidcStrategy tests', () => {
await expect(
strategy.decorateClusterDetailsWithAuth(
{
authMetadata: { [ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: 'okta' },
...baseClusterDetails,
name: 'test',
url: '',
authMetadata: {
authProvider: 'oidc',
[ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: 'okta',
},
},
{},
),
@@ -18,14 +18,14 @@ import {
KubernetesRequestAuth,
} from '@backstage/plugin-kubernetes-common';
import { AuthenticationStrategy } from './types';
import { ClusterDetails } from '../types/types';
import { AuthMetadata, ClusterDetails } from '../types/types';
/**
*
* @public
*/
export class OidcStrategy implements AuthenticationStrategy {
async decorateClusterDetailsWithAuth(
public async decorateClusterDetailsWithAuth(
clusterDetails: ClusterDetails,
authConfig: KubernetesRequestAuth,
): Promise<ClusterDetails> {
@@ -35,7 +35,7 @@ export class OidcStrategy implements AuthenticationStrategy {
);
const oidcTokenProvider =
clusterDetails.authMetadata?.[ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER];
clusterDetails.authMetadata[ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER];
if (!oidcTokenProvider || oidcTokenProvider === '') {
throw new Error(
@@ -57,4 +57,12 @@ export class OidcStrategy implements AuthenticationStrategy {
}
return clusterDetailsWithAuthToken;
}
public validate(authMetadata: AuthMetadata) {
const oidcTokenProvider =
authMetadata[ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER];
if (!oidcTokenProvider || oidcTokenProvider === '') {
throw new Error(`Must specify a token provider for 'oidc' strategy`);
}
}
}
+2 -1
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { ClusterDetails } from '../types/types';
import { AuthMetadata, ClusterDetails } from '../types/types';
import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common';
/**
@@ -26,4 +26,5 @@ export interface AuthenticationStrategy {
clusterDetails: ClusterDetails,
authConfig: KubernetesRequestAuth,
): Promise<ClusterDetails>;
validate(authMetadata: AuthMetadata): void;
}
@@ -96,8 +96,8 @@ describe('CatalogClusterLocator', () => {
name: 'owned',
url: 'https://apiserver.com',
caData: 'caData',
authProvider: 'oidc',
authMetadata: {
authProvider: 'oidc',
'kubernetes.io/api-server': 'https://apiserver.com',
'kubernetes.io/api-server-certificate-authority': 'caData',
'kubernetes.io/auth-provider': 'oidc',
@@ -124,8 +124,8 @@ describe('CatalogClusterLocator', () => {
name: 'owned',
url: 'https://apiserver.com',
caData: 'caData',
authProvider: 'aws',
authMetadata: {
authProvider: 'aws',
'kubernetes.io/api-server': 'https://apiserver.com',
'kubernetes.io/api-server-certificate-authority': 'caData',
'kubernetes.io/auth-provider': 'aws',
@@ -59,9 +59,15 @@ export class CatalogClusterLocator implements KubernetesClustersSupplier {
url: entity.metadata.annotations![ANNOTATION_KUBERNETES_API_SERVER]!,
caData:
entity.metadata.annotations![ANNOTATION_KUBERNETES_API_SERVER_CA]!,
authProvider:
entity.metadata.annotations![ANNOTATION_KUBERNETES_AUTH_PROVIDER]!,
...{ authMetadata: entity.metadata.annotations },
...{
authMetadata: {
authProvider:
entity.metadata.annotations![
ANNOTATION_KUBERNETES_AUTH_PROVIDER
]!,
...entity.metadata.annotations,
},
},
skipMetricsLookup:
entity.metadata.annotations![
ANNOTATION_KUBERNETES_SKIP_METRICS_LOOKUP
@@ -55,7 +55,7 @@ describe('ConfigClusterLocator', () => {
{
name: 'cluster1',
url: 'http://localhost:8080',
authProvider: 'serviceAccount',
authMetadata: { authProvider: 'serviceAccount' },
skipMetricsLookup: false,
skipTLSVerify: false,
caData: undefined,
@@ -95,17 +95,19 @@ describe('ConfigClusterLocator', () => {
name: 'cluster1',
dashboardUrl: 'https://k8s.foo.com',
url: 'http://localhost:8080',
authProvider: 'serviceAccount',
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
},
skipTLSVerify: false,
skipMetricsLookup: true,
caData: undefined,
caFile: undefined,
authMetadata: { serviceAccountToken: 'token' },
},
{
name: 'cluster2',
url: 'http://localhost:8081',
authProvider: 'google',
authMetadata: { authProvider: 'google' },
skipTLSVerify: true,
skipMetricsLookup: false,
caData: undefined,
@@ -150,8 +152,8 @@ describe('ConfigClusterLocator', () => {
{
name: 'cluster1',
url: 'http://localhost:8080',
authProvider: 'aws',
authMetadata: {
authProvider: 'aws',
serviceAccountToken: 'token',
},
skipTLSVerify: false,
@@ -162,8 +164,8 @@ describe('ConfigClusterLocator', () => {
{
name: 'cluster2',
url: 'http://localhost:8081',
authProvider: 'aws',
authMetadata: {
authProvider: 'aws',
[ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE]: 'SomeRole',
},
skipTLSVerify: true,
@@ -174,8 +176,8 @@ describe('ConfigClusterLocator', () => {
{
name: 'cluster2',
url: 'http://localhost:8081',
authProvider: 'aws',
authMetadata: {
authProvider: 'aws',
[ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE]: 'SomeRole',
[ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID]: 'SomeExternalId',
},
@@ -212,7 +214,7 @@ describe('ConfigClusterLocator', () => {
{
name: 'cluster1',
url: 'http://localhost:8080',
authProvider: 'serviceAccount',
authMetadata: { authProvider: 'serviceAccount' },
skipMetricsLookup: false,
skipTLSVerify: false,
caData: undefined,
@@ -248,7 +250,7 @@ describe('ConfigClusterLocator', () => {
{
name: 'cluster1',
url: 'http://localhost:8080',
authProvider: 'serviceAccount',
authMetadata: { authProvider: 'serviceAccount' },
skipMetricsLookup: false,
skipTLSVerify: false,
caData: undefined,
@@ -260,18 +262,31 @@ describe('ConfigClusterLocator', () => {
});
it('supports aks authProvider', async () => {
const cluster = {
name: 'aks-cluster',
url: 'https://aks.test',
authProvider: 'aks',
};
const sut = ConfigClusterLocator.fromConfig(
new ConfigReader({ clusters: [cluster] }),
new ConfigReader({
clusters: [
{
name: 'aks-cluster',
url: 'https://aks.test',
authProvider: 'aks',
},
],
}),
);
const result = await sut.getClusters();
expect(result).toMatchObject([cluster]);
expect(result).toStrictEqual([
{
name: 'aks-cluster',
url: 'https://aks.test',
authMetadata: { authProvider: 'aks' },
skipMetricsLookup: false,
skipTLSVerify: false,
caData: undefined,
caFile: undefined,
},
]);
});
it('has cluster level defined customResources returns clusterDetails with those CRDs', async () => {
@@ -300,7 +315,7 @@ describe('ConfigClusterLocator', () => {
{
name: 'cluster1',
url: 'http://localhost:8080',
authProvider: 'serviceAccount',
authMetadata: { authProvider: 'serviceAccount' },
skipMetricsLookup: false,
skipTLSVerify: false,
caData: undefined,
@@ -316,18 +331,22 @@ describe('ConfigClusterLocator', () => {
]);
});
// TODO move this to a test on OidcStrategy#validate
it('errors when authProvider is oidc but oidcTokenProvider is missing', async () => {
const cluster = {
name: 'oidc-cluster',
url: 'https://aks.test',
authProvider: 'oidc',
};
expect(() =>
ConfigClusterLocator.fromConfig(
new ConfigReader({ clusters: [cluster] }),
new ConfigReader({
clusters: [
{
name: 'oidc-cluster',
url: 'https://aks.test',
authProvider: 'oidc',
},
],
}),
),
).toThrow(
`Cluster 'oidc-cluster' missing required config value for 'oidcTokenProvider'`,
`Invalid cluster 'oidc-cluster': Must specify a token provider for 'oidc' strategy`,
);
});
});
@@ -21,38 +21,39 @@ import {
ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER,
} from '@backstage/plugin-kubernetes-common';
import { ClusterDetails, KubernetesClustersSupplier } from '../types/types';
import {
AuthenticationStrategy,
AksStrategy,
DispatchStrategy,
GoogleStrategy,
GoogleServiceAccountStrategy,
NoopStrategy,
OidcStrategy,
} from '../auth';
const defaultAuthStrategies: Record<string, AuthenticationStrategy> = {
aks: new AksStrategy(),
aws: new NoopStrategy(),
google: new GoogleStrategy(),
googleServiceAccount: new GoogleServiceAccountStrategy(),
localKubectlProxy: new NoopStrategy(),
oidc: new OidcStrategy(),
serviceAccount: new NoopStrategy(),
};
export class ConfigClusterLocator implements KubernetesClustersSupplier {
private readonly clusterDetails: ClusterDetails[];
private static authValidators: Record<
string,
(clusterDetails: ClusterDetails) => void
> = {
google: _ => {},
aws: _ => {},
azure: _ => {},
oidc: clusterDetails => {
if (
!clusterDetails.authMetadata?.[
ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER
]
) {
throw new Error(
`Cluster '${clusterDetails.name}' missing required config value for 'oidcTokenProvider'`,
);
}
},
serviceAccount: _ => {},
googleServiceAccount: _ => {},
aks: _ => {},
};
constructor(clusterDetails: ClusterDetails[]) {
this.clusterDetails = clusterDetails;
}
static fromConfig(config: Config): ConfigClusterLocator {
static fromConfig(
config: Config,
authStrategy: AuthenticationStrategy = new DispatchStrategy({
authStrategyMap: defaultAuthStrategies,
}),
): ConfigClusterLocator {
return new ConfigClusterLocator(
config.getConfigArray('clusters').map(c => {
const authProvider = c.getString('authProvider');
@@ -63,8 +64,10 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier {
skipMetricsLookup: c.getOptionalBoolean('skipMetricsLookup') ?? false,
caData: c.getOptionalString('caData'),
caFile: c.getOptionalString('caFile'),
authProvider: authProvider,
...ConfigClusterLocator.parseAuthMetadata(c),
authMetadata: {
authProvider,
...ConfigClusterLocator.parseAuthMetadata(c),
},
};
const customResources = c.getOptionalConfigArray('customResources');
@@ -90,20 +93,21 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier {
clusterDetails.dashboardParameters = c.get('dashboardParameters');
}
if (authProvider in ConfigClusterLocator.authValidators) {
ConfigClusterLocator.authValidators[authProvider](clusterDetails);
return clusterDetails;
try {
authStrategy.validate(clusterDetails.authMetadata);
} catch (e) {
throw new Error(
`Invalid cluster '${clusterDetails.name}': ${e.message}`,
);
}
throw new Error(
`authProvider "${authProvider}" has no config associated with it`,
);
return clusterDetails;
}),
);
}
private static parseAuthMetadata(
clusterConfig: Config,
): { authMetadata: Record<string, string> } | undefined {
): Record<string, string> | undefined {
const serviceAccountToken = clusterConfig.getOptionalString(
'serviceAccountToken',
);
@@ -114,18 +118,16 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier {
return serviceAccountToken || assumeRole || externalId || oidcTokenProvider
? {
authMetadata: {
...(serviceAccountToken && { serviceAccountToken }),
...(assumeRole && {
[ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE]: assumeRole,
}),
...(externalId && {
[ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID]: externalId,
}),
...(oidcTokenProvider && {
[ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: oidcTokenProvider,
}),
},
...(serviceAccountToken && { serviceAccountToken }),
...(assumeRole && {
[ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE]: assumeRole,
}),
...(externalId && {
[ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID]: externalId,
}),
...(oidcTokenProvider && {
[ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: oidcTokenProvider,
}),
}
: undefined;
}
@@ -104,9 +104,9 @@ describe('GkeClusterLocator', () => {
expect(result).toStrictEqual([
{
authProvider: 'google',
name: 'some-cluster',
url: 'https://1.2.3.4',
authMetadata: { authProvider: 'google' },
skipTLSVerify: false,
skipMetricsLookup: true,
},
@@ -141,9 +141,9 @@ describe('GkeClusterLocator', () => {
expect(result).toStrictEqual([
{
authProvider: 'google',
name: 'some-cluster',
url: 'https://1.2.3.4',
authMetadata: { authProvider: 'google' },
skipTLSVerify: false,
skipMetricsLookup: false,
},
@@ -183,16 +183,16 @@ describe('GkeClusterLocator', () => {
expect(result).toStrictEqual([
{
authProvider: 'google',
name: 'some-cluster',
url: 'https://1.2.3.4',
authMetadata: { authProvider: 'google' },
skipTLSVerify: false,
skipMetricsLookup: false,
},
{
authProvider: 'google',
name: 'some-other-cluster',
url: 'https://6.7.8.9',
authMetadata: { authProvider: 'google' },
skipTLSVerify: false,
skipMetricsLookup: false,
},
@@ -238,16 +238,16 @@ describe('GkeClusterLocator', () => {
expect(result).toStrictEqual([
{
authProvider: 'google',
name: 'some-cluster',
url: 'https://1.2.3.4',
authMetadata: { authProvider: 'google' },
skipTLSVerify: false,
skipMetricsLookup: false,
},
{
authProvider: 'google',
name: 'some-other-cluster',
url: 'https://6.7.8.9',
authMetadata: { authProvider: 'google' },
skipTLSVerify: false,
skipMetricsLookup: false,
},
@@ -299,9 +299,9 @@ describe('GkeClusterLocator', () => {
expect(result).toStrictEqual([
{
authProvider: 'google',
name: 'some-cluster',
url: 'https://1.2.3.4',
authMetadata: { authProvider: 'google' },
skipTLSVerify: false,
skipMetricsLookup: false,
},
@@ -363,9 +363,9 @@ describe('GkeClusterLocator', () => {
expect(result).toStrictEqual([
{
authProvider: 'google',
name: 'some-cluster',
url: 'https://1.2.3.4',
authMetadata: { authProvider: 'google' },
skipTLSVerify: false,
skipMetricsLookup: true,
dashboardApp: 'gke',
@@ -120,7 +120,7 @@ export class GkeClusterLocator implements KubernetesClustersSupplier {
// TODO filter out clusters which don't have name or endpoint
name: r.name ?? 'unknown',
url: `https://${r.endpoint ?? ''}`,
authProvider: 'google',
authMetadata: { authProvider: 'google' },
skipTLSVerify,
skipMetricsLookup,
...(exposeDashboard
@@ -26,7 +26,7 @@ export class LocalKubectlProxyClusterLocator
{
name: 'local',
url: 'http:/localhost:8001',
authProvider: 'localKubectlProxy',
authMetadata: { authProvider: 'localKubectlProxy' },
skipMetricsLookup: true,
},
];
@@ -18,6 +18,7 @@ import { Config, ConfigReader } from '@backstage/config';
import { getCombinedClusterSupplier } from './index';
import { CatalogApi } from '@backstage/catalog-client';
import { ClusterDetails } from '../types/types';
import { AuthenticationStrategy, DispatchStrategy } from '../auth';
describe('getCombinedClusterSupplier', () => {
let catalogApi: CatalogApi;
@@ -48,16 +49,31 @@ describe('getCombinedClusterSupplier', () => {
},
'ctx',
);
const mockStrategy: jest.Mocked<AuthenticationStrategy> = {
decorateClusterDetailsWithAuth: jest.fn(),
validate: jest.fn(),
};
const clusterSupplier = getCombinedClusterSupplier(config, catalogApi);
const clusterSupplier = getCombinedClusterSupplier(
config,
catalogApi,
new DispatchStrategy({
authStrategyMap: {
serviceAccount: mockStrategy,
google: mockStrategy,
},
}),
);
const result = await clusterSupplier.getClusters();
expect(result).toStrictEqual<ClusterDetails[]>([
{
name: 'cluster1',
url: 'http://localhost:8080',
authProvider: 'serviceAccount',
authMetadata: { serviceAccountToken: 'token' },
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
},
skipMetricsLookup: false,
skipTLSVerify: false,
caData: undefined,
@@ -66,7 +82,7 @@ describe('getCombinedClusterSupplier', () => {
{
name: 'cluster2',
url: 'http://localhost:8081',
authProvider: 'google',
authMetadata: { authProvider: 'google' },
skipMetricsLookup: false,
skipTLSVerify: false,
caData: undefined,
@@ -77,35 +93,17 @@ describe('getCombinedClusterSupplier', () => {
it('throws an error when using an unsupported cluster locator', async () => {
const config: Config = new ConfigReader(
{
kubernetes: {
clusterLocatorMethods: [
{
type: 'config',
clusters: [
{
name: 'cluster1',
serviceAccountToken: 'token',
url: 'http://localhost:8080',
authProvider: 'serviceAccount',
},
{
name: 'cluster2',
url: 'http://localhost:8081',
authProvider: 'google',
},
],
},
{
type: 'magic',
},
],
},
},
{ kubernetes: { clusterLocatorMethods: [{ type: 'magic' }] } },
'ctx',
);
expect(() => getCombinedClusterSupplier(config, catalogApi)).toThrow(
expect(() =>
getCombinedClusterSupplier(
config,
catalogApi,
new DispatchStrategy({ authStrategyMap: {} }),
),
).toThrow(
new Error('Unsupported kubernetes.clusterLocatorMethods: "magic"'),
);
});
@@ -17,6 +17,7 @@
import { Config } from '@backstage/config';
import { Duration } from 'luxon';
import { ClusterDetails, KubernetesClustersSupplier } from '../types/types';
import { AuthenticationStrategy } from '../auth/types';
import { ConfigClusterLocator } from './ConfigClusterLocator';
import { GkeClusterLocator } from './GkeClusterLocator';
import { CatalogClusterLocator } from './CatalogClusterLocator';
@@ -42,6 +43,7 @@ class CombinedClustersSupplier implements KubernetesClustersSupplier {
export const getCombinedClusterSupplier = (
rootConfig: Config,
catalogClient: CatalogApi,
authStrategy: AuthenticationStrategy,
refreshInterval: Duration | undefined = undefined,
): KubernetesClustersSupplier => {
const clusterSuppliers = rootConfig
@@ -54,7 +56,10 @@ export const getCombinedClusterSupplier = (
case 'localKubectlProxy':
return new LocalKubectlProxyClusterLocator();
case 'config':
return ConfigClusterLocator.fromConfig(clusterLocatorMethod);
return ConfigClusterLocator.fromConfig(
clusterLocatorMethod,
authStrategy,
);
case 'gke':
return GkeClusterLocator.fromConfig(
clusterLocatorMethod,
@@ -16,7 +16,7 @@
import '@backstage/backend-common';
import { Entity } from '@backstage/catalog-model';
import { ServiceLocatorRequestContext } from '../types/types';
import { AuthMetadata, ServiceLocatorRequestContext } from '../types/types';
import { MultiTenantServiceLocator } from './MultiTenantServiceLocator';
describe('MultiTenantConfigClusterLocator', () => {
@@ -40,8 +40,10 @@ describe('MultiTenantConfigClusterLocator', () => {
{
name: 'cluster1',
url: 'http://localhost:8080',
authProvider: 'serviceAccount',
serviceAccountToken: '12345',
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: '12345',
},
},
];
},
@@ -56,9 +58,11 @@ describe('MultiTenantConfigClusterLocator', () => {
clusters: [
{
name: 'cluster1',
serviceAccountToken: '12345',
url: 'http://localhost:8080',
authProvider: 'serviceAccount',
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: '12345',
},
},
],
});
@@ -70,14 +74,16 @@ describe('MultiTenantConfigClusterLocator', () => {
return [
{
name: 'cluster1',
serviceAccountToken: 'token',
url: 'http://localhost:8080',
authProvider: 'serviceAccount',
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
} as AuthMetadata,
},
{
name: 'cluster2',
url: 'http://localhost:8081',
authProvider: 'google',
authMetadata: { authProvider: 'google' } as AuthMetadata,
},
];
},
@@ -92,14 +98,16 @@ describe('MultiTenantConfigClusterLocator', () => {
clusters: [
{
name: 'cluster1',
serviceAccountToken: 'token',
url: 'http://localhost:8080',
authProvider: 'serviceAccount',
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
},
},
{
name: 'cluster2',
url: 'http://localhost:8081',
authProvider: 'google',
authMetadata: { authProvider: 'google' },
},
],
});
@@ -62,13 +62,13 @@ describe('KubernetesBuilder', () => {
const clusters: ClusterDetails[] = [
{
name: 'some-cluster',
authProvider: 'serviceAccount',
url: 'https://localhost:1234',
authMetadata: { authProvider: 'serviceAccount' },
},
{
name: 'some-other-cluster',
url: 'https://localhost:1235',
authProvider: 'google',
authMetadata: { authProvider: 'google' },
},
];
const clusterSupplier: KubernetesClustersSupplier = {
@@ -189,15 +189,15 @@ describe('KubernetesBuilder', () => {
const logger = getVoidLogger();
const someCluster: ClusterDetails = {
name: 'some-cluster',
authProvider: 'serviceAccount',
url: 'https://localhost:1234',
authMetadata: { authProvider: 'serviceAccount' },
};
const clusters: ClusterDetails[] = [
someCluster,
{
name: 'some-other-cluster',
url: 'https://localhost:1235',
authProvider: 'google',
authMetadata: { authProvider: 'google' },
},
];
const clusterSupplier: KubernetesClustersSupplier = {
@@ -225,6 +225,7 @@ export class KubernetesBuilder {
this.clusterSupplier = getCombinedClusterSupplier(
config,
this.env.catalogApi,
new DispatchStrategy({ authStrategyMap: this.getAuthStrategyMap() }),
refreshInterval,
);
@@ -340,11 +341,11 @@ export class KubernetesBuilder {
const clusterDetails = await this.fetchClusterDetails(clusterSupplier);
res.json({
items: clusterDetails.map(cd => {
const oidcTokenProvider = cd.authMetadata?.oidcTokenProvider;
const oidcTokenProvider = cd.authMetadata.oidcTokenProvider;
return {
name: cd.name,
dashboardUrl: cd.dashboardUrl,
authProvider: cd.authProvider,
authProvider: cd.authMetadata.authProvider,
...(oidcTokenProvider && { oidcTokenProvider }),
};
}),
@@ -358,14 +359,14 @@ export class KubernetesBuilder {
protected buildAuthStrategyMap() {
this.authStrategyMap = {
google: new GoogleStrategy(),
aks: new AksStrategy(),
aws: new AwsIamStrategy({ config: this.env.config }),
azure: new AzureIdentityStrategy(this.env.logger),
serviceAccount: new NoopStrategy(),
google: new GoogleStrategy(),
googleServiceAccount: new GoogleServiceAccountStrategy(),
oidc: new OidcStrategy(),
localKubectlProxy: new NoopStrategy(),
oidc: new OidcStrategy(),
serviceAccount: new NoopStrategy(),
};
return this.authStrategyMap;
}
@@ -34,7 +34,10 @@ import { Entity } from '@backstage/catalog-model';
describe('KubernetesFanOutHandler', () => {
const fetchObjectsForService = jest.fn();
const fetchPodMetricsByNamespaces = jest.fn();
const getClustersByEntity = jest.fn();
const getClustersByEntity = jest.fn<
Promise<{ clusters: ClusterDetails[] }>,
[Entity]
>();
let config: Config;
let sut: KubernetesFanOutHandler;
@@ -73,7 +76,8 @@ describe('KubernetesFanOutHandler', () => {
const cluster1 = {
name: 'test-cluster',
authProvider: 'serviceAccount',
url: '',
authMetadata: { authProvider: 'serviceAccount' },
customResources: [
{
group: 'some-other-crd.example.com',
@@ -85,7 +89,8 @@ describe('KubernetesFanOutHandler', () => {
const cluster2 = {
name: 'cluster-two',
authProvider: 'serviceAccount',
url: '',
authMetadata: { authProvider: 'serviceAccount' },
customResources: [
{
group: 'crd-two.example.com',
@@ -183,6 +188,7 @@ describe('KubernetesFanOutHandler', () => {
decorateClusterDetailsWithAuth: async (clusterDetails, _) => {
return clusterDetails;
},
validate: jest.fn(),
},
config,
});
@@ -345,7 +351,8 @@ describe('KubernetesFanOutHandler', () => {
clusters: [
{
name: 'test-cluster',
authProvider: 'serviceAccount',
url: '',
authMetadata: { authProvider: 'serviceAccount' },
},
],
}),
@@ -459,7 +466,8 @@ describe('KubernetesFanOutHandler', () => {
clusters: [
{
name: 'test-cluster',
authProvider: 'serviceAccount',
url: '',
authMetadata: { authProvider: 'serviceAccount' },
},
cluster2,
],
@@ -513,7 +521,8 @@ describe('KubernetesFanOutHandler', () => {
clusters: [
{
name: 'profile-cluster-1',
authProvider: 'serviceAccount',
url: '',
authMetadata: { authProvider: 'serviceAccount' },
customResourceProfile: 'build',
customResources: [
{
@@ -561,7 +570,8 @@ describe('KubernetesFanOutHandler', () => {
clusters: [
{
name: 'profile-cluster-1',
authProvider: 'serviceAccount',
url: '',
authMetadata: { authProvider: 'serviceAccount' },
},
],
}),
@@ -601,7 +611,8 @@ describe('KubernetesFanOutHandler', () => {
clusters: [
{
name: 'test-cluster',
authProvider: 'serviceAccount',
url: '',
authMetadata: { authProvider: 'serviceAccount' },
},
],
}),
@@ -698,7 +709,8 @@ describe('KubernetesFanOutHandler', () => {
clusters: [
{
name: 'test-cluster',
authProvider: 'serviceAccount',
url: '',
authMetadata: { authProvider: 'serviceAccount' },
},
],
}),
@@ -737,12 +749,14 @@ describe('KubernetesFanOutHandler', () => {
clusters: [
{
name: 'test-cluster',
authProvider: 'serviceAccount',
url: '',
authMetadata: { authProvider: 'serviceAccount' },
dashboardUrl: 'https://k8s.foo.coom',
},
{
name: 'other-cluster',
authProvider: 'google',
url: '',
authMetadata: { authProvider: 'google' },
},
],
}),
@@ -788,15 +802,18 @@ describe('KubernetesFanOutHandler', () => {
clusters: [
{
name: 'test-cluster',
authProvider: 'serviceAccount',
url: '',
authMetadata: { authProvider: 'serviceAccount' },
},
{
name: 'other-cluster',
authProvider: 'google',
url: '',
authMetadata: { authProvider: 'google' },
},
{
name: 'empty-cluster',
authProvider: 'google',
url: '',
authMetadata: { authProvider: 'google' },
},
],
}),
@@ -841,19 +858,23 @@ describe('KubernetesFanOutHandler', () => {
clusters: [
{
name: 'test-cluster',
authProvider: 'serviceAccount',
url: '',
authMetadata: { authProvider: 'serviceAccount' },
},
{
name: 'other-cluster',
authProvider: 'google',
url: '',
authMetadata: { authProvider: 'google' },
},
{
name: 'empty-cluster',
authProvider: 'google',
url: '',
authMetadata: { authProvider: 'google' },
},
{
name: 'error-cluster',
authProvider: 'google',
url: '',
authMetadata: { authProvider: 'google' },
},
],
}),
@@ -919,12 +940,14 @@ describe('KubernetesFanOutHandler', () => {
clusters: [
{
name: 'test-cluster',
authProvider: 'serviceAccount',
url: '',
authMetadata: { authProvider: 'serviceAccount' },
dashboardUrl: 'https://k8s.foo.coom',
},
{
name: 'other-cluster',
authProvider: 'google',
url: '',
authMetadata: { authProvider: 'google' },
},
],
}),
@@ -1017,7 +1040,8 @@ describe('KubernetesFanOutHandler', () => {
clusters: [
{
name: 'test-cluster',
authProvider: 'serviceAccount',
url: '',
authMetadata: { authProvider: 'serviceAccount' },
skipMetricsLookup: true,
},
],
@@ -1066,16 +1090,20 @@ describe('KubernetesFanOutHandler', () => {
{
name: 'works',
url: 'https://works',
authProvider: 'serviceAccount',
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
},
skipMetricsLookup: true,
authMetadata: { serviceAccountToken: 'token' },
},
{
name: 'fails',
url: 'https://fails',
authProvider: 'serviceAccount',
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
},
skipMetricsLookup: true,
authMetadata: { serviceAccountToken: 'token' },
},
],
}),
@@ -1104,6 +1132,7 @@ describe('KubernetesFanOutHandler', () => {
decorateClusterDetailsWithAuth: async (clusterDetails, _) => {
return clusterDetails;
},
validate: jest.fn(),
},
config,
});
@@ -1199,7 +1228,8 @@ describe('KubernetesFanOutHandler', () => {
clusters: [
{
name: 'test-cluster',
authProvider: 'serviceAccount',
url: '',
authMetadata: { authProvider: 'serviceAccount' },
},
cluster2,
],
@@ -1272,7 +1302,8 @@ describe('KubernetesFanOutHandler', () => {
clusters: [
{
name: 'profile-cluster-1',
authProvider: 'serviceAccount',
url: '',
authMetadata: { authProvider: 'serviceAccount' },
customResourceProfile: 'build',
},
],
@@ -138,8 +138,10 @@ describe('KubernetesFetcher', () => {
clusterDetails: {
name: 'cluster1',
url: 'http://localhost:9999',
authProvider: 'serviceAccount',
authMetadata: { serviceAccountToken: 'token' },
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
},
},
objectTypesToFetch: OBJECTS_TO_FETCH,
labelSelector: '',
@@ -199,8 +201,10 @@ describe('KubernetesFetcher', () => {
clusterDetails: {
name: 'cluster1',
url: 'http://localhost:9999/k8s/clusters/1234',
authProvider: 'serviceAccount',
authMetadata: { serviceAccountToken: 'token' },
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
},
},
objectTypesToFetch: OBJECTS_TO_FETCH,
labelSelector: '',
@@ -253,7 +257,7 @@ describe('KubernetesFetcher', () => {
clusterDetails: {
name: 'cluster1',
url: 'http://localhost:9999/k8s/clusters/1234',
authProvider: 'localKubectlProxy',
authMetadata: { authProvider: 'localKubectlProxy' },
},
objectTypesToFetch: new Set([
{
@@ -309,8 +313,10 @@ describe('KubernetesFetcher', () => {
clusterDetails: {
name: 'cluster1',
url: 'http://localhost:9999',
authProvider: 'serviceAccount',
authMetadata: { serviceAccountToken: 'token' },
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
},
},
objectTypesToFetch: OBJECTS_TO_FETCH,
labelSelector: '',
@@ -383,8 +389,10 @@ describe('KubernetesFetcher', () => {
clusterDetails: {
name: 'cluster1',
url: 'http://localhost:9999',
authProvider: 'serviceAccount',
authMetadata: { serviceAccountToken: 'token' },
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
},
},
objectTypesToFetch: OBJECTS_TO_FETCH,
labelSelector: '',
@@ -475,8 +483,10 @@ describe('KubernetesFetcher', () => {
clusterDetails: {
name: 'cluster1',
url: 'http://localhost:9999',
authProvider: 'serviceAccount',
authMetadata: { serviceAccountToken: 'token' },
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
},
},
objectTypesToFetch: OBJECTS_TO_FETCH,
labelSelector: '',
@@ -563,8 +573,10 @@ describe('KubernetesFetcher', () => {
clusterDetails: {
name: 'cluster1',
url: 'http://badurl.does.not.exist',
authProvider: 'serviceAccount',
authMetadata: { serviceAccountToken: 'token' },
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
},
},
objectTypesToFetch: OBJECTS_TO_FETCH,
labelSelector: '',
@@ -600,8 +612,10 @@ describe('KubernetesFetcher', () => {
clusterDetails: {
name: 'cluster1',
url: 'http://localhost:9999',
authProvider: 'serviceAccount',
authMetadata: { serviceAccountToken: 'token' },
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
},
},
objectTypesToFetch: OBJECTS_TO_FETCH,
labelSelector: 'service-label=value',
@@ -667,8 +681,10 @@ describe('KubernetesFetcher', () => {
clusterDetails: {
name: 'cluster1',
url: 'https://localhost:9999',
authProvider: 'serviceAccount',
authMetadata: { serviceAccountToken: 'token' },
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
},
caData: 'MOCKCA',
},
objectTypesToFetch: new Set<ObjectToFetch>([
@@ -704,8 +720,10 @@ describe('KubernetesFetcher', () => {
clusterDetails: {
name: 'cluster1',
url: 'https://localhost:9999',
authProvider: 'serviceAccount',
authMetadata: { serviceAccountToken: 'token' },
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
},
},
objectTypesToFetch: new Set<ObjectToFetch>([
{
@@ -747,8 +765,10 @@ describe('KubernetesFetcher', () => {
clusterDetails: {
name: 'cluster1',
url: 'https://localhost:9999',
authProvider: 'serviceAccount',
authMetadata: { serviceAccountToken: 'token' },
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
},
caFile: '/path/to/ca.crt',
},
objectTypesToFetch: new Set<ObjectToFetch>([
@@ -785,8 +805,10 @@ describe('KubernetesFetcher', () => {
clusterDetails: {
name: 'cluster1',
url: 'https://localhost:9999',
authProvider: 'serviceAccount',
authMetadata: { serviceAccountToken: 'token' },
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
},
skipTLSVerify: true,
},
objectTypesToFetch: new Set<ObjectToFetch>([
@@ -836,8 +858,10 @@ describe('KubernetesFetcher', () => {
clusterDetails: {
name: 'cluster1',
url: 'http://localhost:9999',
authProvider: 'serviceAccount',
authMetadata: { serviceAccountToken: 'token' },
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
},
},
objectTypesToFetch: OBJECTS_TO_FETCH,
labelSelector: '',
@@ -880,7 +904,7 @@ describe('KubernetesFetcher', () => {
clusterDetails: {
name: 'unauthenticated-cluster',
url: 'http://ignored',
authProvider: 'serviceAccount',
authMetadata: { authProvider: 'serviceAccount' },
},
objectTypesToFetch: OBJECTS_TO_FETCH,
labelSelector: '',
@@ -923,7 +947,7 @@ describe('KubernetesFetcher', () => {
clusterDetails: {
name: 'overridden-to-in-cluster',
url: 'http://ignored',
authProvider: 'serviceAccount',
authMetadata: { authProvider: 'serviceAccount' },
},
objectTypesToFetch: new Set<ObjectToFetch>([
{
@@ -1019,8 +1043,10 @@ describe('KubernetesFetcher', () => {
{
name: 'cluster1',
url: 'http://localhost:9999',
authProvider: 'serviceAccount',
authMetadata: { serviceAccountToken: 'token' },
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
},
},
new Set(['ns-a']),
);
@@ -1106,8 +1132,10 @@ describe('KubernetesFetcher', () => {
{
name: 'cluster1',
url: 'http://localhost:9999',
authProvider: 'serviceAccount',
authMetadata: { serviceAccountToken: 'token' },
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
},
},
new Set(['ns-a', 'ns-b']),
);
@@ -201,8 +201,8 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher {
let url: URL;
let requestInit: RequestInit;
if (
clusterDetails?.authMetadata?.serviceAccountToken ||
clusterDetails.authProvider === 'localKubectlProxy'
clusterDetails.authMetadata.serviceAccountToken ||
clusterDetails.authMetadata.authProvider === 'localKubectlProxy'
) {
[url, requestInit] = this.fetchArgsFromClusterDetails(clusterDetails);
} else if (fs.pathExistsSync(Config.SERVICEACCOUNT_TOKEN_PATH)) {
@@ -236,7 +236,7 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${clusterDetails.authMetadata?.serviceAccountToken}`,
Authorization: `Bearer ${clusterDetails.authMetadata.serviceAccountToken}`,
},
};
@@ -61,6 +61,7 @@ describe('KubernetesProxy', () => {
const authStrategy: jest.Mocked<AuthenticationStrategy> = {
decorateClusterDetailsWithAuth: jest.fn(),
validate: jest.fn(),
};
setupRequestMockHandlers(worker);
@@ -145,14 +146,16 @@ describe('KubernetesProxy', () => {
{
name: 'local',
url: 'http:/localhost:8001',
authProvider: 'localKubectlProxy',
authMetadata: { authProvider: 'localKubectlProxy' },
skipMetricsLookup: true,
} as ClusterDetails,
{
name: 'cluster1',
url: 'https://localhost:9999',
authProvider: 'googleServiceAccount',
authMetadata: { serviceAccountToken: 'tokenA' },
authMetadata: {
authProvider: 'googleServiceAccount',
serviceAccountToken: 'tokenA',
},
} as ClusterDetails,
]);
@@ -169,8 +172,10 @@ describe('KubernetesProxy', () => {
{
name: 'cluster1',
url: 'https://localhost:9999',
authProvider: 'googleServiceAccount',
authMetadata: { serviceAccountToken: 'tokenA' },
authMetadata: {
authProvider: 'googleServiceAccount',
serviceAccountToken: 'tokenA',
},
} as ClusterDetails,
]);
@@ -198,14 +203,14 @@ describe('KubernetesProxy', () => {
{
name: 'cluster1',
url: 'https://localhost:9999',
authProvider: 'serviceAccount',
authMetadata: { authProvider: 'serviceAccount' },
},
] as ClusterDetails[]);
authStrategy.decorateClusterDetailsWithAuth.mockResolvedValue({
name: 'cluster1',
url: 'https://localhost:9999',
authProvider: 'serviceAccount',
authMetadata: { authProvider: 'serviceAccount' },
} as ClusterDetails);
worker.use(
@@ -242,14 +247,14 @@ describe('KubernetesProxy', () => {
{
name: 'cluster1',
url: 'https://localhost:9999',
authProvider: 'serviceAccount',
authMetadata: { authProvider: 'serviceAccount' },
},
] as ClusterDetails[]);
authStrategy.decorateClusterDetailsWithAuth.mockResolvedValue({
name: 'cluster1',
url: 'https://localhost:9999',
authProvider: 'serviceAccount',
authMetadata: { authProvider: 'serviceAccount' },
} as ClusterDetails);
worker.use(
@@ -286,7 +291,7 @@ describe('KubernetesProxy', () => {
{
name: 'cluster1',
url: 'http://localhost:9999',
authProvider: '',
authMetadata: { authProvider: '' },
},
]);
authStrategy.decorateClusterDetailsWithAuth.mockImplementation(
@@ -337,15 +342,17 @@ describe('KubernetesProxy', () => {
{
name: 'cluster1',
url: 'https://localhost:9999',
authProvider: 'serviceAccount',
authMetadata: { authProvider: 'serviceAccount' },
},
] as ClusterDetails[]);
authStrategy.decorateClusterDetailsWithAuth.mockResolvedValue({
name: 'cluster1',
url: 'https://localhost:9999',
authProvider: 'serviceAccount',
authMetadata: { serviceAccountToken: 'strategy-provided-token' },
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'strategy-provided-token',
},
} as ClusterDetails);
const requestPromise = setupProxyPromise({
@@ -386,15 +393,17 @@ describe('KubernetesProxy', () => {
{
name: 'cluster1',
url: 'https://localhost:9999',
authProvider: 'googleServiceAccount',
authMetadata: { authProvider: 'googleServiceAccount' },
},
] as ClusterDetails[]);
authStrategy.decorateClusterDetailsWithAuth.mockResolvedValue({
name: 'cluster1',
url: 'https://localhost:9999',
authProvider: 'googleServiceAccount',
authMetadata: { serviceAccountToken: 'my-token' },
authMetadata: {
authProvider: 'googleServiceAccount',
serviceAccountToken: 'my-token',
},
} as ClusterDetails);
const requestPromise = setupProxyPromise({
@@ -440,15 +449,17 @@ describe('KubernetesProxy', () => {
{
name: 'cluster1',
url: 'https://localhost:9999',
authProvider: 'googleServiceAccount',
authMetadata: { authProvider: 'googleServiceAccount' },
},
] as ClusterDetails[]);
authStrategy.decorateClusterDetailsWithAuth.mockResolvedValue({
name: 'cluster1',
url: 'https://localhost:9999',
authProvider: 'googleServiceAccount',
authMetadata: { serviceAccountToken: 'tokenA' },
authMetadata: {
authProvider: 'googleServiceAccount',
serviceAccountToken: 'tokenA',
},
} as ClusterDetails);
const requestPromise = setupProxyPromise({
@@ -497,7 +508,7 @@ describe('KubernetesProxy', () => {
{
name: 'cluster1',
url: 'https://localhost:9999',
authProvider: 'googleServiceAccount',
authMetadata: { authProvider: 'googleServiceAccount' },
},
] as ClusterDetails[]);
@@ -591,8 +602,10 @@ describe('KubernetesProxy', () => {
{
name: 'cluster1',
url: 'https://localhost:9999',
authProvider: 'google',
authMetadata: { serviceAccountToken: 'client-side-token' },
authMetadata: {
authProvider: 'google',
serviceAccountToken: 'client-side-token',
},
},
] as ClusterDetails[]);
@@ -635,7 +648,7 @@ describe('KubernetesProxy', () => {
{
name: 'cluster1',
url: 'http://localhost:9999/subpath',
authProvider: '',
authMetadata: { authProvider: '' },
},
]);
@@ -780,14 +793,14 @@ describe('KubernetesProxy', () => {
{
name: 'local',
url: `http://localhost:${wsPort}`,
authProvider: 'serviceAccount',
authMetadata: { authProvider: 'serviceAccount' },
},
] as ClusterDetails[]);
authStrategy.decorateClusterDetailsWithAuth.mockResolvedValue({
name: 'local',
url: `http://localhost:${wsPort}`,
authProvider: 'serviceAccount',
authMetadata: { authProvider: 'serviceAccount' },
} as ClusterDetails);
const wsProxyAddress = `ws://127.0.0.1:${proxyPort}${proxyPath}${wsPath}`;
@@ -114,7 +114,7 @@ export class KubernetesProxy {
} else {
const serviceAccountToken = await this.getClusterForRequest(req)
.then(cd => this.authStrategy.decorateClusterDetailsWithAuth(cd, {}))
.then(cd => cd?.authMetadata?.serviceAccountToken);
.then(cd => cd.authMetadata.serviceAccountToken);
if (serviceAccountToken) {
req.headers.authorization = `Bearer ${serviceAccountToken}`;
}
@@ -144,6 +144,12 @@ export interface KubernetesServiceLocator {
*/
export type ServiceLocatorMethod = 'multiTenant' | 'http'; // TODO implement http
/**
* Provider-specific authentication configuration
* @public
*/
export type AuthMetadata = Record<string, string>;
/**
*
* @public
@@ -154,11 +160,7 @@ export interface ClusterDetails {
*/
name: string;
url: string;
authProvider: string;
/**
* Provider-specific authentication configuration
*/
authMetadata?: Record<string, string>;
authMetadata: AuthMetadata;
skipTLSVerify?: boolean;
/**
* Whether to skip the lookup to the metrics server to retrieve pod resource usage.