strategies get creds instead of decorating cluster

the fetcher now accept these creds, and the fanouthandler passes them along.

Signed-off-by: Jamie Klassen <jklassen@vmware.com>
This commit is contained in:
Jamie Klassen
2023-09-07 19:08:23 -04:00
parent 279adfc6fd
commit d21796ce54
29 changed files with 330 additions and 515 deletions
@@ -19,11 +19,11 @@ describe('AksStrategy', () => {
it('uses auth.aks value as bearer token', async () => {
const strategy = new AksStrategy();
const details = await strategy.decorateClusterDetailsWithAuth(
{ name: '', url: '', authMetadata: { authProvider: 'aks' } },
const credential = await strategy.getCredential(
{ name: '', url: '', authMetadata: {} },
{ aks: 'aksToken' },
);
expect(details.authMetadata.serviceAccountToken).toBe('aksToken');
expect(credential).toBe('aksToken');
});
});
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { AuthMetadata, ClusterDetails } from '../types/types';
import { AuthenticationStrategy } from './types';
import { AuthenticationStrategy, KubernetesCredential } from './types';
import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common';
/**
@@ -22,19 +22,11 @@ import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common';
* @public
*/
export class AksStrategy implements AuthenticationStrategy {
public async decorateClusterDetailsWithAuth(
clusterDetails: ClusterDetails,
auth: KubernetesRequestAuth,
): Promise<ClusterDetails> {
return {
...clusterDetails,
...(auth.aks && {
authMetadata: {
serviceAccountToken: auth.aks,
...clusterDetails.authMetadata,
},
}),
};
public async getCredential(
_: ClusterDetails,
requestAuth: KubernetesRequestAuth,
): Promise<KubernetesCredential> {
return requestAuth.aks;
}
public validate(_: AuthMetadata) {}
}
@@ -60,12 +60,12 @@ describe('AwsIamStrategy tests', () => {
it('returns a signed url for AWS credentials without assume role', async () => {
const strategy = new AwsIamStrategy({ config });
const authPromise = strategy.decorateClusterDetailsWithAuth({
const credential = await strategy.getCredential({
name: 'test-cluster',
url: '',
authMetadata: { authProvider: 'aws' },
authMetadata: {},
});
expect((await authPromise).authMetadata.serviceAccountToken).toEqual(
expect(credential).toEqual(
'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_',
);
});
@@ -73,15 +73,15 @@ describe('AwsIamStrategy tests', () => {
it('returns a signed url for AWS credentials with assume role', async () => {
const strategy = new AwsIamStrategy({ config });
const authPromise = strategy.decorateClusterDetailsWithAuth({
const credential = await strategy.getCredential({
name: 'test-cluster',
url: '',
authMetadata: {
authProvider: 'aws',
[ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE]: 'SomeRole',
},
});
expect((await authPromise).authMetadata.serviceAccountToken).toEqual(
expect(credential).toEqual(
'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_',
);
expect(fromTemporaryCredentials).toHaveBeenCalledWith({
@@ -101,16 +101,15 @@ describe('AwsIamStrategy tests', () => {
it('returns a signed url for AWS credentials and passes the external id', async () => {
const strategy = new AwsIamStrategy({ config });
const authPromise = strategy.decorateClusterDetailsWithAuth({
const credential = await strategy.getCredential({
name: 'test-cluster',
url: '',
authMetadata: {
authProvider: 'aws',
[ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE]: 'SomeRole',
[ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID]: 'external-id',
},
});
expect((await authPromise).authMetadata.serviceAccountToken).toEqual(
expect(credential).toEqual(
'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_',
);
expect(fromTemporaryCredentials).toHaveBeenCalledWith({
@@ -136,10 +135,10 @@ describe('AwsIamStrategy tests', () => {
it('throws the right error', async () => {
const strategy = new AwsIamStrategy({ config });
await expect(
strategy.decorateClusterDetailsWithAuth({
strategy.getCredential({
name: 'test-cluster',
url: '',
authMetadata: { authProvider: 'aws' },
authMetadata: {},
}),
).rejects.toThrow('no way');
});
@@ -25,8 +25,8 @@ import {
ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE,
ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID,
} from '@backstage/plugin-kubernetes-common';
import { AuthMetadata, ClusterDetails } from '../types/types';
import { AuthenticationStrategy } from './types';
import { ClusterDetails } from '../types/types';
import { AuthenticationStrategy, KubernetesCredential } from './types';
/**
*
@@ -51,26 +51,17 @@ export class AwsIamStrategy implements AuthenticationStrategy {
this.credsManager = DefaultAwsCredentialsManager.fromConfig(opts.config);
}
public async decorateClusterDetailsWithAuth(
public getCredential(
clusterDetails: ClusterDetails,
): Promise<ClusterDetails> {
const clusterDetailsWithAuthToken: ClusterDetails = Object.assign(
{},
clusterDetails,
): Promise<KubernetesCredential> {
return this.getBearerToken(
clusterDetails.name,
clusterDetails.authMetadata[ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE],
clusterDetails.authMetadata[ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID],
);
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) {}
public validate() {}
private async getBearerToken(
clusterName: string,
@@ -40,24 +40,18 @@ class StaticTokenCredential implements TokenCredential {
}
describe('AzureIdentityStrategy tests', () => {
const cd = {
name: 'My Cluster',
url: 'mycluster.privatelink.westeurope.azmk8s.io',
authMetadata: { authProvider: 'azure' },
};
afterEach(() => {
jest.useRealTimers();
});
it('should decorate cluster with Azure token', async () => {
it('should get Azure token', async () => {
const strategy = new AzureIdentityStrategy(
logger,
new StaticTokenCredential(5 * 60 * 1000),
);
const response = await strategy.decorateClusterDetailsWithAuth(cd);
expect(response.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_1');
const credential = await strategy.getCredential();
expect(credential).toEqual('MY_TOKEN_1');
});
it('should re-use token before expiry', async () => {
@@ -66,11 +60,11 @@ describe('AzureIdentityStrategy tests', () => {
new StaticTokenCredential(20 * 60 * 1000),
);
const response = await strategy.decorateClusterDetailsWithAuth(cd);
expect(response.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_1');
const credential = await strategy.getCredential();
expect(credential).toEqual('MY_TOKEN_1');
const response2 = await strategy.decorateClusterDetailsWithAuth(cd);
expect(response2.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_1');
const credential2 = await strategy.getCredential();
expect(credential2).toEqual('MY_TOKEN_1');
});
it('should issue new token 15 minutes befory expiry', async () => {
@@ -81,13 +75,13 @@ describe('AzureIdentityStrategy tests', () => {
new StaticTokenCredential(16 * 60 * 1000), // token expires in 16min
);
const response = await strategy.decorateClusterDetailsWithAuth(cd);
expect(response.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_1');
const credential = await strategy.getCredential();
expect(credential).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');
const credential2 = await strategy.getCredential();
expect(credential2).toEqual('MY_TOKEN_2');
});
it('should re-use existing token if there is afailure', async () => {
@@ -98,21 +92,21 @@ describe('AzureIdentityStrategy tests', () => {
new StaticTokenCredential(16 * 60 * 1000), // new tokens expires in 16min
);
const response = await strategy.decorateClusterDetailsWithAuth(cd);
expect(response.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_1');
const credential = await strategy.getCredential();
expect(credential).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');
const credential2 = await strategy.getCredential();
expect(credential2).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');
const credential3 = await strategy.getCredential();
expect(credential3).toEqual('MY_TOKEN_2');
const response4 = await strategy.decorateClusterDetailsWithAuth(cd);
expect(response4.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_4');
const credential4 = await strategy.getCredential();
expect(credential4).toEqual('MY_TOKEN_4');
});
it('should throw if existing token expired and failed to fetch a new one', async () => {
@@ -123,16 +117,16 @@ describe('AzureIdentityStrategy tests', () => {
new StaticTokenCredential(16 * 60 * 1000), // new tokens expires in 16min
);
const response = await strategy.decorateClusterDetailsWithAuth(cd);
expect(response.authMetadata.serviceAccountToken).toEqual('MY_TOKEN_1');
const credential = await strategy.getCredential();
expect(credential).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');
const credential2 = await strategy.getCredential();
expect(credential2).toEqual('MY_TOKEN_2');
jest.setSystemTime(Date.now() + 17 * 60 * 1000); // advance time by 17min
await expect(strategy.decorateClusterDetailsWithAuth(cd)).rejects.toThrow();
await expect(strategy.getCredential()).rejects.toThrow();
});
});
@@ -15,8 +15,7 @@
*/
import { Logger } from 'winston';
import { AuthenticationStrategy } from './types';
import { AuthMetadata, ClusterDetails } from '../types/types';
import { AuthenticationStrategy, KubernetesCredential } from './types';
import {
AccessToken,
DefaultAzureCredential,
@@ -38,24 +37,7 @@ export class AzureIdentityStrategy implements AuthenticationStrategy {
private readonly tokenCredential: TokenCredential = new DefaultAzureCredential(),
) {}
public async decorateClusterDetailsWithAuth(
clusterDetails: ClusterDetails,
): Promise<ClusterDetails> {
const clusterDetailsWithAuthToken: ClusterDetails = Object.assign(
{},
clusterDetails,
);
clusterDetailsWithAuthToken.authMetadata = {
serviceAccountToken: await this.getToken(),
...clusterDetailsWithAuthToken.authMetadata,
};
return clusterDetailsWithAuthToken;
}
public validate(_: AuthMetadata) {}
private async getToken(): Promise<string> {
public async getCredential(): Promise<KubernetesCredential> {
if (!this.tokenRequiresRefresh()) {
return this.accessToken.token;
}
@@ -67,6 +49,8 @@ export class AzureIdentityStrategy implements AuthenticationStrategy {
return this.newTokenPromise;
}
public validate() {}
private async fetchNewToken(): Promise<string> {
try {
this.logger.info('Fetching new Azure token for AKS');
@@ -22,14 +22,14 @@ import { DispatchStrategy } from './DispatchStrategy';
import { ClusterDetails } from '../types';
import { AuthenticationStrategy } from './types';
describe('decorateClusterDetailsWithAuth', () => {
describe('getCredential', () => {
let strategy: DispatchStrategy;
let mockStrategy: jest.Mocked<AuthenticationStrategy>;
const authObject: KubernetesRequestAuth = {};
beforeEach(() => {
mockStrategy = {
decorateClusterDetailsWithAuth: jest.fn(),
getCredential: jest.fn(),
validate: jest.fn(),
};
strategy = new DispatchStrategy({
@@ -37,43 +37,29 @@ describe('decorateClusterDetailsWithAuth', () => {
});
});
it('can decorate cluster details if the auth provider is in the strategy map', async () => {
const expectedClusterDetails: ClusterDetails = {
it('gets credential if specified auth provider is in the strategy map', async () => {
const clusterDetails: ClusterDetails = {
url: 'notanything.com',
name: 'randomName',
authMetadata: {
[ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google',
serviceAccountToken: 'added by mock strategy',
},
authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google' },
};
mockStrategy.getCredential.mockResolvedValue('added by mock strategy');
mockStrategy.decorateClusterDetailsWithAuth.mockResolvedValue(
expectedClusterDetails,
);
const returnedValue = await strategy.decorateClusterDetailsWithAuth(
{
name: 'googleCluster',
url: 'anything.com',
authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google' },
},
const returnedValue = await strategy.getCredential(
clusterDetails,
authObject,
);
expect(mockStrategy.decorateClusterDetailsWithAuth).toHaveBeenCalledWith(
{
name: 'googleCluster',
url: 'anything.com',
authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google' },
},
expect(mockStrategy.getCredential).toHaveBeenCalledWith(
clusterDetails,
authObject,
);
expect(returnedValue).toBe(expectedClusterDetails);
expect(returnedValue).toBe('added by mock strategy');
});
it('throws an error when asked for a strategy for an unsupported auth type', () => {
expect(() =>
strategy.decorateClusterDetailsWithAuth(
strategy.getCredential(
{
name: 'test-cluster',
url: 'anything.com',
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { AuthenticationStrategy } from './types';
import { AuthenticationStrategy, KubernetesCredential } from './types';
import { AuthMetadata, ClusterDetails } from '../types';
import {
ANNOTATION_KUBERNETES_AUTH_PROVIDER,
@@ -41,17 +41,14 @@ export class DispatchStrategy implements AuthenticationStrategy {
this.strategyMap = options.authStrategyMap;
}
public decorateClusterDetailsWithAuth(
public getCredential(
clusterDetails: ClusterDetails,
auth: KubernetesRequestAuth,
) {
): Promise<KubernetesCredential> {
const authProvider =
clusterDetails.authMetadata[ANNOTATION_KUBERNETES_AUTH_PROVIDER];
if (this.strategyMap[authProvider]) {
return this.strategyMap[authProvider].decorateClusterDetailsWithAuth(
clusterDetails,
auth,
);
return this.strategyMap[authProvider].getCredential(clusterDetails, auth);
}
throw new Error(
`authProvider "${authProvider}" has no AuthenticationStrategy associated with it`,
@@ -13,8 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { AuthenticationStrategy } from './types';
import { AuthMetadata, ClusterDetails } from '../types/types';
import { AuthenticationStrategy, KubernetesCredential } from './types';
import * as container from '@google-cloud/container';
/**
@@ -22,28 +21,17 @@ import * as container from '@google-cloud/container';
* @public
*/
export class GoogleServiceAccountStrategy implements AuthenticationStrategy {
public async decorateClusterDetailsWithAuth(
clusterDetails: ClusterDetails,
): Promise<ClusterDetails> {
const clusterDetailsWithAuthToken: ClusterDetails = Object.assign(
{},
clusterDetails,
);
public async getCredential(): Promise<KubernetesCredential> {
const client = new container.v1.ClusterManagerClient();
const accessToken = await client.auth.getAccessToken();
if (accessToken) {
clusterDetailsWithAuthToken.authMetadata = {
serviceAccountToken: accessToken,
...clusterDetailsWithAuthToken.authMetadata,
};
} else {
if (!accessToken) {
throw new Error(
'Unable to obtain access token for the current Google Application Default Credentials',
);
}
return clusterDetailsWithAuthToken;
return accessToken;
}
public validate(_: AuthMetadata) {}
public validate() {}
}
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { AuthenticationStrategy } from './types';
import { AuthenticationStrategy, KubernetesCredential } from './types';
import { AuthMetadata, ClusterDetails } from '../types/types';
import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common';
@@ -23,27 +23,17 @@ import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common';
* @public
*/
export class GoogleStrategy implements AuthenticationStrategy {
public async decorateClusterDetailsWithAuth(
clusterDetails: ClusterDetails,
authConfig: KubernetesRequestAuth,
): Promise<ClusterDetails> {
const clusterDetailsWithAuthToken: ClusterDetails = Object.assign(
{},
clusterDetails,
);
const authToken: string | undefined = authConfig.google;
if (authToken) {
clusterDetailsWithAuthToken.authMetadata = {
serviceAccountToken: authToken,
...clusterDetailsWithAuthToken.authMetadata,
};
} else {
public async getCredential(
_: ClusterDetails,
requestAuth: KubernetesRequestAuth,
): Promise<KubernetesCredential> {
const authToken = requestAuth.google;
if (!authToken) {
throw new Error(
'Google token not found under auth.google in request body',
);
}
return clusterDetailsWithAuthToken;
return authToken;
}
public validate(_: AuthMetadata) {}
}
@@ -14,19 +14,19 @@
* limitations under the License.
*/
import { AuthenticationStrategy } from './types';
import { AuthMetadata, ClusterDetails } from '../types/types';
import { AuthenticationStrategy, KubernetesCredential } from './types';
import { ClusterDetails } from '../types/types';
/**
*
* @public
*/
export class NoopStrategy implements AuthenticationStrategy {
public async decorateClusterDetailsWithAuth(
public async getCredential(
clusterDetails: ClusterDetails,
): Promise<ClusterDetails> {
return clusterDetails;
): Promise<KubernetesCredential> {
return clusterDetails.authMetadata.serviceAccountToken;
}
public validate(_: AuthMetadata) {}
public validate() {}
}
@@ -15,7 +15,6 @@
*/
import { ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER } from '@backstage/plugin-kubernetes-common';
import { AuthMetadata } from '../types/types';
import { OidcStrategy } from './OidcStrategy';
describe('OidcStrategy', () => {
@@ -24,14 +23,13 @@ describe('OidcStrategy', () => {
strategy = new OidcStrategy();
});
describe('decorateClusterDetailsWithAuth', () => {
it('returns cluster details with auth token', async () => {
const details = await strategy.decorateClusterDetailsWithAuth(
describe('getCredential', () => {
it('returns auth token', async () => {
const credential = await strategy.getCredential(
{
name: 'test',
url: '',
authMetadata: {
authProvider: 'oidc',
[ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: 'okta',
},
},
@@ -40,16 +38,16 @@ describe('OidcStrategy', () => {
},
);
expect(details.authMetadata.serviceAccountToken).toBe('fakeToken');
expect(credential).toBe('fakeToken');
});
it('fails when oidcTokenProvider is not configured', async () => {
it('fails when token provider is not configured', async () => {
await expect(
strategy.decorateClusterDetailsWithAuth(
strategy.getCredential(
{
name: 'test',
url: '',
authMetadata: { authProvider: 'oidc' },
authMetadata: {},
},
{},
),
@@ -60,12 +58,11 @@ describe('OidcStrategy', () => {
it('fails when token is not included in request body', async () => {
await expect(
strategy.decorateClusterDetailsWithAuth(
strategy.getCredential(
{
name: 'test',
url: '',
authMetadata: {
authProvider: 'oidc',
[ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: 'okta',
},
},
@@ -77,10 +74,7 @@ describe('OidcStrategy', () => {
describe('validate', () => {
it('fails when token provider is not specified', () => {
const authMetadata: AuthMetadata = {
authProvider: 'oidc',
};
expect(() => strategy.validate(authMetadata)).toThrow(
expect(() => strategy.validate({})).toThrow(
`Must specify a token provider for 'oidc' strategy`,
);
});
@@ -17,7 +17,7 @@ import {
ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER,
KubernetesRequestAuth,
} from '@backstage/plugin-kubernetes-common';
import { AuthenticationStrategy } from './types';
import { AuthenticationStrategy, KubernetesCredential } from './types';
import { AuthMetadata, ClusterDetails } from '../types/types';
/**
@@ -25,15 +25,10 @@ import { AuthMetadata, ClusterDetails } from '../types/types';
* @public
*/
export class OidcStrategy implements AuthenticationStrategy {
public async decorateClusterDetailsWithAuth(
public async getCredential(
clusterDetails: ClusterDetails,
authConfig: KubernetesRequestAuth,
): Promise<ClusterDetails> {
const clusterDetailsWithAuthToken: ClusterDetails = Object.assign(
{},
clusterDetails,
);
): Promise<KubernetesCredential> {
const oidcTokenProvider =
clusterDetails.authMetadata[ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER];
@@ -43,19 +38,14 @@ export class OidcStrategy implements AuthenticationStrategy {
);
}
const authToken: string | undefined = authConfig.oidc?.[oidcTokenProvider];
const authToken = authConfig.oidc?.[oidcTokenProvider];
if (authToken) {
clusterDetailsWithAuthToken.authMetadata = {
serviceAccountToken: authToken,
...clusterDetailsWithAuthToken.authMetadata,
};
} else {
if (!authToken) {
throw new Error(
`Auth token not found under oidc.${oidcTokenProvider} in request body`,
);
}
return clusterDetailsWithAuthToken;
return authToken;
}
public validate(authMetadata: AuthMetadata) {
+8 -2
View File
@@ -17,14 +17,20 @@
import { AuthMetadata, ClusterDetails } from '../types/types';
import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common';
/**
* Authentication data used to make a request to Kubernetes
* @public
*/
export type KubernetesCredential = string | undefined;
/**
*
* @public
*/
export interface AuthenticationStrategy {
decorateClusterDetailsWithAuth(
getCredential(
clusterDetails: ClusterDetails,
authConfig: KubernetesRequestAuth,
): Promise<ClusterDetails>;
): Promise<KubernetesCredential>;
validate(authMetadata: AuthMetadata): void;
}
@@ -30,7 +30,7 @@ describe('ConfigClusterLocator', () => {
beforeEach(() => {
authStrategy = {
decorateClusterDetailsWithAuth: jest.fn(),
getCredential: jest.fn(),
validate: jest.fn(),
};
});
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import { ANNOTATION_KUBERNETES_AUTH_PROVIDER } from '@backstage/plugin-kubernetes-common';
import '@backstage/backend-common';
import { ConfigReader, Config } from '@backstage/config';
import { GkeClusterLocator } from './GkeClusterLocator';
@@ -106,7 +107,7 @@ describe('GkeClusterLocator', () => {
{
name: 'some-cluster',
url: 'https://1.2.3.4',
authMetadata: { authProvider: 'google' },
authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google' },
skipTLSVerify: false,
skipMetricsLookup: true,
},
@@ -143,7 +144,7 @@ describe('GkeClusterLocator', () => {
{
name: 'some-cluster',
url: 'https://1.2.3.4',
authMetadata: { authProvider: 'google' },
authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google' },
skipTLSVerify: false,
skipMetricsLookup: false,
},
@@ -185,14 +186,14 @@ describe('GkeClusterLocator', () => {
{
name: 'some-cluster',
url: 'https://1.2.3.4',
authMetadata: { authProvider: 'google' },
authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google' },
skipTLSVerify: false,
skipMetricsLookup: false,
},
{
name: 'some-other-cluster',
url: 'https://6.7.8.9',
authMetadata: { authProvider: 'google' },
authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google' },
skipTLSVerify: false,
skipMetricsLookup: false,
},
@@ -240,14 +241,14 @@ describe('GkeClusterLocator', () => {
{
name: 'some-cluster',
url: 'https://1.2.3.4',
authMetadata: { authProvider: 'google' },
authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google' },
skipTLSVerify: false,
skipMetricsLookup: false,
},
{
name: 'some-other-cluster',
url: 'https://6.7.8.9',
authMetadata: { authProvider: 'google' },
authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google' },
skipTLSVerify: false,
skipMetricsLookup: false,
},
@@ -301,7 +302,7 @@ describe('GkeClusterLocator', () => {
{
name: 'some-cluster',
url: 'https://1.2.3.4',
authMetadata: { authProvider: 'google' },
authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google' },
skipTLSVerify: false,
skipMetricsLookup: false,
},
@@ -365,7 +366,7 @@ describe('GkeClusterLocator', () => {
{
name: 'some-cluster',
url: 'https://1.2.3.4',
authMetadata: { authProvider: 'google' },
authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google' },
skipTLSVerify: false,
skipMetricsLookup: true,
dashboardApp: 'gke',
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import { ANNOTATION_KUBERNETES_AUTH_PROVIDER } from '@backstage/plugin-kubernetes-common';
import { Config } from '@backstage/config';
import { ForwardedError } from '@backstage/errors';
import * as container from '@google-cloud/container';
@@ -120,7 +121,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 ?? ''}`,
authMetadata: { authProvider: 'google' },
authMetadata: { [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'google' },
skipTLSVerify,
skipMetricsLookup,
...(exposeDashboard
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import { ANNOTATION_KUBERNETES_AUTH_PROVIDER } from '@backstage/plugin-kubernetes-common';
import { ClusterDetails, KubernetesClustersSupplier } from '../types/types';
export class LocalKubectlProxyClusterLocator
@@ -26,7 +27,9 @@ export class LocalKubectlProxyClusterLocator
{
name: 'local',
url: 'http:/localhost:8001',
authMetadata: { authProvider: 'localKubectlProxy' },
authMetadata: {
[ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'localKubectlProxy',
},
skipMetricsLookup: true,
},
];
@@ -51,7 +51,7 @@ describe('getCombinedClusterSupplier', () => {
'ctx',
);
const mockStrategy: jest.Mocked<AuthenticationStrategy> = {
decorateClusterDetailsWithAuth: jest.fn(),
getCredential: jest.fn(),
validate: jest.fn(),
};
@@ -16,7 +16,7 @@
import '@backstage/backend-common';
import { Entity } from '@backstage/catalog-model';
import { AuthMetadata, ServiceLocatorRequestContext } from '../types/types';
import { ServiceLocatorRequestContext } from '../types/types';
import { MultiTenantServiceLocator } from './MultiTenantServiceLocator';
describe('MultiTenantConfigClusterLocator', () => {
@@ -40,10 +40,7 @@ describe('MultiTenantConfigClusterLocator', () => {
{
name: 'cluster1',
url: 'http://localhost:8080',
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: '12345',
},
authMetadata: {},
},
];
},
@@ -59,10 +56,7 @@ describe('MultiTenantConfigClusterLocator', () => {
{
name: 'cluster1',
url: 'http://localhost:8080',
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: '12345',
},
authMetadata: {},
},
],
});
@@ -75,15 +69,12 @@ describe('MultiTenantConfigClusterLocator', () => {
{
name: 'cluster1',
url: 'http://localhost:8080',
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
} as AuthMetadata,
authMetadata: {},
},
{
name: 'cluster2',
url: 'http://localhost:8081',
authMetadata: { authProvider: 'google' } as AuthMetadata,
authMetadata: {},
},
];
},
@@ -99,15 +90,12 @@ describe('MultiTenantConfigClusterLocator', () => {
{
name: 'cluster1',
url: 'http://localhost:8080',
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
},
authMetadata: {},
},
{
name: 'cluster2',
url: 'http://localhost:8081',
authMetadata: { authProvider: 'google' },
authMetadata: {},
},
],
});
@@ -32,6 +32,7 @@ import {
KubernetesServiceLocator,
ObjectFetchParams,
} from '../types/types';
import { KubernetesCredential } from '../auth/types';
import { KubernetesBuilder } from './KubernetesBuilder';
import { KubernetesFanOutHandler } from './KubernetesFanOutHandler';
import { CatalogApi } from '@backstage/catalog-client';
@@ -251,6 +252,7 @@ describe('KubernetesBuilder', () => {
const fetcher: KubernetesFetcher = {
fetchPodMetricsByNamespaces(
_clusterDetails: ClusterDetails,
_credential: KubernetesCredential,
_namespaces: Set<string>,
): Promise<FetchResponseWrapper> {
return Promise.resolve({ errors: [], responses: [] });
@@ -22,6 +22,7 @@ import {
KubernetesServiceLocator,
ServiceLocatorRequestContext,
} from '../types/types';
import { KubernetesCredential } from '../auth/types';
import { KubernetesFanOutHandler } from './KubernetesFanOutHandler';
import { KubernetesClientBasedFetcher } from './KubernetesFetcher';
import { rest } from 'msw';
@@ -77,7 +78,7 @@ describe('KubernetesFanOutHandler', () => {
const cluster1 = {
name: 'test-cluster',
url: '',
authMetadata: { authProvider: 'serviceAccount' },
authMetadata: {},
customResources: [
{
group: 'some-other-crd.example.com',
@@ -90,7 +91,7 @@ describe('KubernetesFanOutHandler', () => {
const cluster2 = {
name: 'cluster-two',
url: '',
authMetadata: { authProvider: 'serviceAccount' },
authMetadata: {},
customResources: [
{
group: 'crd-two.example.com',
@@ -185,9 +186,7 @@ describe('KubernetesFanOutHandler', () => {
},
customResources: customResources,
authStrategy: {
decorateClusterDetailsWithAuth: async (clusterDetails, _) => {
return clusterDetails;
},
getCredential: jest.fn().mockResolvedValue(undefined),
validate: jest.fn(),
},
config,
@@ -316,7 +315,11 @@ describe('KubernetesFanOutHandler', () => {
);
fetchPodMetricsByNamespaces.mockImplementation(
(_clusterDetails: ClusterDetails, namespaces: Set<string>) =>
(
_clusterDetails: ClusterDetails,
_: KubernetesCredential,
namespaces: Set<string>,
) =>
Promise.resolve({
errors: [],
responses: Array.from(namespaces).map(() => {
@@ -352,7 +355,7 @@ describe('KubernetesFanOutHandler', () => {
{
name: 'test-cluster',
url: '',
authMetadata: { authProvider: 'serviceAccount' },
authMetadata: {},
},
],
}),
@@ -370,6 +373,7 @@ describe('KubernetesFanOutHandler', () => {
expect(fetchPodMetricsByNamespaces).toHaveBeenCalledTimes(1);
expect(fetchPodMetricsByNamespaces).toHaveBeenCalledWith(
expect.anything(),
undefined,
new Set(['ns-test-component-test-cluster']),
expect.anything(),
);
@@ -467,7 +471,7 @@ describe('KubernetesFanOutHandler', () => {
{
name: 'test-cluster',
url: '',
authMetadata: { authProvider: 'serviceAccount' },
authMetadata: {},
},
cluster2,
],
@@ -522,7 +526,7 @@ describe('KubernetesFanOutHandler', () => {
{
name: 'profile-cluster-1',
url: '',
authMetadata: { authProvider: 'serviceAccount' },
authMetadata: {},
customResourceProfile: 'build',
customResources: [
{
@@ -571,7 +575,7 @@ describe('KubernetesFanOutHandler', () => {
{
name: 'profile-cluster-1',
url: '',
authMetadata: { authProvider: 'serviceAccount' },
authMetadata: {},
},
],
}),
@@ -612,7 +616,7 @@ describe('KubernetesFanOutHandler', () => {
{
name: 'test-cluster',
url: '',
authMetadata: { authProvider: 'serviceAccount' },
authMetadata: {},
},
],
}),
@@ -661,6 +665,7 @@ describe('KubernetesFanOutHandler', () => {
expect(fetchPodMetricsByNamespaces).toHaveBeenCalledTimes(1);
expect(fetchPodMetricsByNamespaces).toHaveBeenCalledWith(
expect.anything(),
undefined,
new Set(['ns-a', 'ns-b']),
expect.anything(),
);
@@ -710,7 +715,7 @@ describe('KubernetesFanOutHandler', () => {
{
name: 'test-cluster',
url: '',
authMetadata: { authProvider: 'serviceAccount' },
authMetadata: {},
},
],
}),
@@ -750,13 +755,13 @@ describe('KubernetesFanOutHandler', () => {
{
name: 'test-cluster',
url: '',
authMetadata: { authProvider: 'serviceAccount' },
authMetadata: {},
dashboardUrl: 'https://k8s.foo.coom',
},
{
name: 'other-cluster',
url: '',
authMetadata: { authProvider: 'google' },
authMetadata: {},
},
],
}),
@@ -803,17 +808,17 @@ describe('KubernetesFanOutHandler', () => {
{
name: 'test-cluster',
url: '',
authMetadata: { authProvider: 'serviceAccount' },
authMetadata: {},
},
{
name: 'other-cluster',
url: '',
authMetadata: { authProvider: 'google' },
authMetadata: {},
},
{
name: 'empty-cluster',
url: '',
authMetadata: { authProvider: 'google' },
authMetadata: {},
},
],
}),
@@ -859,22 +864,22 @@ describe('KubernetesFanOutHandler', () => {
{
name: 'test-cluster',
url: '',
authMetadata: { authProvider: 'serviceAccount' },
authMetadata: {},
},
{
name: 'other-cluster',
url: '',
authMetadata: { authProvider: 'google' },
authMetadata: {},
},
{
name: 'empty-cluster',
url: '',
authMetadata: { authProvider: 'google' },
authMetadata: {},
},
{
name: 'error-cluster',
url: '',
authMetadata: { authProvider: 'google' },
authMetadata: {},
},
],
}),
@@ -941,13 +946,13 @@ describe('KubernetesFanOutHandler', () => {
{
name: 'test-cluster',
url: '',
authMetadata: { authProvider: 'serviceAccount' },
authMetadata: {},
dashboardUrl: 'https://k8s.foo.coom',
},
{
name: 'other-cluster',
url: '',
authMetadata: { authProvider: 'google' },
authMetadata: {},
},
],
}),
@@ -957,7 +962,11 @@ describe('KubernetesFanOutHandler', () => {
// and an error for the second call.
fetchPodMetricsByNamespaces
.mockImplementationOnce(
(_clusterDetails: ClusterDetails, namespaces: Set<string>) =>
(
_clusterDetails: ClusterDetails,
_: KubernetesCredential,
namespaces: Set<string>,
) =>
Promise.resolve({
errors: [],
responses: Array.from(namespaces).map(() => {
@@ -1041,7 +1050,7 @@ describe('KubernetesFanOutHandler', () => {
{
name: 'test-cluster',
url: '',
authMetadata: { authProvider: 'serviceAccount' },
authMetadata: {},
skipMetricsLookup: true,
},
],
@@ -1090,19 +1099,13 @@ describe('KubernetesFanOutHandler', () => {
{
name: 'works',
url: 'https://works',
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
},
authMetadata: {},
skipMetricsLookup: true,
},
{
name: 'fails',
url: 'https://fails',
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
},
authMetadata: {},
skipMetricsLookup: true,
},
],
@@ -1129,9 +1132,7 @@ describe('KubernetesFanOutHandler', () => {
},
],
authStrategy: {
decorateClusterDetailsWithAuth: async (clusterDetails, _) => {
return clusterDetails;
},
getCredential: jest.fn().mockResolvedValue('token'),
validate: jest.fn(),
},
config,
@@ -1229,7 +1230,7 @@ describe('KubernetesFanOutHandler', () => {
{
name: 'test-cluster',
url: '',
authMetadata: { authProvider: 'serviceAccount' },
authMetadata: {},
},
cluster2,
],
@@ -1303,7 +1304,7 @@ describe('KubernetesFanOutHandler', () => {
{
name: 'profile-cluster-1',
url: '',
authMetadata: { authProvider: 'serviceAccount' },
authMetadata: {},
customResourceProfile: 'build',
},
],
@@ -27,9 +27,8 @@ import {
CustomResource,
CustomResourcesByEntity,
KubernetesObjectsByEntity,
ServiceLocatorRequestContext,
} from '../types/types';
import { AuthenticationStrategy } from '../auth/types';
import { AuthenticationStrategy, KubernetesCredential } from '../auth/types';
import {
ClientContainerStatus,
ClientCurrentResourceUsage,
@@ -48,14 +47,6 @@ import {
PodStatus,
} from '@kubernetes/client-node';
const isRejected = (
input: PromiseSettledResult<unknown>,
): input is PromiseRejectedResult => input.status === 'rejected';
const isFulfilled = <T>(
input: PromiseSettledResult<T>,
): input is PromiseFulfilledResult<T> => input.status === 'fulfilled';
/**
*
* @public
@@ -245,14 +236,13 @@ export class KubernetesFanOutHandler {
entity.metadata?.annotations?.['backstage.io/kubernetes-id'] ||
entity.metadata?.name;
const clusterDetailsDecoratedForAuth: ClusterDetails[] =
await this.decorateClusterDetailsWithAuth(entity, auth, {
objectTypesToFetch: objectTypesToFetch,
customResources: customResources ?? [],
});
const { clusters } = await this.serviceLocator.getClustersByEntity(entity, {
objectTypesToFetch: objectTypesToFetch,
customResources: customResources ?? [],
});
this.logger.info(
`entity.metadata.name=${entityName} clusterDetails=[${clusterDetailsDecoratedForAuth
`entity.metadata.name=${entityName} clusterDetails=[${clusters
.map(c => c.name)
.join(', ')}]`,
);
@@ -266,16 +256,21 @@ export class KubernetesFanOutHandler {
entity.metadata?.annotations?.['backstage.io/kubernetes-namespace'];
return Promise.all(
clusterDetailsDecoratedForAuth.map(clusterDetailsItem => {
clusters.map(async clusterDetails => {
const credential = await this.authStrategy.getCredential(
clusterDetails,
auth,
);
return this.fetcher
.fetchObjectsForService({
serviceId: entityName,
clusterDetails: clusterDetailsItem,
objectTypesToFetch: objectTypesToFetch,
clusterDetails,
credential,
objectTypesToFetch,
labelSelector,
customResources: (
customResources ||
clusterDetailsItem.customResources ||
clusterDetails.customResources ||
this.customResources
).map(c => ({
...c,
@@ -284,7 +279,12 @@ export class KubernetesFanOutHandler {
namespace,
})
.then(result =>
this.getMetricsForPods(clusterDetailsItem, labelSelector, result),
this.getMetricsForPods(
clusterDetails,
credential,
labelSelector,
result,
),
)
.catch(
(e): Promise<responseWithMetrics> =>
@@ -300,33 +300,11 @@ export class KubernetesFanOutHandler {
])
: Promise.reject(e),
)
.then(r => this.toClusterObjects(clusterDetailsItem, r));
.then(r => this.toClusterObjects(clusterDetails, r));
}),
).then(this.toObjectsByEntityResponse);
}
private async decorateClusterDetailsWithAuth(
entity: Entity,
auth: KubernetesRequestAuth,
requestContext: ServiceLocatorRequestContext,
) {
const clusterDetails: ClusterDetails[] = (
await this.serviceLocator.getClustersByEntity(entity, requestContext)
).clusters;
// Execute all of these async actions simultaneously/without blocking sequentially as no common object is modified by them
const promiseResults = await Promise.allSettled(
clusterDetails.map(cd => {
return this.authStrategy.decorateClusterDetailsWithAuth(cd, auth);
}),
);
promiseResults.filter(isRejected).map(item => {
this.logger.info(`Failed to decorate cluster details: ${item.reason}`);
});
return promiseResults.filter(isFulfilled).map(item => item.value);
}
toObjectsByEntityResponse(
clusterObjects: ClusterObjects[],
): ObjectsByEntityResponse {
@@ -367,6 +345,7 @@ export class KubernetesFanOutHandler {
async getMetricsForPods(
clusterDetails: ClusterDetails,
credential: KubernetesCredential,
labelSelector: string,
result: FetchResponseWrapper,
): Promise<responseWithMetrics> {
@@ -387,6 +366,7 @@ export class KubernetesFanOutHandler {
const podMetrics = await this.fetcher.fetchPodMetricsByNamespaces(
clusterDetails,
credential,
namespaces,
labelSelector,
);
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import { ANNOTATION_KUBERNETES_AUTH_PROVIDER } from '@backstage/plugin-kubernetes-common';
import { getVoidLogger } from '@backstage/backend-common';
import { KubernetesClientBasedFetcher } from './KubernetesFetcher';
import { ObjectToFetch } from '../types/types';
@@ -138,11 +139,9 @@ describe('KubernetesFetcher', () => {
clusterDetails: {
name: 'cluster1',
url: 'http://localhost:9999',
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
},
authMetadata: {},
},
credential: 'token',
objectTypesToFetch: OBJECTS_TO_FETCH,
labelSelector: '',
customResources: [],
@@ -201,11 +200,9 @@ describe('KubernetesFetcher', () => {
clusterDetails: {
name: 'cluster1',
url: 'http://localhost:9999/k8s/clusters/1234',
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
},
authMetadata: {},
},
credential: 'token',
objectTypesToFetch: OBJECTS_TO_FETCH,
labelSelector: '',
customResources: [],
@@ -257,8 +254,11 @@ describe('KubernetesFetcher', () => {
clusterDetails: {
name: 'cluster1',
url: 'http://localhost:9999/k8s/clusters/1234',
authMetadata: { authProvider: 'localKubectlProxy' },
authMetadata: {
[ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'localKubectlProxy',
},
},
credential: undefined,
objectTypesToFetch: new Set([
{
group: '',
@@ -313,11 +313,9 @@ describe('KubernetesFetcher', () => {
clusterDetails: {
name: 'cluster1',
url: 'http://localhost:9999',
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
},
authMetadata: {},
},
credential: 'token',
objectTypesToFetch: OBJECTS_TO_FETCH,
labelSelector: '',
customResources: [],
@@ -389,11 +387,9 @@ describe('KubernetesFetcher', () => {
clusterDetails: {
name: 'cluster1',
url: 'http://localhost:9999',
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
},
authMetadata: {},
},
credential: 'token',
objectTypesToFetch: OBJECTS_TO_FETCH,
labelSelector: '',
customResources: [
@@ -483,11 +479,9 @@ describe('KubernetesFetcher', () => {
clusterDetails: {
name: 'cluster1',
url: 'http://localhost:9999',
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
},
authMetadata: {},
},
credential: 'token',
objectTypesToFetch: OBJECTS_TO_FETCH,
labelSelector: '',
customResources: [],
@@ -573,11 +567,9 @@ describe('KubernetesFetcher', () => {
clusterDetails: {
name: 'cluster1',
url: 'http://badurl.does.not.exist',
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
},
authMetadata: {},
},
credential: 'token',
objectTypesToFetch: OBJECTS_TO_FETCH,
labelSelector: '',
customResources: [],
@@ -612,11 +604,9 @@ describe('KubernetesFetcher', () => {
clusterDetails: {
name: 'cluster1',
url: 'http://localhost:9999',
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
},
authMetadata: {},
},
credential: 'token',
objectTypesToFetch: OBJECTS_TO_FETCH,
labelSelector: 'service-label=value',
customResources: [],
@@ -681,12 +671,10 @@ describe('KubernetesFetcher', () => {
clusterDetails: {
name: 'cluster1',
url: 'https://localhost:9999',
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
},
authMetadata: {},
caData: 'MOCKCA',
},
credential: 'token',
objectTypesToFetch: new Set<ObjectToFetch>([
{
group: '',
@@ -720,11 +708,9 @@ describe('KubernetesFetcher', () => {
clusterDetails: {
name: 'cluster1',
url: 'https://localhost:9999',
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
},
authMetadata: {},
},
credential: 'token',
objectTypesToFetch: new Set<ObjectToFetch>([
{
group: '',
@@ -765,12 +751,10 @@ describe('KubernetesFetcher', () => {
clusterDetails: {
name: 'cluster1',
url: 'https://localhost:9999',
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
},
authMetadata: {},
caFile: '/path/to/ca.crt',
},
credential: 'token',
objectTypesToFetch: new Set<ObjectToFetch>([
{
group: '',
@@ -805,12 +789,10 @@ describe('KubernetesFetcher', () => {
clusterDetails: {
name: 'cluster1',
url: 'https://localhost:9999',
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
},
authMetadata: {},
skipTLSVerify: true,
},
credential: 'token',
objectTypesToFetch: new Set<ObjectToFetch>([
{
group: '',
@@ -858,11 +840,9 @@ describe('KubernetesFetcher', () => {
clusterDetails: {
name: 'cluster1',
url: 'http://localhost:9999',
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
},
authMetadata: {},
},
credential: 'token',
objectTypesToFetch: OBJECTS_TO_FETCH,
labelSelector: '',
namespace: 'some-namespace',
@@ -898,14 +878,15 @@ describe('KubernetesFetcher', () => {
});
});
describe('Backstage not running on k8s', () => {
it('fails if cluster details has no token', () => {
it('fails if no credential is provided', () => {
const result = sut.fetchObjectsForService({
serviceId: 'some-service',
clusterDetails: {
name: 'unauthenticated-cluster',
url: 'http://ignored',
authMetadata: { authProvider: 'serviceAccount' },
authMetadata: {},
},
credential: undefined, // no token
objectTypesToFetch: OBJECTS_TO_FETCH,
labelSelector: '',
customResources: [],
@@ -947,8 +928,9 @@ describe('KubernetesFetcher', () => {
clusterDetails: {
name: 'overridden-to-in-cluster',
url: 'http://ignored',
authMetadata: { authProvider: 'serviceAccount' },
authMetadata: {},
},
credential: undefined, // no token
objectTypesToFetch: new Set<ObjectToFetch>([
{
group: '',
@@ -1043,11 +1025,9 @@ describe('KubernetesFetcher', () => {
{
name: 'cluster1',
url: 'http://localhost:9999',
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
},
authMetadata: {},
},
'token',
new Set(['ns-a']),
);
expect(result).toMatchObject({
@@ -1132,11 +1112,9 @@ describe('KubernetesFetcher', () => {
{
name: 'cluster1',
url: 'http://localhost:9999',
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'token',
},
authMetadata: {},
},
'token',
new Set(['ns-a', 'ns-b']),
);
@@ -32,7 +32,9 @@ import {
KubernetesFetcher,
ObjectFetchParams,
} from '../types/types';
import { KubernetesCredential } from '../auth/types';
import {
ANNOTATION_KUBERNETES_AUTH_PROVIDER,
FetchResponse,
KubernetesFetchError,
KubernetesErrorTypes,
@@ -95,6 +97,7 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher {
.map(({ objectType, group, apiVersion, plural }) =>
this.fetchResource(
params.clusterDetails,
params.credential,
group,
apiVersion,
plural,
@@ -125,6 +128,7 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher {
fetchPodMetricsByNamespaces(
clusterDetails: ClusterDetails,
credential: KubernetesCredential,
namespaces: Set<string>,
labelSelector?: string,
): Promise<FetchResponseWrapper> {
@@ -132,13 +136,22 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher {
const [podMetrics, podList] = await Promise.all([
this.fetchResource(
clusterDetails,
credential,
'metrics.k8s.io',
'v1beta1',
'pods',
ns,
labelSelector,
),
this.fetchResource(clusterDetails, '', 'v1', 'pods', ns, labelSelector),
this.fetchResource(
clusterDetails,
credential,
'',
'v1',
'pods',
ns,
labelSelector,
),
]);
if (podMetrics.ok && podList.ok) {
return topPods(
@@ -183,6 +196,7 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher {
private fetchResource(
clusterDetails: ClusterDetails,
credential: KubernetesCredential,
group: string,
apiVersion: string,
plural: string,
@@ -201,10 +215,11 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher {
let url: URL;
let requestInit: RequestInit;
if (
clusterDetails.authMetadata.serviceAccountToken ||
clusterDetails.authMetadata.authProvider === 'localKubectlProxy'
credential ||
clusterDetails.authMetadata[ANNOTATION_KUBERNETES_AUTH_PROVIDER] ===
'localKubectlProxy'
) {
[url, requestInit] = this.fetchArgsFromClusterDetails(clusterDetails);
[url, requestInit] = this.fetchArgs(clusterDetails, credential);
} else if (fs.pathExistsSync(Config.SERVICEACCOUNT_TOKEN_PATH)) {
[url, requestInit] = this.fetchArgsInCluster();
} else {
@@ -228,15 +243,16 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher {
return fetch(url, requestInit);
}
private fetchArgsFromClusterDetails(
private fetchArgs(
clusterDetails: ClusterDetails,
credential: KubernetesCredential,
): [URL, RequestInit] {
const requestInit: RequestInit = {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${clusterDetails.authMetadata.serviceAccountToken}`,
...(credential && { Authorization: `Bearer ${credential}` }),
},
};
@@ -51,7 +51,7 @@ describe('KubernetesProxy', () => {
const logger = getVoidLogger();
const clusterSupplier: jest.Mocked<KubernetesClustersSupplier> = {
getClusters: jest.fn(),
getClusters: jest.fn<Promise<ClusterDetails[]>, []>(),
};
const permissionApi: jest.Mocked<PermissionEvaluator> = {
@@ -60,7 +60,7 @@ describe('KubernetesProxy', () => {
};
const authStrategy: jest.Mocked<AuthenticationStrategy> = {
decorateClusterDetailsWithAuth: jest.fn(),
getCredential: jest.fn(),
validate: jest.fn(),
};
@@ -146,17 +146,14 @@ describe('KubernetesProxy', () => {
{
name: 'local',
url: 'http:/localhost:8001',
authMetadata: { authProvider: 'localKubectlProxy' },
authMetadata: {},
skipMetricsLookup: true,
} as ClusterDetails,
},
{
name: 'cluster1',
url: 'https://localhost:9999',
authMetadata: {
authProvider: 'googleServiceAccount',
serviceAccountToken: 'tokenA',
},
} as ClusterDetails,
authMetadata: {},
},
]);
const req = buildMockRequest(undefined, 'api');
@@ -172,11 +169,8 @@ describe('KubernetesProxy', () => {
{
name: 'cluster1',
url: 'https://localhost:9999',
authMetadata: {
authProvider: 'googleServiceAccount',
serviceAccountToken: 'tokenA',
},
} as ClusterDetails,
authMetadata: {},
},
]);
const req = buildMockRequest('test', 'api');
@@ -203,15 +197,9 @@ describe('KubernetesProxy', () => {
{
name: 'cluster1',
url: 'https://localhost:9999',
authMetadata: { authProvider: 'serviceAccount' },
authMetadata: {},
},
] as ClusterDetails[]);
authStrategy.decorateClusterDetailsWithAuth.mockResolvedValue({
name: 'cluster1',
url: 'https://localhost:9999',
authMetadata: { authProvider: 'serviceAccount' },
} as ClusterDetails);
]);
worker.use(
rest.get('https://localhost:9999/api', (_: any, res: any, ctx: any) =>
@@ -247,15 +235,9 @@ describe('KubernetesProxy', () => {
{
name: 'cluster1',
url: 'https://localhost:9999',
authMetadata: { authProvider: 'serviceAccount' },
authMetadata: {},
},
] as ClusterDetails[]);
authStrategy.decorateClusterDetailsWithAuth.mockResolvedValue({
name: 'cluster1',
url: 'https://localhost:9999',
authMetadata: { authProvider: 'serviceAccount' },
} as ClusterDetails);
]);
worker.use(
rest.get('https://localhost:9999/api', (_: any, res: any, ctx: any) =>
@@ -291,12 +273,9 @@ describe('KubernetesProxy', () => {
{
name: 'cluster1',
url: 'http://localhost:9999',
authMetadata: { authProvider: '' },
authMetadata: {},
},
]);
authStrategy.decorateClusterDetailsWithAuth.mockImplementation(
async x => x,
);
const requestPromise = setupProxyPromise({
proxyPath: '/mountpath',
@@ -310,7 +289,7 @@ describe('KubernetesProxy', () => {
expect(response.status).toEqual(200);
});
it('should default to using an authStrategy-provided serviceAccountToken as authorization headers to kubeapi when backstage-kubernetes-auth field is not provided', async () => {
it('should default to using a strategy-provided bearer token as authorization headers to kubeapi when backstage-kubernetes-auth field is not provided', async () => {
worker.use(
rest.get(
'https://localhost:9999/api/v1/namespaces',
@@ -342,18 +321,11 @@ describe('KubernetesProxy', () => {
{
name: 'cluster1',
url: 'https://localhost:9999',
authMetadata: { authProvider: 'serviceAccount' },
authMetadata: {},
},
] as ClusterDetails[]);
]);
authStrategy.decorateClusterDetailsWithAuth.mockResolvedValue({
name: 'cluster1',
url: 'https://localhost:9999',
authMetadata: {
authProvider: 'serviceAccount',
serviceAccountToken: 'strategy-provided-token',
},
} as ClusterDetails);
authStrategy.getCredential.mockResolvedValue('strategy-provided-token');
const requestPromise = setupProxyPromise({
proxyPath: '/mountpath',
@@ -393,18 +365,11 @@ describe('KubernetesProxy', () => {
{
name: 'cluster1',
url: 'https://localhost:9999',
authMetadata: { authProvider: 'googleServiceAccount' },
authMetadata: {},
},
] as ClusterDetails[]);
]);
authStrategy.decorateClusterDetailsWithAuth.mockResolvedValue({
name: 'cluster1',
url: 'https://localhost:9999',
authMetadata: {
authProvider: 'googleServiceAccount',
serviceAccountToken: 'my-token',
},
} as ClusterDetails);
authStrategy.getCredential.mockResolvedValue('my-token');
const requestPromise = setupProxyPromise({
proxyPath: '/mountpath',
@@ -449,18 +414,11 @@ describe('KubernetesProxy', () => {
{
name: 'cluster1',
url: 'https://localhost:9999',
authMetadata: { authProvider: 'googleServiceAccount' },
authMetadata: {},
},
] as ClusterDetails[]);
]);
authStrategy.decorateClusterDetailsWithAuth.mockResolvedValue({
name: 'cluster1',
url: 'https://localhost:9999',
authMetadata: {
authProvider: 'googleServiceAccount',
serviceAccountToken: 'tokenA',
},
} as ClusterDetails);
authStrategy.getCredential.mockResolvedValue('tokenA');
const requestPromise = setupProxyPromise({
proxyPath: '/mountpath',
@@ -508,9 +466,9 @@ describe('KubernetesProxy', () => {
{
name: 'cluster1',
url: 'https://localhost:9999',
authMetadata: { authProvider: 'googleServiceAccount' },
authMetadata: {},
},
] as ClusterDetails[]);
]);
const requestPromise = setupProxyPromise({
proxyPath: '/mountpath',
@@ -524,9 +482,7 @@ describe('KubernetesProxy', () => {
const response = await requestPromise;
expect(authStrategy.decorateClusterDetailsWithAuth).toHaveBeenCalledTimes(
0,
);
expect(authStrategy.getCredential).toHaveBeenCalledTimes(0);
expect(response.status).toEqual(200);
expect(response.body).toStrictEqual({
kind: 'NamespaceList',
@@ -602,16 +558,11 @@ describe('KubernetesProxy', () => {
{
name: 'cluster1',
url: 'https://localhost:9999',
authMetadata: {
authProvider: 'google',
serviceAccountToken: 'client-side-token',
},
authMetadata: {},
},
] as ClusterDetails[]);
]);
authStrategy.decorateClusterDetailsWithAuth.mockRejectedValue(
Error('some internal error'),
);
authStrategy.getCredential.mockRejectedValue(Error('some internal error'));
const requestPromise = setupProxyPromise({
proxyPath: '/mountpath',
@@ -648,14 +599,10 @@ describe('KubernetesProxy', () => {
{
name: 'cluster1',
url: 'http://localhost:9999/subpath',
authMetadata: { authProvider: '' },
authMetadata: {},
},
]);
authStrategy.decorateClusterDetailsWithAuth.mockImplementation(
async x => x,
);
const requestPromise = setupProxyPromise({
proxyPath: '/mountpath',
requestPath: '/api/v1/namespaces',
@@ -701,19 +648,11 @@ describe('KubernetesProxy', () => {
{
name: 'cluster1',
url: 'https://localhost:9999',
serviceAccountToken: '',
authProvider: 'serviceAccount',
authMetadata: {},
caFile: resolvePath(__dirname, '__fixtures__/mock-ca.crt'),
},
] as ClusterDetails[]);
authTranslator.decorateClusterDetailsWithAuth.mockResolvedValue({
name: 'cluster1',
url: 'https://localhost:9999',
serviceAccountToken: '',
authProvider: 'serviceAccount',
} as ClusterDetails);
worker.use(
rest.get('https://localhost:9999/api', (_: any, res: any, ctx: any) =>
res(ctx.status(299), ctx.json(apiResponse)),
@@ -793,15 +732,9 @@ describe('KubernetesProxy', () => {
{
name: 'local',
url: `http://localhost:${wsPort}`,
authMetadata: { authProvider: 'serviceAccount' },
authMetadata: {},
},
] as ClusterDetails[]);
authStrategy.decorateClusterDetailsWithAuth.mockResolvedValue({
name: 'local',
url: `http://localhost:${wsPort}`,
authMetadata: { authProvider: 'serviceAccount' },
} as ClusterDetails);
]);
const wsProxyAddress = `ws://127.0.0.1:${proxyPort}${proxyPath}${wsPath}`;
const wsAddress = `ws://localhost:${wsPort}${wsPath}`;
@@ -112,9 +112,9 @@ export class KubernetesProxy {
if (authHeader) {
req.headers.authorization = authHeader;
} else {
const serviceAccountToken = await this.getClusterForRequest(req)
.then(cd => this.authStrategy.decorateClusterDetailsWithAuth(cd, {}))
.then(cd => cd.authMetadata.serviceAccountToken);
const serviceAccountToken = await this.getClusterForRequest(req).then(
cd => this.authStrategy.getCredential(cd, {}),
);
if (serviceAccountToken) {
req.headers.authorization = `Bearer ${serviceAccountToken}`;
}
@@ -26,6 +26,7 @@ import type {
ObjectsByEntityResponse,
} from '@backstage/plugin-kubernetes-common';
import { Config } from '@backstage/config';
import { KubernetesCredential } from '../auth/types';
/**
*
@@ -34,6 +35,7 @@ import { Config } from '@backstage/config';
export interface ObjectFetchParams {
serviceId: string;
clusterDetails: ClusterDetails;
credential: KubernetesCredential;
objectTypesToFetch: Set<ObjectToFetch>;
labelSelector: string;
customResources: CustomResource[];
@@ -51,6 +53,7 @@ export interface KubernetesFetcher {
): Promise<FetchResponseWrapper>;
fetchPodMetricsByNamespaces(
clusterDetails: ClusterDetails,
credential: KubernetesCredential,
namespaces: Set<string>,
labelSelector?: string,
): Promise<FetchResponseWrapper>;