diff --git a/.changeset/popular-starfishes-laugh.md b/.changeset/popular-starfishes-laugh.md new file mode 100644 index 000000000..2c0121f4b --- /dev/null +++ b/.changeset/popular-starfishes-laugh.md @@ -0,0 +1,5 @@ +--- +'@roadiehq/backstage-plugin-argo-cd-backend': minor +--- + +Add endpoint that grabs all argo projects and another endpoint that checks if app already exists in a given cluster with the same repo and source 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 c8106ca7b..30c476ba4 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 @@ -125,8 +125,8 @@ export class ArgoService implements ArgoServiceApi { getArgoAppDataResp = await this.getArgoAppData( argoInstance.url, argoInstance.name, - options, token, + options, ); } catch (error: any) { return null; @@ -208,15 +208,19 @@ export class ArgoService implements ArgoServiceApi { async getArgoAppData( baseUrl: string, argoInstanceName: string, - options: { + argoToken: string, + options?: { name?: string; selector?: string; }, - argoToken: string, ): Promise { - const urlSuffix = options.name - ? `/${options.name}` - : `?selector=${options.selector}`; + let urlSuffix = ''; + if (options?.name) { + urlSuffix = `/${options.name}`; + } + if (options?.selector) { + urlSuffix = `?selector=${options.selector}`; + } const requestOptions: RequestInit = { method: 'GET', headers: { @@ -239,7 +243,7 @@ export class ArgoService implements ArgoServiceApi { (data.items as any[]).forEach(item => { item.metadata.instance = { name: argoInstanceName }; }); - } else if (data && options.name) { + } else if (data && options?.name) { data.instance = argoInstanceName; } return data; @@ -688,8 +692,8 @@ export class ArgoService implements ArgoServiceApi { const argoApp = await this.getArgoAppData( matchedArgoInstance.url, matchedArgoInstance.name, - { name: argoAppName }, token, + { name: argoAppName }, ); isAppPendingDelete = 'metadata' in argoApp; @@ -813,8 +817,8 @@ export class ArgoService implements ArgoServiceApi { const appData = await this.getArgoAppData( instanceConfig.url, instanceConfig.name, - { name: appName }, argoToken, + { name: appName }, ); if (!appData.spec?.source?.repoURL) { this.logger.error(`No repo URL found for argo app ${projectName}`); 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 1768f9d24..2b6dc0fbf 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 @@ -118,6 +118,55 @@ describe('ArgoCD service', () => { ).rejects.toThrow(); }); + it('should get all argo app data if no option is provided', async () => { + fetchMock.mockResponseOnce( + JSON.stringify({ + items: [ + { + metadata: { + name: 'testAppName', + namespace: 'testNamespace', + }, + }, + { + metadata: { + name: 'testAppName2', + namespace: 'testNamespace2', + }, + }, + ], + }), + ); + const resp = await argoService.getArgoAppData( + 'https://argoInstance1.com', + 'argoInstance1', + 'testToken', + ); + + expect(resp).toStrictEqual({ + items: [ + { + metadata: { + name: 'testAppName', + namespace: 'testNamespace', + instance: { + name: 'argoInstance1', + }, + }, + }, + { + metadata: { + name: 'testAppName2', + namespace: 'testNamespace2', + instance: { + name: 'argoInstance1', + }, + }, + }, + ], + }); + }); + it('should get argo app data', async () => { fetchMock.mockResponseOnce( JSON.stringify({ @@ -131,8 +180,8 @@ describe('ArgoCD service', () => { const resp = await argoService.getArgoAppData( 'https://argoInstance1.com', 'argoInstance1', - { name: 'testApp' }, 'testToken', + { name: 'testApp' }, ); expect(resp).toStrictEqual({ @@ -151,8 +200,8 @@ describe('ArgoCD service', () => { argoService.getArgoAppData( 'https://argoInstance1.com', 'argoInstance1', - { name: 'testApp' }, 'testToken', + { name: 'testApp' }, ), ).rejects.toThrow(); }); @@ -245,8 +294,8 @@ describe('ArgoCD service', () => { const resp = await argoService.getArgoAppData( 'https://argoInstance1.com', 'argoInstance1', - { selector: 'service=testApp' }, 'testToken', + { selector: 'service=testApp' }, ); expect(resp).toStrictEqual({ diff --git a/plugins/backend/backstage-plugin-argo-cd-backend/src/service/router.test.ts b/plugins/backend/backstage-plugin-argo-cd-backend/src/service/router.test.ts index f8a4af785..5266f2fff 100644 --- a/plugins/backend/backstage-plugin-argo-cd-backend/src/service/router.test.ts +++ b/plugins/backend/backstage-plugin-argo-cd-backend/src/service/router.test.ts @@ -96,7 +96,6 @@ describe('router', () => { password: 'password', }, ]); - const response = await request(app).post('/createArgo').send({ clusterName: 'argoInstance1', namespace: 'test-namespace', @@ -116,6 +115,64 @@ describe('router', () => { }); }); + it('checks to see if source already exists in argo', async () => { + mockGetArgoInstanceArray.mockReturnValue([ + { + name: 'argoInstance1', + url: 'https://argoInstance1.com', + token: 'token', + username: 'username', + password: 'password', + }, + { + name: 'argoInstance2', + url: 'https://argoInstance2.com', + token: 'token', + username: 'username', + password: 'password', + }, + ]); + mockGetArgoToken.mockReturnValue('token'); + mockGetArgoAppData.mockReturnValue({ + items: [ + { + metadata: { + name: 'testAppName', + namespace: 'testNamespace', + }, + spec: { + source: { + repoURL: 'test.repo.url', + path: 'source/path', + }, + }, + }, + { + metadata: { + name: 'testAppName2', + namespace: 'testNamespace2', + }, + spec: { + source: { + repoURL: 'test.repo.url.two', + path: 'source/path', + }, + }, + }, + ], + }); + + const response = await request(app).get( + '/argoInstance/argoInstance1/repo/testrepo/source/testsource', + ); + expect(response.body).toEqual(false); + + const response2 = await request(app).get( + '/argoInstance/argoInstance1/repo/test.repo.url/source/source%2Fpath', + ); + expect(response2.body).toEqual(true); + }); + it('delete sends back status of app and project deletion', async () => { mockDeleteAppandProject.mockResolvedValue({ argoDeleteAppResp: { diff --git a/plugins/backend/backstage-plugin-argo-cd-backend/src/service/router.ts b/plugins/backend/backstage-plugin-argo-cd-backend/src/service/router.ts index dea3dcda9..6e420ad1b 100644 --- a/plugins/backend/backstage-plugin-argo-cd-backend/src/service/router.ts +++ b/plugins/backend/backstage-plugin-argo-cd-backend/src/service/router.ts @@ -47,6 +47,70 @@ export function createRouter({ return token; } + router.get('/allArgoApps/:argoInstanceName', async (request, response) => { + const argoInstanceName = request.params.argoInstanceName; + const matchedArgoInstance = findArgoInstance(argoInstanceName); + if (matchedArgoInstance === undefined) { + return response.status(500).send({ + status: 'failed', + message: 'cannot find an argo instance to match this cluster', + }); + } + const token: string = await findMatchedArgoInstanceToken( + matchedArgoInstance, + ); + if (!token) { + return response.status(500).send({ + status: 'failed', + message: 'could not generate token', + }); + } + return response.send( + await argoSvc.getArgoAppData( + matchedArgoInstance.url, + matchedArgoInstance.name, + token, + ), + ); + }); + + router.get( + '/argoInstance/:argoInstance/repo/:repo/source/:source', + async (request, response) => { + const argoInstanceName = request.params.argoInstance; + const matchedArgoInstance = findArgoInstance(argoInstanceName); + if (matchedArgoInstance === undefined) { + return response.status(500).send({ + status: 'failed', + message: 'cannot find an argo instance to match this cluster', + }); + } + const token: string = await findMatchedArgoInstanceToken( + matchedArgoInstance, + ); + if (!token) { + return response.status(500).send({ + status: 'failed', + message: 'could not generate token', + }); + } + const argoData = await argoSvc.getArgoAppData( + matchedArgoInstance.url, + matchedArgoInstance.name, + token, + ); + const repoAndSource = argoData.items.map( + (argoApp: any) => + `${argoApp?.spec?.source?.repoURL}/${argoApp?.spec?.source?.path}`, + ); + return response.send( + repoAndSource.includes( + `${request.params.repo}/${decodeURIComponent(request.params.source)}`, + ), + ); + }, + ); + router.get('/find/name/:argoAppName', async (request, response) => { const argoAppName = request.params.argoAppName; response.send(await argoSvc.findArgoApp({ name: argoAppName })); @@ -100,8 +164,8 @@ export function createRouter({ const resp = await argoSvc.getArgoAppData( matchedArgoInstance.url, matchedArgoInstance.name, - { name: argoAppName }, token, + { name: argoAppName }, ); return response.send(resp); }, @@ -131,8 +195,8 @@ export function createRouter({ const resp = await argoSvc.getArgoAppData( matchedArgoInstance.url, matchedArgoInstance.name, - { selector: argoAppSelector }, token, + { selector: argoAppSelector }, ); return response.send(resp); }, diff --git a/plugins/backend/backstage-plugin-argo-cd-backend/src/service/types.ts b/plugins/backend/backstage-plugin-argo-cd-backend/src/service/types.ts index e46d6f064..4bb810812 100644 --- a/plugins/backend/backstage-plugin-argo-cd-backend/src/service/types.ts +++ b/plugins/backend/backstage-plugin-argo-cd-backend/src/service/types.ts @@ -124,11 +124,11 @@ export interface ArgoServiceApi { getArgoAppData: ( baseUrl: string, argoInstanceName: string, + argoToken: string, options: { name: string; selector: string; }, - argoToken: string, ) => Promise; createArgoProject: (props: CreateArgoProjectProps) => Promise; createArgoApplication: (props: CreateArgoApplicationProps) => Promise;