Skip to content

Commit

Permalink
Merge pull request #1095 from AmericanAirlines/899-argoOutageBug
Browse files Browse the repository at this point in the history
fix: findArgoApp no longer throws error when some argo instances unavailable
  • Loading branch information
Xantier authored Aug 30, 2023
2 parents 3d097fd + db42b18 commit bdc592a
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 95 deletions.
5 changes: 5 additions & 0 deletions .changeset/shy-ghosts-sleep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@roadiehq/backstage-plugin-argo-cd-backend': patch
---

fix bug that throws error when some argo instances are unavailable
Original file line number Diff line number Diff line change
Expand Up @@ -118,17 +118,20 @@ export class ArgoService implements ArgoServiceApi {
}
const resp = await Promise.all(
this.instanceConfigs.map(async (argoInstance: any) => {
const token =
argoInstance.token || (await this.getArgoToken(argoInstance));
let getArgoAppDataResp: any;
try {
const token =
argoInstance.token || (await this.getArgoToken(argoInstance));
getArgoAppDataResp = await this.getArgoAppData(
argoInstance.url,
argoInstance.name,
token,
options,
);
} catch (error: any) {
this.logger.error(
`Error getting token from Argo Instance ${argoInstance.name}: ${error.message}`,
);
return null;
}

Expand Down Expand Up @@ -450,32 +453,34 @@ export class ArgoService implements ArgoServiceApi {
const argoAppResp: findArgoAppResp[] = await this.findArgoApp({
selector: appSelector,
});

const parallelSyncCalls = argoAppResp.map(
async (argoInstance: any): Promise<SyncResponse[]> => {
try {
const token = await this.getArgoToken(argoInstance);
if (argoAppResp) {
const parallelSyncCalls = argoAppResp.map(
async (argoInstance: any): Promise<SyncResponse[]> => {
try {
const resp = argoInstance.appName.map(
(argoApp: any): Promise<SyncResponse> => {
return this.syncArgoApp({
argoInstance,
argoToken: token,
appName: argoApp,
});
},
);
return await Promise.all(resp);
const token = await this.getArgoToken(argoInstance);
try {
const resp = argoInstance.appName.map(
(argoApp: any): Promise<SyncResponse> => {
return this.syncArgoApp({
argoInstance,
argoToken: token,
appName: argoApp,
});
},
);
return await Promise.all(resp);
} catch (e: any) {
return [{ status: 'Failure', message: e.message }];
}
} catch (e: any) {
return [{ status: 'Failure', message: e.message }];
}
} catch (e: any) {
return [{ status: 'Failure', message: e.message }];
}
},
);
},
);

return await Promise.all(parallelSyncCalls);
return await Promise.all(parallelSyncCalls);
}
return [];
}

async syncArgoApp({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,15 @@ import {
import fetchMock from 'jest-fetch-mock';
import { timer } from './timer.services';
import { mocked } from 'ts-jest/utils';
import { Logger } from 'winston';
import { UpdateArgoProjectAndAppProps } from './types';

fetchMock.enableMocks();
jest.mock('./timer.services');
const loggerMock = {
error: jest.fn(),
info: jest.fn(),
} as unknown as Logger;

const config = ConfigReader.fromConfigs([
{
Expand Down Expand Up @@ -67,19 +72,20 @@ describe('ArgoCD service', () => {
'testusername',
'testpassword',
config,
getVoidLogger(),
loggerMock,
);

const argoServiceForNoToken = new ArgoService(
'testusername',
'testpassword',
configWithoutToken,
getVoidLogger(),
loggerMock,
);

beforeEach(() => {
mocked(timer).mockResolvedValue(0);
fetchMock.resetMocks();
jest.clearAllMocks();
});

it('should get revision data', async () => {
Expand Down Expand Up @@ -206,71 +212,6 @@ describe('ArgoCD service', () => {
).rejects.toThrow();
});

it('should return the argo instances an argo app is on', async () => {
fetchMock.mockResponseOnce(
JSON.stringify({
metadata: {
name: 'testApp-nonprod',
namespace: 'argocd',
status: {},
},
}),
);

const resp = await argoService.findArgoApp({ name: 'testApp-nonprod' });

expect(resp).toStrictEqual([
{
name: 'argoInstance1',
url: 'https://argoInstance1.com',
appName: ['testApp-nonprod'],
},
]);
});

it('should fail to return the argo instances an argo app is on', async () => {
fetchMock.mockResponseOnce('', { status: 500 });

return expect(async () => {
await argoServiceForNoToken.findArgoApp({ name: 'testApp' });
}).rejects.toThrow();
});

it('should return an empty array even when the request fails', async () => {
fetchMock.mockRejectOnce(new Error());
expect(await argoService.findArgoApp({ name: 'test-app' })).toStrictEqual(
[],
);
});

it('should return the argo instances using the app selector', async () => {
fetchMock.mockResponseOnce(
JSON.stringify({
items: [
{
metadata: {
name: 'testApp-nonprod',
namespace: 'argocd',
status: {},
},
},
],
}),
);

const resp = await argoService.findArgoApp({
selector: 'name=testApp-nonprod',
});

expect(resp).toStrictEqual([
{
appName: ['testApp-nonprod'],
name: 'argoInstance1',
url: 'https://argoInstance1.com',
},
]);
});

it('should successfully decorate the items when using the app selector', async () => {
fetchMock.mockResponseOnce(
JSON.stringify({
Expand Down Expand Up @@ -735,7 +676,7 @@ describe('ArgoCD service', () => {
]);
});

it('should fail to sync all apps when bad token', async () => {
it('should return empty array when bad token', async () => {
// token
fetchMock.mockOnceIf(
/.*\/api\/v1\/session/g,
Expand All @@ -745,13 +686,11 @@ describe('ArgoCD service', () => {
{ status: 401, statusText: 'Unauthorized' },
);

const resp = argoServiceForNoToken.resyncAppOnAllArgos({
const resp = await argoServiceForNoToken.resyncAppOnAllArgos({
appSelector: 'testApp',
});

await expect(resp).rejects.toThrow(
'Getting unauthorized for Argo CD instance https://argoInstance1.com',
);
expect(resp).toStrictEqual([]);
});

it('should fail to sync all apps when bad permissions', async () => {
Expand Down Expand Up @@ -1032,6 +971,87 @@ describe('ArgoCD service', () => {
});
});

describe('findArgoApp', () => {
it('should return the argo instances an argo app is on', async () => {
fetchMock.mockResponseOnce(
JSON.stringify({
metadata: {
name: 'testApp-nonprod',
namespace: 'argocd',
status: {},
},
}),
);

const resp = await argoService.findArgoApp({ name: 'testApp-nonprod' });

expect(resp).toStrictEqual([
{
name: 'argoInstance1',
url: 'https://argoInstance1.com',
appName: ['testApp-nonprod'],
},
]);
});

it('should return an empty array even when the request fails', async () => {
fetchMock.mockRejectOnce(new Error());
expect(await argoService.findArgoApp({ name: 'test-app' })).toStrictEqual(
[],
);
});

it('should return the argo instances using the app selector', async () => {
fetchMock.mockResponseOnce(
JSON.stringify({
items: [
{
metadata: {
name: 'testApp-nonprod',
namespace: 'argocd',
status: {},
},
},
],
}),
);

const resp = await argoService.findArgoApp({
selector: 'name=testApp-nonprod',
});

expect(resp).toStrictEqual([
{
appName: ['testApp-nonprod'],
name: 'argoInstance1',
url: 'https://argoInstance1.com',
},
]);
});

it('returns empty array when get token call fails', async () => {
fetchMock.mockRejectedValueOnce(new Error('FetchError'));

const resp = await argoServiceForNoToken.findArgoApp({
selector: 'name=testApp-nonprod',
});

expect(resp).toStrictEqual([]);
});

it('logs error when token call fails', async () => {
fetchMock.mockRejectedValueOnce(new Error('FetchError'));

await argoServiceForNoToken.findArgoApp({
selector: 'name=testApp-nonprod',
});

expect(loggerMock.error).toHaveBeenCalledWith(
'Error getting token from Argo Instance argoInstance1: FetchError',
);
});
});

describe('updateArgoProjectAndApp', () => {
const data: UpdateArgoProjectAndAppProps = {
instanceConfig: { name: 'argoInstanceName', url: 'url' },
Expand Down

0 comments on commit bdc592a

Please sign in to comment.