Skip to content

Commit 54be1a6

Browse files
committed
feat(cdk): add HTTP API
1 parent 226b869 commit 54be1a6

File tree

10 files changed

+165
-84
lines changed

10 files changed

+165
-84
lines changed

cdk/resources/CustomDevicesAPI.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import { STACK_NAME } from '../stacks/stackConfig.js'
1212
import type { PublicDevices } from './PublicDevices.js'
1313

1414
export class CustomDevicesAPI extends Construct {
15-
public readonly createCredentialsURL: Lambda.FunctionUrl
15+
public readonly createCredentials: Lambda.IFunction
16+
1617
constructor(
1718
parent: Construct,
1819
{
@@ -50,7 +51,7 @@ export class CustomDevicesAPI extends Construct {
5051
...new LambdaLogGroup(this, 'openSSLFnLogs'),
5152
})
5253

53-
const createCredentials = new Lambda.Function(this, 'createCredentialsFn', {
54+
this.createCredentials = new Lambda.Function(this, 'createCredentialsFn', {
5455
handler: lambdaSources.createCredentials.handler,
5556
architecture: Lambda.Architecture.ARM_64,
5657
runtime: Lambda.Runtime.NODEJS_20_X,
@@ -69,10 +70,7 @@ export class CustomDevicesAPI extends Construct {
6970
...new LambdaLogGroup(this, 'createCredentialsFnLogs'),
7071
initialPolicy: [SettingsPermissions(Stack.of(this))],
7172
})
72-
this.createCredentialsURL = createCredentials.addFunctionUrl({
73-
authType: Lambda.FunctionUrlAuthType.NONE,
74-
})
75-
openSSLFn.grantInvoke(createCredentials)
76-
publicDevices.publicDevicesTable.grantReadData(createCredentials)
73+
openSSLFn.grantInvoke(this.createCredentials)
74+
publicDevices.publicDevicesTable.grantReadData(this.createCredentials)
7775
}
7876
}

cdk/resources/DevicesAPI.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import { LambdaLogGroup } from '@bifravst/aws-cdk-lambda-helpers/cdk'
55
import type { BackendLambdas } from '../BackendLambdas.js'
66

77
export class DevicesAPI extends Construct {
8-
public readonly devicesURL: Lambda.FunctionUrl
8+
public readonly devicesFn: Lambda.IFunction
9+
910
constructor(
1011
parent: Construct,
1112
{
@@ -20,7 +21,7 @@ export class DevicesAPI extends Construct {
2021
) {
2122
super(parent, 'devicesAPI')
2223

23-
const devicesFn = new Lambda.Function(this, 'devicesFn', {
24+
this.devicesFn = new Lambda.Function(this, 'devicesFn', {
2425
handler: lambdaSources.devicesData.handler,
2526
architecture: Lambda.Architecture.ARM_64,
2627
runtime: Lambda.Runtime.NODEJS_20_X,
@@ -45,9 +46,6 @@ export class DevicesAPI extends Construct {
4546
}),
4647
],
4748
})
48-
publicDevices.publicDevicesTable.grantReadData(devicesFn)
49-
this.devicesURL = devicesFn.addFunctionUrl({
50-
authType: Lambda.FunctionUrlAuthType.NONE,
51-
})
49+
publicDevices.publicDevicesTable.grantReadData(this.devicesFn)
5250
}
5351
}

cdk/resources/LwM2MObjectsHistory.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ import type { BackendLambdas } from '../BackendLambdas.js'
1818
* Store history of LwM2M objects
1919
*/
2020
export class LwM2MObjectsHistory extends Construct {
21+
public readonly historyFn: Lambda.IFunction
22+
2123
public readonly table: Timestream.CfnTable
22-
public readonly historyURL: Lambda.FunctionUrl
2324
public constructor(
2425
parent: Construct,
2526
{
@@ -131,7 +132,7 @@ export class LwM2MObjectsHistory extends Construct {
131132
sourceArn: rule.attrArn,
132133
})
133134

134-
const historyFn = new Lambda.Function(this, 'historyFn', {
135+
this.historyFn = new Lambda.Function(this, 'historyFn', {
135136
handler: lambdaSources.queryHistory.handler,
136137
architecture: Lambda.Architecture.ARM_64,
137138
runtime: Lambda.Runtime.NODEJS_20_X,
@@ -166,8 +167,5 @@ export class LwM2MObjectsHistory extends Construct {
166167
}),
167168
],
168169
})
169-
this.historyURL = historyFn.addFunctionUrl({
170-
authType: Lambda.FunctionUrlAuthType.NONE,
171-
})
172170
}
173171
}

cdk/resources/ShareAPI.ts

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import { LambdaLogGroup } from '@bifravst/aws-cdk-lambda-helpers/cdk'
1010
import type { BackendLambdas } from '../BackendLambdas.js'
1111

1212
export class ShareAPI extends Construct {
13-
public readonly shareURL: Lambda.FunctionUrl
14-
public readonly confirmOwnershipURL: Lambda.FunctionUrl
15-
public readonly sharingStatusURL: Lambda.FunctionUrl
13+
public readonly shareFn: Lambda.IFunction
14+
public readonly confirmOwnershipFn: Lambda.IFunction
15+
public readonly sharingStatusFn: Lambda.IFunction
1616
constructor(
1717
parent: Construct,
1818
{
@@ -32,7 +32,7 @@ export class ShareAPI extends Construct {
3232

3333
const domain = this.node.getContext('domain')
3434

35-
const shareFn = new Lambda.Function(this, 'shareFn', {
35+
this.shareFn = new Lambda.Function(this, 'shareFn', {
3636
handler: lambdaSources.shareDevice.handler,
3737
architecture: Lambda.Architecture.ARM_64,
3838
runtime: Lambda.Runtime.NODEJS_20_X,
@@ -65,12 +65,9 @@ export class ShareAPI extends Construct {
6565
}),
6666
],
6767
})
68-
publicDevices.publicDevicesTable.grantWriteData(shareFn)
69-
this.shareURL = shareFn.addFunctionUrl({
70-
authType: Lambda.FunctionUrlAuthType.NONE,
71-
})
68+
publicDevices.publicDevicesTable.grantWriteData(this.shareFn)
7269

73-
const confirmOwnershipFn = new Lambda.Function(this, 'confirmOwnershipFn', {
70+
this.confirmOwnershipFn = new Lambda.Function(this, 'confirmOwnershipFn', {
7471
handler: lambdaSources.confirmOwnership.handler,
7572
architecture: Lambda.Architecture.ARM_64,
7673
runtime: Lambda.Runtime.NODEJS_20_X,
@@ -87,12 +84,9 @@ export class ShareAPI extends Construct {
8784
},
8885
...new LambdaLogGroup(this, 'confirmOwnershipFnLogs'),
8986
})
90-
publicDevices.publicDevicesTable.grantReadWriteData(confirmOwnershipFn)
91-
this.confirmOwnershipURL = confirmOwnershipFn.addFunctionUrl({
92-
authType: Lambda.FunctionUrlAuthType.NONE,
93-
})
87+
publicDevices.publicDevicesTable.grantReadWriteData(this.confirmOwnershipFn)
9488

95-
const sharingStatusFn = new Lambda.Function(this, 'sharingStatusFn', {
89+
this.sharingStatusFn = new Lambda.Function(this, 'sharingStatusFn', {
9690
handler: lambdaSources.sharingStatus.handler,
9791
architecture: Lambda.Architecture.ARM_64,
9892
runtime: Lambda.Runtime.NODEJS_20_X,
@@ -108,9 +102,6 @@ export class ShareAPI extends Construct {
108102
},
109103
...new LambdaLogGroup(this, 'sharingStatusFnLogs'),
110104
})
111-
publicDevices.publicDevicesTable.grantReadData(sharingStatusFn)
112-
this.sharingStatusURL = sharingStatusFn.addFunctionUrl({
113-
authType: Lambda.FunctionUrlAuthType.NONE,
114-
})
105+
publicDevices.publicDevicesTable.grantReadData(this.sharingStatusFn)
115106
}
116107
}

cdk/resources/api/API.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { Construct } from 'constructs'
2+
import {
3+
aws_apigatewayv2 as HttpApi,
4+
aws_lambda as Lambda,
5+
Stack,
6+
} from 'aws-cdk-lib'
7+
import { ApiRoute } from './ApiRoute.js'
8+
9+
export class API extends Construct {
10+
public readonly api: HttpApi.CfnApi
11+
public readonly stage: HttpApi.CfnStage
12+
public readonly deployment: HttpApi.CfnDeployment
13+
public readonly URL: string
14+
15+
constructor(parent: Construct) {
16+
super(parent, 'api')
17+
18+
this.api = new HttpApi.CfnApi(this, 'api', {
19+
name: 'hello.nrfcloud.com/map API',
20+
protocolType: 'HTTP',
21+
})
22+
23+
this.stage = new HttpApi.CfnStage(this, 'stage', {
24+
apiId: this.api.ref,
25+
stageName: '2024-04-12',
26+
autoDeploy: true,
27+
})
28+
29+
this.deployment = new HttpApi.CfnDeployment(this, 'deployment', {
30+
apiId: this.api.ref,
31+
stageName: this.stage.stageName,
32+
})
33+
this.deployment.node.addDependency(this.stage)
34+
this.URL = `https://${this.api.ref}.execute-api.${Stack.of(this).region}.amazonaws.com/${this.stage.stageName}/`
35+
}
36+
37+
public addRoute(methodAndRoute: string, fn: Lambda.IFunction): void {
38+
const [method, resource] = methodAndRoute.split(' ', 2)
39+
if (!isMethod(method)) throw new Error(`${method} is not a HTTP method.`)
40+
if (resource === undefined) throw new Error(`Must provide a route`)
41+
if (!resource.startsWith('/'))
42+
throw new Error(`Route ${resource} must start with a slash!`)
43+
const id = resource.replaceAll(/[^a-z0-9]/gi, '_')
44+
const { route } = new ApiRoute(this, `${method}-${id}-Route`, {
45+
api: this.api,
46+
stage: this.stage,
47+
function: fn,
48+
method,
49+
resource,
50+
})
51+
this.deployment.node.addDependency(route)
52+
// Add OPTIONS route for CORS
53+
const { route: CORS } = new ApiRoute(this, `OPTIONS-${id}-Route`, {
54+
api: this.api,
55+
stage: this.stage,
56+
function: fn,
57+
method: 'OPTIONS' as Lambda.HttpMethod,
58+
resource,
59+
})
60+
this.deployment.node.addDependency(CORS)
61+
}
62+
}
63+
64+
const isMethod = (method?: string): method is Lambda.HttpMethod =>
65+
['GET', 'PUT', 'HEAD', 'POST', 'DELETE', 'PATCH'].includes(method ?? '')

cdk/resources/api/ApiRoute.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import {
2+
aws_apigatewayv2 as HttpApi,
3+
aws_iam as IAM,
4+
aws_lambda as Lambda,
5+
Stack,
6+
} from 'aws-cdk-lib'
7+
import { Construct } from 'constructs'
8+
9+
export const integrationUri = (parent: Stack, f: Lambda.IFunction): string =>
10+
`arn:aws:apigateway:${parent.region}:lambda:path/2015-03-31/functions/arn:aws:lambda:${parent.region}:${parent.account}:function:${f.functionName}/invocations`
11+
12+
export class ApiRoute extends Construct {
13+
public readonly route: HttpApi.CfnRoute
14+
constructor(
15+
parent: Construct,
16+
id: string,
17+
{
18+
function: fn,
19+
api,
20+
stage,
21+
method,
22+
resource,
23+
}: {
24+
function: Lambda.IFunction
25+
api: HttpApi.CfnApi
26+
stage: HttpApi.CfnStage
27+
method: Lambda.HttpMethod
28+
resource: string
29+
},
30+
) {
31+
super(parent, id)
32+
33+
const integration = new HttpApi.CfnIntegration(this, 'Integration', {
34+
apiId: api.ref,
35+
integrationType: 'AWS_PROXY',
36+
integrationUri: integrationUri(Stack.of(parent), fn),
37+
integrationMethod: 'POST',
38+
payloadFormatVersion: '2.0',
39+
})
40+
41+
this.route = new HttpApi.CfnRoute(this, `Route`, {
42+
apiId: api.ref,
43+
routeKey: `${method} ${resource}`,
44+
target: `integrations/${integration.ref}`,
45+
authorizationType: 'NONE',
46+
})
47+
48+
fn.addPermission(
49+
`invokeByHttpApi-${method}-${resource.slice(1).replaceAll('/', '_')}`,
50+
{
51+
principal: new IAM.ServicePrincipal('apigateway.amazonaws.com'),
52+
sourceArn: `arn:aws:execute-api:${Stack.of(parent).region}:${Stack.of(parent).account}:${api.ref}/${stage.stageName}/${method}${resource}`,
53+
},
54+
)
55+
}
56+
}

cdk/stacks/BackendStack.ts

Lines changed: 15 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { SenMLMessages } from '../resources/SenMLMessage.js'
2020
import { ContainerRepositoryId } from '../../aws/ecr.js'
2121
import { repositoryName } from '@bifravst/aws-cdk-ecr-helpers/repository'
2222
import { ContinuousDeployment } from '@bifravst/ci'
23+
import { API } from '../resources/api/API.js'
2324

2425
/**
2526
* Provides resources for the backend serving data to hello.nrfcloud.com/map
@@ -76,22 +77,29 @@ export class BackendStack extends Stack {
7677
lambdaSources,
7778
})
7879

80+
const api = new API(this)
81+
7982
const shareAPI = new ShareAPI(this, {
8083
baseLayer,
8184
lambdaSources,
8285
publicDevices,
8386
})
87+
api.addRoute('POST /share', shareAPI.shareFn)
88+
api.addRoute('POST /share/confirm', shareAPI.confirmOwnershipFn)
89+
api.addRoute('GET /device/{id}', shareAPI.sharingStatusFn)
8490

8591
const devicesAPI = new DevicesAPI(this, {
8692
baseLayer,
8793
lambdaSources,
8894
publicDevices,
8995
})
96+
api.addRoute('GET /devices', devicesAPI.devicesFn)
9097

9198
const lwm2mObjectHistory = new LwM2MObjectsHistory(this, {
9299
baseLayer,
93100
lambdaSources,
94101
})
102+
api.addRoute('GET /history', lwm2mObjectHistory.historyFn)
95103

96104
const customDevicesAPI = new CustomDevicesAPI(this, {
97105
baseLayer,
@@ -110,41 +118,18 @@ export class BackendStack extends Stack {
110118
publicDevices,
111119
})
112120

121+
api.addRoute('PUT /credentials', customDevicesAPI.createCredentials)
122+
113123
const cd = new ContinuousDeployment(this, {
114124
repository,
115125
gitHubOICDProviderArn,
116126
})
117127

118128
// Outputs
119-
new CfnOutput(this, 'shareAPIURL', {
120-
exportName: `${this.stackName}:shareAPI`,
121-
description: 'API endpoint for sharing devices',
122-
value: shareAPI.shareURL.url,
123-
})
124-
new CfnOutput(this, 'confirmOwnershipAPIURL', {
125-
exportName: `${this.stackName}:confirmOwnershipAPI`,
126-
description: 'API endpoint for confirming ownership',
127-
value: shareAPI.confirmOwnershipURL.url,
128-
})
129-
new CfnOutput(this, 'sharingStatusAPIURL', {
130-
exportName: `${this.stackName}:sharingStatusAPI`,
131-
description: 'API endpoint for checking the sharing status of a device',
132-
value: shareAPI.sharingStatusURL.url,
133-
})
134-
new CfnOutput(this, 'devicesAPIURL', {
135-
exportName: `${this.stackName}:devicesAPI`,
136-
description: 'API endpoint for retrieving public device information',
137-
value: devicesAPI.devicesURL.url,
138-
})
139-
new CfnOutput(this, 'queryHistoryAPIURL', {
140-
exportName: `${this.stackName}:queryHistoryAPI`,
141-
description: 'API endpoint for querying device history',
142-
value: lwm2mObjectHistory.historyURL.url,
143-
})
144-
new CfnOutput(this, 'createCredentialsAPIURL', {
145-
exportName: `${this.stackName}:createCredentialsAPIURL`,
146-
description: 'API endpoint for creating credentials for custom devices',
147-
value: customDevicesAPI.createCredentialsURL.url,
129+
new CfnOutput(this, 'APIURL', {
130+
exportName: `${this.stackName}:APIURL`,
131+
description: 'API endpoint',
132+
value: api.URL,
148133
})
149134
new CfnOutput(this, 'publicDevicesTableName', {
150135
exportName: `${this.stackName}:publicDevicesTableName`,
@@ -160,12 +145,7 @@ export class BackendStack extends Stack {
160145
}
161146

162147
export type StackOutputs = {
163-
shareAPIURL: string // e.g. 'https://iiet67bnlmbtuhiblik4wcy4ni0oujot.lambda-url.eu-west-1.on.aws/'
164-
confirmOwnershipAPIURL: string // e.g. 'https://aqt7qs3nzyo4uh2v74quysvmxe0ubeth.lambda-url.eu-west-1.on.aws/'
165-
sharingStatusAPIURL: string // e.g. 'https://aqt7qs3nzyo4uh2v74quysvmxe0ubeth.lambda-url.eu-west-1.on.aws/'
166-
devicesAPIURL: string // e.g. 'https://a2udxgawcxd5tbmmfagi726jsm0obxov.lambda-url.eu-west-1.on.aws/'
167-
queryHistoryAPIURL: string
168-
createCredentialsAPIURL: string
148+
APIURL: string // e.g. 'https://iiet67bnlmbtuhiblik4wcy4ni0oujot.execute-api.eu-west-1.amazonaws.com/2024-04-12/'
169149
publicDevicesTableName: string
170150
/**
171151
* Role ARN to use in the deploy GitHub Actions Workflow

feature-runner/run-features.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { IoTDataPlaneClient } from '@aws-sdk/client-iot-data-plane'
1313
import { fromEnv } from '@nordicsemiconductor/from-env'
1414
import { mock as httpApiMock } from '@bifravst/http-api-mock/mock'
1515
import { DynamoDBClient } from '@aws-sdk/client-dynamodb'
16+
import { slashless } from '@hello.nrfcloud.com/nrfcloud-api-helpers/api'
1617

1718
/**
1819
* This file configures the BDD Feature runner
@@ -96,10 +97,7 @@ runner
9697
)
9798

9899
const res = await runner.run({
99-
shareDeviceAPI: new URL(backendConfig.shareAPIURL),
100-
confirmOwnershipAPI: new URL(backendConfig.confirmOwnershipAPIURL),
101-
sharingStatusAPI: new URL(backendConfig.sharingStatusAPIURL),
102-
devicesAPI: new URL(backendConfig.devicesAPIURL),
100+
API: slashless(new URL(backendConfig.APIURL)),
103101
describeOOBDeviceAPI: new URL(
104102
`${mockApiEndpoint}${describeOOBDeviceAPIBasePath}`,
105103
),

0 commit comments

Comments
 (0)