@cyc-seattle/platform 1.17.0 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,12 +6,12 @@ app owns. The apps that run on it live in their own repos, such as
6
6
 
7
7
  ## Stacks
8
8
 
9
- | Stack | Holds | Applied by |
10
- | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
11
- | [`bootstrap`](stacks/bootstrap) | Service accounts and project IAM, the Pulumi state buckets and KMS keys, and the org's GitHub repos and settings | A human with project Owner and `gh` `admin:org`. Never CI. |
12
- | [`infra`](stacks/infra) | The VPC, the cluster subnet and its Cloud NAT on a static address, the private services peering, the prod and dev Cloud SQL instances, the DNS zones, Artifact Registry, the GKE Autopilot cluster, the shared Gateway, its address, certificates, and dev's DNS records, the `dev` and `prod` namespaces with their quotas and network policies, dev's RBAC, the prod Job alerts, and each environment's `platform/outputs.json` | `deploy-runner`, from Actions after approval. A deployer, with `mise run deploy`, when breaking glass. |
13
- | [`smoke`](stacks/smoke) | One `WebService` in dev, at `smoke.dev.cycsail.team`, for the integration tests | `deploy-dev`, from Actions on every PR. A deployer, with `PULUMI_STACK=dev mise run deploy`, when breaking glass. |
14
- | [`backend`](stacks/backend) | Directus, with the schema apply from `schemas/`, its roles, policies, and permission rules, the sync machine users, its OIDC client, and MCP, and the clubspot-sync and gsuite-sync CronJobs, with dev's Clubspot and Workspace fakes | `deploy-dev`, from Actions on every PR, and `deploy-runner` on a push to `main`, after `infra`. |
9
+ | Stack | Holds | Applied by |
10
+ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
11
+ | [`bootstrap`](stacks/bootstrap) | Service accounts and project IAM, the Pulumi state buckets and KMS keys, and the org's GitHub repos and settings | A human with project Owner and `gh` `admin:org`. Never CI. |
12
+ | [`infra`](stacks/infra) | The VPC, the cluster subnet and its Cloud NAT on a static address, the private services peering, the prod and dev Cloud SQL instances, the DNS zones, Artifact Registry and its cleanup policy, the GKE Autopilot cluster, the shared Gateway, its address, certificates, and dev's DNS records, the `dev` and `prod` namespaces with their quotas and network policies, dev's RBAC, the prod Job alerts, the log exclusions, and each environment's `platform/outputs.json` | `deploy-runner`, from Actions after approval. A deployer, with `mise run deploy`, when breaking glass. |
13
+ | [`smoke`](stacks/smoke) | One `WebService` in dev, at `smoke.dev.cycsail.team`, for the integration tests | `deploy-dev`, from Actions on every PR. A deployer, with `PULUMI_STACK=dev mise run deploy`, when breaking glass. |
14
+ | [`backend`](stacks/backend) | Directus, with the schema apply from `schemas/`, its roles, policies, and permission rules, the sync machine users, its OIDC client, and MCP, and the clubspot-sync and gsuite-sync CronJobs, with dev's Clubspot and Workspace fakes | `deploy-dev`, from Actions on every PR, and `deploy-runner` on a push to `main`, after `infra`. |
15
15
 
16
16
  ## @cyc-seattle/platform
17
17
 
@@ -31,7 +31,8 @@ app owns. The apps that run on it live in their own repos, such as
31
31
  `podAnnotations` sets annotations such as `cluster-autoscaler.kubernetes.io/safe-to-evict`.
32
32
  - `WebService` is a `Workload` behind the environment's Gateway at `hostname`. Its Service exposes
33
33
  `port` as port 80, and its readiness probe must be an HTTP GET, which the load balancer's health
34
- check reuses.
34
+ check reuses. With `paths`, it claims only those path prefixes on `hostname`, and another
35
+ `WebService` can serve the rest of it.
35
36
  - `ScheduledJob` runs a CronJob on `schedule`, in `America/Los_Angeles` unless `timeZone` says
36
37
  otherwise. A run never overlaps the one before it, gets an hour, and doesn't retry, by default.
37
38
  - All three run non-root with a read-only root filesystem, no capabilities, and no service account
@@ -0,0 +1,896 @@
1
+ /** Every collection, for `createDirectus<Schema>()`. */
2
+ export interface Schema {
3
+ accounts: Accounts[];
4
+ audit_findings: AuditFindings[];
5
+ boat_classes: BoatClasses[];
6
+ clubspot_camps: ClubspotCamps[];
7
+ clubspot_classes: ClubspotClasses[];
8
+ clubspot_custom_field_definitions: ClubspotCustomFieldDefinitions[];
9
+ clubspot_custom_field_responses: ClubspotCustomFieldResponses[];
10
+ clubspot_entry_caps: ClubspotEntryCaps[];
11
+ clubspot_participants: ClubspotParticipants[];
12
+ clubspot_promoted_fields: ClubspotPromotedFields[];
13
+ clubspot_registration_billing: ClubspotRegistrationBilling[];
14
+ clubspot_registration_entries: ClubspotRegistrationEntries[];
15
+ clubspot_registrations: ClubspotRegistrations[];
16
+ clubspot_session_classes: ClubspotSessionClasses[];
17
+ clubspot_sessions: ClubspotSessions[];
18
+ contact_points: ContactPoints[];
19
+ contacts: Contacts[];
20
+ google_group_memberships: GoogleGroupMemberships[];
21
+ google_groups: GoogleGroups[];
22
+ group_members: GroupMembers[];
23
+ group_policies: GroupPolicies[];
24
+ groups: Groups[];
25
+ medical_profiles: MedicalProfiles[];
26
+ offerings: Offerings[];
27
+ organization_cost_accounts: OrganizationCostAccounts[];
28
+ organizations: Organizations[];
29
+ people: People[];
30
+ plan_activations: PlanActivations[];
31
+ plan_activities: PlanActivities[];
32
+ plan_activity_block_rules: PlanActivityBlockRules[];
33
+ plan_activity_resources: PlanActivityResources[];
34
+ plan_boat_rates: PlanBoatRates[];
35
+ plan_bonus_owner_products: PlanBonusOwnerProducts[];
36
+ plan_bonus_owners: PlanBonusOwners[];
37
+ plan_budget_lines: PlanBudgetLines[];
38
+ plan_labor_lines: PlanLaborLines[];
39
+ plan_offering_boat_classes: PlanOfferingBoatClasses[];
40
+ plan_offerings: PlanOfferings[];
41
+ plan_pools: PlanPools[];
42
+ plan_rates: PlanRates[];
43
+ plan_resources: PlanResources[];
44
+ plan_revenue_charges: PlanRevenueCharges[];
45
+ plan_salaried_bookings: PlanSalariedBookings[];
46
+ plan_salaried_positions: PlanSalariedPositions[];
47
+ plan_staff_levels: PlanStaffLevels[];
48
+ plan_trips: PlanTrips[];
49
+ plan_versions: PlanVersions[];
50
+ plans: Plans[];
51
+ products: Products[];
52
+ program_area_role_assignments: ProgramAreaRoleAssignments[];
53
+ program_areas: ProgramAreas[];
54
+ program_role_assignments: ProgramRoleAssignments[];
55
+ program_roles: ProgramRoles[];
56
+ programs: Programs[];
57
+ qualification_covers: QualificationCovers[];
58
+ qualifications: Qualifications[];
59
+ registrations: Registrations[];
60
+ resource_kinds: ResourceKinds[];
61
+ sync_runs: SyncRuns[];
62
+ sync_tasks: SyncTasks[];
63
+ }
64
+ /** The chart of accounts that plans, results, and actuals book to. */
65
+ export interface Accounts {
66
+ id: string;
67
+ /** The chart-of-accounts code. */
68
+ number: string;
69
+ name: string;
70
+ type: "revenue" | "expense" | "asset" | "liability" | "net_assets";
71
+ form_990_part: string | null;
72
+ form_990_line: string | null;
73
+ /** The functional-expense column a line booked here takes unless it sets its own. */
74
+ default_function: "program" | "management" | "fundraising" | null;
75
+ parent_id: string | Accounts | null;
76
+ /** The account this one offsets. */
77
+ contra_account_id: string | Accounts | null;
78
+ /** Kept for history, and closed to new lines. */
79
+ retired: boolean;
80
+ }
81
+ /** Something a sync noticed but didn't fix. A dismissed finding stays dismissed on later runs. */
82
+ export interface AuditFindings {
83
+ id: string;
84
+ /** Which sync found it. */
85
+ source: string;
86
+ /** What kind of problem, such as an unexpected group member. Each sync names its own. */
87
+ kind: string;
88
+ /** What the finding is about, such as a group's address or a person. */
89
+ subject: string;
90
+ detail: string;
91
+ status: "open" | "approved" | "resolved" | "dismissed";
92
+ /** Identifies the finding, so a dismissal survives the next run. */
93
+ fingerprint: string;
94
+ }
95
+ /** A kind of boat offerings sail, such as Opti or 420. */
96
+ export interface BoatClasses {
97
+ id: string;
98
+ name: string;
99
+ sailors_per_boat: number;
100
+ /** Dry-storage area one boat takes. */
101
+ footprint_sq_ft: string | null;
102
+ }
103
+ /** A dated event, such as a season's race team. Copied from Clubspot, so edit it there. */
104
+ export interface ClubspotCamps {
105
+ /** Clubspot's own ID for this row. */
106
+ id: string;
107
+ name: string;
108
+ start_date: string | null;
109
+ end_date: string | null;
110
+ archived: boolean;
111
+ /** When this camp last synced from Clubspot. Empty means never. */
112
+ synced_through: string | null;
113
+ /** How many syncs in a row found nothing new. The more there are, the less often the sync checks this camp, down to once a week. */
114
+ quiet_runs: number;
115
+ /** The account code this camp sells against in Clubspot. Empty means the camp has no accounting set up. */
116
+ clubspot_sales_account: string | null;
117
+ }
118
+ /** An age or skill group within a camp. Copied from Clubspot, so edit it there. Only its program is set here. */
119
+ export interface ClubspotClasses {
120
+ /** Clubspot's own ID for this row. */
121
+ id: string;
122
+ camp_id: string | ClubspotCamps;
123
+ name: string;
124
+ /** Set by hand, once for each class. The sync never changes it. */
125
+ program_id: string | Programs | null;
126
+ /** The registrations signed up for this class. */
127
+ registration_entries: (string | ClubspotRegistrationEntries)[];
128
+ }
129
+ /** A question a camp's registration form asks, such as School. Each camp has its own copy, so the same question can be worded differently from camp to camp. Copied from Clubspot. */
130
+ export interface ClubspotCustomFieldDefinitions {
131
+ /** Clubspot's own ID for this row. */
132
+ id: string;
133
+ camp_id: string | ClubspotCamps;
134
+ /** The question as Clubspot words it. It can be long. */
135
+ label: string;
136
+ /** Clubspot's kind of question: text, select, radio, or file upload. */
137
+ field_type: string;
138
+ required: boolean;
139
+ }
140
+ /** One registration's answer to a camp's question. Copied from Clubspot. */
141
+ export interface ClubspotCustomFieldResponses {
142
+ /** Made from the registration's and the question's Clubspot IDs. */
143
+ id: string;
144
+ registration_id: string | ClubspotRegistrations;
145
+ definition_id: string | ClubspotCustomFieldDefinitions;
146
+ value: string | null;
147
+ }
148
+ /** How many sailors a class can take in a session. Copied from Clubspot, so edit it there. */
149
+ export interface ClubspotEntryCaps {
150
+ /** Clubspot's own ID for this row. */
151
+ id: string;
152
+ class_id: string | ClubspotClasses;
153
+ session_id: string | ClubspotSessions | null;
154
+ cap: number;
155
+ }
156
+ /** A sailor as their Clubspot registration form gives them: guardians, emergency contacts, and medical answers. Copied from Clubspot. Edit the person in People instead. */
157
+ export interface ClubspotParticipants {
158
+ /** Clubspot's own ID for this row. */
159
+ id: string;
160
+ person_id: string | People | null;
161
+ last_sync_run_id: string | SyncRuns | null;
162
+ first_name: string | null;
163
+ last_name: string | null;
164
+ email: string | null;
165
+ phone: string | null;
166
+ date_of_birth: string | null;
167
+ gender: string | null;
168
+ street: string | null;
169
+ city: string | null;
170
+ state: string | null;
171
+ postal_code: string | null;
172
+ guardian_1_name: string | null;
173
+ guardian_1_email: string | null;
174
+ guardian_1_mobile: string | null;
175
+ guardian_2_name: string | null;
176
+ guardian_2_email: string | null;
177
+ guardian_2_mobile: string | null;
178
+ emergency_1_name: string | null;
179
+ emergency_1_phone: string | null;
180
+ emergency_1_email: string | null;
181
+ emergency_1_relationship: string | null;
182
+ emergency_2_name: string | null;
183
+ emergency_2_phone: string | null;
184
+ emergency_2_email: string | null;
185
+ emergency_2_relationship: string | null;
186
+ medical_conditions: string | null;
187
+ medical_allergies: string | null;
188
+ medical_medications: string | null;
189
+ medical_last_tetanus: string | null;
190
+ medical_physician_name: string | null;
191
+ medical_physician_phone: string | null;
192
+ /** As typed on the Clubspot form, so it may not be a number. */
193
+ medical_weight: string | null;
194
+ /** This participant's registrations. */
195
+ registrations: (string | ClubspotRegistrations)[];
196
+ }
197
+ /** Which Clubspot registration questions fill in a field on a person's CRM record, such as School. Staff keep this list. */
198
+ export interface ClubspotPromotedFields {
199
+ id: string;
200
+ /** The field on a person's CRM record that the answers fill in. */
201
+ target_field: "school" | "share_contact";
202
+ /** The Clubspot questions, worded exactly as on the form, whose answers fill in this field. */
203
+ labels: string[];
204
+ }
205
+ /** What a registration was charged and paid. Copied from Clubspot, so edit it there. */
206
+ export interface ClubspotRegistrationBilling {
207
+ /** Clubspot's own ID for this row. */
208
+ id: string;
209
+ registration_id: string | ClubspotRegistrations;
210
+ /** The total charged. In cents: 12500 is $125.00. */
211
+ amount: number;
212
+ /** Charged but not paid yet. In cents: 12500 is $125.00. */
213
+ amount_pending: number;
214
+ /** In cents: 12500 is $125.00. */
215
+ amount_received: number;
216
+ /** In cents: 12500 is $125.00. */
217
+ amount_refunded: number;
218
+ /** Approved on a card but not collected yet, as Clubspot reports it. In cents: 12500 is $125.00. */
219
+ amount_capturable: number;
220
+ /** Left to bill later, such as on a payment plan, as Clubspot reports it. In cents: 12500 is $125.00. */
221
+ amount_deferred: number;
222
+ /** How much of the amount left to bill later has been billed so far. In cents: 12500 is $125.00. */
223
+ deferred_amount_billed: number;
224
+ /** In cents: 12500 is $125.00. */
225
+ discount: number;
226
+ /** In cents: 12500 is $125.00. */
227
+ processing_fee: number;
228
+ /** Card processing fees the family paid on top. In cents: 12500 is $125.00. */
229
+ processing_passed_on: number;
230
+ /** Clubspot's own fee on this charge. In cents: 12500 is $125.00. */
231
+ application_fee_amount: number;
232
+ /** In cents: 12500 is $125.00. */
233
+ tax: number;
234
+ currency: string | null;
235
+ }
236
+ /** One session and class a registration signed up for. Copied from Clubspot. */
237
+ export interface ClubspotRegistrationEntries {
238
+ /** Clubspot's own ID for this row. */
239
+ id: string;
240
+ registration_id: string | ClubspotRegistrations;
241
+ session_id: string | ClubspotSessions;
242
+ class_id: string | ClubspotClasses;
243
+ status: "confirmed" | "waitlist" | "cancelled";
244
+ /** The status exactly as Clubspot gives it. Use Status instead. */
245
+ clubspot_status: string | null;
246
+ confirmed_at: string | null;
247
+ waitlist_number: number | null;
248
+ accepted_from_waitlist: boolean | null;
249
+ /** Clubspot's priority for this entry. Often empty. */
250
+ priority: number | null;
251
+ }
252
+ /** One sailor's registration for a camp. Copied from Clubspot. */
253
+ export interface ClubspotRegistrations {
254
+ /** Clubspot's own ID for this row. */
255
+ id: string;
256
+ camp_id: string | ClubspotCamps;
257
+ registered_at: string;
258
+ status: string;
259
+ waiver_status: string | null;
260
+ archived: boolean;
261
+ /** Null means never answered - treated as not shared. */
262
+ share_contact: boolean | null;
263
+ participant_id: string | ClubspotParticipants;
264
+ last_sync_run_id: string | SyncRuns | null;
265
+ /** The sessions and classes this registration signed up for. */
266
+ registration_entries: (string | ClubspotRegistrationEntries)[];
267
+ }
268
+ /** Which classes a session offers. Copied from Clubspot. */
269
+ export interface ClubspotSessionClasses {
270
+ /** Made from the session's and the class's Clubspot IDs. */
271
+ id: string;
272
+ session_id: string | ClubspotSessions;
273
+ class_id: string | ClubspotClasses;
274
+ }
275
+ /** A dated part of a camp, such as one week. Copied from Clubspot, so edit it there. */
276
+ export interface ClubspotSessions {
277
+ /** Clubspot's own ID for this row. */
278
+ id: string;
279
+ camp_id: string | ClubspotCamps;
280
+ start_date: string | null;
281
+ end_date: string | null;
282
+ name: string | null;
283
+ archived: boolean;
284
+ }
285
+ /** Every email and phone known for a person, from forms and from staff, not only their main ones. */
286
+ export interface ContactPoints {
287
+ /** The last Clubspot form that gave this email or phone, if any. */
288
+ participant_id: string | ClubspotParticipants | null;
289
+ id: string;
290
+ person_id: string | People;
291
+ kind: "email" | "phone";
292
+ value: string;
293
+ /** The email or phone written the same way every time, so the sync can match it. */
294
+ normalized: string;
295
+ source: "form" | "staff";
296
+ last_seen_at: string | null;
297
+ }
298
+ /** A person's guardians and emergency contacts, from their Clubspot forms. */
299
+ export interface Contacts {
300
+ id: string;
301
+ subject_id: string | People;
302
+ contact_id: string | People;
303
+ relationship_type: "guardian" | "emergency_contact";
304
+ contact_order: number | null;
305
+ relationship_detail: string | null;
306
+ }
307
+ /** One row for each person the sync has seen in a program's Google Group. Set Removed at to stop the sync adding them back. Delete the row to let the sync add them again. */
308
+ export interface GoogleGroupMemberships {
309
+ id: string;
310
+ group_id: string | GoogleGroups;
311
+ person_id: string | People;
312
+ /** The address the sync added, kept for reading. The sync matches on person, and updates this when the person's email changes. */
313
+ email: string;
314
+ /** When the sync first saw this person in the group, whether it added them or they were already there. */
315
+ added_at: string;
316
+ /** Set when the person left or was taken out of the group. While set, the sync never adds them back. Delete the row to let it add them again. */
317
+ removed_at: string | null;
318
+ }
319
+ /** One row for each Google Group: class and program groups, and others such as the staff list. Copied from Google Workspace. */
320
+ export interface GoogleGroups {
321
+ id: string;
322
+ /** The group's email address. */
323
+ email: string;
324
+ name: string | null;
325
+ /** Which standard settings the sync gives this group. Empty means the group is left as someone set it up by hand. */
326
+ settings_template: "announcement" | "crew" | "inbox" | "participants" | null;
327
+ /** The group this one sits under, such as a class group's program group. Not used yet. */
328
+ parent_id: string | GoogleGroups | null;
329
+ /** Copied from Google Workspace. A change here isn't sent back. */
330
+ description: string | null;
331
+ /** Set when the group no longer exists in Google Workspace, and cleared if it comes back. */
332
+ archived: boolean;
333
+ }
334
+ export interface GroupMembers {
335
+ id: string;
336
+ group_id: string | Groups;
337
+ /** Gets the group's policies through their login, once they have one. */
338
+ person_id: string | People;
339
+ }
340
+ export interface GroupPolicies {
341
+ id: string;
342
+ group_id: string | Groups;
343
+ /** Only a policy that groups may give. */
344
+ policy_id: string;
345
+ }
346
+ /** A set of people, such as a committee, that gives its members one or more policies. It never sets a role. */
347
+ export interface Groups {
348
+ id: string;
349
+ name: string;
350
+ description: string | null;
351
+ /** The people in this group. Someone not yet in People is added there first. */
352
+ members: (string | GroupMembers)[];
353
+ /** The policies this group gives its members. */
354
+ policies: (string | GroupPolicies)[];
355
+ }
356
+ /** A person's medical information, kept apart from People so only some staff see it. */
357
+ export interface MedicalProfiles {
358
+ id: string;
359
+ person_id: string | People;
360
+ allergies: string | null;
361
+ medications: string | null;
362
+ conditions: string | null;
363
+ physician_name: string | null;
364
+ physician_phone: string | null;
365
+ last_tetanus: string | null;
366
+ /** A whole number, from the Clubspot form. */
367
+ weight: number | null;
368
+ /** When this profile last changed. */
369
+ date_updated: string | null;
370
+ }
371
+ /** A program sold for one dated period, such as Guppies in one summer week. The Clubspot sync makes one for each Clubspot class and session whose class has a program, so edit it in Clubspot. */
372
+ export interface Offerings {
373
+ id: string;
374
+ program_id: string | Programs;
375
+ starts_on: string | null;
376
+ ends_on: string | null;
377
+ /** How many sailors it takes. Empty means Clubspot sets no limit. */
378
+ capacity: number | null;
379
+ clubspot_class_id: string | ClubspotClasses;
380
+ /** Empty when the class's limit covers every session. */
381
+ clubspot_session_id: string | ClubspotSessions | null;
382
+ registrations: (string | Registrations)[];
383
+ }
384
+ /** The account each cost the planning engine calculates is recorded in. One row per cost type. */
385
+ export interface OrganizationCostAccounts {
386
+ id: string;
387
+ organization_id: string | Organizations;
388
+ cost_type: "coach_wages" | "salaries" | "payroll_tax" | "ribs" | "boat_maintenance" | "resources" | "travel" | "cogs" | "card_fees";
389
+ account_id: string | Accounts;
390
+ }
391
+ /** A legal entity that keeps its own books. The fiscal year is set here, per organization. */
392
+ export interface Organizations {
393
+ id: string;
394
+ name: string;
395
+ /** The month the fiscal year starts in. */
396
+ fiscal_year_start_month: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
397
+ }
398
+ /** One row for each person the club knows. */
399
+ export interface People {
400
+ /** The Clubspot participants linked to this person. */
401
+ participant_links: (string | ClubspotParticipants)[];
402
+ id: string;
403
+ first_name: string;
404
+ last_name: string | null;
405
+ email: string | null;
406
+ phone: string | null;
407
+ date_of_birth: string | null;
408
+ /** This person's guardians and emergency contacts. */
409
+ my_contacts: (string | Contacts)[];
410
+ gender: string | null;
411
+ street: string | null;
412
+ city: string | null;
413
+ state: string | null;
414
+ postal_code: string | null;
415
+ /** The people this person is a guardian or emergency contact for. */
416
+ contact_for: (string | Contacts)[];
417
+ /** Every email and phone known for this person. */
418
+ contact_point_links: (string | ContactPoints)[];
419
+ school: string | null;
420
+ /** Null means never answered - treated as not shared. */
421
+ share_contact: boolean | null;
422
+ /** The person's own Directus login, if they have one. Usually empty. */
423
+ directus_user_id: string | null;
424
+ }
425
+ /** Each time a saved version was made active. A plan's newest activation is its active version. */
426
+ export interface PlanActivations {
427
+ id: string;
428
+ plan_id: string | Plans;
429
+ /** A saved version of the plan. */
430
+ version_id: string | PlanVersions;
431
+ /** Set when the version is made active. */
432
+ activated_at: string;
433
+ activated_by: string | null;
434
+ /** Set when the version is made active. The plan's active version is the one with the highest. */
435
+ sequence: number;
436
+ }
437
+ /** One dated activity, such as a practice, a regatta day, or a travel day. */
438
+ export interface PlanActivities {
439
+ id: string;
440
+ /** The version this row belongs to. */
441
+ version_id: string | PlanVersions;
442
+ /** Stays the same across versions. Copy it where another row asks for this one's Key. */
443
+ key: string;
444
+ offering_id: string | PlanOfferings;
445
+ /** The trip a travel day belongs to. */
446
+ trip_id: string | PlanTrips | null;
447
+ date: string;
448
+ start_time: string | null;
449
+ end_time: string | null;
450
+ on_water_minutes: number | null;
451
+ setup_minutes: number | null;
452
+ putaway_minutes: number | null;
453
+ kind: "practice" | "regatta" | "clinic" | "travel";
454
+ uses_boats: boolean;
455
+ /** Overrides the coaches the engine works out. */
456
+ fixed_coaches: number | null;
457
+ /** Overrides the coach boats the engine works out. */
458
+ fixed_ribs: number | null;
459
+ /** Overrides the boats the engine works out. */
460
+ fixed_boats: number | null;
461
+ }
462
+ /** Offerings whose activities share coaches and boats on certain days. */
463
+ export interface PlanActivityBlockRules {
464
+ id: string;
465
+ /** The version this row belongs to. */
466
+ version_id: string | PlanVersions;
467
+ /** Stays the same across versions. Copy it where another row asks for this one's Key. */
468
+ key: string;
469
+ name: string;
470
+ /** The offerings this rule fills: add the Key of each one. */
471
+ offering_keys: string[];
472
+ /** ISO weekdays, 1 for Monday to 7 for Sunday. */
473
+ weekdays: string[] | null;
474
+ starts_on: string | null;
475
+ ends_on: string | null;
476
+ coach_level_id: string | PlanStaffLevels | null;
477
+ /** The salaried positions it books: add the Key of each one. */
478
+ salaried_positions: string[] | null;
479
+ }
480
+ /** Equipment an activity uses beyond its coach boats. */
481
+ export interface PlanActivityResources {
482
+ id: string;
483
+ /** The version this row belongs to. */
484
+ version_id: string | PlanVersions;
485
+ /** Stays the same across versions. Copy it where another row asks for this one's Key. */
486
+ key: string;
487
+ activity_id: string | PlanActivities;
488
+ /** The resource it uses: copy the Key from a resource in the Operations Plan. */
489
+ resource_key: string;
490
+ quantity: string;
491
+ hours: string | null;
492
+ }
493
+ /** What an hour of sailing costs in maintenance, for each boat class. */
494
+ export interface PlanBoatRates {
495
+ id: string;
496
+ /** The version this row belongs to. */
497
+ version_id: string | PlanVersions;
498
+ /** Stays the same across versions. Copy it where another row asks for this one's Key. */
499
+ key: string;
500
+ boat_class_id: string | BoatClasses;
501
+ /** In cents: 12500 is $125.00. */
502
+ maintenance_per_boat_hour_cents: number;
503
+ }
504
+ /** The products a bonus owner owns. */
505
+ export interface PlanBonusOwnerProducts {
506
+ id: string;
507
+ /** The version this row belongs to. */
508
+ version_id: string | PlanVersions;
509
+ /** Stays the same across versions. Copy it where another row asks for this one's Key. */
510
+ key: string;
511
+ bonus_owner_id: string | PlanBonusOwners;
512
+ product_id: string | Products;
513
+ }
514
+ /** A role, such as a head coach, that earns a bonus on the products it owns. */
515
+ export interface PlanBonusOwners {
516
+ id: string;
517
+ /** The version this row belongs to. */
518
+ version_id: string | PlanVersions;
519
+ /** Stays the same across versions. Copy it where another row asks for this one's Key. */
520
+ key: string;
521
+ role_name: string;
522
+ /** A share of its products' combined net, after pools. 0.05 means 5%. */
523
+ bonus_rate: string;
524
+ account_id: string | Accounts;
525
+ }
526
+ /** A cost or revenue entered as an amount. */
527
+ export interface PlanBudgetLines {
528
+ id: string;
529
+ /** The version this row belongs to. */
530
+ version_id: string | PlanVersions;
531
+ /** Stays the same across versions. Copy it where another row asks for this one's Key. */
532
+ key: string;
533
+ name: string;
534
+ account_id: string | Accounts;
535
+ function: "program" | "management" | "fundraising" | null;
536
+ cost_center_type: "offering" | "product" | "program_area" | "pool" | "org";
537
+ /** Who the cost is charged to: copy the Key from an offering or a pool, or a product's ID from the end of its page's web address. Leave empty for the whole club. */
538
+ cost_center_key: string | null;
539
+ /** In cents: 12500 is $125.00. */
540
+ amount_cents: number;
541
+ fiscal_year: number;
542
+ /** Set for a cost left out of modified total direct costs (MTDC), such as equipment. */
543
+ excluded_from_mtdc: boolean;
544
+ note: string | null;
545
+ }
546
+ /** Labor not tied to an activity, such as training, priced at a staff level. */
547
+ export interface PlanLaborLines {
548
+ id: string;
549
+ /** The version this row belongs to. */
550
+ version_id: string | PlanVersions;
551
+ /** Stays the same across versions. Copy it where another row asks for this one's Key. */
552
+ key: string;
553
+ name: string;
554
+ cost_center_type: "offering" | "product" | "program_area" | "pool" | "org";
555
+ /** Who the cost is charged to: copy the Key from an offering or a pool, or a product's ID from the end of its page's web address. Leave empty for the whole club. */
556
+ cost_center_key: string | null;
557
+ /** Who does the work: copy the Key from a staff level in the Operations Plan. */
558
+ staff_level_key: string;
559
+ hours: string;
560
+ account_id: string | Accounts;
561
+ function: "program" | "management" | "fundraising" | null;
562
+ note: string | null;
563
+ }
564
+ /** The boat classes an offering sails, and each one's share of its sailors. */
565
+ export interface PlanOfferingBoatClasses {
566
+ id: string;
567
+ /** The version this row belongs to. */
568
+ version_id: string | PlanVersions;
569
+ /** Stays the same across versions. Copy it where another row asks for this one's Key. */
570
+ key: string;
571
+ offering_id: string | PlanOfferings;
572
+ boat_class_id: string | BoatClasses;
573
+ /** This boat class's share of the offering's sailors. 0.5 means 50%. */
574
+ share: string;
575
+ }
576
+ /** A product sold for a period, such as a season or a week. */
577
+ export interface PlanOfferings {
578
+ id: string;
579
+ /** The version this row belongs to. */
580
+ version_id: string | PlanVersions;
581
+ /** Stays the same across versions. Copy it where another row asks for this one's Key. */
582
+ key: string;
583
+ product_id: string | Products;
584
+ name: string;
585
+ season_term: string | null;
586
+ starts_on: string;
587
+ ends_on: string;
588
+ capacity: number | null;
589
+ /** Whole kids. Or set Fill rate instead. */
590
+ target_enrollment: number | null;
591
+ /** A share of capacity, used when Target enrollment is empty. 0.8 means 80%. */
592
+ fill_rate: string | null;
593
+ /** Per kid. In cents: 12500 is $125.00. */
594
+ price_cents: number | null;
595
+ sailors_per_coach: string | null;
596
+ min_coaches: number | null;
597
+ /** The coach level: copy the Key from a staff level in the Operations Plan. */
598
+ coach_level_key: string | null;
599
+ required_qualification: string | Qualifications | null;
600
+ ribs_per_coach: string | null;
601
+ /** The share of sailors who bring their own boat. 0.25 means 25%. */
602
+ byob_share: string | null;
603
+ /** Overrides the coaches the engine works out. */
604
+ fixed_coaches: number | null;
605
+ /** Overrides the coach boats the engine works out. */
606
+ fixed_ribs: number | null;
607
+ /** Overrides the boats the engine works out. */
608
+ fixed_boats: number | null;
609
+ /** Cost of goods sold, as a share of revenue. 0.1 means 10%. */
610
+ cogs_rate: string | null;
611
+ note: string | null;
612
+ }
613
+ /** Shared costs no single offering owns, such as G&A, divided among the year's offerings by MTDC. */
614
+ export interface PlanPools {
615
+ id: string;
616
+ /** The version this row belongs to. */
617
+ version_id: string | PlanVersions;
618
+ /** Stays the same across versions. Copy it where another row asks for this one's Key. */
619
+ key: string;
620
+ name: string;
621
+ }
622
+ /** The year's rates. One row at most in each version. */
623
+ export interface PlanRates {
624
+ id: string;
625
+ /** The version this row belongs to. */
626
+ version_id: string | PlanVersions;
627
+ /** Stays the same across versions. Copy it where another row asks for this one's Key. */
628
+ key: string;
629
+ /** Employer payroll costs, as a share of wages. 0.1 means 10%. */
630
+ payroll_tax_rate: string | null;
631
+ /** In cents: 12500 is $125.00. */
632
+ lodging_per_night_cents: number | null;
633
+ /** In cents: 12500 is $125.00. */
634
+ mileage_rate_cents: number | null;
635
+ /** The kind of resource whose cost prices coach boat (RIB) hours. */
636
+ rib_resource_kind_id: string | ResourceKinds | null;
637
+ }
638
+ /** Equipment programs share, such as coach boats, and what an hour or a day of it costs. */
639
+ export interface PlanResources {
640
+ id: string;
641
+ /** The version this row belongs to. */
642
+ version_id: string | PlanVersions;
643
+ /** Stays the same across versions. Copy it where another row asks for this one's Key. */
644
+ key: string;
645
+ resource_kind_id: string | ResourceKinds;
646
+ name: string;
647
+ quantity: number | null;
648
+ /** In cents: 12500 is $125.00. */
649
+ cost_per_hour_cents: number | null;
650
+ /** In cents: 12500 is $125.00. */
651
+ cost_per_day_cents: number | null;
652
+ /** In cents: 12500 is $125.00. */
653
+ fuel_per_hour_cents: number | null;
654
+ /** In cents: 12500 is $125.00. */
655
+ maintenance_per_hour_cents: number | null;
656
+ /** Set for a rental, whose cost is left out of modified total direct costs (MTDC). */
657
+ excluded_from_mtdc: boolean;
658
+ }
659
+ /** A charge set as a share of each offering's revenue, such as scholarships or card fees. Operations Plan only. */
660
+ export interface PlanRevenueCharges {
661
+ id: string;
662
+ /** The version this row belongs to. */
663
+ version_id: string | PlanVersions;
664
+ /** Stays the same across versions. Copy it where another row asks for this one's Key. */
665
+ key: string;
666
+ name: string;
667
+ /** The share of each offering's revenue: 0.02 means 2%. */
668
+ rate: string;
669
+ /** The kinds of product it applies to. Empty applies it to none. */
670
+ product_kinds: ("program" | "charter" | "storage_lease" | "merchandise" | "event_hosting" | "other")[] | null;
671
+ /** A revenue account adds revenue, such as card fees families pay. An expense account adds a cost. */
672
+ account_id: string | Accounts;
673
+ /** Set for a charge 2 CFR 200.1 leaves out of MTDC, such as scholarships. */
674
+ excluded_from_mtdc: boolean;
675
+ note: string | null;
676
+ }
677
+ /** Hours of a salaried position booked to a product. */
678
+ export interface PlanSalariedBookings {
679
+ id: string;
680
+ /** The version this row belongs to. */
681
+ version_id: string | PlanVersions;
682
+ /** Stays the same across versions. Copy it where another row asks for this one's Key. */
683
+ key: string;
684
+ position_id: string | PlanSalariedPositions;
685
+ /** The product the hours are for. Set this or the program area, not both. */
686
+ product_id: string | Products | null;
687
+ /** The program area the hours are for, shared among its offerings. Set this or the product, not both. */
688
+ program_area_id: string | ProgramAreas | null;
689
+ kind: "management";
690
+ hours: string;
691
+ }
692
+ /** A salaried position, and where the part of its salary not charged to programs goes. */
693
+ export interface PlanSalariedPositions {
694
+ id: string;
695
+ /** The version this row belongs to. */
696
+ version_id: string | PlanVersions;
697
+ /** Stays the same across versions. Copy it where another row asks for this one's Key. */
698
+ key: string;
699
+ name: string;
700
+ /** In cents: 12500 is $125.00. */
701
+ annual_salary_cents: number;
702
+ paid_hours_per_year: number | null;
703
+ /** The qualifications this position needs. Add each one's ID, from the end of its page's web address. */
704
+ qualifications: string[] | null;
705
+ /** The share of its salary that is general and administrative (G&A). 0.2 means 20%. */
706
+ ga_share: string | null;
707
+ /** The pool that takes the salary its hours don't charge to programs. */
708
+ remainder_pool_id: string | PlanPools | null;
709
+ form_990_line: string | null;
710
+ }
711
+ /** A level hourly staff are paid at, such as head coach. */
712
+ export interface PlanStaffLevels {
713
+ id: string;
714
+ /** The version this row belongs to. */
715
+ version_id: string | PlanVersions;
716
+ /** Stays the same across versions. Copy it where another row asks for this one's Key. */
717
+ key: string;
718
+ name: string;
719
+ pay_type: "hourly" | "volunteer";
720
+ /** In cents: 12500 is $125.00. */
721
+ hourly_rate_cents: number | null;
722
+ /** The qualifications this level needs. Add each one's ID, from the end of its page's web address. */
723
+ qualifications: string[] | null;
724
+ }
725
+ /** A trip an offering travels for, such as an away regatta. */
726
+ export interface PlanTrips {
727
+ id: string;
728
+ /** The version this row belongs to. */
729
+ version_id: string | PlanVersions;
730
+ /** Stays the same across versions. Copy it where another row asks for this one's Key. */
731
+ key: string;
732
+ offering_id: string | PlanOfferings;
733
+ name: string;
734
+ nights: number | null;
735
+ /** Per coach. In cents: 12500 is $125.00. */
736
+ airfare_cents: number | null;
737
+ miles: number | null;
738
+ }
739
+ /** A plan's draft, or a version saved from it, which never changes. */
740
+ export interface PlanVersions {
741
+ id: string;
742
+ plan_id: string | Plans;
743
+ /** The saved version this draft started from. */
744
+ parent_version_id: string | PlanVersions | null;
745
+ /** Saving a draft sets this to saved. A saved version never changes, even for an admin. */
746
+ status: "draft" | "saved";
747
+ /** What changed in this version. */
748
+ message: string | null;
749
+ /** Set when the version is saved. */
750
+ saved_by: string | null;
751
+ /** Set when the version is saved. */
752
+ saved_at: string | null;
753
+ /** The version of the planning engine that calculated its numbers. Required to save. */
754
+ platform_version: string | null;
755
+ /** For a program area's plan: the saved Operations Plan version its numbers were calculated with, for the same organization and year. Required to save. Empty for the Operations Plan. */
756
+ operations_version_id: string | PlanVersions | null;
757
+ /** What you want that the plan's numbers don't capture, for your AI to fill in. For example: every Monday, Wednesday and Friday, except on SPS school holidays, from 4pm until dusk. */
758
+ rules: string | null;
759
+ }
760
+ /** One part of the club's plan: a program area's plan, or the Operations Plan. */
761
+ export interface Plans {
762
+ id: string;
763
+ kind: "program_area" | "operations";
764
+ /** The program area the plan is for. Empty for the Operations Plan. */
765
+ program_area_id: string | ProgramAreas | null;
766
+ /** The organization the plan is for. Can't change once the plan has a saved version. */
767
+ organization_id: string | Organizations;
768
+ /** The plan year. Fiscal year N ends in calendar year N. Can't change once the plan has a saved version. */
769
+ fiscal_year: number;
770
+ name: string;
771
+ /** A plan with a saved version can't be deleted, so archive it instead. */
772
+ archived: boolean;
773
+ /** The plan's draft and saved versions. */
774
+ versions: (string | PlanVersions)[];
775
+ }
776
+ /** A lasting thing the club sells. A program also has a row in Programs. */
777
+ export interface Products {
778
+ id: string;
779
+ name: string;
780
+ /** A program also needs a row in Programs. Directus can't check that, so keep them matching. */
781
+ kind: "program" | "charter" | "storage_lease" | "merchandise" | "event_hosting" | "other";
782
+ /** Where the product's sales are booked, for a product in no program area. A product in a program area uses the area's revenue account. */
783
+ revenue_account_id: string | Accounts | null;
784
+ /** This product's program details, if it is a program. */
785
+ programs: (string | Programs)[];
786
+ /** The program area a program belongs to. Merchandise, storage, and charters belong to none. */
787
+ program_area_id: string | ProgramAreas | null;
788
+ }
789
+ /** Who holds a role, such as Director, in a program area, and when. */
790
+ export interface ProgramAreaRoleAssignments {
791
+ id: string;
792
+ person_id: string | People;
793
+ /** The program area the role is in. */
794
+ program_area_id: string | ProgramAreas;
795
+ program_role_id: string | ProgramRoles;
796
+ starts_on: string | null;
797
+ ends_on: string | null;
798
+ }
799
+ /** A part of the club's programs, such as Summer or Singlehanded, with its revenue account. People hold roles in it, such as Director. Each is one of the club's program services on Form 990 Part III. */
800
+ export interface ProgramAreas {
801
+ id: string;
802
+ name: string;
803
+ /** Where the area's programs' fees are booked. */
804
+ revenue_account_id: string | Accounts;
805
+ /** The products in the area: its programs. */
806
+ products: (string | Products)[];
807
+ /** Who holds a role in the area, such as Director. */
808
+ role_assignments: (string | ProgramAreaRoleAssignments)[];
809
+ }
810
+ /** The role someone has in a program, such as Parent Coordinator. Entered by staff, never by a sync. With no dates, it lasts until someone ends it. */
811
+ export interface ProgramRoleAssignments {
812
+ id: string;
813
+ person_id: string | People;
814
+ program_id: string | Programs;
815
+ program_role_id: string | ProgramRoles;
816
+ starts_on: string | null;
817
+ ends_on: string | null;
818
+ }
819
+ /** The roles staff can give someone in a program, such as Parent Coordinator. */
820
+ export interface ProgramRoles {
821
+ id: string;
822
+ name: string;
823
+ description: string | null;
824
+ }
825
+ /** A lasting program, such as Youth Camp, that staff link each Clubspot class to. Not the same as a Clubspot camp. */
826
+ export interface Programs {
827
+ /** The Clubspot classes linked to this program. */
828
+ classes: (string | ClubspotClasses)[];
829
+ id: string;
830
+ name: string;
831
+ /** The product this program belongs to. Its kind must be Program. */
832
+ product_id: string | Products;
833
+ /** Who has a role in this program. */
834
+ role_assignments: (string | ProgramRoleAssignments)[];
835
+ /** The program's Google Group, if it has one. Most programs don't. */
836
+ google_group_id: string | GoogleGroups | null;
837
+ }
838
+ /** Holding one qualification also meets a requirement for another. */
839
+ export interface QualificationCovers {
840
+ id: string;
841
+ qualification_id: string | Qualifications;
842
+ covers_id: string | Qualifications;
843
+ }
844
+ /** A certification a coach holds or an offering requires. */
845
+ export interface Qualifications {
846
+ id: string;
847
+ name: string;
848
+ /** The qualifications this one satisfies a requirement for. */
849
+ covers: (string | QualificationCovers)[];
850
+ }
851
+ /** One person registered in one offering. The Clubspot sync makes one for each sailor in a class and session, so edit it in Clubspot. */
852
+ export interface Registrations {
853
+ id: string;
854
+ person_id: string | People;
855
+ offering_id: string | Offerings;
856
+ status: "confirmed" | "waitlist" | "cancelled";
857
+ clubspot_entry_id: string | ClubspotRegistrationEntries;
858
+ }
859
+ /** A kind of shared resource, such as RIB or trailer. */
860
+ export interface ResourceKinds {
861
+ id: string;
862
+ name: string;
863
+ }
864
+ /** One row each time a sync runs. */
865
+ export interface SyncRuns {
866
+ id: string;
867
+ /** Which sync ran. */
868
+ source: string;
869
+ started_at: string;
870
+ finished_at: string | null;
871
+ status: "running" | "succeeded" | "failed";
872
+ counts: unknown;
873
+ error: string | null;
874
+ }
875
+ /** The work each sync has queued, run, or given up on. Written by the syncs. */
876
+ export interface SyncTasks {
877
+ id: string;
878
+ /** Which sync owns this task. */
879
+ queue: string;
880
+ /** What the task does, such as syncing one camp. Each sync names its own. */
881
+ kind: string;
882
+ /** What the task is about, such as a camp. Queuing the same thing again updates this row. */
883
+ key: string;
884
+ /** The task that queued this one, if any. */
885
+ parent_id: string | SyncTasks | null;
886
+ status: "pending" | "running" | "done" | "failed" | "cancelled";
887
+ attempts: number;
888
+ max_attempts: number;
889
+ /** The task waits until this time. Empty means it can run now. */
890
+ run_after: string | null;
891
+ last_error: string | null;
892
+ started_at: string | null;
893
+ finished_at: string | null;
894
+ /** Set once the task has failed too many times. It keeps retrying; this flags it for staff. */
895
+ needs_attention: boolean;
896
+ }
@@ -0,0 +1,2 @@
1
+ // Generated from schemas/*.yaml by `mise run schema-types`. Don't edit it by hand.
2
+ export {};
@@ -5,6 +5,12 @@ type Probe = k8s.types.input.core.v1.Probe;
5
5
  export interface WebServiceArgs extends WorkloadArgs {
6
6
  /** The public hostname. It must be in the environment's domain. */
7
7
  hostname: string;
8
+ /**
9
+ * Path prefixes the route claims on the hostname, such as `["/plans"]`. Leave it out to claim the
10
+ * whole hostname. Another route on the same hostname keeps the rest: the Gateway sends a request to
11
+ * the route whose prefix matches the most of its path.
12
+ */
13
+ paths?: string[];
8
14
  /** The port the container serves HTTP on. The Service exposes it as port 80, named `http`. */
9
15
  port: number;
10
16
  /** An HTTP GET probe. The load balancer's health check uses the same path. */
@@ -22,6 +28,8 @@ export interface WebServiceArgs extends WorkloadArgs {
22
28
  }
23
29
  /** Returns `hostname` if it is the environment's domain or a name under it, and throws otherwise. */
24
30
  export declare function checkHostname(hostname: string, domain: string): string;
31
+ /** Returns `paths` if each is an absolute path prefix, with no repeats, and throws otherwise. */
32
+ export declare function checkPaths(name: string, paths: string[]): string[];
25
33
  /**
26
34
  * An HTTP service behind the environment's shared Gateway: a `Workload`'s Deployment, Service, and
27
35
  * Kubernetes ServiceAccount, plus an HTTPRoute and a HealthCheckPolicy. Its pods get no IAM; grant
@@ -10,6 +10,22 @@ export function checkHostname(hostname, domain) {
10
10
  }
11
11
  return hostname;
12
12
  }
13
+ /** Returns `paths` if each is an absolute path prefix, with no repeats, and throws otherwise. */
14
+ export function checkPaths(name, paths) {
15
+ if (paths.length === 0) {
16
+ throw new Error(`WebService "${name}" lists no paths. Leave \`paths\` out to claim the whole hostname.`);
17
+ }
18
+ for (const path of paths) {
19
+ if (!/^\/[^?#\s]*$/.test(path)) {
20
+ throw new Error(`WebService "${name}" has path "${path}", which isn't an absolute path.`);
21
+ }
22
+ }
23
+ const repeated = paths.find((path, i) => paths.indexOf(path) !== i);
24
+ if (repeated !== undefined) {
25
+ throw new Error(`WebService "${name}" lists path "${repeated}" twice.`);
26
+ }
27
+ return paths;
28
+ }
13
29
  /**
14
30
  * An HTTP service behind the environment's shared Gateway: a `Workload`'s Deployment, Service, and
15
31
  * Kubernetes ServiceAccount, plus an HTTPRoute and a HealthCheckPolicy. Its pods get no IAM; grant
@@ -33,7 +49,9 @@ export class WebService extends pulumi.ComponentResource {
33
49
  const hostname = platform.domain.apply((domain) => checkHostname(args.hostname, domain));
34
50
  const metadata = { name, namespace: platform.namespace, labels: { "app.kubernetes.io/name": name } };
35
51
  const child = { parent: this, provider: args.provider };
52
+ const paths = args.paths && checkPaths(name, args.paths);
36
53
  const { serviceAccount, service } = deployment(this, name, args, args.port);
54
+ const backendRefs = [{ name: service.metadata.name, port: 80 }];
37
55
  new k8s.apiextensions.CustomResource(name, {
38
56
  apiVersion: "gateway.networking.k8s.io/v1",
39
57
  kind: "HTTPRoute",
@@ -41,7 +59,11 @@ export class WebService extends pulumi.ComponentResource {
41
59
  spec: {
42
60
  parentRefs: [{ name: platform.gateway.name, namespace: platform.gateway.namespace }],
43
61
  hostnames: [hostname],
44
- rules: [{ backendRefs: [{ name: service.metadata.name, port: 80 }] }],
62
+ rules: [
63
+ paths
64
+ ? { matches: paths.map((value) => ({ path: { type: "PathPrefix", value } })), backendRefs }
65
+ : { backendRefs },
66
+ ],
45
67
  },
46
68
  }, child);
47
69
  // Without it, the load balancer checks `/` on the serving port, which an app may not answer.
@@ -66,7 +88,7 @@ export class WebService extends pulumi.ComponentResource {
66
88
  this.serviceAccountName = serviceAccount.metadata.name;
67
89
  this.member = workloadMember(platform, this.serviceAccountName);
68
90
  this.host = name;
69
- this.url = pulumi.interpolate `https://${hostname}/`;
91
+ this.url = pulumi.interpolate `https://${hostname}${paths?.[0] ?? "/"}`;
70
92
  this.registerOutputs({
71
93
  member: this.member,
72
94
  serviceAccountName: this.serviceAccountName,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyc-seattle/platform",
3
- "version": "1.17.0",
3
+ "version": "1.18.0",
4
4
  "description": "Pulumi components for CYC's shared Google Cloud platform: WebService, the platform outputs reader, and the stacks that build the platform",
5
5
  "author": "CYC Community Sailing Center",
6
6
  "license": "Apache-2.0",
@@ -30,6 +30,10 @@
30
30
  "types": "./dist/planning/index.d.ts",
31
31
  "default": "./dist/planning/index.js"
32
32
  },
33
+ "./schema": {
34
+ "types": "./dist/schema/index.d.ts",
35
+ "default": "./dist/schema/index.js"
36
+ },
33
37
  "./package.json": "./package.json"
34
38
  },
35
39
  "files": [
@@ -47,6 +51,17 @@
47
51
  "@pulumi/kubernetes": "^4",
48
52
  "@pulumi/pulumi": "^3"
49
53
  },
54
+ "peerDependenciesMeta": {
55
+ "@pulumi/gcp": {
56
+ "optional": true
57
+ },
58
+ "@pulumi/kubernetes": {
59
+ "optional": true
60
+ },
61
+ "@pulumi/pulumi": {
62
+ "optional": true
63
+ }
64
+ },
50
65
  "devDependencies": {
51
66
  "@amiceli/vitest-cucumber": "7.0.0",
52
67
  "@cyc-seattle/ts-config": "^0.1.2",