diff --git a/.changeset/shy-ghosts-sleep.md b/.changeset/shy-ghosts-sleep.md new file mode 100644 index 000000000..123045d05 --- /dev/null +++ b/.changeset/shy-ghosts-sleep.md @@ -0,0 +1,5 @@ +--- +'@roadiehq/backstage-plugin-argo-cd-backend': patch +--- + +fix bug that throws error when some argo instances are unavailable diff --git a/plugins/backend/backstage-plugin-argo-cd-backend/src/service/argocd.service.ts b/plugins/backend/backstage-plugin-argo-cd-backend/src/service/argocd.service.ts index 85c86cdaa..a90f065c8 100644 --- a/plugins/backend/backstage-plugin-argo-cd-backend/src/service/argocd.service.ts +++ b/plugins/backend/backstage-plugin-argo-cd-backend/src/service/argocd.service.ts @@ -118,10 +118,10 @@ 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, @@ -129,6 +129,9 @@ export class ArgoService implements ArgoServiceApi { options, ); } catch (error: any) { + this.logger.error( + `Error getting token from Argo Instance ${argoInstance.name}: ${error.message}`, + ); return null; } @@ -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 => { - try { - const token = await this.getArgoToken(argoInstance); + if (argoAppResp) { + const parallelSyncCalls = argoAppResp.map( + async (argoInstance: any): Promise => { try { - const resp = argoInstance.appName.map( - (argoApp: any): Promise => { - 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 => { + 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({ diff --git a/plugins/backend/backstage-plugin-argo-cd-backend/src/service/argocd.test.ts b/plugins/backend/backstage-plugin-argo-cd-backend/src/service/argocd.test.ts index 2835806aa..62a1341a2 100644 --- a/plugins/backend/backstage-plugin-argo-cd-backend/src/service/argocd.test.ts +++ b/plugins/backend/backstage-plugin-argo-cd-backend/src/service/argocd.test.ts @@ -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([ { @@ -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 () => { @@ -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({ @@ -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, @@ -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 () => { @@ -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' },