mirror of
https://github.com/Sea-Haven-Industries/afterhours-shift-manager.git
synced 2026-10-07 16:19:00 +00:00
Compare commits
2 commits
26e9716df4
...
c611fd5d2b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c611fd5d2b | ||
|
|
df3f0c037d |
12 changed files with 955 additions and 320 deletions
24
.github/workflows/ci.yaml
vendored
24
.github/workflows/ci.yaml
vendored
|
|
@ -62,9 +62,29 @@ jobs:
|
|||
- name: Terraform validate
|
||||
run: terraform validate
|
||||
|
||||
openapi:
|
||||
name: OpenAPI
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: "24.19.0"
|
||||
cache: npm
|
||||
|
||||
- name: Install JavaScript tooling
|
||||
run: npm ci
|
||||
|
||||
- name: Lint OpenAPI
|
||||
run: npm run openapi:lint
|
||||
|
||||
ci:
|
||||
name: ci / ci
|
||||
needs: [pytest, terraform]
|
||||
needs: [pytest, terraform, openapi]
|
||||
if: ${{ always() && !cancelled() }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
|
|
@ -73,6 +93,7 @@ jobs:
|
|||
env:
|
||||
PYTEST_RESULT: ${{ needs.pytest.result }}
|
||||
TERRAFORM_RESULT: ${{ needs.terraform.result }}
|
||||
OPENAPI_RESULT: ${{ needs.openapi.result }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
fail=0
|
||||
|
|
@ -91,4 +112,5 @@ jobs:
|
|||
}
|
||||
check pytest "${PYTEST_RESULT}"
|
||||
check terraform "${TERRAFORM_RESULT}"
|
||||
check openapi "${OPENAPI_RESULT}"
|
||||
exit "${fail}"
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
|||
node_modules/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.aws-sam/
|
||||
|
|
|
|||
32
.redocly.yaml
Normal file
32
.redocly.yaml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# Same Redocly recommended ruleset as internal-portal (DEV-223 / DEV-289).
|
||||
# Recommended operation-2xx-response does not count 302. Login-style redirects
|
||||
# succeed with 302, so that rule is replaced by operation-2xx-or-3xx-response
|
||||
# at error. operation-4xx-response is promoted to error so missing 4xx fails CI.
|
||||
extends:
|
||||
- recommended
|
||||
|
||||
rules:
|
||||
operation-2xx-response: off
|
||||
operation-4xx-response: error
|
||||
rule/operation-2xx-or-3xx-response:
|
||||
subject:
|
||||
type: Responses
|
||||
message: Operation must define a 2XX or 3XX response.
|
||||
severity: error
|
||||
assertions:
|
||||
requireAny:
|
||||
- "200"
|
||||
- "201"
|
||||
- "202"
|
||||
- "204"
|
||||
- "301"
|
||||
- "302"
|
||||
- "303"
|
||||
- "307"
|
||||
- "308"
|
||||
- "2XX"
|
||||
- "3XX"
|
||||
|
||||
apis:
|
||||
afterhours@v1:
|
||||
root: openapi.yaml
|
||||
31
README.md
31
README.md
|
|
@ -59,15 +59,19 @@ Example: `/oncall admin holiday add 2026-07-04 2 x2 Independence Day` schedules
|
|||
## Architecture
|
||||
|
||||
- **Runtime**: Python 3.12 on ECS Fargate (arm64) plus dual-run Lambdas until cutover. seahaven-prod `011934824531`, seahaven-dev `710827005802`
|
||||
- **VPC**: This stack owns `10.70.0.0/16`. Meals and portal Fargate attach with HCP `existing_vpc_id` / `existing_public_subnet_ids` from outputs `vpc_id` and `public_subnet_ids`.
|
||||
- **HTTP contract**: `openapi.yaml`, linted in CI with `npm run openapi:lint` (Redocly `extends: recommended`, same as internal-portal and meal-order-manager).
|
||||
- **Data**: DynamoDB single-table (`afterhours-shifts`)
|
||||
- **IaC**: HCP Terraform workspaces tagged `app:afterhours-shift-manager` (`afterhours-shift-manager-dev` / `-prod`) plus GitHub Actions `deploy-api.yaml` (image). Terraform does not package `src/`.
|
||||
- **Slack**: Slack Bolt on `POST /slack/events`
|
||||
- **3CX Integration**: Queue routing updated directly via 3CX Queue XAPI
|
||||
- **Secrets**: AWS Secrets Manager (`afterhours-shift-manager/*`)
|
||||
|
||||
### Lambda Functions
|
||||
### Leftover zip handlers (packaging only)
|
||||
|
||||
| Function | Trigger | Purpose |
|
||||
Fargate is the live path. These `src/*/app.py` zip sources remain for `scripts/package_lambdas.py` only. Leftover Lambda IAM roles were removed after Paychex dropped AfterhoursWeeklyPostSend (PLAT-218).
|
||||
|
||||
| Handler | Former trigger | Purpose |
|
||||
|---|---|---|
|
||||
| `afterhours-shift-manager` | API Gateway (POST /slack/events) | Slack bot — handles `/oncall` commands and interactive buttons |
|
||||
| `afterhours-weekly-post` | EventBridge (Monday 7am ET) | Posts weekly schedule to Slack, sends pay report email |
|
||||
|
|
@ -81,17 +85,17 @@ Example: `/oncall admin holiday add 2026-07-04 2 x2 Independence Day` schedules
|
|||
```
|
||||
src/
|
||||
server/ Flask + gunicorn + SQS worker (Fargate)
|
||||
slack-bot/ Slack Bolt app; Lambda handler until cutover; ships CHANGELOG.md for App Home
|
||||
weekly-post/ Monday schedule + pay post
|
||||
roster-sync/ Daily 3CX roster sync
|
||||
roster-api/ HTTP PUT/DELETE /roster for identity hire/offboard
|
||||
portal-api/ Cognito employee/admin shift API for the internal portal
|
||||
ring-scheduler/ 3CX queue routing updates
|
||||
holiday-router/ 3CX IVR/queue repoint for holiday day shifts (activate/deactivate)
|
||||
shared/ Bundled into each function zip and the Fargate image
|
||||
terraform/ HCP Terraform (Lambda skeletons, ECS/ALB, API, DDB, IAM, schedules)
|
||||
slack-bot/ Slack Bolt app; leftover zip source for packaging; ships CHANGELOG.md for App Home
|
||||
weekly-post/ Monday schedule + pay post (leftover zip source for packaging)
|
||||
roster-sync/ Daily 3CX roster sync (leftover zip source for packaging)
|
||||
roster-api/ HTTP PUT/DELETE /roster for identity hire/offboard (leftover zip source for packaging)
|
||||
portal-api/ Cognito employee/admin shift API for the internal portal (leftover zip source for packaging)
|
||||
ring-scheduler/ 3CX queue routing updates (leftover zip source for packaging)
|
||||
holiday-router/ 3CX IVR/queue repoint for holiday day shifts (leftover zip source for packaging)
|
||||
shared/ Bundled into leftover function zips and the Fargate image
|
||||
terraform/ HCP Terraform (ECS/ALB, API, DDB, IAM, leftover zip packaging locals, schedules)
|
||||
scripts/ in-package changelog copy sync + cutover
|
||||
tests/ pytest suite (mirrors src/, one dir per Lambda + shared + server)
|
||||
tests/ pytest suite (mirrors src/, one dir per leftover zip handler + shared + server)
|
||||
```
|
||||
|
||||
### DynamoDB Schema
|
||||
|
|
@ -299,7 +303,8 @@ pytest
|
|||
|
||||
Each Lambda has its own `app.py`, so the per-package `conftest.py` loads each one
|
||||
under a unique module name (importlib mode) to avoid collisions. CI runs the same
|
||||
suite on every PR via pytest plus `terraform fmt` / `init -backend=false` / `validate`.
|
||||
suite on every PR via pytest plus `terraform fmt` / `init -backend=false` /
|
||||
`validate` and `npm run openapi:lint`.
|
||||
|
||||
Local Fargate process (needs the same DynamoDB table and Secrets Manager names
|
||||
the Lambdas use, plus `JOBS_QUEUE_URL` to consume jobs):
|
||||
|
|
|
|||
5
SETUP.md
5
SETUP.md
|
|
@ -115,9 +115,8 @@ and `ecs_schedules_enabled=false` until the Fargate cutover below.
|
|||
|
||||
HCP outputs to copy: `slack_request_url`, `api_origin`, `fargate_origin`,
|
||||
`holiday_scheduler_role_arn`, `github_deploy_role_arn`, `jobs_queue_arn`,
|
||||
`ecs_task_role_arn`. Add `ecs_task_role_arn` to paychex-checkcomponents
|
||||
(alongside `afterhours-shift-manager-weekly-post`) before Fargate weekly_post
|
||||
runs. Dual-run keeps the Lambda principal until zip CD is retired.
|
||||
`ecs_task_role_arn`. Paychex checkcomponents allows `afterhours-shift-manager-api`;
|
||||
leftover weekly-post principal is gone.
|
||||
|
||||
## 4. Set the Slack Request URL
|
||||
|
||||
|
|
|
|||
771
openapi.yaml
Normal file
771
openapi.yaml
Normal file
|
|
@ -0,0 +1,771 @@
|
|||
openapi: 3.1.0
|
||||
info:
|
||||
title: After Hours Shift Manager
|
||||
version: 0.1.0
|
||||
description: >
|
||||
Flask HTTP API on ECS Fargate. Slack `/oncall` stays on POST /slack/events
|
||||
and is not part of this contract. The internal portal SPA calls /api/shifts
|
||||
with a Cognito ID token from GET /api/auth/meals-token. Paychex calls
|
||||
PUT/DELETE /roster with a shared bearer token. Error shape for portal
|
||||
routes is `{ error: { code, message } }`. Health matches the portal BFF
|
||||
`{ stage, sha }`. Lint with the same Redocly `extends: recommended` config
|
||||
as internal-portal and meal-order-manager.
|
||||
contact:
|
||||
name: Sea Haven Engineering
|
||||
license:
|
||||
name: Proprietary
|
||||
identifier: LicenseRef-SeaHaven
|
||||
|
||||
servers:
|
||||
- url: /
|
||||
description: After Hours ALB origin (VITE_SHIFTS_API_BASE)
|
||||
|
||||
tags:
|
||||
- name: Runtime
|
||||
description: Unauthenticated health
|
||||
- name: Roster
|
||||
description: Paychex hire and offboard
|
||||
- name: Shifts
|
||||
description: Employee and admin After Hours for the portal SPA
|
||||
|
||||
security: []
|
||||
|
||||
paths:
|
||||
/api/health:
|
||||
get:
|
||||
operationId: getHealth
|
||||
tags: [Runtime]
|
||||
summary: Runtime health
|
||||
security: []
|
||||
responses:
|
||||
"200":
|
||||
description: Process is up
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Health"
|
||||
"403":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
|
||||
/roster:
|
||||
put:
|
||||
operationId: putRoster
|
||||
tags: [Roster]
|
||||
summary: Upsert a roster row
|
||||
security:
|
||||
- rosterBearer: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/RosterUpsert"
|
||||
responses:
|
||||
"200":
|
||||
description: Row written
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/RosterOk"
|
||||
"400":
|
||||
$ref: "#/components/responses/RosterError"
|
||||
"401":
|
||||
$ref: "#/components/responses/RosterError"
|
||||
"503":
|
||||
$ref: "#/components/responses/RosterError"
|
||||
|
||||
/roster/{extension}:
|
||||
delete:
|
||||
operationId: deleteRoster
|
||||
tags: [Roster]
|
||||
summary: Remove a roster row
|
||||
security:
|
||||
- rosterBearer: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Extension"
|
||||
responses:
|
||||
"204":
|
||||
description: Gone, including when the row was already missing
|
||||
"400":
|
||||
$ref: "#/components/responses/RosterError"
|
||||
"401":
|
||||
$ref: "#/components/responses/RosterError"
|
||||
"503":
|
||||
$ref: "#/components/responses/RosterError"
|
||||
|
||||
/api/shifts:
|
||||
get:
|
||||
operationId: getShifts
|
||||
tags: [Shifts]
|
||||
summary: Week snapshot for the signed-in employee
|
||||
security:
|
||||
- portalCognito: []
|
||||
parameters:
|
||||
- name: week
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
enum: [this, next]
|
||||
default: this
|
||||
responses:
|
||||
"200":
|
||||
description: Linked snapshot or unlinked Google account
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ShiftsSnapshot"
|
||||
"401":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
"503":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
|
||||
/api/shifts/pick:
|
||||
post:
|
||||
operationId: pickShift
|
||||
tags: [Shifts]
|
||||
summary: Pick up a shift or request late pickup
|
||||
security:
|
||||
- portalCognito: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ShiftDateBody"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/MutationOk"
|
||||
"400":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
"401":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
"404":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
"409":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
"503":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
|
||||
/api/shifts/drop:
|
||||
post:
|
||||
operationId: dropShift
|
||||
tags: [Shifts]
|
||||
summary: Drop a held shift
|
||||
security:
|
||||
- portalCognito: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ShiftDateBody"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/MutationOk"
|
||||
"400":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
"401":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
"404":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
"409":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
|
||||
/api/shifts/swap:
|
||||
post:
|
||||
operationId: requestSwap
|
||||
tags: [Shifts]
|
||||
summary: Request a swap
|
||||
security:
|
||||
- portalCognito: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/SwapBody"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/MutationOk"
|
||||
"400":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
"401":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
"404":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
"409":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
|
||||
/api/shifts/swaps/{date}/{shiftType}/accept:
|
||||
post:
|
||||
operationId: acceptSwap
|
||||
tags: [Shifts]
|
||||
summary: Accept a pending swap
|
||||
security:
|
||||
- portalCognito: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ShiftDate"
|
||||
- $ref: "#/components/parameters/ShiftType"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/MutationOk"
|
||||
"401":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
"404":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
"409":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
|
||||
/api/shifts/swaps/{date}/{shiftType}/decline:
|
||||
post:
|
||||
operationId: declineSwap
|
||||
tags: [Shifts]
|
||||
summary: Decline a pending swap
|
||||
security:
|
||||
- portalCognito: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ShiftDate"
|
||||
- $ref: "#/components/parameters/ShiftType"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/MutationOk"
|
||||
"401":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
"404":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
|
||||
/api/shifts/admin/override:
|
||||
post:
|
||||
operationId: adminOverride
|
||||
tags: [Shifts]
|
||||
summary: Assign a shift
|
||||
security:
|
||||
- portalCognito: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AdminOverrideBody"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/MutationOk"
|
||||
"400":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
"401":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
"403":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
"404":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
|
||||
/api/shifts/admin/open:
|
||||
post:
|
||||
operationId: adminOpen
|
||||
tags: [Shifts]
|
||||
summary: Mark a shift open
|
||||
security:
|
||||
- portalCognito: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ShiftDateBody"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/MutationOk"
|
||||
"400":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
"401":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
"403":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
|
||||
/api/shifts/admin/clear:
|
||||
post:
|
||||
operationId: adminClear
|
||||
tags: [Shifts]
|
||||
summary: Clear an override
|
||||
security:
|
||||
- portalCognito: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ShiftDateBody"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/MutationOk"
|
||||
"400":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
"401":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
"403":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
|
||||
/api/shifts/admin/holidays:
|
||||
post:
|
||||
operationId: adminHolidayAdd
|
||||
tags: [Shifts]
|
||||
summary: Schedule a holiday day shift
|
||||
security:
|
||||
- portalCognito: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/HolidayBody"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/MutationOk"
|
||||
"400":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
"401":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
"403":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
"409":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
|
||||
/api/shifts/admin/holidays/{date}:
|
||||
delete:
|
||||
operationId: adminHolidayRemove
|
||||
tags: [Shifts]
|
||||
summary: Remove a holiday
|
||||
security:
|
||||
- portalCognito: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ShiftDate"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/MutationOk"
|
||||
"401":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
"403":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
"404":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
|
||||
/api/shifts/admin/pickups/{date}/{shiftType}/{extension}/approve:
|
||||
post:
|
||||
operationId: adminPickupApprove
|
||||
tags: [Shifts]
|
||||
summary: Approve a late pickup
|
||||
security:
|
||||
- portalCognito: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ShiftDate"
|
||||
- $ref: "#/components/parameters/ShiftType"
|
||||
- $ref: "#/components/parameters/Extension"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/MutationOk"
|
||||
"401":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
"403":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
"404":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
"409":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
|
||||
/api/shifts/admin/pickups/{date}/{shiftType}/{extension}/deny:
|
||||
post:
|
||||
operationId: adminPickupDeny
|
||||
tags: [Shifts]
|
||||
summary: Deny a late pickup
|
||||
security:
|
||||
- portalCognito: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ShiftDate"
|
||||
- $ref: "#/components/parameters/ShiftType"
|
||||
- $ref: "#/components/parameters/Extension"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/MutationOk"
|
||||
"401":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
"403":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
"404":
|
||||
$ref: "#/components/responses/PortalError"
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
portalCognito:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: Portal Cognito ID token
|
||||
rosterBearer:
|
||||
type: http
|
||||
scheme: bearer
|
||||
description: Shared Paychex roster token
|
||||
|
||||
parameters:
|
||||
ShiftDate:
|
||||
name: date
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: date
|
||||
ShiftType:
|
||||
name: shiftType
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
enum: [day, night]
|
||||
Extension:
|
||||
name: extension
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
minLength: 1
|
||||
|
||||
responses:
|
||||
MutationOk:
|
||||
description: Mutation applied
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/MutationResult"
|
||||
PortalError:
|
||||
description: Portal JSON error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PortalErrorEnvelope"
|
||||
RosterError:
|
||||
description: Roster string error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/RosterErrorBody"
|
||||
|
||||
schemas:
|
||||
Health:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [stage, sha]
|
||||
properties:
|
||||
stage:
|
||||
type: string
|
||||
minLength: 1
|
||||
description: Workspace stage (`dev`, `prod`, or `local`)
|
||||
sha:
|
||||
type: string
|
||||
minLength: 1
|
||||
description: Git SHA or `unknown` locally
|
||||
|
||||
PortalErrorEnvelope:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [error]
|
||||
properties:
|
||||
error:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [code, message]
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
minLength: 1
|
||||
message:
|
||||
type: string
|
||||
minLength: 1
|
||||
|
||||
RosterErrorBody:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [error]
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
minLength: 1
|
||||
|
||||
RosterUpsert:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [name, extension, slack_user_id]
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
minLength: 1
|
||||
extension:
|
||||
type: string
|
||||
minLength: 1
|
||||
slack_user_id:
|
||||
type: string
|
||||
minLength: 1
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
|
||||
RosterOk:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [ok]
|
||||
properties:
|
||||
ok:
|
||||
type: boolean
|
||||
const: true
|
||||
|
||||
ShiftDateBody:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [date]
|
||||
properties:
|
||||
date:
|
||||
type: string
|
||||
format: date
|
||||
shiftType:
|
||||
type: string
|
||||
enum: [day, night, holiday]
|
||||
|
||||
SwapBody:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [date, targetExtension]
|
||||
properties:
|
||||
date:
|
||||
type: string
|
||||
format: date
|
||||
shiftType:
|
||||
type: string
|
||||
enum: [day, night]
|
||||
targetExtension:
|
||||
type: string
|
||||
minLength: 1
|
||||
|
||||
AdminOverrideBody:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [date, extension]
|
||||
properties:
|
||||
date:
|
||||
type: string
|
||||
format: date
|
||||
extension:
|
||||
type: string
|
||||
minLength: 1
|
||||
shiftType:
|
||||
type: string
|
||||
enum: [day, night]
|
||||
|
||||
HolidayBody:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [date, slots, label]
|
||||
properties:
|
||||
date:
|
||||
type: string
|
||||
format: date
|
||||
slots:
|
||||
type: integer
|
||||
minimum: 1
|
||||
label:
|
||||
type: string
|
||||
minLength: 1
|
||||
multiplier:
|
||||
type: [number, string, "null"]
|
||||
|
||||
MutationResult:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [ok]
|
||||
properties:
|
||||
ok:
|
||||
type: boolean
|
||||
message:
|
||||
type: string
|
||||
latePickup:
|
||||
type: boolean
|
||||
repointed:
|
||||
type: boolean
|
||||
|
||||
RosterPerson:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [extension, name, email]
|
||||
properties:
|
||||
extension:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
email:
|
||||
type: string
|
||||
slackUserId:
|
||||
type: string
|
||||
|
||||
ShiftAssignee:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [extension, name]
|
||||
properties:
|
||||
extension:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
|
||||
ShiftSlot:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- kind
|
||||
- shiftType
|
||||
- label
|
||||
- slots
|
||||
- openSlots
|
||||
- multiplier
|
||||
- assignees
|
||||
- mine
|
||||
- canPick
|
||||
- canDrop
|
||||
- latePickup
|
||||
properties:
|
||||
kind:
|
||||
type: string
|
||||
enum: [holiday, override, available, weekly]
|
||||
shiftType:
|
||||
type: string
|
||||
enum: [day, night]
|
||||
label:
|
||||
type: string
|
||||
slots:
|
||||
type: integer
|
||||
openSlots:
|
||||
type: integer
|
||||
multiplier:
|
||||
type: number
|
||||
assignees:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/ShiftAssignee"
|
||||
mine:
|
||||
type: boolean
|
||||
canPick:
|
||||
type: boolean
|
||||
canDrop:
|
||||
type: boolean
|
||||
latePickup:
|
||||
type: boolean
|
||||
|
||||
ShiftDay:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [date, dayName, slots]
|
||||
properties:
|
||||
date:
|
||||
type: string
|
||||
format: date
|
||||
dayName:
|
||||
type: string
|
||||
slots:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/ShiftSlot"
|
||||
|
||||
PendingSwap:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- date
|
||||
- shiftType
|
||||
- requesterExt
|
||||
- requesterName
|
||||
- targetExt
|
||||
- targetName
|
||||
- incoming
|
||||
properties:
|
||||
date:
|
||||
type: string
|
||||
shiftType:
|
||||
type: string
|
||||
enum: [day, night]
|
||||
requesterExt:
|
||||
type: string
|
||||
requesterName:
|
||||
type: string
|
||||
targetExt:
|
||||
type: string
|
||||
targetName:
|
||||
type: string
|
||||
incoming:
|
||||
type: boolean
|
||||
|
||||
PendingPickup:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [date, shiftType, requesterExt, requesterName, isHoliday]
|
||||
properties:
|
||||
date:
|
||||
type: string
|
||||
shiftType:
|
||||
type: string
|
||||
enum: [day, night]
|
||||
requesterExt:
|
||||
type: string
|
||||
requesterName:
|
||||
type: string
|
||||
isHoliday:
|
||||
type: boolean
|
||||
|
||||
HolidaySummary:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [date, label, slots, multiplier]
|
||||
properties:
|
||||
date:
|
||||
type: string
|
||||
label:
|
||||
type: string
|
||||
slots:
|
||||
type: integer
|
||||
multiplier:
|
||||
type: number
|
||||
|
||||
ShiftsSnapshot:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [linked, isAdmin]
|
||||
properties:
|
||||
linked:
|
||||
type: boolean
|
||||
email:
|
||||
type: string
|
||||
week:
|
||||
type: string
|
||||
enum: [this, next]
|
||||
weekStart:
|
||||
type: string
|
||||
format: date
|
||||
me:
|
||||
$ref: "#/components/schemas/RosterPerson"
|
||||
isAdmin:
|
||||
type: boolean
|
||||
days:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/ShiftDay"
|
||||
pendingSwaps:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/PendingSwap"
|
||||
pendingPickups:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/PendingPickup"
|
||||
upcomingHolidays:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/HolidaySummary"
|
||||
roster:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/RosterPerson"
|
||||
30
package-lock.json
generated
Normal file
30
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"name": "afterhours-shift-manager",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "afterhours-shift-manager",
|
||||
"version": "1.0.0",
|
||||
"devDependencies": {
|
||||
"@redocly/cli": "2.52.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@redocly/cli": {
|
||||
"version": "2.52.1",
|
||||
"resolved": "https://registry.npmjs.org/@redocly/cli/-/cli-2.52.1.tgz",
|
||||
"integrity": "sha512-gwfx1WelVDHU/cw+GkpLV9+xksOI3obJGgL1nUVrAFUyXNDxM+aifXDUZk7BfbOIpF69Sea2ant0cOqbQfg/KQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"openapi": "bin/cli.js",
|
||||
"redocly": "bin/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.12.0 || >=20.19.0 <21.0.0",
|
||||
"npm": ">=10"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
package.json
Normal file
11
package.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"name": "afterhours-shift-manager",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"openapi:lint": "redocly lint --config .redocly.yaml openapi.yaml"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@redocly/cli": "2.52.1"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,291 +1,3 @@
|
|||
# Leftover Lambda IAM roles from dual-run. The seven functions are gone;
|
||||
# weekly-post remains so paychex-checkcomponents can keep that principal
|
||||
# until a follow-up queue-policy apply drops it.
|
||||
|
||||
data "aws_iam_policy_document" "lambda_assume" {
|
||||
statement {
|
||||
effect = "Allow"
|
||||
actions = ["sts:AssumeRole"]
|
||||
|
||||
principals {
|
||||
type = "Service"
|
||||
identifiers = ["lambda.amazonaws.com"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
locals {
|
||||
table_arn = aws_dynamodb_table.shifts.arn
|
||||
|
||||
lambda_identity = {
|
||||
slack_bot = [
|
||||
{
|
||||
sid = "DdbCrud"
|
||||
actions = ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:UpdateItem", "dynamodb:DeleteItem", "dynamodb:Query", "dynamodb:Scan", "dynamodb:BatchGetItem", "dynamodb:BatchWriteItem", "dynamodb:DescribeTable", "dynamodb:ConditionCheckItem"]
|
||||
resources = [local.table_arn, "${local.table_arn}/*"]
|
||||
condition = null
|
||||
},
|
||||
{
|
||||
sid = "Secrets"
|
||||
actions = ["secretsmanager:GetSecretValue"]
|
||||
resources = ["arn:aws:secretsmanager:${var.aws_region}:${local.account_id}:secret:afterhours-shift-manager/*"]
|
||||
condition = null
|
||||
},
|
||||
{
|
||||
sid = "HolidaySchedules"
|
||||
actions = ["scheduler:CreateSchedule", "scheduler:DeleteSchedule", "scheduler:GetSchedule"]
|
||||
resources = ["arn:aws:scheduler:${var.aws_region}:${local.account_id}:schedule/default/holiday-*"]
|
||||
condition = null
|
||||
},
|
||||
{
|
||||
sid = "PassHolidayScheduler"
|
||||
actions = ["iam:PassRole"]
|
||||
resources = [local.holiday_scheduler_role_arn]
|
||||
condition = {
|
||||
test = "StringEquals"
|
||||
variable = "iam:PassedToService"
|
||||
values = ["scheduler.amazonaws.com"]
|
||||
}
|
||||
},
|
||||
{
|
||||
sid = "InvokeHolidayRouter"
|
||||
actions = ["lambda:InvokeFunction"]
|
||||
resources = [local.holiday_router_arn]
|
||||
condition = null
|
||||
},
|
||||
]
|
||||
weekly_post = concat(
|
||||
[
|
||||
{
|
||||
sid = "DdbCrud"
|
||||
actions = ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:UpdateItem", "dynamodb:DeleteItem", "dynamodb:Query", "dynamodb:Scan", "dynamodb:BatchGetItem", "dynamodb:BatchWriteItem", "dynamodb:DescribeTable", "dynamodb:ConditionCheckItem"]
|
||||
resources = [local.table_arn, "${local.table_arn}/*"]
|
||||
condition = null
|
||||
},
|
||||
{
|
||||
sid = "Secrets"
|
||||
actions = ["secretsmanager:GetSecretValue"]
|
||||
resources = ["arn:aws:secretsmanager:${var.aws_region}:${local.account_id}:secret:afterhours-shift-manager/slack-bot-token-*"]
|
||||
condition = null
|
||||
},
|
||||
],
|
||||
var.checkcomponents_queue_arn == "" ? [] : [
|
||||
{
|
||||
sid = "CheckcomponentsSend"
|
||||
actions = ["sqs:SendMessage"]
|
||||
resources = [var.checkcomponents_queue_arn]
|
||||
condition = null
|
||||
},
|
||||
],
|
||||
),
|
||||
roster_sync = [
|
||||
{
|
||||
sid = "DdbCrud"
|
||||
actions = ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:UpdateItem", "dynamodb:DeleteItem", "dynamodb:Query", "dynamodb:Scan", "dynamodb:BatchGetItem", "dynamodb:BatchWriteItem", "dynamodb:DescribeTable", "dynamodb:ConditionCheckItem"]
|
||||
resources = [local.table_arn, "${local.table_arn}/*"]
|
||||
condition = null
|
||||
},
|
||||
{
|
||||
sid = "Secrets"
|
||||
actions = ["secretsmanager:GetSecretValue"]
|
||||
resources = ["arn:aws:secretsmanager:${var.aws_region}:${local.account_id}:secret:afterhours-shift-manager/3cx-*"]
|
||||
condition = null
|
||||
},
|
||||
]
|
||||
roster_api = [
|
||||
{
|
||||
sid = "DdbWrite"
|
||||
actions = ["dynamodb:UpdateItem", "dynamodb:DeleteItem"]
|
||||
resources = [local.table_arn]
|
||||
condition = null
|
||||
},
|
||||
{
|
||||
sid = "Secrets"
|
||||
actions = ["secretsmanager:GetSecretValue"]
|
||||
resources = ["arn:aws:secretsmanager:${var.aws_region}:${local.account_id}:secret:afterhours-shift-manager/roster-api-token-*"]
|
||||
condition = null
|
||||
},
|
||||
]
|
||||
ring_scheduler = [
|
||||
{
|
||||
sid = "DdbRead"
|
||||
actions = ["dynamodb:GetItem", "dynamodb:Query", "dynamodb:Scan", "dynamodb:BatchGetItem", "dynamodb:DescribeTable"]
|
||||
resources = [local.table_arn, "${local.table_arn}/*"]
|
||||
condition = null
|
||||
},
|
||||
{
|
||||
sid = "Secrets"
|
||||
actions = ["secretsmanager:GetSecretValue"]
|
||||
resources = ["arn:aws:secretsmanager:${var.aws_region}:${local.account_id}:secret:afterhours-shift-manager/3cx-*"]
|
||||
condition = null
|
||||
},
|
||||
]
|
||||
holiday_router = [
|
||||
{
|
||||
sid = "DdbCrud"
|
||||
actions = ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:UpdateItem", "dynamodb:DeleteItem", "dynamodb:Query", "dynamodb:Scan", "dynamodb:BatchGetItem", "dynamodb:BatchWriteItem", "dynamodb:DescribeTable", "dynamodb:ConditionCheckItem"]
|
||||
resources = [local.table_arn, "${local.table_arn}/*"]
|
||||
condition = null
|
||||
},
|
||||
{
|
||||
sid = "Secrets"
|
||||
actions = ["secretsmanager:GetSecretValue"]
|
||||
resources = ["arn:aws:secretsmanager:${var.aws_region}:${local.account_id}:secret:afterhours-shift-manager/3cx-*"]
|
||||
condition = null
|
||||
},
|
||||
]
|
||||
portal_api = [
|
||||
{
|
||||
sid = "DdbCrud"
|
||||
actions = ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:UpdateItem", "dynamodb:DeleteItem", "dynamodb:Query", "dynamodb:Scan", "dynamodb:BatchGetItem", "dynamodb:BatchWriteItem", "dynamodb:DescribeTable", "dynamodb:ConditionCheckItem"]
|
||||
resources = [local.table_arn, "${local.table_arn}/*"]
|
||||
condition = null
|
||||
},
|
||||
{
|
||||
sid = "Secrets"
|
||||
actions = ["secretsmanager:GetSecretValue"]
|
||||
resources = ["arn:aws:secretsmanager:${var.aws_region}:${local.account_id}:secret:afterhours-shift-manager/*"]
|
||||
condition = null
|
||||
},
|
||||
{
|
||||
sid = "HolidaySchedules"
|
||||
actions = ["scheduler:CreateSchedule", "scheduler:DeleteSchedule", "scheduler:GetSchedule"]
|
||||
resources = ["arn:aws:scheduler:${var.aws_region}:${local.account_id}:schedule/default/holiday-*"]
|
||||
condition = null
|
||||
},
|
||||
{
|
||||
sid = "PassHolidayScheduler"
|
||||
actions = ["iam:PassRole"]
|
||||
resources = [local.holiday_scheduler_role_arn]
|
||||
condition = {
|
||||
test = "StringEquals"
|
||||
variable = "iam:PassedToService"
|
||||
values = ["scheduler.amazonaws.com"]
|
||||
}
|
||||
},
|
||||
{
|
||||
sid = "InvokeHolidayRouter"
|
||||
actions = ["lambda:InvokeFunction"]
|
||||
resources = [local.holiday_router_arn]
|
||||
condition = null
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
lambda_env = {
|
||||
slack_bot = {
|
||||
SHIFT_TABLE = aws_dynamodb_table.shifts.name
|
||||
SLACK_BOT_TOKEN_SECRET = "afterhours-shift-manager/slack-bot-token"
|
||||
SLACK_SIGNING_SECRET = "afterhours-shift-manager/slack-signing-secret"
|
||||
SHIFT_CHANNEL = var.shift_channel
|
||||
TCX_SECRET_PREFIX = "afterhours-shift-manager/3cx-"
|
||||
QUEUE_NUMBER = var.queue_number
|
||||
TZ = var.timezone
|
||||
HOLIDAY_ROUTER_ARN = local.holiday_router_arn
|
||||
HOLIDAY_SCHEDULER_ROLE_ARN = local.holiday_scheduler_role_arn
|
||||
SENTRY_DSN = var.sentry_dsn
|
||||
}
|
||||
weekly_post = {
|
||||
SHIFT_TABLE = aws_dynamodb_table.shifts.name
|
||||
SLACK_BOT_TOKEN_SECRET = "afterhours-shift-manager/slack-bot-token"
|
||||
SHIFT_CHANNEL = var.shift_channel
|
||||
PAY_REPORT_USER = var.pay_report_user
|
||||
TZ = var.timezone
|
||||
CHECKCOMPONENTS_QUEUE_URL = var.checkcomponents_queue_url
|
||||
SENTRY_DSN = var.sentry_dsn
|
||||
}
|
||||
roster_sync = {
|
||||
SHIFT_TABLE = aws_dynamodb_table.shifts.name
|
||||
TCX_SECRET_PREFIX = "afterhours-shift-manager/3cx-"
|
||||
SYNC_GROUP = "DEFAULT"
|
||||
TZ = var.timezone
|
||||
SENTRY_DSN = var.sentry_dsn
|
||||
}
|
||||
roster_api = {
|
||||
SHIFT_TABLE = aws_dynamodb_table.shifts.name
|
||||
ROSTER_API_TOKEN_SECRET = "afterhours-shift-manager/roster-api-token"
|
||||
TZ = var.timezone
|
||||
SENTRY_DSN = var.sentry_dsn
|
||||
}
|
||||
ring_scheduler = {
|
||||
SHIFT_TABLE = aws_dynamodb_table.shifts.name
|
||||
TCX_SECRET_PREFIX = "afterhours-shift-manager/3cx-"
|
||||
QUEUE_NUMBER = var.queue_number
|
||||
TZ = var.timezone
|
||||
SENTRY_DSN = var.sentry_dsn
|
||||
}
|
||||
holiday_router = {
|
||||
SHIFT_TABLE = aws_dynamodb_table.shifts.name
|
||||
TCX_SECRET_PREFIX = "afterhours-shift-manager/3cx-"
|
||||
TZ = var.timezone
|
||||
SENTRY_DSN = var.sentry_dsn
|
||||
}
|
||||
portal_api = {
|
||||
SHIFT_TABLE = aws_dynamodb_table.shifts.name
|
||||
SLACK_BOT_TOKEN_SECRET = "afterhours-shift-manager/slack-bot-token"
|
||||
SHIFT_CHANNEL = var.shift_channel
|
||||
TCX_SECRET_PREFIX = "afterhours-shift-manager/3cx-"
|
||||
QUEUE_NUMBER = var.queue_number
|
||||
TZ = var.timezone
|
||||
HOLIDAY_ROUTER_ARN = local.holiday_router_arn
|
||||
HOLIDAY_SCHEDULER_ROLE_ARN = local.holiday_scheduler_role_arn
|
||||
SENTRY_DSN = var.sentry_dsn
|
||||
PORTAL_COGNITO_ISSUER = var.portal_cognito_issuer
|
||||
PORTAL_COGNITO_AUDIENCE = var.portal_cognito_audience
|
||||
PORTAL_COGNITO_TRUST = jsonencode(concat(
|
||||
var.portal_cognito_issuer != "" && var.portal_cognito_audience != "" ? [{ issuer = var.portal_cognito_issuer, audience = var.portal_cognito_audience }] : [],
|
||||
var.portal_cognito_extra_trust,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "lambda" {
|
||||
for_each = local.functions
|
||||
|
||||
name = each.value.role_name
|
||||
path = "/tf-managed/"
|
||||
description = "Lambda execution role for ${each.value.function_name}"
|
||||
assume_role_policy = data.aws_iam_policy_document.lambda_assume.json
|
||||
permissions_boundary = aws_iam_policy.lambda_boundary.arn
|
||||
}
|
||||
|
||||
data "aws_iam_policy_document" "lambda" {
|
||||
for_each = local.functions
|
||||
|
||||
dynamic "statement" {
|
||||
for_each = local.lambda_identity[each.key]
|
||||
|
||||
content {
|
||||
sid = statement.value.sid
|
||||
effect = "Allow"
|
||||
actions = statement.value.actions
|
||||
resources = statement.value.resources
|
||||
|
||||
dynamic "condition" {
|
||||
for_each = try(statement.value.condition, null) == null ? [] : [statement.value.condition]
|
||||
|
||||
content {
|
||||
test = condition.value.test
|
||||
variable = condition.value.variable
|
||||
values = condition.value.values
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy" "lambda" {
|
||||
for_each = local.functions
|
||||
|
||||
name = each.key
|
||||
role = aws_iam_role.lambda[each.key].id
|
||||
policy = data.aws_iam_policy_document.lambda[each.key].json
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy_attachment" "lambda_basic" {
|
||||
for_each = local.functions
|
||||
|
||||
role = aws_iam_role.lambda[each.key].name
|
||||
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
|
||||
}
|
||||
# Leftover Lambda execution roles were removed after Paychex dropped
|
||||
# AfterhoursWeeklyPostSend (PLAT-218). Zip handlers in src/*/app.py remain for
|
||||
# packaging via scripts/package_lambdas.py.
|
||||
|
|
|
|||
|
|
@ -15,9 +15,8 @@ locals {
|
|||
artifacts_bucket_name = "afterhours-shift-manager-artifacts-${local.account_id}"
|
||||
ssm_prefix = "/afterhours-shift-manager"
|
||||
|
||||
# Prod has no default VPC. 10.70 is unused in 011934824531
|
||||
# (10.0 proposal-system, 10.20 payments-dashboard, 10.40 syslog,
|
||||
# 10.60 meal-order-manager, 10.80 apm-wo).
|
||||
# Prod has no default VPC. This stack owns 10.70. Meals attaches with
|
||||
# existing_vpc_id. Portal Fargate (PLAT-217) should too. Do not mint 10.62.
|
||||
vpc_cidr = "10.70.0.0/16"
|
||||
public_subnet_cidrs = ["10.70.0.0/24", "10.70.1.0/24"]
|
||||
table_name = "afterhours-shifts"
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ output "jobs_queue_arn" {
|
|||
}
|
||||
|
||||
output "ecs_task_role_arn" {
|
||||
description = "ECS task role. paychex-checkcomponents queue policy must allow this ARN before Fargate weekly_post."
|
||||
description = "ECS task role. Paychex already allows this ARN."
|
||||
value = aws_iam_role.ecs_task.arn
|
||||
}
|
||||
|
||||
|
|
@ -57,3 +57,13 @@ output "hcptf_plan_role_arn" {
|
|||
description = "HCP plan role ARN. Set TFC_AWS_PLAN_ROLE_ARN after the bootstrap window."
|
||||
value = aws_iam_role.hcptf_plan.arn
|
||||
}
|
||||
|
||||
output "vpc_id" {
|
||||
description = "VPC that meals already attaches to. Portal Fargate prod sets existing_vpc_id to this value."
|
||||
value = aws_vpc.this.id
|
||||
}
|
||||
|
||||
output "public_subnet_ids" {
|
||||
description = "Public subnet IDs for ALB and Fargate. Portal HCP existing_public_subnet_ids."
|
||||
value = aws_subnet.public[*].id
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,13 +4,16 @@ from pathlib import Path
|
|||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
TERRAFORM = ROOT / "terraform"
|
||||
LAMBDA_TF = (TERRAFORM / "lambda.tf").read_text()
|
||||
HCP_IAM = (TERRAFORM / "hcp_iam.tf").read_text()
|
||||
CI = (ROOT / ".github" / "workflows" / "ci.yaml").read_text()
|
||||
LOCALS = (TERRAFORM / "locals.tf").read_text()
|
||||
VARIABLES = (TERRAFORM / "variables.tf").read_text()
|
||||
|
||||
|
||||
def _tf_without_comments(text: str) -> str:
|
||||
return "\n".join(line.split("#", 1)[0] for line in text.splitlines())
|
||||
|
||||
|
||||
def test_sam_template_removed():
|
||||
assert not (ROOT / "template.yaml").exists()
|
||||
assert not (ROOT / "samconfig.toml.example").exists()
|
||||
|
|
@ -18,7 +21,8 @@ def test_sam_template_removed():
|
|||
|
||||
def test_zip_cd_removed():
|
||||
assert not (ROOT / ".github" / "workflows" / "deploy.yaml").exists()
|
||||
assert 'resource "aws_lambda_function"' not in LAMBDA_TF
|
||||
for path in TERRAFORM.glob("*.tf"):
|
||||
assert 'resource "aws_lambda_function"' not in path.read_text()
|
||||
assert not (TERRAFORM / "apigateway.tf").exists()
|
||||
assert not (TERRAFORM / "events.tf").exists()
|
||||
|
||||
|
|
@ -116,11 +120,35 @@ def test_ci_runs_pytest_and_terraform_validate():
|
|||
assert "terraform fmt -check" in CI
|
||||
assert "terraform init -backend=false" in CI
|
||||
assert "terraform validate" in CI
|
||||
assert "openapi:lint" in CI
|
||||
assert "npm ci" in CI
|
||||
|
||||
|
||||
def test_openapi_uses_redocly_recommended():
|
||||
redocly = (ROOT / ".redocly.yaml").read_text()
|
||||
package = (ROOT / "package.json").read_text()
|
||||
spec = (ROOT / "openapi.yaml").read_text()
|
||||
assert "extends:" in redocly
|
||||
assert "- recommended" in redocly
|
||||
assert "operation-2xx-response: off" in redocly
|
||||
assert "operation-4xx-response: error" in redocly
|
||||
assert "rule/operation-2xx-or-3xx-response" in redocly
|
||||
assert "severity: error" in redocly
|
||||
assert "optionsShifts" not in spec
|
||||
health = spec.split("/api/health:", 1)[1].split("\n /", 1)[0]
|
||||
assert '"403":' in health
|
||||
assert '"400":' not in health
|
||||
assert 'root: openapi.yaml' in redocly
|
||||
assert '"openapi:lint"' in package
|
||||
assert '"@redocly/cli": "2.52.1"' in package
|
||||
assert "openapi: 3.1.0" in spec
|
||||
assert "required: [stage, sha]" in spec
|
||||
|
||||
|
||||
def test_checkcomponents_queue_arn_variable_matches_iam_references():
|
||||
assert 'variable "checkcomponents_queue_arn"' in VARIABLES
|
||||
assert "var.checkcomponents_queue_arn" in LAMBDA_TF
|
||||
iam = (TERRAFORM / "iam.tf").read_text()
|
||||
assert "var.checkcomponents_queue_arn" in iam
|
||||
boundary = (TERRAFORM / "lambda_boundary.tf").read_text()
|
||||
assert "var.checkcomponents_queue_arn" in boundary
|
||||
data_tf = (TERRAFORM / "data.tf").read_text()
|
||||
|
|
@ -128,7 +156,22 @@ def test_checkcomponents_queue_arn_variable_matches_iam_references():
|
|||
assert "checkcomponents_pair" in data_tf
|
||||
|
||||
|
||||
def test_seven_function_names_still_on_leftover_roles():
|
||||
def test_leftover_lambda_iam_roles_removed():
|
||||
for path in TERRAFORM.glob("*.tf"):
|
||||
body = _tf_without_comments(path.read_text())
|
||||
assert 'resource "aws_iam_role" "lambda"' not in body
|
||||
assert 'resource "aws_iam_role_policy" "lambda"' not in body
|
||||
assert 'resource "aws_iam_role_policy_attachment" "lambda_basic"' not in body
|
||||
assert 'data "aws_iam_policy_document" "lambda_assume"' not in body
|
||||
|
||||
|
||||
def test_lambda_boundary_policy_remains():
|
||||
boundary = (TERRAFORM / "lambda_boundary.tf").read_text()
|
||||
assert 'resource "aws_iam_policy" "lambda_boundary"' in boundary
|
||||
assert "afterhours-shift-manager-lambda-boundary" in boundary
|
||||
|
||||
|
||||
def test_local_functions_still_lists_packaging_keys():
|
||||
for name in (
|
||||
"afterhours-shift-manager",
|
||||
"afterhours-weekly-post",
|
||||
|
|
@ -140,10 +183,7 @@ def test_seven_function_names_still_on_leftover_roles():
|
|||
):
|
||||
assert name in LOCALS
|
||||
assert "afterhours-release-notifier" not in LOCALS
|
||||
|
||||
|
||||
def test_weekly_post_role_is_tf_managed_name():
|
||||
assert 'role_name = "afterhours-shift-manager-weekly-post"' in LOCALS
|
||||
assert "weekly_post" in LOCALS
|
||||
|
||||
|
||||
def test_github_deploy_trust_covers_image_only():
|
||||
|
|
@ -167,6 +207,9 @@ def test_origins_use_fargate_url():
|
|||
assert "aws_apigatewayv2_api.http" not in outputs
|
||||
assert '${local.api_url}/slack/events' in outputs
|
||||
assert "value = local.api_url" in outputs
|
||||
assert "output \"vpc_id\"" in outputs
|
||||
assert "output \"public_subnet_ids\"" in outputs
|
||||
assert "aws_vpc.this.id" in outputs
|
||||
|
||||
|
||||
def test_alb_alarms_remain():
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue