From e5cca661d97f68547f801acd96c9ec1641760848 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Thu, 22 Aug 2024 10:29:05 +0200 Subject: [PATCH] fix: serialize API token data correctly in instance stats (#7953) Turns out we've been trying to return API token data in instance stats for a while, but that the serialization has failed. Serializing a JS map just yields an empty object. This PR fixes that serialization and also adds API tokens to the instance stats schema (it wasn't before, but we did return it). Adding it to the schema is also part of making resource usage visible as part of the soft limits project. --- .../spec/instance-admin-stats-schema.ts | 24 +++++++++++ src/lib/routes/admin-api/instance-admin.ts | 27 +++++++++---- .../e2e/api/admin/instance-admin.e2e.test.ts | 40 ++++++++++++++++++- 3 files changed, 83 insertions(+), 8 deletions(-) diff --git a/src/lib/openapi/spec/instance-admin-stats-schema.ts b/src/lib/openapi/spec/instance-admin-stats-schema.ts index cdbc1f33cf..0a54a3e7b3 100644 --- a/src/lib/openapi/spec/instance-admin-stats-schema.ts +++ b/src/lib/openapi/spec/instance-admin-stats-schema.ts @@ -220,6 +220,30 @@ export const instanceAdminStatsSchema = { example: 0, minimum: 0, }, + apiTokens: { + type: 'object', + description: 'The number of API tokens in Unleash, split by type', + properties: { + admin: { + type: 'number', + description: 'The number of admin tokens.', + minimum: 0, + example: 5, + }, + client: { + type: 'number', + description: 'The number of client tokens.', + minimum: 0, + example: 5, + }, + frontend: { + type: 'number', + description: 'The number of frontend tokens.', + minimum: 0, + example: 5, + }, + }, + }, sum: { type: 'string', description: diff --git a/src/lib/routes/admin-api/instance-admin.ts b/src/lib/routes/admin-api/instance-admin.ts index 65e1b457b7..89d81fabbf 100644 --- a/src/lib/routes/admin-api/instance-admin.ts +++ b/src/lib/routes/admin-api/instance-admin.ts @@ -5,7 +5,6 @@ import type { IUnleashServices } from '../../types/services'; import type { IUnleashConfig } from '../../types/option'; import Controller from '../controller'; import { NONE } from '../../types/permissions'; -import type { UiConfigSchema } from '../../openapi/spec/ui-config-schema'; import type { InstanceStatsService, InstanceStatsSigned, @@ -15,6 +14,8 @@ import { createCsvResponseSchema, createResponseSchema, } from '../../openapi/util/create-response-schema'; +import type { InstanceAdminStatsSchema } from '../../openapi'; +import { serializeDates } from '../../types'; class InstanceAdminController extends Controller { private instanceStatsService: InstanceStatsService; @@ -128,17 +129,29 @@ class InstanceAdminController extends Controller { }; } + private serializeStats( + instanceStats: InstanceStatsSigned, + ): InstanceAdminStatsSchema { + const apiTokensObj = Object.fromEntries( + instanceStats.apiTokens.entries(), + ); + return serializeDates({ + ...instanceStats, + apiTokens: apiTokensObj, + }); + } + async getStatistics( - req: AuthedRequest, - res: Response, + _: AuthedRequest, + res: Response, ): Promise { const instanceStats = await this.instanceStatsService.getSignedStats(); - res.json(instanceStats); + res.json(this.serializeStats(instanceStats)); } async getStatisticsCSV( - req: AuthedRequest, - res: Response, + _: AuthedRequest, + res: Response, ): Promise { const instanceStats = await this.instanceStatsService.getSignedStats(); const fileName = `unleash-${ @@ -146,7 +159,7 @@ class InstanceAdminController extends Controller { }-${Date.now()}.csv`; const json2csvParser = new Parser(); - const csv = json2csvParser.parse(instanceStats); + const csv = json2csvParser.parse(this.serializeStats(instanceStats)); res.contentType('csv'); res.attachment(fileName); diff --git a/src/test/e2e/api/admin/instance-admin.e2e.test.ts b/src/test/e2e/api/admin/instance-admin.e2e.test.ts index ec82497990..5534b4bd80 100644 --- a/src/test/e2e/api/admin/instance-admin.e2e.test.ts +++ b/src/test/e2e/api/admin/instance-admin.e2e.test.ts @@ -5,6 +5,7 @@ import { } from '../../helpers/test-helper'; import getLogger from '../../../fixtures/no-logger'; import type { IUnleashStores } from '../../../../lib/types'; +import { ApiTokenType } from '../../../../lib/types/models/api-token'; let app: IUnleashTest; let db: ITestDb; @@ -47,6 +48,43 @@ test('should return instance statistics', async () => { }); }); +test('api tokens are serialized correctly', async () => { + await app.services.apiTokenService.createApiTokenWithProjects({ + tokenName: 'admin', + type: ApiTokenType.ADMIN, + environment: '*', + projects: ['*'], + }); + await app.services.apiTokenService.createApiTokenWithProjects({ + tokenName: 'frontend', + type: ApiTokenType.FRONTEND, + environment: 'default', + projects: ['*'], + }); + await app.services.apiTokenService.createApiTokenWithProjects({ + tokenName: 'client', + type: ApiTokenType.CLIENT, + environment: 'default', + projects: ['*'], + }); + + const { body } = await app.request + .get('/api/admin/instance-admin/statistics') + .expect('Content-Type', /json/) + .expect(200); + + expect(body).toMatchObject({ + apiTokens: { client: 1, admin: 1, frontend: 1 }, + }); + + const { text: csv } = await app.request + .get('/api/admin/instance-admin/statistics/csv') + .expect('Content-Type', /text\/csv/) + .expect(200); + + expect(csv).toMatch(/{""client"":1,""admin"":1,""frontend"":1}/); +}); + test('should return instance statistics with correct number of projects', async () => { await stores.projectStore.create({ id: 'test', @@ -77,7 +115,7 @@ test('should return signed instance statistics', async () => { }); }); -test('should return instance statistics as CVS', async () => { +test('should return instance statistics as CSV', async () => { await stores.featureToggleStore.create('default', { name: 'TestStats2', createdByUserId: 9999,