Skip to content

Commit

Permalink
feat: start extracting project from session object (#6856)
Browse files Browse the repository at this point in the history
Previously, we were extracting the project from the token, but now we
will retrieve it from the session, which contains the full list of
projects.

This change also resolves an issue we encountered when the token was a
multi-project token, formatted as []:dev:token. Previously, it was
unable to display the exact list of projects. Now, it will show the
exact project names.
  • Loading branch information
sjaanus committed Apr 16, 2024
1 parent 8dbd680 commit f455931
Show file tree
Hide file tree
Showing 9 changed files with 150 additions and 22 deletions.
1 change: 1 addition & 0 deletions src/lib/__snapshots__/create-config.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ exports[`should create default config 1`] = `
},
"migrationLock": true,
"outdatedSdksBanner": false,
"parseProjectFromSession": false,
"personalAccessTokensKillSwitch": false,
"projectListFilterMyProjects": false,
"projectOverviewRefactor": false,
Expand Down
51 changes: 39 additions & 12 deletions src/lib/db/client-applications-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { Logger, LogProvider } from '../logger';
import type { Db } from './db';
import type { IApplicationOverview } from '../features/metrics/instance/models';
import { applySearchFilters } from '../features/feature-search/search-utils';
import type { IFlagResolver } from '../types';

const COLUMNS = [
'app_name',
Expand Down Expand Up @@ -110,39 +111,39 @@ const remapRow = (input) => {
return temp;
};

const remapUsageRow = (input) => {
return {
app_name: input.appName,
project: input.project || '*',
environment: input.environment || '*',
};
};

export default class ClientApplicationsStore
implements IClientApplicationsStore
{
private db: Db;

private logger: Logger;

constructor(db: Db, eventBus: EventEmitter, getLogger: LogProvider) {
private flagResolver: IFlagResolver;

constructor(
db: Db,
eventBus: EventEmitter,
getLogger: LogProvider,
flagResolver: IFlagResolver,
) {
this.db = db;
this.flagResolver = flagResolver;
this.logger = getLogger('client-applications-store.ts');
}

async upsert(details: Partial<IClientApplication>): Promise<void> {
const row = remapRow(details);
await this.db(TABLE).insert(row).onConflict('app_name').merge();
const usageRow = remapUsageRow(details);
const usageRows = this.remapUsageRow(details);
await this.db(TABLE_USAGE)
.insert(usageRow)
.insert(usageRows)
.onConflict(['app_name', 'project', 'environment'])
.merge();
}

async bulkUpsert(apps: Partial<IClientApplication>[]): Promise<void> {
const rows = apps.map(remapRow);
const usageRows = apps.map(remapUsageRow);
const usageRows = apps.flatMap(this.remapUsageRow);
await this.db(TABLE).insert(rows).onConflict('app_name').merge();
await this.db(TABLE_USAGE)
.insert(usageRows)
Expand Down Expand Up @@ -420,4 +421,30 @@ export default class ClientApplicationsStore
},
};
}

private remapUsageRow = (input) => {
if (this.flagResolver.isEnabled('parseProjectFromSession')) {
if (!input.projects || input.projects.length === 0) {
return [
{
app_name: input.appName,
project: '*',
environment: input.environment || '*',
},
];
} else {
return input.projects.map((project) => ({
app_name: input.appName,
project: project,
environment: input.environment || '*',
}));
}
} else {
return {
app_name: input.appName,
project: input.project || '*',
environment: input.environment || '*',
};
}
};
}
1 change: 1 addition & 0 deletions src/lib/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export const createStores = (
db,
eventBus,
getLogger,
config.flagResolver,
),
clientInstanceStore: new ClientInstanceStore(db, eventBus, getLogger),
clientMetricsStoreV2: new ClientMetricsStoreV2(
Expand Down
1 change: 1 addition & 0 deletions src/lib/features/metrics/instance/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface IApplication {
instances?: IClientInstance[];
seenToggles?: Record<string, any>;
project?: string;
projects?: string[];
environment?: string;
links?: Record<string, string>;
}
Expand Down
25 changes: 20 additions & 5 deletions src/lib/features/metrics/instance/register.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Response } from 'express';
import Controller from '../../../routes/controller';
import type { IUnleashServices } from '../../../types';
import type { IFlagResolver, IUnleashServices } from '../../../types';
import type { IUnleashConfig } from '../../../types/option';
import type { Logger } from '../../../logger';
import type ClientInstanceService from './instance-service';
Expand All @@ -24,6 +24,8 @@ export default class RegisterController extends Controller {

openApiService: OpenApiService;

flagResolver: IFlagResolver;

constructor(
{
clientInstanceService,
Expand All @@ -35,6 +37,7 @@ export default class RegisterController extends Controller {
this.logger = config.getLogger('/api/client/register');
this.clientInstanceService = clientInstanceService;
this.openApiService = openApiService;
this.flagResolver = config.flagResolver;

this.route({
method: 'post',
Expand Down Expand Up @@ -62,7 +65,7 @@ export default class RegisterController extends Controller {
});
}

private static resolveEnvironment(
private resolveEnvironment(
user: IUser | IApiUser,
data: Partial<IClientApp>,
) {
Expand All @@ -76,7 +79,14 @@ export default class RegisterController extends Controller {
return 'default';
}

private static extractProjectFromRequest(
private resolveProject(user: IUser | IApiUser) {
if (user instanceof ApiUser) {
return user.projects;
}
return ['default'];
}

private extractProjectFromRequest(
req: IAuthRequest<unknown, void, ClientApplicationSchema>,
) {
const token = req.get('Authorisation') || req.headers.authorization;
Expand All @@ -91,8 +101,13 @@ export default class RegisterController extends Controller {
res: Response<void>,
): Promise<void> {
const { body: data, ip: clientIp, user } = req;
data.environment = RegisterController.resolveEnvironment(user, data);
data.project = RegisterController.extractProjectFromRequest(req);
data.environment = this.resolveEnvironment(user, data);
if (this.flagResolver.isEnabled('parseProjectFromSession')) {
data.projects = this.resolveProject(user);
} else {
data.project = this.extractProjectFromRequest(req);
}

await this.clientInstanceService.registerClient(data, clientIp);
res.header('X-Unleash-Version', version).status(202).end();
}
Expand Down
1 change: 1 addition & 0 deletions src/lib/features/metrics/shared/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,5 @@ export const clientRegisterSchema = joi
interval: joi.number().required(),
environment: joi.string().optional(),
project: joi.string().optional(),
projects: joi.array().optional().items(joi.string()),
});
7 changes: 6 additions & 1 deletion src/lib/types/experimental.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ export type IFlagKey =
| 'bearerTokenMiddleware'
| 'projectOverviewRefactorFeedback'
| 'featureLifecycle'
| 'projectListFilterMyProjects';
| 'projectListFilterMyProjects'
| 'parseProjectFromSession';

export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;

Expand Down Expand Up @@ -287,6 +288,10 @@ const flags: IFlags = {
process.env.UNLEASH_EXPERIMENTAL_PROJECTS_LIST_MY_PROJECTS,
false,
),
parseProjectFromSession: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_PARSE_PROJECT_FROM_SESSION,
false,
),
};

export const defaultExperimentalOptions: IExperimentalOptions = {
Expand Down
1 change: 1 addition & 0 deletions src/server-dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ process.nextTick(async () => {
projectOverviewRefactorFeedback: true,
featureLifecycle: true,
projectListFilterMyProjects: true,
parseProjectFromSession: true,
},
},
authentication: {
Expand Down
84 changes: 80 additions & 4 deletions src/test/e2e/api/admin/metrics.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,31 @@ import {
setupAppWithCustomConfig,
} from '../../helpers/test-helper';
import getLogger from '../../../fixtures/no-logger';
import { ApiTokenType } from '../../../../lib/types/models/api-token';

let app: IUnleashTest;
let db: ITestDb;

beforeAll(async () => {
db = await dbInit('metrics_serial', getLogger, {});
app = await setupAppWithCustomConfig(db.stores, {}, db.rawDatabase);
db = await dbInit('metrics_serial', getLogger, {
experimental: {
flags: {
parseProjectFromSession: true,
},
},
});
app = await setupAppWithCustomConfig(
db.stores,
{
experimental: {
flags: {
strictSchemaValidation: true,
parseProjectFromSession: true,
},
},
},
db.rawDatabase,
);
});

beforeEach(async () => {
Expand Down Expand Up @@ -52,7 +70,7 @@ beforeEach(async () => {
appName: 'usage-app',
strategies: ['default'],
description: 'Some desc',
project: 'default',
projects: ['default'],
environment: 'dev',
});
});
Expand Down Expand Up @@ -123,6 +141,64 @@ test('should get list of application usage', async () => {
);
expect(application).toMatchObject({
appName: 'usage-app',
usage: [{ project: 'default', environments: ['dev'] }],
usage: [
{
project: 'default',
environments: ['dev'],
},
],
});
});

test('should save multiple projects from token', async () => {
await db.reset();
await db.stores.projectStore.create({
id: 'mainProject',
name: 'mainProject',
});

const multiProjectToken =
await app.services.apiTokenService.createApiTokenWithProjects({
type: ApiTokenType.CLIENT,
projects: ['default', 'mainProject'],
environment: 'default',
tokenName: 'tester',
});

await app.request
.post('/api/client/register')
.set('Authorization', multiProjectToken.secret)
.send({
appName: 'multi-project-app',
instanceId: 'instance-1',
strategies: ['default'],
started: Date.now(),
interval: 10,
});

await app.services.clientInstanceService.bulkAdd();

const { body } = await app.request
.get('/api/admin/metrics/applications')
.expect('Content-Type', /json/)
.expect(200);

expect(body).toMatchObject({
applications: [
{
appName: 'multi-project-app',
usage: [
{
environments: ['default'],
project: 'default',
},
{
environments: ['default'],
project: 'mainProject',
},
],
},
],
total: 1,
});
});

0 comments on commit f455931

Please sign in to comment.