Skip to content

Commit

Permalink
feat(fota): abort fota jobs (#954)
Browse files Browse the repository at this point in the history
Implement the ability for users to cancel multi-bundle FOTA jobs.

This also cancels running FOTA jobs on nRF Cloud.

Closes #962
Closes #957
  • Loading branch information
coderbyheart authored Sep 26, 2024
1 parent 7344da4 commit 95ed330
Show file tree
Hide file tree
Showing 15 changed files with 692 additions and 298 deletions.
5 changes: 5 additions & 0 deletions cdk/BackendStack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,11 @@ export class BackendStack extends Stack {
mbff.startMultiBundleFOTAFlow.fn,
)

api.addRoute(
'DELETE /device/{deviceId}/fota/job/{jobId}',
mbff.abortMultiBundleFOTAFlow.fn,
)

const updateDevice = new UpdateDevice(this, {
lambdaSources,
layers: [baseLayerVersion],
Expand Down
15 changes: 15 additions & 0 deletions cdk/packBackendLambdas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,12 @@ export type BackendLambdas = {
createCNAMERecord: PackedLambda
multiBundleFOTAFlow: {
start: PackedLambda
abort: PackedLambda
onFail: PackedLambda
getDeviceFirmwareDetails: PackedLambda
getNextBundle: PackedLambda
createFOTAJob: PackedLambda
cancelFOTAJob: PackedLambda
WaitForFOTAJobCompletionCallback: PackedLambda
waitForFOTAJobCompletion: PackedLambda
waitForUpdateAppliedCallback: PackedLambda
Expand Down Expand Up @@ -134,6 +137,14 @@ export const packBackendLambdas = async (): Promise<BackendLambdas> => ({
'multiBundleFOTAFlowStart',
'lambda/fota/multi-bundle-flow/start.ts',
),
abort: await packLambdaFromPath(
'multiBundleFOTAFlowAbort',
'lambda/fota/multi-bundle-flow/abort.ts',
),
onFail: await packLambdaFromPath(
'multiBundleFOTAFlowOnFail',
'lambda/fota/multi-bundle-flow/onFail.ts',
),
getDeviceFirmwareDetails: await packLambdaFromPath(
'multiBundleFOTAFlowGetDeviceFirmareDetails',
'lambda/fota/multi-bundle-flow/getDeviceFirmwareDetails.ts',
Expand All @@ -146,6 +157,10 @@ export const packBackendLambdas = async (): Promise<BackendLambdas> => ({
'multiBundleFOTAFlowCreateFOTAJob',
'lambda/fota/multi-bundle-flow/createFOTAJob.ts',
),
cancelFOTAJob: await packLambdaFromPath(
'multiBundleFOTAFlowCancelFOTAJob',
'lambda/fota/multi-bundle-flow/cancelFOTAJob.ts',
),
WaitForFOTAJobCompletionCallback: await packLambdaFromPath(
'multiBundleFOTAFlowWaitForFOTAJobCompletionCallback',
'lambda/fota/multi-bundle-flow/waitForFOTAJobCompletionCallback.ts',
Expand Down
123 changes: 116 additions & 7 deletions cdk/resources/FOTA/MultiBundleFlow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@ import { definitions, LwM2MObjectID } from '@hello.nrfcloud.com/proto-map/lwm2m'
import { FOTAJobStatus } from '@hello.nrfcloud.com/proto/hello'
import {
Duration,
aws_dynamodb as DynamoDB,
aws_events as Events,
aws_lambda_event_sources as EventSources,
aws_events_targets as EventsTargets,
aws_iam as IAM,
aws_iot as IoT,
aws_lambda as Lambda,
Stack,
aws_stepfunctions_tasks as StepFunctionsTasks,
type aws_logs as Logs,
} from 'aws-cdk-lib'
Expand All @@ -27,7 +31,6 @@ import {
StateMachineType,
Succeed,
TaskInput,
type IStateMachine,
} from 'aws-cdk-lib/aws-stepfunctions'
import {
DynamoAttributeValue,
Expand All @@ -44,7 +47,7 @@ import type { DeviceStorage } from '../DeviceStorage.js'
* save the amount of data that needs to be transferred.
*/
export class MultiBundleFOTAFlow extends Construct {
public readonly stateMachine: IStateMachine
public readonly stateMachine: StateMachine
public readonly GetDeviceFirmwareDetails: PackedLambdaFn
public readonly GetNextBundle: PackedLambdaFn
public readonly CreateFOTAJob: PackedLambdaFn
Expand All @@ -53,6 +56,7 @@ export class MultiBundleFOTAFlow extends Construct {
public readonly WaitForUpdateAppliedCallback: PackedLambdaFn
public readonly WaitForUpdateApplied: PackedLambdaFn
public readonly startMultiBundleFOTAFlow: PackedLambdaFn
public readonly abortMultiBundleFOTAFlow: PackedLambdaFn

public constructor(
parent: Construct,
Expand Down Expand Up @@ -205,6 +209,9 @@ export class MultiBundleFOTAFlow extends Construct {
nextUpdateAt: DynamoAttributeValue.fromString(
JsonPath.stateEnteredTime,
),
parentJobId: DynamoAttributeValue.fromString(
JsonPath.executionName,
),
},
resultPath: '$.DynamoDB',
}),
Expand Down Expand Up @@ -437,7 +444,7 @@ export class MultiBundleFOTAFlow extends Construct {
})
deviceFOTA.nrfCloudJobStatusTable.grantReadWriteData(this.stateMachine)

const startMultiBundleFOTAFlow = new PackedLambdaFn(
this.startMultiBundleFOTAFlow = new PackedLambdaFn(
this,
'startMultiBundleFOTAFlow',
lambdas.start,
Expand All @@ -459,10 +466,9 @@ export class MultiBundleFOTAFlow extends Construct {
logGroup: deviceFOTA.logGroup,
},
)
this.startMultiBundleFOTAFlow = startMultiBundleFOTAFlow
this.stateMachine.grantStartExecution(startMultiBundleFOTAFlow.fn)
deviceStorage.devicesTable.grantReadData(startMultiBundleFOTAFlow.fn)
deviceFOTA.jobTable.grantWriteData(startMultiBundleFOTAFlow.fn)
this.stateMachine.grantStartExecution(this.startMultiBundleFOTAFlow.fn)
deviceStorage.devicesTable.grantReadData(this.startMultiBundleFOTAFlow.fn)
deviceFOTA.jobTable.grantWriteData(this.startMultiBundleFOTAFlow.fn)

this.WaitForFOTAJobCompletion = new PackedLambdaFn(
this,
Expand Down Expand Up @@ -556,6 +562,109 @@ export class MultiBundleFOTAFlow extends Construct {
principal: new IAM.ServicePrincipal('iot.amazonaws.com'),
sourceArn: waitForFirmwareVersionReportRule.attrArn,
})

// Users can abort the FOTA flow
this.abortMultiBundleFOTAFlow = new PackedLambdaFn(
this,
'abortMultiBundleFOTAFlow',
lambdas.abort,
{
description: 'REST entry point for aborting running FOTA flows',
environment: {
DEVICES_TABLE_NAME: deviceStorage.devicesTable.tableName,
STATE_MACHINE_ARN: this.stateMachine.stateMachineArn,
},
layers,
logGroup: deviceFOTA.logGroup,
initialPolicy: [
new IAM.PolicyStatement({
actions: ['states:DescribeExecution', 'states:StopExecution'],
resources: [
`arn:aws:states:${Stack.of(this).region}:${Stack.of(this).account}:execution:${this.stateMachine.stateMachineName}:*`,
],
}),
],
},
)
deviceStorage.devicesTable.grantReadData(this.abortMultiBundleFOTAFlow.fn)

// Handles failed or cancelled step function executions
const onStepFunctionFail = new PackedLambdaFn(
this,
'onFail',
lambdas.onFail,
{
description: 'Handles failed or cancelled step function executions',
environment: {
STATE_MACHINE_ARN: this.stateMachine.stateMachineArn,
JOB_TABLE_NAME: deviceFOTA.jobTable.tableName,
},
layers,
logGroup: deviceFOTA.logGroup,
},
)
deviceFOTA.jobTable.grantReadWriteData(onStepFunctionFail.fn)
new Events.Rule(this, 'onStepFunctionFailRule', {
eventPattern: {
source: ['aws.states'],
detail: {
status: ['FAILED', 'TIMED_OUT', 'ABORTED'],
stateMachineArn: [this.stateMachine.stateMachineArn],
},
},
targets: [new EventsTargets.LambdaFunction(onStepFunctionFail.fn)],
})

// Cancel the FOTA job on nRF Cloud if the step function is cancelled
const parentJobIdIndexName = 'parentJobIdIndex'
deviceFOTA.nrfCloudJobStatusTable.addGlobalSecondaryIndex({
indexName: parentJobIdIndexName,
partitionKey: {
name: 'parentJobId',
type: DynamoDB.AttributeType.STRING,
},
sortKey: {
name: 'jobId',
type: DynamoDB.AttributeType.STRING,
},
projectionType: DynamoDB.ProjectionType.INCLUDE,
nonKeyAttributes: ['status'],
})
const cancelFOTAJob = new PackedLambdaFn(
this,
'cancelFOTAJob',
lambdas.cancelFOTAJob,
{
description:
'Cancel the FOTA job on nRF Cloud if the step function is cancelled',
layers,
timeout: Duration.minutes(1),
logGroup: deviceFOTA.logGroup,
environment: {
NRF_CLOUD_JOB_STATUS_TABLE_NAME:
deviceFOTA.nrfCloudJobStatusTable.tableName,
NRF_CLOUD_JOB_STATUS_TABLE_PARENT_JOB_ID_INDEX_NAME:
parentJobIdIndexName,
},
},
)
deviceFOTA.nrfCloudJobStatusTable.grantReadData(cancelFOTAJob.fn)
cancelFOTAJob.fn.addEventSource(
new EventSources.DynamoEventSource(deviceFOTA.jobTable, {
startingPosition: Lambda.StartingPosition.LATEST,
filters: [
Lambda.FilterCriteria.filter({
dynamodb: {
NewImage: {
status: {
S: [FOTAJobStatus.FAILED],
},
},
},
}),
],
}),
)
}
}

Expand Down
Loading

0 comments on commit 95ed330

Please sign in to comment.