Skip to content

Latest commit

 

History

History
708 lines (535 loc) · 16.4 KB

File metadata and controls

708 lines (535 loc) · 16.4 KB

schedules service

Русская версия

On-call schedule management service for Induty.
Handles schedule creation, shift management, and current on-call lookup.


Table of Contents


Configuration

The service reads its config from config.yaml. Any field can be overridden by an environment variable.

config.yaml

port: "8004"
database_dsn: "host=localhost user=induty password=secret dbname=induty_schedules port=5432 sslmode=disable"
jwt_secret: "change-me-in-production"
read_timeout: "10s"
write_timeout: "10s"

teams_service_url: "https://teams:8443"
tls_client_cert_file: "./certs/schedules/client.crt"
tls_client_key_file: "./certs/schedules/client.key"
tls_ca_cert_file: "./certs/ca/ca.crt"

Environment variables

Variable Description Example
INDUTY_SCHEDULES_PORT Service port 8004
INDUTY_SCHEDULES_DATABASE_DSN PostgreSQL DSN string host=postgres user=...
INDUTY_SCHEDULES_JWT_SECRET JWT signing secret supersecret
INDUTY_SCHEDULES_TEAMS_SERVICE_URL Base URL of the teams service https://teams:8443
INDUTY_SCHEDULES_TLS_CLIENT_CERT_FILE Path to the mTLS client certificate /certs/schedules/client.crt
INDUTY_SCHEDULES_TLS_CLIENT_KEY_FILE Path to the mTLS client key /certs/schedules/client.key
INDUTY_SCHEDULES_TLS_CA_CERT_FILE Path to the CA certificate for verifying teams service /certs/ca/ca.crt

Environment variables take priority over values in config.yaml.


Access Model

General rules

  • GET /healthz and GET /readyz require no authentication.
  • All /api/v1/... routes require a JWT in the Authorization: Bearer <access_token> header.
  • The global admin has full access to all schedules and shifts.
  • For non-admin users, access is restricted to teams they belong to.
  • Team membership is verified through the teams service.

Permissions by operation

⚠️ Key difference from incidents: most write operations require not just team membership, but the owner role in that team.

Operation Who can perform it
Create schedule admin or team owner
List schedules admin (all) or member (own teams only)
View schedule admin or team member
Update schedule admin or team owner
Delete schedule admin or team owner
View shifts admin or team member
Add shift admin or team owner
Update shift admin or team owner
Delete shift admin or team owner
View on-call by schedule admin or team member
View on-call by team admin or team member

Team-scoped access notes

  • If a non-admin has no teams, GET /api/v1/schedules returns an empty array.
  • If the specified team_id is one the user doesn't belong to, the service returns 403.
  • Schedule names must be unique; on conflict the service returns 409 Conflict.
  • The team_id of a schedule cannot be changed after creation.

Integration with teams service

schedules uses the teams service to verify team access and user roles.

mTLS

Communication with the teams service is secured with mTLS:

  • the schedules client loads a client certificate and key;
  • the teams service certificate is validated against the configured CA;
  • the TLS config uses ServerName: "teams";
  • minimum TLS version is TLS 1.3;
  • HTTP client timeout is 3 seconds.

The teams service certificate must include teams in its CN or SAN.

Internal endpoints used

Method Path Purpose
GET /internal/users/:user_id/teams Get the list of teams a user belongs to
GET /internal/teams/:team_id/members/:user_id Check if a user is a member of a team
GET /internal/teams/:team_id/members/:user_id/owner Check if a user is a team owner

If the teams service is unavailable, operations will return 503 Service Unavailable.


API

Base URL: http://localhost:8004

All responses are returned as JSON:

// Success
{ "success": true, "data": { ... } }

// Success for delete operations
{ "success": true, "message": "..." }

// Error
{ "error": "error description" }

GET /healthz /readyz

Service health check.

Auth: not required

Response 200:

{ "status": "ok" }
  • /healthz — checks that the HTTP server is up.
  • /readyz — additionally pings the database.

POST /api/v1/schedules

Create a new schedule.

Auth: Authorization: Bearer <access_token>
Access: global admin or owner of team_id

Request body:

Field Type Required Description
name string Name, 2–255 characters; must be unique
description string Schedule description
team_id uint Team ID; cannot be changed after creation

Example request:

curl -X POST http://localhost:8004/api/v1/schedules \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <access_token>" \
  -d '{
    "name": "Backend On-Call",
    "description": "Primary on-call rotation for the backend team",
    "team_id": 10
  }'

Response 201:

{
  "success": true,
  "data": {
    "id": 1,
    "name": "Backend On-Call",
    "description": "Primary on-call rotation for the backend team",
    "team_id": 10,
    "created_at": "2026-04-01T10:00:00Z",
    "updated_at": "2026-04-01T10:00:00Z"
  }
}

Errors:

Code Reason
400 Invalid request body
401 Token missing or invalid
403 No owner role in the specified team
409 A schedule with this name already exists
503 Failed to verify role via teams service

GET /api/v1/schedules

Returns a list of schedules.

Auth: Authorization: Bearer <access_token>

Access:

  • admin sees all schedules;
  • non-admin sees only schedules for their teams.

Query parameters:

Parameter Type Description
team_id uint Filter by team

Example request:

curl "http://localhost:8004/api/v1/schedules?team_id=10" \
  -H "Authorization: Bearer <access_token>"

Response 200:

{
  "success": true,
  "data": [
    {
      "id": 1,
      "name": "Backend On-Call",
      "description": "Primary on-call rotation for the backend team",
      "team_id": 10,
      "created_at": "2026-04-01T10:00:00Z",
      "updated_at": "2026-04-01T10:00:00Z"
    }
  ]
}

Notes:

  • If the user has no accessible teams, an empty array is returned.
  • If the specified team_id is one the user doesn't belong to, the service returns 403.

Errors:

Code Reason
401 Token missing or invalid
403 No access to the specified team
503 Failed to fetch team membership from teams service
500 Database error

GET /api/v1/schedules/:id

Returns a schedule by ID.

Auth: Authorization: Bearer <access_token>
Access: admin or member of the schedule's team

Example request:

curl http://localhost:8004/api/v1/schedules/1 \
  -H "Authorization: Bearer <access_token>"

Response 200:

{
  "success": true,
  "data": {
    "id": 1,
    "name": "Backend On-Call",
    "description": "Primary on-call rotation for the backend team",
    "team_id": 10,
    "created_at": "2026-04-01T10:00:00Z",
    "updated_at": "2026-04-01T10:00:00Z"
  }
}

Errors:

Code Reason
400 Invalid ID
401 Token missing or invalid
403 No access to the schedule
404 Schedule not found

PUT /api/v1/schedules/:id

Update a schedule's name and description. The team (team_id) cannot be changed.

Auth: Authorization: Bearer <access_token>
Access: admin or owner of the schedule's team

Request body:

Field Type Required Description
name string New name, 2–255 characters
description string New description

Example request:

curl -X PUT http://localhost:8004/api/v1/schedules/1 \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <access_token>" \
  -d '{
    "name": "Backend On-Call v2",
    "description": "Updated rotation schedule"
  }'

Response 200:

{
  "success": true,
  "data": {
    "id": 1,
    "name": "Backend On-Call v2",
    "description": "Updated rotation schedule",
    "team_id": 10,
    "created_at": "2026-04-01T10:00:00Z",
    "updated_at": "2026-04-01T11:00:00Z"
  }
}

Errors:

Code Reason
400 Invalid request body
401 Token missing or invalid
403 No owner role
404 Schedule not found
500 Failed to update schedule

DELETE /api/v1/schedules/:id

Delete a schedule.

Auth: Authorization: Bearer <access_token>
Access: admin or owner of the schedule's team

Example request:

curl -X DELETE http://localhost:8004/api/v1/schedules/1 \
  -H "Authorization: Bearer <access_token>"

Response 200:

{
  "success": true,
  "message": "schedule deleted"
}

Errors:

Code Reason
401 Token missing or invalid
403 No owner role
404 Schedule not found
500 Failed to delete schedule

GET /api/v1/schedules/:id/shifts

Returns the list of shifts for a schedule.

Auth: Authorization: Bearer <access_token>
Access: admin or member of the schedule's team

Example request:

curl http://localhost:8004/api/v1/schedules/1/shifts \
  -H "Authorization: Bearer <access_token>"

Response 200:

{
  "success": true,
  "data": [
    {
      "id": 1,
      "schedule_id": 1,
      "user_id": 42,
      "starts_at": "2026-04-07T09:00:00Z",
      "ends_at": "2026-04-08T09:00:00Z",
      "created_at": "2026-04-01T10:00:00Z",
      "updated_at": "2026-04-01T10:00:00Z"
    }
  ]
}

Errors:

Code Reason
401 Token missing or invalid
403 No access to the schedule
404 Schedule not found
500 Failed to read shifts

POST /api/v1/schedules/:id/shifts

Add a shift to a schedule.

Auth: Authorization: Bearer <access_token>
Access: admin or owner of the schedule's team

Request body:

Field Type Required Description
user_id uint On-call user ID
starts_at string (RFC3339) Shift start time
ends_at string (RFC3339) Shift end time; must be after starts_at

Example request:

curl -X POST http://localhost:8004/api/v1/schedules/1/shifts \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <access_token>" \
  -d '{
    "user_id": 42,
    "starts_at": "2026-04-07T09:00:00Z",
    "ends_at": "2026-04-08T09:00:00Z"
  }'

Response 201:

{
  "success": true,
  "data": {
    "id": 1,
    "schedule_id": 1,
    "user_id": 42,
    "starts_at": "2026-04-07T09:00:00Z",
    "ends_at": "2026-04-08T09:00:00Z",
    "created_at": "2026-04-01T10:05:00Z",
    "updated_at": "2026-04-01T10:05:00Z"
  }
}

Errors:

Code Reason
400 Invalid request body or ends_at is not after starts_at
401 Token missing or invalid
403 No owner role
404 Schedule not found
500 Failed to add shift

PUT /api/v1/schedules/:id/shifts/:shift_id

Update a shift.

Auth: Authorization: Bearer <access_token>
Access: admin or owner of the schedule's team

Request body:

Field Type Required Description
user_id uint New on-call user
starts_at string (RFC3339) New shift start time
ends_at string (RFC3339) New shift end time; must be after starts_at

Example request:

curl -X PUT http://localhost:8004/api/v1/schedules/1/shifts/1 \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <access_token>" \
  -d '{
    "user_id": 55,
    "starts_at": "2026-04-07T09:00:00Z",
    "ends_at": "2026-04-08T09:00:00Z"
  }'

Response 200:

{
  "success": true,
  "data": {
    "id": 1,
    "schedule_id": 1,
    "user_id": 55,
    "starts_at": "2026-04-07T09:00:00Z",
    "ends_at": "2026-04-08T09:00:00Z",
    "created_at": "2026-04-01T10:05:00Z",
    "updated_at": "2026-04-01T12:00:00Z"
  }
}

Errors:

Code Reason
400 Invalid request body or ends_at is not after starts_at
401 Token missing or invalid
403 No owner role
404 Schedule or shift not found
500 Failed to update shift

DELETE /api/v1/schedules/:id/shifts/:shift_id

Delete a shift.

Auth: Authorization: Bearer <access_token>
Access: admin or owner of the schedule's team

Example request:

curl -X DELETE http://localhost:8004/api/v1/schedules/1/shifts/1 \
  -H "Authorization: Bearer <access_token>"

Response 200:

{
  "success": true,
  "message": "shift deleted"
}

Errors:

Code Reason
401 Token missing or invalid
403 No owner role
404 Schedule or shift not found
500 Failed to delete shift

GET /api/v1/schedules/:id/oncall

Returns the current on-call person for a specific schedule (at the time of the request).

Auth: Authorization: Bearer <access_token>
Access: admin or member of the schedule's team

Example request:

curl http://localhost:8004/api/v1/schedules/1/oncall \
  -H "Authorization: Bearer <access_token>"

Response 200 (someone is on-call):

{
  "success": true,
  "data": {
    "schedule_id": 1,
    "schedule_name": "Backend On-Call",
    "team_id": 10,
    "user_id": 42,
    "shift_id": 1,
    "starts_at": "2026-04-07T09:00:00Z",
    "ends_at": "2026-04-08T09:00:00Z"
  }
}

Response 200 (no one on-call):

{
  "success": true,
  "data": null,
  "message": "no one is on-call right now"
}

Errors:

Code Reason
400 Invalid schedule ID
401 Token missing or invalid
403 No access to the schedule
404 Schedule not found

GET /api/v1/schedules/oncall

Returns all current on-call users across all schedules for a team.

Auth: Authorization: Bearer <access_token>
Access: admin or team member

Query parameters:

Parameter Type Required Description
team_id uint Team ID

Example request:

curl "http://localhost:8004/api/v1/schedules/oncall?team_id=10" \
  -H "Authorization: Bearer <access_token>"

Response 200 (someone is on-call):

{
  "success": true,
  "data": [
    {
      "schedule_id": 1,
      "schedule_name": "Backend On-Call",
      "team_id": 10,
      "user_id": 42,
      "shift_id": 1,
      "starts_at": "2026-04-07T09:00:00Z",
      "ends_at": "2026-04-08T09:00:00Z"
    },
    {
      "schedule_id": 2,
      "schedule_name": "Infra On-Call",
      "team_id": 10,
      "user_id": 7,
      "shift_id": 5,
      "starts_at": "2026-04-07T00:00:00Z",
      "ends_at": "2026-04-14T00:00:00Z"
    }
  ]
}

Response 200 (no one on-call):

{
  "success": true,
  "data": [],
  "message": "no one is on-call right now"
}