Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1. A, C, & E: ClearImageErrors, ImageError for Unknown Serials #103

Merged
merged 28 commits into from
Jul 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b9a97f2
Add ClearImages API
ingalls Jul 21, 2023
b5bdb83
Add ClearImageErrors Payloads
ingalls Jul 21, 2023
656ed47
Fix lints
ingalls Jul 21, 2023
6a2152e
Generate ImageError for unknown serials
ingalls Jul 21, 2023
c20f630
Merge pull request #104 from tnc-ca-geo/bobo-clear-unknown-serial
ingalls Jul 21, 2023
ab23bfa
Migrate to ES Module
ingalls Jul 21, 2023
4390aff
Fix Lints
ingalls Jul 21, 2023
96f2fb9
Finalize ES6 Refactor
ingalls Jul 24, 2023
c7406f6
Update mongoose to bugfixed version
ingalls Jul 24, 2023
0a19ac2
Switch to ECR Image Call
ingalls Jul 24, 2023
bf5b1a4
Merge branch 'bobo-clear-unknown-serial' into bobo-clear
ingalls Jul 24, 2023
c5155ad
Fix Lint Errors
ingalls Jul 24, 2023
1ee2468
Update use of __dirname in tests
ingalls Jul 24, 2023
9b42c85
Call deleteMany for ImageError
ingalls Jul 24, 2023
52333a8
Keep the linter happy
ingalls Jul 24, 2023
44ab59a
MVP of Batch Redrive Task
ingalls Jul 25, 2023
9ac082b
Flip Source/Dest
ingalls Jul 25, 2023
efde065
Add ability to clear BatchErrors
ingalls Jul 25, 2023
07fac45
Merge branch 'main' of github.com:tnc-ca-geo/animl-api into bobo-clear
ingalls Jul 26, 2023
0d589b6
Gracefully handle unknown DateTimeOriginals
ingalls Jul 27, 2023
a5abffd
Avoid lookup up non-existant Deployments when an error is created ear…
ingalls Jul 27, 2023
b9153c6
Fix timezone mapping
ingalls Jul 27, 2023
092ad4d
Populate Deployment.timezone
ingalls Jul 27, 2023
2552344
Ensure null deployments are omitted from API response
ingalls Jul 27, 2023
ac2b3e9
Fix ES Export for ML Fn
ingalls Jul 27, 2023
08a9d7a
Fix lints
ingalls Jul 27, 2023
4d93a3f
Fix export handler
ingalls Jul 27, 2023
4c11a1c
log inference predition results
nathanielrindlaub Jul 27, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
ref: ${{github.event.pull_request.head.sha || github.sha}}

- name: Docker API Build
run: docker build -t api .
run: docker build -t test -f Dockerfile-test .

- name: Docker API Test Run
run: docker run api
run: docker run test
17 changes: 2 additions & 15 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,18 +1,5 @@
FROM ubuntu:22.04
FROM public.ecr.aws/lambda/nodejs:18

ENV HOME=/home/animl
WORKDIR $HOME
COPY ./ $HOME/

RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y curl git

RUN export NODEV='18.16.0' \
&& curl "https://nodejs.org/dist/v${NODEV}/node-v${NODEV}-linux-x64.tar.gz" | tar -xzv \
&& cp ./node-v${NODEV}-linux-x64/bin/node /usr/bin/ \
&& ./node-v${NODEV}-linux-x64/bin/npm install -g npm
COPY . ${LAMBDA_TASK_ROOT}/

RUN npm install

CMD npm run lint && npm test

18 changes: 18 additions & 0 deletions Dockerfile-test
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
FROM ubuntu:22.04

ENV HOME=/home/animl
WORKDIR $HOME
COPY ./ $HOME/

RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y curl git

RUN export NODEV='18.16.0' \
&& curl "https://nodejs.org/dist/v${NODEV}/node-v${NODEV}-linux-x64.tar.gz" | tar -xzv \
&& cp ./node-v${NODEV}-linux-x64/bin/node /usr/bin/ \
&& ./node-v${NODEV}-linux-x64/bin/npm install -g npm

RUN npm install

CMD npm run lint && npm test

3,177 changes: 1,656 additions & 1,521 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"name": "animl-serverless",
"type": "module",
"version": "1.0.0",
"description": "Lambda-based graphQL API for camera trap data managment platform",
"main": "src/handler.js",
Expand Down Expand Up @@ -48,7 +49,7 @@
"lodash": "^4.17.20",
"luxon": "^3.2.1",
"mongo-cursor-pagination": "^8.1.2",
"mongodb-query-parser": "^2.4.3",
"mongodb-query-parser": "^3.0.0",
"mongoose": "^7.0.0",
"prompt": "^1.2.0",
"stream-transform": "^3.2.0",
Expand Down
24 changes: 18 additions & 6 deletions serverless.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
org: animl
app: animl
service: animl-api
# variablesResolutionMode: 20210326

plugins:
- serverless-offline
Expand All @@ -12,14 +11,19 @@ package:

provider:
name: aws
runtime: nodejs18.x
stage: ${opt:stage, 'dev'}
region: us-west-2
profile: animl
environment:
REGION: ${self:provider.region}
STAGE: ${self:provider.stage}
ACCOUNT: ${aws:accountId}

ecr:
images:
base:
path: .

iam:
role:
statements:
Expand Down Expand Up @@ -90,7 +94,9 @@ provider:

functions:
graphql:
handler: src/api/handler.server
image:
name: base
command: src/api/handler.server
events:
- http:
path: /
Expand All @@ -113,7 +119,9 @@ functions:
memorySize: 3008
timeout: 30
inference:
handler: src/ml/handler.inference
image:
name: base
command: src/ml/handler.inference
reservedConcurrency: 20
events:
- sqs:
Expand All @@ -125,11 +133,15 @@ functions:
functionResponseType: ReportBatchItemFailures
timeout: 120
batchinference:
handler: src/ml/handler.inference
image:
name: base
command: src/ml/handler.inference
reservedConcurrency: 8
timeout: 120
export:
handler: src/export/handler.export
image:
name: base
command: src/export/handler.handler
reservedConcurrency: 10
events:
- sqs:
Expand Down
4 changes: 2 additions & 2 deletions src/api/auth/authorization.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const jwt = require('jwt-simple');
import jwt from 'jwt-simple';

const BEARER_TOKEN_PATTERN = /^Bearer [-_=.0-9a-zA-Z]+$/i;

Expand Down Expand Up @@ -60,6 +60,6 @@ async function getUserInfo(req, config) {

}

module.exports = {
export {
getUserInfo
};
2 changes: 1 addition & 1 deletion src/api/auth/roles.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const WRITE_DEPLOYMENTS_ROLES = [MANAGER];
const WRITE_AUTOMATION_RULES_ROLES = [MANAGER];
const WRITE_CAMERA_REGISTRATION_ROLES = [MANAGER];

module.exports = {
export {
EXPORT_DATA_ROLES,
WRITE_OBJECTS_ROLES,
WRITE_VIEWS_ROLES,
Expand Down
11 changes: 8 additions & 3 deletions src/api/db/connect.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
const mongoose = require('mongoose');
const { ApolloError } = require('apollo-server-errors');
import mongoose from 'mongoose';
import { ApolloError } from 'apollo-server-errors';

// TODO: consider using multiple connections (one per model)
// to reduce risk of slow trains
// https://mongoosejs.com/docs/connections.html#multiple_connections
// https://thecodebarbarian.com/slow-trains-in-mongodb-and-nodejs

let cachedConnectionPromise = null;

module.exports.connectToDatabase = async function connectToDb(config) {
async function connectToDatabase(config) {
if (!cachedConnectionPromise) {
// If no connection promise is cached, create a new one.
// We cache the promise instead of the connection itself to prevent race
Expand All @@ -26,4 +27,8 @@ module.exports.connectToDatabase = async function connectToDb(config) {
} catch (err) {
throw new ApolloError(err);
}
}

export {
connectToDatabase
};
87 changes: 59 additions & 28 deletions src/api/db/models/Batch.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
const { ApolloError, ForbiddenError } = require('apollo-server-errors');
const MongoPaging = require('mongo-cursor-pagination');
const { WRITE_IMAGES_ROLES } = require('../../auth/roles');
const { randomUUID } = require('crypto');
const S3 = require('@aws-sdk/client-s3');
const SQS = require('@aws-sdk/client-sqs');
const Lambda = require('@aws-sdk/client-lambda');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
const Batch = require('../schemas/Batch');
const BatchError = require('../schemas/BatchError');
const retry = require('async-retry');
const utils = require('./utils');
import { ApolloError, ForbiddenError } from 'apollo-server-errors';
import MongoPaging from 'mongo-cursor-pagination';
import { WRITE_IMAGES_ROLES } from '../../auth/roles.js';
import { randomUUID } from 'node:crypto';
import S3 from '@aws-sdk/client-s3';
import SQS from '@aws-sdk/client-sqs';
import Lambda from '@aws-sdk/client-lambda';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import Batch from '../schemas/Batch.js';
import BatchError from '../schemas/BatchError.js';
import retry from 'async-retry';
import { hasRole } from './utils.js';

const generateBatchModel = ({ user } = {}) => ({
queryByFilter: async (input) => {
Expand Down Expand Up @@ -90,22 +90,19 @@ const generateBatchModel = ({ user } = {}) => ({
},

get stopBatch() {
if (!utils.hasRole(user, WRITE_IMAGES_ROLES)) throw new ForbiddenError;
if (!hasRole(user, WRITE_IMAGES_ROLES)) throw new ForbiddenError;

return async (input) => {
const operation = async (input) => {
return await retry(async (bail, attempt) => {
if (attempt > 1) {
console.log(`Retrying updateObject operation! Try #: ${attempt}`);
}
if (attempt > 1) console.log(`Retrying stopBatch operation! Try #: ${attempt}`);

return await this.queryById(input._id);
}, { retries: 2 });
};

try {
const batch = await operation({
_id: input.batch
});
const batch = await operation({ _id: input.batch });
if (batch.processingEnd) throw new Error('Stack has already terminated');

const lambda = new Lambda.LambdaClient({ region: process.env.REGION });
Expand All @@ -118,9 +115,44 @@ const generateBatchModel = ({ user } = {}) => ({
})
}));

return {
message: 'Batch Scheduled for Deletion'
};
return { message: 'Batch Scheduled for Deletion' };
} catch (err) {
console.error(err);
// if error is uncontrolled, throw new ApolloError
if (err instanceof ApolloError) throw err;
throw new ApolloError(err);
}
};
},

get redriveBatch() {
if (!hasRole(user, WRITE_IMAGES_ROLES)) throw new ForbiddenError;

return async (input) => {
const operation = async (input) => {
return await retry(async (bail, attempt) => {
if (attempt > 1) console.log(`Retrying redriveBatch operation! Try #: ${attempt}`);

// Ensure the batch actually exists
const batch = await this.queryById(input.batch);
if (batch.processingEnd) throw new Error('Stack has already terminated');

const sqs = new SQS.SQSClient({ region: process.env.REGION });

await sqs.send(new SQS.StartMessageMoveTaskCommand({
SourceArn: `arn:aws:sqs:${process.env.REGION}:${process.env.ACCOUNT}:animl-ingest-${process.env.STAGE}-${batch._id}-dlq`,
DestinationArn: `arn:aws:sqs:${process.env.REGION}:${process.env.ACCOUNT}:animl-ingest-${process.env.STAGE}-${batch._id}`
}));

return batch;

}, { retries: 2 });
};

try {
await operation(input);

return { message: 'Batch Redrive Initiated' };
} catch (err) {
console.error(err);
// if error is uncontrolled, throw new ApolloError
Expand All @@ -131,14 +163,13 @@ const generateBatchModel = ({ user } = {}) => ({
},

get updateBatch() {
if (!utils.hasRole(user, WRITE_IMAGES_ROLES)) throw new ForbiddenError;
if (!hasRole(user, WRITE_IMAGES_ROLES)) throw new ForbiddenError;

return async (input) => {
const operation = async (input) => {
return await retry(async (bail, attempt) => {
if (attempt > 1) {
console.log(`Retrying updateObject operation! Try #: ${attempt}`);
}
if (attempt > 1) console.log(`Retrying updateBatch operation! Try #: ${attempt}`);

// find image, apply object updates, and save
const batch = await this.queryById(input._id);

Expand All @@ -162,7 +193,7 @@ const generateBatchModel = ({ user } = {}) => ({
},

get createUpload() {
if (!utils.hasRole(user, WRITE_IMAGES_ROLES)) throw new ForbiddenError;
if (!hasRole(user, WRITE_IMAGES_ROLES)) throw new ForbiddenError;

return async (input) => {
const operation = async (input) => {
Expand Down Expand Up @@ -208,4 +239,4 @@ const generateBatchModel = ({ user } = {}) => ({
}
});

module.exports = generateBatchModel;
export default generateBatchModel;
38 changes: 31 additions & 7 deletions src/api/db/models/BatchError.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
const { ApolloError, ForbiddenError } = require('apollo-server-errors');
const { WRITE_IMAGES_ROLES } = require('../../auth/roles');
const BatchError = require('../schemas/BatchError');
const retry = require('async-retry');
const utils = require('./utils');
import { ApolloError, ForbiddenError } from 'apollo-server-errors';
import { WRITE_IMAGES_ROLES } from '../../auth/roles.js';
import BatchError from '../schemas/BatchError.js';
import retry from 'async-retry';
import { hasRole } from './utils.js';

const generateBatchErrorModel = ({ user } = {}) => ({
get createError() {
if (!utils.hasRole(user, WRITE_IMAGES_ROLES)) throw new ForbiddenError;
if (!hasRole(user, WRITE_IMAGES_ROLES)) throw new ForbiddenError;

return async (input) => {
const operation = async (input) => {
Expand Down Expand Up @@ -35,7 +35,31 @@ const generateBatchErrorModel = ({ user } = {}) => ({
throw new ApolloError(err);
}
};
},

get clearErrors() {
if (!hasRole(user, WRITE_IMAGES_ROLES)) throw new ForbiddenError;

return async (input) => {
const operation = async (input) => {
return await retry(async () => {
return await BatchError.deleteMany(input);
}, { retries: 2 });
};

try {
await operation({
batch: input.batch
});

return { message: 'Cleared' };
} catch (err) {
// if error is uncontrolled, throw new ApolloError
if (err instanceof ApolloError) throw err;
throw new ApolloError(err);
}
};
}
});

module.exports = generateBatchErrorModel;
export default generateBatchErrorModel;
16 changes: 7 additions & 9 deletions src/api/db/models/Camera.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
const { ApolloError, ForbiddenError } = require('apollo-server-errors');
const { CameraRegistrationError } = require('../../errors');
const WirelessCamera = require('../schemas/WirelessCamera');
const retry = require('async-retry');
const { WRITE_CAMERA_REGISTRATION_ROLES } = require('../../auth/roles');
const { hasRole, idMatch } = require('./utils');

import { ApolloError, ForbiddenError } from 'apollo-server-errors';
import { CameraRegistrationError } from '../../errors.js';
import WirelessCamera from '../schemas/WirelessCamera.js';
import retry from 'async-retry';
import { WRITE_CAMERA_REGISTRATION_ROLES } from '../../auth/roles.js';
import { hasRole, idMatch } from './utils.js';

const generateCameraModel = ({ user } = {}) => ({

getWirelessCameras: async (_ids) => {
const query = _ids ? { _id: { $in: _ids } } : {};
// if user has curr_project, limit returned cameras to those that
Expand Down Expand Up @@ -220,4 +218,4 @@ const generateCameraModel = ({ user } = {}) => ({

});

module.exports = generateCameraModel;
export default generateCameraModel;
Loading
Loading