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