Repository navigation
feat(settings): configurable EC2 RI OfferingClass (Convertible vs Standard) #694
Copy link
Copy link
Closed
Labels
effort/mDaysDaysimpact/fewLimited audienceLimited audiencepriority/p3Polish / idea / may never shipPolish / idea / may never shipseverity/lowMinor harmMinor harmtriagedItem has been triagedItem has been triagedtype/featNew capabilityNew capabilityurgency/eventuallyNo deadlineNo deadline
Description
Summary
The EC2 RI purchase path hardcodes OfferingClass: types.OfferingClassTypeConvertible (providers/aws/services/ec2/client.go:381, inside describeInputFromQuery). Make this configurable per tenant via the existing Settings UI, defaulting to Convertible (current behaviour).
Motivation
Convertible RIs are the safer default -- they can be exchanged for different instance families/sizes/regions/OS as workloads evolve. But Standard RIs are ~5% cheaper for the same coverage. A tenant that has stable, well-characterised workloads and never expects to need exchange may prefer Standard for the extra savings; today they have no way to express that and CUDly always buys Convertible.
Scope
Backend
- Add
OfferingClass string(orRIOfferingClass) tointernal/config/types.goGlobalConfigstruct, JSON-taggedoffering_class. Values:"convertible"(default) or"standard". - Add a SQL migration mirroring
000036_purchase_grace_period.up.sql:ALTER TABLE global_config ADD COLUMN IF NOT EXISTS offering_class TEXT NOT NULL DEFAULT 'convertible'plus the matching.down.sql. - Wire
store_postgres.goSELECT and INSERT/UPDATE statements to round-trip the new column. - Replace the hardcoded
types.OfferingClassTypeConvertibleindescribeInputFromQuerywith a value resolved from the loadedGlobalConfig, mapping"convertible"->types.OfferingClassTypeConvertibleand"standard"->types.OfferingClassTypeStandard. Validate the config value at load time -- unknown values must surface an explicit error, never silently coerce (perfeedback_empty_string_vs_error.md). - Delete the always-returns-"convertible"
getOfferingClasshelper if it is now unused (verifygit grepafter the wiring).
Exchange listing
ListConvertibleReservedInstances(providers/aws/services/ec2/client.go:601) filters byoffering-class=convertibleserver-side. With the new setting it makes sense to:- keep this method as-is (Standard RIs cannot be exchanged, so the exchange-list filter on Convertible stays correct), OR
- rename to
ListExchangeableReservedInstancesfor clarity and document that Standard RIs are intentionally excluded.
API
internal/api/handler_*endpoints that return / accept settings: include the new field on GET and validate on PUT.internal/api/handler_ri_exchange.go:246currently setsOfferingClass: "convertible"on the exchange-record shape. Audit whether this stays a fixed string or also needs to read from config (probably stays fixed -- the exchange flow only ever applies to Convertible RIs).
Frontend
- Add a setting to
frontend/src/settings.tsmirroring the existing grace-period pattern (frontend/src/__tests__/settings-purchasing-grace.test.ts). - UI label: "Reserved Instance class" with two options: "Convertible (default -- exchangeable for different families/sizes/OS later)" and "Standard (~5% cheaper, locked to the exact instance for the full term)".
- Default to Convertible if the API returns null/missing.
- Add a frontend test mirroring
settings-purchasing-grace.test.ts.
Tests
- Unit test for
describeInputFromQuery(or its caller) assertingOfferingClassreflects the config value. - Integration test (or mock-level) asserting a Standard-configured purchase actually sends
OfferingClass=Standardto AWS. - Settings round-trip test on
store_postgres.go.
Out of scope
- Per-recommendation override (the dropdown only on individual purchase rows). Possible follow-up if customers ask for finer-grained control; tenant-level is enough for the first cut.
- Standard-RI -> Convertible-RI migration helper. Standard RIs cannot be converted; that is intentional and a property of AWS, not something CUDly can or should mask.
- Offering-class fields on services other than EC2. RDS / ElastiCache / OpenSearch / Redshift / MemoryDB don't expose a comparable Convertible/Standard split via the AWS APIs CUDly uses.
Acceptance criteria
- Setting added to
GlobalConfig+ migration applied - EC2 purchase path reads the setting; default "convertible" preserves current behaviour byte-for-byte
- Settings UI gains the dropdown with the two options
- Tests cover both values end-to-end
- Unknown value (e.g. typo in the DB) fails the purchase with an explicit error, never silently falls back
Cross-references
- PR fix(purchases): narrow Describe*Offerings + cap pagination (#688) #690 (where the EC2 purchase shape became typed-fields)
- Issue fix(aws): paginate CE recommendations + RDS engine versions (closes #692) #693 (fix(aws/recommendations): paginate CE recommendation + RDS engine-version describe (single-page truncation) #692 fix) -- same call-site neighbourhood
frontend/src/__tests__/settings-purchasing-grace.test.ts-- the prior-art pattern to mirror
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
effort/mDaysDaysimpact/fewLimited audienceLimited audiencepriority/p3Polish / idea / may never shipPolish / idea / may never shipseverity/lowMinor harmMinor harmtriagedItem has been triagedItem has been triagedtype/featNew capabilityNew capabilityurgency/eventuallyNo deadlineNo deadline
Activity
cristim commentedon May 28, 2026
Verification: cannot-verify-autopilot. PR #696 is linked in the input but its commit message says 'closes #695', and the PR body says 'Refs #694' (not closes). The acceptance criteria for #694 require OfferingClass field in GlobalConfig, a DB migration, settings UI dropdown, and tests -- none of these are present (grep for 'offering_class' in internal/config/types.go returns 0 matches). Issue remains open and unimplemented.
feat(settings): configurable EC2 RI OfferingClass (closes #694)
feat(settings): configurable EC2 RI OfferingClass (closes #694)
fix(settings): validate OfferingClass on PUT + assert it reaches the …
refactor(config): extract validateScheduleAndNotifications to reduce …
feat(settings): configurable EC2 RI OfferingClass (closes #694)
fix(settings): validate OfferingClass on PUT + assert it reaches the …
refactor(config): extract validateScheduleAndNotifications to reduce …
12 remaining items
fix(settings): validate OfferingClass on PUT + assert it reaches the …
refactor(config): extract validateScheduleAndNotifications to reduce …
feat(settings): configurable EC2 RI OfferingClass (closes #694)
fix(settings): validate OfferingClass on PUT + assert it reaches the …
refactor(config): extract validateScheduleAndNotifications to reduce …
style(purchase,config): clear new-from-rev lint on OfferingClass diff
feat(settings): configurable EC2 RI OfferingClass (closes #694)
fix(settings): validate OfferingClass on PUT + assert it reaches the …
refactor(config): extract validateScheduleAndNotifications to reduce …
style(purchase,config): clear new-from-rev lint on OfferingClass diff
feat(settings): configurable EC2 RI OfferingClass (closes #694)
fix(settings): validate OfferingClass on PUT + assert it reaches the …
refactor(config): extract validateScheduleAndNotifications to reduce …
style(purchase,config): clear new-from-rev lint on OfferingClass diff