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:
@@ -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`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user