Skip to content

Commit

Permalink
feat: connect metering concrete implementation to control plane (#83)
Browse files Browse the repository at this point in the history
* feat: connect metering concrete implementation to control plane

* update ingest usage function name

* add scopes and authorization for api endpoints
  • Loading branch information
suhussai authored Aug 21, 2024
1 parent 3819693 commit 531ad38
Show file tree
Hide file tree
Showing 10 changed files with 463 additions and 98 deletions.
215 changes: 203 additions & 12 deletions API.md

Large diffs are not rendered by default.

18 changes: 1 addition & 17 deletions src/control-plane/billing/billing-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,9 @@

import { Schedule } from 'aws-cdk-lib/aws-events';
import { IFunction } from 'aws-cdk-lib/aws-lambda';
import { DetailType } from '../../utils';
import { IFunctionTrigger } from '../../utils';
import { IDataIngestorAggregator } from '../ingestor-aggregator/ingestor-aggregator-interface';

/**
* Optional interface that allows specifying both
* the function to trigger and the event that will trigger it.
*/
export interface IFunctionTrigger {
/**
* The function definition.
*/
readonly handler: IFunction;

/**
* The detail-type that will trigger the handler function.
*/
readonly trigger: DetailType;
}

/**
* Optional interface that allows specifying both
* the function to trigger and the schedule by which to trigger it.
Expand Down
75 changes: 27 additions & 48 deletions src/control-plane/billing/billing-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@ import * as apigatewayV2 from 'aws-cdk-lib/aws-apigatewayv2';
import * as apigatewayV2Integrations from 'aws-cdk-lib/aws-apigatewayv2-integrations';
import * as events from 'aws-cdk-lib/aws-events';
import * as targets from 'aws-cdk-lib/aws-events-targets';
import { IFunction } from 'aws-cdk-lib/aws-lambda';
import { NagSuppressions } from 'cdk-nag';
import { Construct } from 'constructs';
import { IBilling, IFunctionTrigger } from './billing-interface';
import { DetailType, IEventManager, addTemplateTag } from '../../utils';
import { IBilling } from './billing-interface';
import { DetailType, IEventManager, addTemplateTag, createEventTarget } from '../../utils';

/**
* Encapsulates the list of properties for a BillingProvider.
Expand Down Expand Up @@ -57,29 +56,31 @@ export class BillingProvider extends Construct {
super(scope, id);
addTemplateTag(this, 'BillingProvider');

this.createEventTarget(
props.eventManager,
DetailType.ONBOARDING_REQUEST,
props.billing.createCustomerFunction
);

this.createEventTarget(
props.eventManager,
DetailType.OFFBOARDING_REQUEST,
props.billing.deleteCustomerFunction
);

this.createEventTarget(
props.eventManager,
DetailType.TENANT_USER_CREATED,
props.billing.createUserFunction
);

this.createEventTarget(
props.eventManager,
DetailType.TENANT_USER_DELETED,
props.billing.deleteUserFunction
);
[
{
defaultFunctionTrigger: DetailType.ONBOARDING_REQUEST,
functionDefinition: props.billing.createCustomerFunction,
},
{
defaultFunctionTrigger: DetailType.OFFBOARDING_REQUEST,
functionDefinition: props.billing.deleteCustomerFunction,
},
{
defaultFunctionTrigger: DetailType.TENANT_USER_CREATED,
functionDefinition: props.billing.createUserFunction,
},
{
defaultFunctionTrigger: DetailType.TENANT_USER_DELETED,
functionDefinition: props.billing.deleteUserFunction,
},
].forEach((target) => {
createEventTarget(
this,
props.eventManager,
target.defaultFunctionTrigger,
target.functionDefinition
);
});

if (props.billing.putUsageFunction) {
const schedule =
Expand Down Expand Up @@ -130,26 +131,4 @@ export class BillingProvider extends Construct {
);
}
}

private getFunctionProps(
fn: IFunction | IFunctionTrigger,
defaultTrigger: DetailType
): IFunctionTrigger {
return 'handler' in fn
? { handler: fn.handler, trigger: fn.trigger }
: { handler: fn, trigger: defaultTrigger };
}

private createEventTarget(
eventManager: IEventManager,
defaultEvent: DetailType,
fn?: IFunction | IFunctionTrigger
) {
if (!fn) {
return;
}

const { handler, trigger } = this.getFunctionProps(fn, defaultEvent);
eventManager.addTargetToEvent(this, trigger, new targets.LambdaFunction(handler));
}
}
16 changes: 16 additions & 0 deletions src/control-plane/control-plane.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { IAuth } from './auth/auth-interface';
import { CognitoAuth } from './auth/cognito-auth';
import { BillingProvider, IBilling } from './billing';
import { ControlPlaneAPI } from './control-plane-api';
import { IMetering } from './metering';
import { MeteringProvider } from './metering/metering-provider';
import { TenantConfigService } from './tenant-config';
import { TenantManagementService } from './tenant-management/tenant-management.service';
import { UserManagementService } from './user-management/user-management.service';
Expand Down Expand Up @@ -44,6 +46,11 @@ export interface ControlPlaneProps {
*/
readonly billing?: IBilling;

/**
* The metering provider configuration.
*/
readonly metering?: IMetering;

/**
* The event manager instance. If not provided, a new instance will be created.
*/
Expand Down Expand Up @@ -140,6 +147,15 @@ export class ControlPlane extends Construct {
});
}
}

if (props.metering) {
new MeteringProvider(this, 'Metering', {
metering: props.metering,
api: api,
eventManager: eventManager,
});
}

this.eventManager = eventManager;

// defined suppression here to suppress EventsRole Default policy
Expand Down
1 change: 1 addition & 0 deletions src/control-plane/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export * from './ingestor-aggregator/index';
export * from './tenant-config';
export * from './tenant-management';
export * from './user-management';
export * from './metering';
4 changes: 4 additions & 0 deletions src/control-plane/metering/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

export * from './metering-interface';
54 changes: 33 additions & 21 deletions src/control-plane/metering/metering-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,65 +2,77 @@
// SPDX-License-Identifier: Apache-2.0

import { IFunction } from 'aws-cdk-lib/aws-lambda';
import { DetailType } from '../../utils';
import { IFunctionTrigger } from '../../utils';

/**
* Optional interface that allows specifying both
* the function to trigger and the event that will trigger it.
* Encapsulates the list of properties for an IMetering construct.
*/
export interface IFunctionTrigger {
export interface IMetering {
/**
* The function definition.
* The function to trigger to create a meter -- POST /meters
* Once created, the meter can be used to track and analyze the specific usage metrics for tenants.
*/
readonly handler: IFunction;
createMeterFunction: IFunction;

/**
* The detail-type that will trigger the handler function.
* The scope required to authorize requests for creating a new meter.
*/
readonly trigger: DetailType;
}
createMeterScope?: string;

/**
* Encapsulates the list of properties for an IMetering construct.
*/
export interface IMetering {
/**
* The function to trigger to create a meter.
* Once created, the meter can be used to track and analyze the specific usage metrics for tenants.
* The function to trigger to update a meter -- PUT /meters/meterId
*/
createMeterFunction: IFunction | IFunctionTrigger;
updateMeterFunction?: IFunction;

/**
* The function to trigger to update a meter.
* The scope required to authorize requests for updating a meter.
*/
updateMeterFunction?: IFunction | IFunctionTrigger;
updateMeterScope?: string;

/**
* The function to trigger to ingest a usage event.
* Usage events are used to measure and track the usage metrics associated with the meter.
*
* Default event trigger: INGEST_USAGE
*/
ingestFunction: IFunction | IFunctionTrigger;
ingestUsageEventFunction: IFunction | IFunctionTrigger;

/**
* The function to trigger to get the usage data that has been recorded for a specific meter.
* -- GET /usage/meterId
*/
fetchUsageFunction: IFunction; // use 'fetch*' instead of 'get*' to avoid error JSII5000

/**
* The scope required to authorize requests for fetching metering usage.
*/
getUsageFunction: IFunction | IFunctionTrigger;
fetchUsageScope?: string;

/**
* The function to trigger to exclude specific events from being recorded or included in the usage data.
* Used for canceling events that were incorrectly ingested.
* -- DELETE /usage
*/
cancelUsageEventsFunction?: IFunction;

/**
* The scope required to authorize requests for cancelling usage events.
*/
cancelUsageEventsFunction?: IFunction | IFunctionTrigger;
cancelUsageEventsScope?: string;

/**
* The function to trigger to create a new customer.
* (Customer in this context is a tenant.)
*
* Default event trigger: ONBOARDING_REQUEST
*/
createCustomerFunction?: IFunction | IFunctionTrigger;

/**
* The function to trigger to delete a customer.
* (Customer in this context is a tenant.)
*
* Default event trigger: OFFBOARDING_REQUEST
*/
deleteCustomerFunction?: IFunction | IFunctionTrigger;
}
121 changes: 121 additions & 0 deletions src/control-plane/metering/metering-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import * as apigatewayV2 from 'aws-cdk-lib/aws-apigatewayv2';
import * as apigatewayV2Integrations from 'aws-cdk-lib/aws-apigatewayv2-integrations';
import { IFunction } from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';
import { IMetering } from './metering-interface';
import * as utils from '../../utils';
import { ControlPlaneAPI } from '../control-plane-api';

/**
* Encapsulates the list of properties for a MeteringProvider.
*/
export interface MeteringProviderProps {
/**
* An implementation of the IMetering interface.
*/
readonly metering: IMetering;

/**
* An IEventManager object to help coordinate events.
*/
readonly eventManager: utils.IEventManager;

/**
* An API resource to use when setting up API endpoints.
*/
readonly api: ControlPlaneAPI;
}

/**
* Represents a Metering Provider that handles metering-related operations and
* connects the concrete IMetering implementation (provided via props.metering)
* to the control plane.
*
* This construct sets up event targets for various metering-related events
* and adds API routes for 'usage' and 'meters'.
*/
export class MeteringProvider extends Construct {
constructor(scope: Construct, id: string, props: MeteringProviderProps) {
super(scope, id);
utils.addTemplateTag(this, 'MeteringProvider');

const usagePath = '/usage';
const metersPath = '/meters';
const functionTriggerMappings: {
defaultFunctionTrigger: utils.DetailType;
functionDefinition?: IFunction | utils.IFunctionTrigger;
}[] = [
{
defaultFunctionTrigger: utils.DetailType.ONBOARDING_REQUEST,
functionDefinition: props.metering.createCustomerFunction,
},
{
defaultFunctionTrigger: utils.DetailType.OFFBOARDING_REQUEST,
functionDefinition: props.metering.deleteCustomerFunction,
},
{
defaultFunctionTrigger: utils.DetailType.INGEST_USAGE,
functionDefinition: props.metering.ingestUsageEventFunction,
},
];

functionTriggerMappings.forEach((target) => {
utils.createEventTarget(
this,
props.eventManager,
target.defaultFunctionTrigger,
target.functionDefinition
);
});

const routes: utils.IRoute[] = [
{
path: `${usagePath}/meterId`,
method: apigatewayV2.HttpMethod.GET,
integration: new apigatewayV2Integrations.HttpLambdaIntegration(
'fetchUsageHttpLambdaIntegration',
props.metering.fetchUsageFunction
),
scope: props.metering.fetchUsageScope,
},
{
path: metersPath,
method: apigatewayV2.HttpMethod.POST,
integration: new apigatewayV2Integrations.HttpLambdaIntegration(
'createMeterHttpLambdaIntegration',
props.metering.createMeterFunction
),
scope: props.metering.createMeterScope,
},
];

if (props.metering.cancelUsageEventsFunction) {
routes.push({
path: usagePath,
method: apigatewayV2.HttpMethod.DELETE,
integration: new apigatewayV2Integrations.HttpLambdaIntegration(
'deleteUsageHttpLambdaIntegration',
props.metering.cancelUsageEventsFunction
),
scope: props.metering.cancelUsageEventsScope,
});
}

if (props.metering.updateMeterFunction) {
routes.push({
path: `${metersPath}/meterId`,
method: apigatewayV2.HttpMethod.PUT,
integration: new apigatewayV2Integrations.HttpLambdaIntegration(
'updateMeterHttpLambdaIntegration',
props.metering.updateMeterFunction
),
scope: props.metering.updateMeterScope,
});
}

utils.generateRoutes(props.api.api, routes, props.api.jwtAuthorizer);
}
}
Loading

0 comments on commit 531ad38

Please sign in to comment.