mirror of
https://github.com/Unleash/unleash.git
synced 2025-03-04 00:18:40 +01:00
feat: outdated sdks api (#6539)
This commit is contained in:
parent
3c22a302c7
commit
9438400e77
@ -205,6 +205,20 @@ export default class ClientInstanceStore implements IClientInstanceStore {
|
|||||||
return rows.map(mapRow);
|
return rows.map(mapRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async groupApplicationsBySdk(): Promise<
|
||||||
|
{ sdkVersion: string; applications: string[] }[]
|
||||||
|
> {
|
||||||
|
const rows = await this.db
|
||||||
|
.select([
|
||||||
|
'sdk_version as sdkVersion',
|
||||||
|
this.db.raw('ARRAY_AGG(DISTINCT app_name) as applications'),
|
||||||
|
])
|
||||||
|
.from(TABLE)
|
||||||
|
.groupBy('sdk_version');
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
async getDistinctApplications(): Promise<string[]> {
|
async getDistinctApplications(): Promise<string[]> {
|
||||||
const rows = await this.db
|
const rows = await this.db
|
||||||
.distinct('app_name')
|
.distinct('app_name')
|
||||||
|
@ -14,10 +14,7 @@ const config: SDKConfig = {
|
|||||||
'unleash-client-php': '1.13.0',
|
'unleash-client-php': '1.13.0',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function findOutdatedSDKs(sdkVersions: string[]): string[] {
|
export const isOutdatedSdk = (sdkVersion: string) => {
|
||||||
const uniqueSdkVersions = Array.from(new Set(sdkVersions));
|
|
||||||
|
|
||||||
return uniqueSdkVersions.filter((sdkVersion) => {
|
|
||||||
const result = sdkVersion.split(':');
|
const result = sdkVersion.split(':');
|
||||||
if (result.length !== 2) return false;
|
if (result.length !== 2) return false;
|
||||||
const [sdkName, version] = result;
|
const [sdkName, version] = result;
|
||||||
@ -25,5 +22,10 @@ export function findOutdatedSDKs(sdkVersions: string[]): string[] {
|
|||||||
if (!minVersion) return false;
|
if (!minVersion) return false;
|
||||||
if (semver.lt(version, minVersion)) return true;
|
if (semver.lt(version, minVersion)) return true;
|
||||||
return false;
|
return false;
|
||||||
});
|
};
|
||||||
|
|
||||||
|
export function findOutdatedSDKs(sdkVersions: string[]): string[] {
|
||||||
|
const uniqueSdkVersions = Array.from(new Set(sdkVersions));
|
||||||
|
|
||||||
|
return uniqueSdkVersions.filter(isOutdatedSdk);
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,8 @@ import { IPrivateProjectChecker } from '../../private-project/privateProjectChec
|
|||||||
import { IFlagResolver, SYSTEM_USER } from '../../../types';
|
import { IFlagResolver, SYSTEM_USER } from '../../../types';
|
||||||
import { ALL_PROJECTS, parseStrictSemVer } from '../../../util';
|
import { ALL_PROJECTS, parseStrictSemVer } from '../../../util';
|
||||||
import { Logger } from '../../../logger';
|
import { Logger } from '../../../logger';
|
||||||
import { findOutdatedSDKs } from './findOutdatedSdks';
|
import { findOutdatedSDKs, isOutdatedSdk } from './findOutdatedSdks';
|
||||||
|
import { OutdatedSdksSchema } from '../../../openapi/spec/outdated-sdks-schema';
|
||||||
|
|
||||||
export default class ClientInstanceService {
|
export default class ClientInstanceService {
|
||||||
apps = {};
|
apps = {};
|
||||||
@ -261,6 +262,12 @@ export default class ClientInstanceService {
|
|||||||
return this.clientInstanceStore.removeInstancesOlderThanTwoDays();
|
return this.clientInstanceStore.removeInstancesOlderThanTwoDays();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getOutdatedSdks(): Promise<OutdatedSdksSchema['sdks']> {
|
||||||
|
const sdkApps = await this.clientInstanceStore.groupApplicationsBySdk();
|
||||||
|
|
||||||
|
return sdkApps.filter((sdkApp) => isOutdatedSdk(sdkApp.sdkVersion));
|
||||||
|
}
|
||||||
|
|
||||||
async usesSdkOlderThan(
|
async usesSdkOlderThan(
|
||||||
sdkName: string,
|
sdkName: string,
|
||||||
sdkVersion: string,
|
sdkVersion: string,
|
||||||
|
@ -112,6 +112,7 @@ export * from './login-schema';
|
|||||||
export * from './maintenance-schema';
|
export * from './maintenance-schema';
|
||||||
export * from './me-schema';
|
export * from './me-schema';
|
||||||
export * from './name-schema';
|
export * from './name-schema';
|
||||||
|
export * from './outdated-sdks-schema';
|
||||||
export * from './override-schema';
|
export * from './override-schema';
|
||||||
export * from './parameters-schema';
|
export * from './parameters-schema';
|
||||||
export * from './parent-feature-options-schema';
|
export * from './parent-feature-options-schema';
|
||||||
|
41
src/lib/openapi/spec/outdated-sdks-schema.ts
Normal file
41
src/lib/openapi/spec/outdated-sdks-schema.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { FromSchema } from 'json-schema-to-ts';
|
||||||
|
|
||||||
|
export const outdatedSdksSchema = {
|
||||||
|
$id: '#/components/schemas/outdatedSdksSchema',
|
||||||
|
type: 'object',
|
||||||
|
description: 'Data about outdated SDKs that should be upgraded.',
|
||||||
|
additionalProperties: false,
|
||||||
|
required: ['sdks'],
|
||||||
|
properties: {
|
||||||
|
sdks: {
|
||||||
|
type: 'array',
|
||||||
|
description: 'A list of SDKs',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['sdkVersion', 'applications'],
|
||||||
|
additionalProperties: false,
|
||||||
|
properties: {
|
||||||
|
sdkVersion: {
|
||||||
|
type: 'string',
|
||||||
|
description:
|
||||||
|
'An outdated SDK version identifier. Usually formatted as "unleash-client-<language>:<version>"',
|
||||||
|
example: 'unleash-client-java:7.0.0',
|
||||||
|
},
|
||||||
|
applications: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
description: 'Name of the application',
|
||||||
|
type: 'string',
|
||||||
|
example: 'accounting',
|
||||||
|
},
|
||||||
|
description:
|
||||||
|
'A list of applications using the SDK version',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type OutdatedSdksSchema = FromSchema<typeof outdatedSdksSchema>;
|
@ -29,6 +29,10 @@ import {
|
|||||||
applicationEnvironmentInstancesSchema,
|
applicationEnvironmentInstancesSchema,
|
||||||
ApplicationEnvironmentInstancesSchema,
|
ApplicationEnvironmentInstancesSchema,
|
||||||
} from '../../openapi/spec/application-environment-instances-schema';
|
} from '../../openapi/spec/application-environment-instances-schema';
|
||||||
|
import {
|
||||||
|
outdatedSdksSchema,
|
||||||
|
OutdatedSdksSchema,
|
||||||
|
} from '../../openapi/spec/outdated-sdks-schema';
|
||||||
|
|
||||||
class MetricsController extends Controller {
|
class MetricsController extends Controller {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
@ -177,6 +181,25 @@ class MetricsController extends Controller {
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
this.route({
|
||||||
|
method: 'get',
|
||||||
|
path: '/sdks/outdated',
|
||||||
|
handler: this.getOutdatedSdks,
|
||||||
|
permission: NONE,
|
||||||
|
middleware: [
|
||||||
|
openApiService.validPath({
|
||||||
|
tags: ['Unstable'],
|
||||||
|
operationId: 'getOutdatedSdks',
|
||||||
|
summary: 'Get outdated SDKs',
|
||||||
|
description:
|
||||||
|
'Returns a list of the outdated SDKS with the applications using them.',
|
||||||
|
responses: {
|
||||||
|
200: createResponseSchema('outdatedSdksSchema'),
|
||||||
|
...getStandardResponses(404),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async deprecated(req: Request, res: Response): Promise<void> {
|
async deprecated(req: Request, res: Response): Promise<void> {
|
||||||
@ -277,6 +300,20 @@ class MetricsController extends Controller {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getOutdatedSdks(req: Request, res: Response<OutdatedSdksSchema>) {
|
||||||
|
if (!this.flagResolver.isEnabled('sdkReporting')) {
|
||||||
|
throw new NotFoundError();
|
||||||
|
}
|
||||||
|
const outdatedSdks = await this.clientInstanceService.getOutdatedSdks();
|
||||||
|
|
||||||
|
this.openApiService.respondWithValidation(
|
||||||
|
200,
|
||||||
|
res,
|
||||||
|
outdatedSdksSchema.$id,
|
||||||
|
{ sdks: outdatedSdks },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async getApplicationEnvironmentInstances(
|
async getApplicationEnvironmentInstances(
|
||||||
req: Request<{ appName: string; environment: string }>,
|
req: Request<{ appName: string; environment: string }>,
|
||||||
res: Response<ApplicationEnvironmentInstancesSchema>,
|
res: Response<ApplicationEnvironmentInstancesSchema>,
|
||||||
|
@ -26,6 +26,9 @@ export interface IClientInstanceStore
|
|||||||
environment: string,
|
environment: string,
|
||||||
): Promise<IClientInstance[]>;
|
): Promise<IClientInstance[]>;
|
||||||
getBySdkName(sdkName: string): Promise<IClientInstance[]>;
|
getBySdkName(sdkName: string): Promise<IClientInstance[]>;
|
||||||
|
groupApplicationsBySdk(): Promise<
|
||||||
|
{ sdkVersion: string; applications: string[] }[]
|
||||||
|
>;
|
||||||
getDistinctApplications(): Promise<string[]>;
|
getDistinctApplications(): Promise<string[]>;
|
||||||
getDistinctApplicationsCount(daysBefore?: number): Promise<number>;
|
getDistinctApplicationsCount(daysBefore?: number): Promise<number>;
|
||||||
deleteForApplication(appName: string): Promise<void>;
|
deleteForApplication(appName: string): Promise<void>;
|
||||||
|
@ -85,7 +85,7 @@ afterAll(async () => {
|
|||||||
await db.destroy();
|
await db.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should show correct number of total', async () => {
|
test('should show correct application metrics', async () => {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
app.createFeature('toggle-name-1'),
|
app.createFeature('toggle-name-1'),
|
||||||
app.createFeature('toggle-name-2'),
|
app.createFeature('toggle-name-2'),
|
||||||
@ -94,7 +94,7 @@ test('should show correct number of total', async () => {
|
|||||||
appName: metrics.appName,
|
appName: metrics.appName,
|
||||||
instanceId: metrics.instanceId,
|
instanceId: metrics.instanceId,
|
||||||
strategies: ['default'],
|
strategies: ['default'],
|
||||||
sdkVersion: 'unleash-client-test',
|
sdkVersion: 'unleash-client-node:3.2.1',
|
||||||
started: Date.now(),
|
started: Date.now(),
|
||||||
interval: 10,
|
interval: 10,
|
||||||
}),
|
}),
|
||||||
@ -102,7 +102,7 @@ test('should show correct number of total', async () => {
|
|||||||
appName: metrics.appName,
|
appName: metrics.appName,
|
||||||
instanceId: 'another-instance',
|
instanceId: 'another-instance',
|
||||||
strategies: ['default'],
|
strategies: ['default'],
|
||||||
sdkVersion: 'unleash-client-test2',
|
sdkVersion: 'unleash-client-node:3.2.2',
|
||||||
started: Date.now(),
|
started: Date.now(),
|
||||||
interval: 10,
|
interval: 10,
|
||||||
}),
|
}),
|
||||||
@ -129,7 +129,10 @@ test('should show correct number of total', async () => {
|
|||||||
{
|
{
|
||||||
instanceCount: 2,
|
instanceCount: 2,
|
||||||
name: 'default',
|
name: 'default',
|
||||||
sdks: ['unleash-client-test', 'unleash-client-test2'],
|
sdks: [
|
||||||
|
'unleash-client-node:3.2.1',
|
||||||
|
'unleash-client-node:3.2.2',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
featureCount: 3,
|
featureCount: 3,
|
||||||
@ -143,12 +146,31 @@ test('should show correct number of total', async () => {
|
|||||||
)
|
)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(instancesBody).toMatchObject({
|
expect(
|
||||||
instances: [
|
instancesBody.instances.sort((a, b) =>
|
||||||
{ instanceId: 'instanceId', sdkVersion: 'unleash-client-test' },
|
a.instanceId.localeCompare(b.instanceId),
|
||||||
|
),
|
||||||
|
).toMatchObject([
|
||||||
{
|
{
|
||||||
instanceId: 'another-instance',
|
instanceId: 'another-instance',
|
||||||
sdkVersion: 'unleash-client-test2',
|
sdkVersion: 'unleash-client-node:3.2.2',
|
||||||
|
},
|
||||||
|
{ instanceId: 'instanceId', sdkVersion: 'unleash-client-node:3.2.1' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { body: outdatedSdks } = await app.request
|
||||||
|
.get(`/api/admin/metrics/sdks/outdated`)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(outdatedSdks).toMatchObject({
|
||||||
|
sdks: [
|
||||||
|
{
|
||||||
|
sdkVersion: 'unleash-client-node:3.2.1',
|
||||||
|
applications: ['appName'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sdkVersion: 'unleash-client-node:3.2.2',
|
||||||
|
applications: ['appName'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
16
src/test/fixtures/fake-client-instance-store.ts
vendored
16
src/test/fixtures/fake-client-instance-store.ts
vendored
@ -4,6 +4,7 @@ import {
|
|||||||
INewClientInstance,
|
INewClientInstance,
|
||||||
} from '../../lib/types/stores/client-instance-store';
|
} from '../../lib/types/stores/client-instance-store';
|
||||||
import NotFoundError from '../../lib/error/notfound-error';
|
import NotFoundError from '../../lib/error/notfound-error';
|
||||||
|
import groupBy from 'lodash.groupby';
|
||||||
|
|
||||||
export default class FakeClientInstanceStore implements IClientInstanceStore {
|
export default class FakeClientInstanceStore implements IClientInstanceStore {
|
||||||
instances: IClientInstance[] = [];
|
instances: IClientInstance[] = [];
|
||||||
@ -32,10 +33,19 @@ export default class FakeClientInstanceStore implements IClientInstanceStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getBySdkName(sdkName: string): Promise<IClientInstance[]> {
|
async getBySdkName(sdkName: string): Promise<IClientInstance[]> {
|
||||||
return Promise.resolve(
|
return this.instances.filter((instance) =>
|
||||||
this.instances.filter((instance) =>
|
|
||||||
instance.sdkVersion?.startsWith(sdkName),
|
instance.sdkVersion?.startsWith(sdkName),
|
||||||
),
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async groupApplicationsBySdk(): Promise<
|
||||||
|
{ sdkVersion: string; applications: string[] }[]
|
||||||
|
> {
|
||||||
|
return Object.entries(groupBy(this.instances, 'sdkVersion')).map(
|
||||||
|
([sdkVersion, apps]) => ({
|
||||||
|
sdkVersion,
|
||||||
|
applications: apps.map((item) => item.appName),
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user