Skip to content

Commit

Permalink
feat(tenant-management): add event connector to avoid coupling with c…
Browse files Browse the repository at this point in the history
…odebuild

in the tenant provisioning flow

GH-17
  • Loading branch information
shubhamp-sf committed Jul 16, 2024
1 parent 3453011 commit e27f77d
Show file tree
Hide file tree
Showing 21 changed files with 278 additions and 1,591 deletions.
1,504 changes: 171 additions & 1,333 deletions package-lock.json

Large diffs are not rendered by default.

24 changes: 12 additions & 12 deletions services/tenant-management-service/README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
# tenant-management-service

[![LoopBack](<https://github.com/strongloop/loopback-next/raw/master/docs/site/imgs/branding/Powered-by-LoopBack-Badge-(blue)-@2x.png>)](http://loopback.io/)

This is the primary service of the control plane responsible for onboarding a tenant and triggering it's provisioning.


## Overview

A Microservice for handling tenant management operations. It provides -

- lead creation and verification
- Tenant Onboarding of both pooled and silo tenants
- Billing and Invoicing
- Provisioning of resources for silo and pooled tenants

### work flow
![image](https://github.com/sourcefuse/arc-saas/assets/107617248/25cb5c15-30d6-4e3a-8a43-05cca121eeaf)


![image](https://github.com/sourcefuse/arc-saas/assets/107617248/25cb5c15-30d6-4e3a-8a43-05cca121eeaf)

## Installation

Expand Down Expand Up @@ -53,7 +53,8 @@ $ [npm install | yarn add] @sourceloop/tenant-management-service
- The mail has a link which should direct to a front end application, which in turn would call the upcoming api's using a temporary authorization code included in the mail.
- The front end application first calls the `/leads/{id}/verify` which updates the validated status of the lead in the DB and returns a new JWT Token that can be used for subsequent calls
- If the token is validated in the previous step, the UI should call the `/leads/{id}/tenants` endpoint with the necessary payload(as per swagger documentation).
- This endpoint would onboard the tenant in the DB, and the facade is then supposed to trigger the relevant pipeline using the `/tenants/{id}/provision` endpoint
- This endpoint would onboard the tenant in the DB, and the facade is then supposed to trigger the relevant events using the `/tenants/{id}/provision` endpoint.
- The provisioning endpoint will invoke the publish method on the `EventConnector`. This connector's purpose is to provide a place for consumer to write the event publishing logic. And your custom service can be bound to the key `EventConnectorBinding` exported by the service. Refer the example with Amazon EventBridge implementation in the [sandbox](./sandbox).

## Webhook Integration

Expand All @@ -80,8 +81,6 @@ const signature = crypto
.digest('hex');
```



### Environment Variables

<table>
Expand Down Expand Up @@ -272,12 +271,12 @@ const signature = crypto

### Setting up a `DataSource`

Here is a sample Implementation `DataSource` implementation using environment variables and PostgreSQL as the data source.
Here is a sample Implementation `DataSource` implementation using environment variables and PostgreSQL as the data source.

```typescript
import {inject, lifeCycleObserver, LifeCycleObserver} from '@loopback/core';
import {juggler} from '@loopback/repository';
import {TenantManagementDbSourceName} from "@sourceloop/tenant-management-service";
import {TenantManagementDbSourceName} from '@sourceloop/tenant-management-service';

const config = {
name: TenantManagementDbSourceName,
Expand All @@ -301,7 +300,9 @@ export class AuthenticationDbDataSource

constructor(
// You need to set datasource configuration name as 'datasources.config.Authentication' otherwise you might get Errors
@inject(`datasources.config.${TenantManagementDbSourceName}`, {optional: true})
@inject(`datasources.config.${TenantManagementDbSourceName}`, {
optional: true,
})
dsConfig: object = config,
) {
super(dsConfig);
Expand All @@ -314,7 +315,7 @@ create one more datasource with redis as connector and db name 'TenantManagement
```typescript
import {inject, lifeCycleObserver, LifeCycleObserver} from '@loopback/core';
import {AnyObject, juggler} from '@loopback/repository';
import { readFileSync } from 'fs';
import {readFileSync} from 'fs';

const config = {
name: 'TenantManagementCacheDB',
Expand Down Expand Up @@ -376,8 +377,8 @@ export class RedisDataSource
super(dsConfig);
}
}

```

### Migrations

The migrations required for this service can be copied from the service. You can customize or cherry-pick the migrations in the copied files according to your specific requirements and then apply them to the DB.
Expand All @@ -386,7 +387,6 @@ The migrations required for this service can be copied from the service. You can

![alt text](./docs/tenants.png)


The major tables in the schema are briefly described below -

**Address** - this model represents the address of a company or lead
Expand Down
1 change: 0 additions & 1 deletion services/tenant-management-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@
"!*/__tests__"
],
"dependencies": {
"@aws-sdk/client-codebuild": "^3.504.0",
"@loopback/boot": "^7.0.0",
"@loopback/context": "^7.0.0",
"@loopback/core": "^6.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,6 @@ import {
import {getRepo, getToken, setupApplication} from './test-helper';
import {ILogger, LOGGER, STATUS_CODE} from '@sourceloop/core';
import {BindingScope} from '@loopback/context';
import {AWS_CODEBUILD_CLIENT} from '../../services';
import {CodeBuildClient, StartBuildCommand} from '@aws-sdk/client-codebuild';
import {PlanTier} from '../../enums';
import {PIPELINES} from '../../keys';

describe('TenantController', () => {
let app: TenantMgmtServiceApplication;
Expand All @@ -46,10 +42,6 @@ describe('TenantController', () => {

const logger = app.getSync<ILogger>(LOGGER.LOGGER_INJECT);
app.bind(LOGGER.LOGGER_INJECT).to(logger).inScope(BindingScope.SINGLETON);
app.bind(PIPELINES).to({
[PlanTier.POOLED]: 'free-pipeline',
[PlanTier.SILO]: '',
});
secretRepo = await getRepo(app, 'repositories.WebhookSecretRepository');
});

Expand Down Expand Up @@ -106,7 +98,7 @@ describe('TenantController', () => {

const webhookSecret = await secretRepo.get(tenant.id);
expect(webhookSecret).to.not.be.null();
expect(webhookSecret.context).to.eql('test-id');
expect(webhookSecret.context).to.not.undefined();
expect(webhookSecret.secret).to.be.String();
});

Expand Down Expand Up @@ -173,61 +165,13 @@ describe('TenantController', () => {
.expect(STATUS_CODE.INTERNAL_SERVER_ERROR);
});

it('invokes POST /tenants/{id}/provision but throws 500 for missing plan pipeline in a subscription', async () => {
const invalidSubscription = {
id: mockSubscriptionId,
plan: {
tier: PlanTier.SILO,
},
};
const token = getToken([
PermissionKey.CreateTenant,
PermissionKey.ProvisionTenant,
]);
const {body: tenant} = await client
.post('/tenants')
.set('Authorization', token)
.send(mockTenantOnboardDTO)
.expect(STATUS_CODE.OK);

await client
.post(`/tenants/${tenant.id}/provision`)
.set('Authorization', token)
.send(invalidSubscription)
.expect(STATUS_CODE.INTERNAL_SERVER_ERROR);
});

it('invokes POST /tenants/{id}/provision but throws 500 for missing build id', async () => {
app.bind(AWS_CODEBUILD_CLIENT).to({
send: (cmd: StartBuildCommand) => {
return {
build: {},
};
},
} as unknown as CodeBuildClient);
const token = getToken([
PermissionKey.CreateTenant,
PermissionKey.ProvisionTenant,
]);
const {body: tenant} = await client
.post('/tenants')
.set('Authorization', token)
.send({...mockTenantOnboardDTO, name: 'test2'})
.expect(STATUS_CODE.OK);

await client
.post(`/tenants/${tenant.id}/provision`)
.set('Authorization', token)
.send(mockSusbcription)
.expect(STATUS_CODE.INTERNAL_SERVER_ERROR);
});

it('invokes GET /tenants with valid token', async () => {
const token = getToken([PermissionKey.ViewTenant]);
const {body} = await client
.get('/tenants')
.set('Authorization', token)
.expect(STATUS_CODE.OK);
console.log(body);
expect(body.length).to.eql(1);
expect(body[0].name).to.eql(mockTenant.name);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ import {MemoryStore} from 'express-rate-limit';
import {sign} from 'jsonwebtoken';
import {AuthenticationBindings} from 'loopback4-authentication';
import {RateLimitSecurityBindings} from 'loopback4-ratelimiter';
import {TenantMgmtServiceApplication} from '../..';
import {EventConnectorBinding, TenantMgmtServiceApplication} from '../..';
import {
ContactRepository,
ResourceRepository,
TenantRepository,
} from '../../repositories';
import {AWS_CODEBUILD_CLIENT, NotificationService} from '../../services';
import {NotificationService} from '../../services';
import {Transaction} from '../fixtures';
import {MOCK_CODEBUILD_CLIENT} from '../fixtures/mock-codebuild-client';
import {DbDataSource, RedisDataSource} from '../helper/datasources';
import {IEventConnector} from '../../types/i-event-connector.interface';

export async function setupApplication(
notifStub?: sinon.SinonStub,
Expand All @@ -45,7 +45,7 @@ export async function setupApplication(
ResourceRepository.prototype.beginTransaction = async () => new Transaction();

setUpRateLimitMemory(app);
app.bind(AWS_CODEBUILD_CLIENT).to(MOCK_CODEBUILD_CLIENT);
setupEventConnector(app);

await app.boot();

Expand Down Expand Up @@ -86,6 +86,16 @@ function setUpRateLimitMemory(app: RestApplication) {
app.bind(RateLimitSecurityBindings.DATASOURCEPROVIDER).to(store);
}

function setupEventConnector(app: RestApplication) {
class EventConnector implements IEventConnector<unknown> {
publish(event: unknown): Promise<void> {
return Promise.resolve();
}
}

app.bind(EventConnectorBinding).toClass(EventConnector);
}

export interface AppWithClient {
app: TenantMgmtServiceApplication;
client: Client;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export * from './mock-codebuild-client';
export * from './mock-transaction';

This file was deleted.

This file was deleted.

23 changes: 17 additions & 6 deletions services/tenant-management-service/src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
import {
Binding,
Component,
Constructor,
ControllerClass,
CoreBindings,
createBindingFromClass,
createServiceBinding,
inject,
ProviderMap,
Expand All @@ -29,6 +31,7 @@ import {
AuthorizationComponent,
} from 'loopback4-authorization';
import {
EventConnectorBinding,
LEAD_TOKEN_VERIFIER,
SYSTEM_USER,
TenantManagementServiceBindings,
Expand Down Expand Up @@ -70,10 +73,8 @@ import {
} from './repositories';
import {LeadTokenVerifierProvider, SystemUserProvider} from './providers';
import {
AWS_CODEBUILD_CLIENT,
CodebuildClientProvider,
CodeBuildService,
CryptoHelperService,
EventConnector,
InvoicePDFGenerator,
LeadAuthenticator,
NotificationService,
Expand Down Expand Up @@ -150,20 +151,20 @@ export class TenantManagementServiceComponent implements Component {
this.bindings = [
Binding.bind(LEAD_TOKEN_VERIFIER).toProvider(LeadTokenVerifierProvider),
Binding.bind(SYSTEM_USER).toProvider(SystemUserProvider),
Binding.bind(AWS_CODEBUILD_CLIENT).toProvider(CodebuildClientProvider),
createServiceBinding(ProvisioningService),
createServiceBinding(OnboardingService),
createServiceBinding(LeadAuthenticator),
createServiceBinding(CryptoHelperService),
Binding.bind('services.NotificationService').toClass(NotificationService),
createServiceBinding(CodeBuildService),
createServiceBinding(InvoicePDFGenerator),
];

this.addClassBindingIfNotPresent(EventConnectorBinding.key, EventConnector);
}

providers?: ProviderMap = {};

bindings?: Binding[] = [];
bindings: Binding[] = [];

services?: ServiceOrProviderClass[];

Expand Down Expand Up @@ -206,4 +207,14 @@ export class TenantManagementServiceComponent implements Component {
});
this.application.component(AuthorizationComponent);
}

private addClassBindingIfNotPresent<T>(key: string, cls: Constructor<T>) {
if (!this.application.isBound(key)) {
this.bindings.push(
createBindingFromClass(cls, {
key: key,
}),
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,17 @@ export class TenantController {
},
},
})
dto: SubscriptionDTO,
subscription: SubscriptionDTO,
@param.path.string('id') id: string,
): Promise<void> {
const existing = await this.tenantRepository.findById(id, {
const tenantDetails = await this.tenantRepository.findById(id, {
include: ['contacts', 'address'],
});
return this.provisioningService.provisionTenant(existing, dto);

return this.provisioningService.provisionTenant(
tenantDetails,
subscription,
);
}

@authorize({
Expand Down
9 changes: 5 additions & 4 deletions services/tenant-management-service/src/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
extensionFor,
} from '@loopback/core';
import {BINDING_PREFIX} from '@sourceloop/core';
import {IEventConnector} from './types/i-event-connector.interface';

export namespace TenantManagementServiceBindings {
export const Config =
Expand All @@ -30,10 +31,6 @@ export const LEAD_TOKEN_VERIFIER = BindingKey.create<
VerifyFunction.BearerFn<LeadUser>
>('sf.user.lead.verifier');

export const PIPELINES = BindingKey.create<Record<string, string>>(
'sf.tenant.pipelines',
);

/**
* Binding key for the system user.
*/
Expand Down Expand Up @@ -73,3 +70,7 @@ export const WebhookNotificationService =
BindingKey.create<WebhookNotificationServiceType>(
'sf.webhook.handler.notification.service',
);

export const EventConnectorBinding = BindingKey.create<
IEventConnector<unknown>
>('arc-saas.services.tenant-management.event-connector');

This file was deleted.

Loading

0 comments on commit e27f77d

Please sign in to comment.