Skip to content

Commit 7557300

Browse files
authored
Setup infracost (#264)
1 parent 0d21931 commit 7557300

File tree

7 files changed

+1121
-80
lines changed

7 files changed

+1121
-80
lines changed

.github/workflows/infracost.yml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: Generate Infra cost report
2+
on:
3+
pull_request:
4+
types: [opened, synchronize, closed]
5+
push:
6+
branches:
7+
- main
8+
9+
env:
10+
SSH_AUTH_SOCK: /tmp/ssh_agent.sock
11+
jobs:
12+
infracost-pull-request-checks:
13+
name: Infracost Pull Request Checks
14+
if: github.event_name == 'pull_request' && (github.event.action == 'opened' || github.event.action == 'synchronize')
15+
runs-on: ubuntu-latest
16+
permissions:
17+
contents: read
18+
pull-requests: write # Required to post comments
19+
steps:
20+
- name: Setup Infracost
21+
uses: infracost/actions/setup@v3
22+
with:
23+
api-key: ${{ secrets.INFRACOST_API_KEY }}
24+
25+
- name: Checkout base branch
26+
uses: actions/checkout@v4
27+
with:
28+
ref: '${{ github.event.pull_request.base.ref }}'
29+
30+
- name: Generate Infracost cost estimate baseline
31+
run: |
32+
infracost breakdown --path=terraform/ \
33+
--format=json \
34+
--usage-file infracost-usage.yml \
35+
--out-file=/tmp/infracost-base.json
36+
37+
- name: Checkout PR branch
38+
uses: actions/checkout@v4
39+
40+
- name: Generate Infracost diff
41+
run: |
42+
infracost diff --path=terraform/ \
43+
--format=json \
44+
--usage-file infracost-usage.yml \
45+
--compare-to=/tmp/infracost-base.json \
46+
--out-file=/tmp/infracost.json
47+
48+
- name: Post Infracost comment
49+
run: |
50+
infracost comment github --path=/tmp/infracost.json \
51+
--repo=$GITHUB_REPOSITORY \
52+
--github-token=${{ github.token }} \
53+
--pull-request=${{ github.event.pull_request.number }} \
54+
--behavior=update

infracost-usage.yml

Lines changed: 982 additions & 0 deletions
Large diffs are not rendered by default.

src/api/routes/events.ts

Lines changed: 30 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ const eventsPlugin: FastifyPluginAsyncZodOpenApi = async (
318318
schema: withRoles(
319319
[AppRoles.EVENTS_MANAGER],
320320
withTags(["Events"], {
321-
body: postRequestSchema.partial(),
321+
body: postRequestSchema,
322322
params: z.object({
323323
id: z.string().min(1).meta({
324324
description: "Event ID to modify.",
@@ -327,12 +327,7 @@ const eventsPlugin: FastifyPluginAsyncZodOpenApi = async (
327327
}),
328328
response: {
329329
201: {
330-
description: "The event has been modified.",
331-
content: {
332-
"application/json": {
333-
schema: z.null(),
334-
},
335-
},
330+
description: "The event has been modified successfully.",
336331
},
337332
404: notFoundError,
338333
},
@@ -345,64 +340,29 @@ const eventsPlugin: FastifyPluginAsyncZodOpenApi = async (
345340
if (!request.username) {
346341
throw new UnauthenticatedError({ message: "Username not found." });
347342
}
343+
348344
try {
349-
const updatableFields = Object.keys(postRequestSchema.shape);
350345
const entryUUID = request.params.id;
351-
const requestData = request.body;
352-
353-
const setParts: string[] = [];
354-
const removeParts: string[] = [];
355-
const expressionAttributeNames: Record<string, string> = {};
356-
const expressionAttributeValues: Record<string, any> = {};
357-
358-
setParts.push("#updatedAt = :updatedAt");
359-
expressionAttributeNames["#updatedAt"] = "updatedAt";
360-
expressionAttributeValues[":updatedAt"] = new Date().toISOString();
361-
362-
updatableFields.forEach((key) => {
363-
if (Object.hasOwn(requestData, key)) {
364-
setParts.push(`#${key} = :${key}`);
365-
expressionAttributeNames[`#${key}`] = key;
366-
expressionAttributeValues[`:${key}`] =
367-
requestData[key as keyof typeof requestData];
368-
} else {
369-
removeParts.push(`#${key}`);
370-
expressionAttributeNames[`#${key}`] = key;
371-
}
372-
});
373-
374-
// Construct the final UpdateExpression by combining SET and REMOVE
375-
let updateExpression = `SET ${setParts.join(", ")}`;
376-
if (removeParts.length > 0) {
377-
updateExpression += ` REMOVE ${removeParts.join(", ")}`;
378-
}
346+
const updatedItem = {
347+
...request.body,
348+
id: entryUUID,
349+
updatedAt: new Date().toISOString(),
350+
};
379351

380-
const command = new UpdateItemCommand({
352+
const command = new PutItemCommand({
381353
TableName: genericConfig.EventsDynamoTableName,
382-
Key: { id: { S: entryUUID } },
383-
UpdateExpression: updateExpression,
384-
ExpressionAttributeNames: expressionAttributeNames,
354+
Item: marshall(updatedItem),
385355
ConditionExpression: "attribute_exists(id)",
386-
ExpressionAttributeValues: marshall(expressionAttributeValues),
387356
ReturnValues: "ALL_OLD",
388357
});
358+
389359
let oldAttributes;
390-
let updatedEntry;
391360
try {
392-
oldAttributes = (await fastify.dynamoClient.send(command)).Attributes;
393-
361+
const result = await fastify.dynamoClient.send(command);
362+
oldAttributes = result.Attributes;
394363
if (!oldAttributes) {
395-
throw new DatabaseInsertError({
396-
message: "Item not found or update failed.",
397-
});
364+
throw new NotFoundError({ endpointName: request.url });
398365
}
399-
400-
const oldEntry = oldAttributes ? unmarshall(oldAttributes) : null;
401-
// we know updateData has no undefines because we filtered them out.
402-
updatedEntry = {
403-
...oldEntry,
404-
...requestData,
405-
} as unknown as IUpdateDiscord;
406366
} catch (e: unknown) {
407367
if (
408368
e instanceof Error &&
@@ -414,16 +374,24 @@ const eventsPlugin: FastifyPluginAsyncZodOpenApi = async (
414374
throw e;
415375
}
416376
request.log.error(e);
417-
throw new DiscordEventError({});
377+
throw new DatabaseInsertError({
378+
message: "Failed to update event in Dynamo table.",
379+
});
418380
}
419-
if (updatedEntry.featured && !updatedEntry.repeats) {
381+
382+
const updatedEntryForDiscord = updatedItem as unknown as IUpdateDiscord;
383+
384+
if (
385+
updatedEntryForDiscord.featured &&
386+
!updatedEntryForDiscord.repeats
387+
) {
420388
try {
421389
await updateDiscord(
422390
{
423391
botToken: fastify.secretConfig.discord_bot_token,
424392
guildId: fastify.environmentConfig.DiscordGuildId,
425393
},
426-
updatedEntry,
394+
updatedEntryForDiscord,
427395
request.username,
428396
false,
429397
request.log,
@@ -437,10 +405,11 @@ const eventsPlugin: FastifyPluginAsyncZodOpenApi = async (
437405
);
438406

439407
if (e instanceof Error) {
440-
request.log.error(`Failed to publish event to Discord: ${e} `);
408+
request.log.error(`Failed to publish event to Discord: ${e}`);
441409
}
442410
}
443411
}
412+
444413
const postUpdatePromises = [
445414
atomicIncrementCacheCounter(
446415
fastify.dynamoClient,
@@ -466,14 +435,8 @@ const eventsPlugin: FastifyPluginAsyncZodOpenApi = async (
466435
}),
467436
];
468437
await Promise.all(postUpdatePromises);
469-
470-
reply
471-
.status(201)
472-
.header(
473-
"Location",
474-
`${fastify.environmentConfig.UserFacingUrl}/api/v1/events/${entryUUID}`,
475-
)
476-
.send();
438+
reply.header("location", request.url);
439+
reply.status(201).send();
477440
} catch (e: unknown) {
478441
if (e instanceof Error) {
479442
request.log.error(`Failed to update DynamoDB: ${e.toString()}`);
@@ -487,6 +450,7 @@ const eventsPlugin: FastifyPluginAsyncZodOpenApi = async (
487450
}
488451
},
489452
);
453+
490454
fastify.withTypeProvider<FastifyZodOpenApiTypeProvider>().post(
491455
"",
492456
{

terraform/modules/frontend/main.tf

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,40 @@ resource "aws_s3_bucket" "frontend" {
22
bucket = "${var.BucketPrefix}-${var.ProjectId}"
33
}
44

5+
resource "aws_s3_bucket_lifecycle_configuration" "frontend" {
6+
bucket = aws_s3_bucket.frontend.id
7+
8+
rule {
9+
id = "AbortIncompleteMultipartUploads"
10+
status = "Enabled"
11+
12+
abort_incomplete_multipart_upload {
13+
days_after_initiation = 1
14+
}
15+
}
16+
17+
rule {
18+
id = "ObjectLifecycle"
19+
status = "Enabled"
20+
21+
filter {}
22+
23+
transition {
24+
days = 30
25+
storage_class = "INTELLIGENT_TIERING"
26+
}
27+
28+
noncurrent_version_transition {
29+
noncurrent_days = 30
30+
storage_class = "STANDARD_IA"
31+
}
32+
33+
noncurrent_version_expiration {
34+
noncurrent_days = 60
35+
}
36+
}
37+
}
38+
539
data "archive_file" "ui" {
640
type = "zip"
741
source_dir = "${path.module}/../../../dist_ui/"

terraform/modules/lambdas/main.tf

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -418,13 +418,13 @@ resource "aws_lambda_function_url" "slow_api_lambda_function_url" {
418418
}
419419

420420
module "lambda_warmer_main" {
421-
source = "github.com/acm-uiuc/terraform-modules/lambda-warmer?ref=b52f22e32c6c07af9b1b4750a226882aaccc769d"
421+
source = "git::https://github.com/acm-uiuc/terraform-modules.git//lambda-warmer?ref=b52f22e32c6c07af9b1b4750a226882aaccc769d"
422422
function_to_warm = local.core_api_lambda_name
423423
is_streaming_lambda = true
424424
}
425425

426426
module "lambda_warmer_slow" {
427-
source = "github.com/acm-uiuc/terraform-modules/lambda-warmer?ref=b52f22e32c6c07af9b1b4750a226882aaccc769d"
427+
source = "git::https://github.com/acm-uiuc/terraform-modules.git//lambda-warmer?ref=b52f22e32c6c07af9b1b4750a226882aaccc769d"
428428
function_to_warm = local.core_api_slow_lambda_name
429429
is_streaming_lambda = true
430430
}

tests/live/events.test.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,14 @@ describe("Event lifecycle tests", async () => {
129129
"Content-Type": "application/json",
130130
},
131131
body: JSON.stringify({
132-
description: "An event of all time THAT HAS BEEN MODIFIED",
132+
title: "Modified Live Testing Event",
133+
description: "An event of all time",
134+
start: "2024-12-31T02:00:00",
135+
end: "2024-12-31T03:30:00",
136+
location: "ACM Room (Siebel 1104)",
137+
host: "ACM",
138+
featured: true,
139+
repeats: "weekly",
133140
}),
134141
},
135142
);
@@ -154,9 +161,7 @@ describe("Event lifecycle tests", async () => {
154161
expect(response.status).toBe(200);
155162
expect(responseJson).toHaveProperty("id");
156163
expect(responseJson).toHaveProperty("description");
157-
expect(responseJson["description"]).toStrictEqual(
158-
"An event of all time THAT HAS BEEN MODIFIED",
159-
);
164+
expect(responseJson["title"]).toStrictEqual("Modified Live Testing Event");
160165
});
161166

162167
test("deleting a previously-created event", { timeout: 30000 }, async () => {

tests/unit/eventPost.test.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -498,9 +498,9 @@ describe("Event modification tests", async () => {
498498
const ourError = new Error("Nonexistent event.");
499499
ourError.name = "ConditionalCheckFailedException";
500500
ddbMock
501-
.on(UpdateItemCommand, {
501+
.on(PutItemCommand, {
502502
TableName: genericConfig.EventsDynamoTableName,
503-
Key: { id: { S: eventUuid } },
503+
Item: { id: { S: eventUuid } },
504504
})
505505
.rejects(ourError);
506506
const testJwt = createJwt();
@@ -509,7 +509,12 @@ describe("Event modification tests", async () => {
509509
.patch(`/api/v1/events/${eventUuid}`)
510510
.set("authorization", `Bearer ${testJwt}`)
511511
.send({
512-
paidEventId: "sp24_semiformal_2",
512+
description: "First test event",
513+
host: "Social Committee",
514+
location: "Siebel Center",
515+
start: "2024-09-25T18:00:00",
516+
title: "Event 1",
517+
featured: false,
513518
});
514519

515520
expect(response.statusCode).toBe(404);
@@ -531,19 +536,16 @@ describe("Event modification tests", async () => {
531536
};
532537
ddbMock.reset();
533538
ddbMock
534-
.on(UpdateItemCommand, {
539+
.on(PutItemCommand, {
535540
TableName: genericConfig.EventsDynamoTableName,
536-
Key: { id: { S: eventUuid } },
537541
})
538542
.resolves({ Attributes: marshall(event) });
539543
const testJwt = createJwt();
540544
await app.ready();
541545
const response = await supertest(app.server)
542546
.patch(`/api/v1/events/${eventUuid}`)
543547
.set("authorization", `Bearer ${testJwt}`)
544-
.send({
545-
paidEventId: "sp24_semiformal_2",
546-
});
548+
.send(event);
547549

548550
expect(response.statusCode).toBe(201);
549551
expect(response.header["location"]).toBeDefined();

0 commit comments

Comments
 (0)