Skip to content

feat(settings): configurable EC2 RI OfferingClass (Convertible vs Standard) #694

Closed

Description

@cristim

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 (or RIOfferingClass) to internal/config/types.go GlobalConfig struct, JSON-tagged offering_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.go SELECT and INSERT/UPDATE statements to round-trip the new column.
  • Replace the hardcoded types.OfferingClassTypeConvertible in describeInputFromQuery with a value resolved from the loaded GlobalConfig, mapping "convertible" -> types.OfferingClassTypeConvertible and "standard" -> types.OfferingClassTypeStandard. Validate the config value at load time -- unknown values must surface an explicit error, never silently coerce (per feedback_empty_string_vs_error.md).
  • Delete the always-returns-"convertible" getOfferingClass helper if it is now unused (verify git grep after the wiring).

Exchange listing

  • ListConvertibleReservedInstances (providers/aws/services/ec2/client.go:601) filters by offering-class=convertible server-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 ListExchangeableReservedInstances for 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:246 currently sets OfferingClass: "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.ts mirroring 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) asserting OfferingClass reflects the config value.
  • Integration test (or mock-level) asserting a Standard-configured purchase actually sends OfferingClass=Standard to 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

Activity

  1. cristim commented on May 28, 2026

    @cristim
    MemberAuthor

    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.

  2. added 7 commits that reference this issue on Jun 5, 2026
    ef13ff3
    04c5551
    18d71d9
    1f0686e
    37c7930
    0d9f12b
    db8203c
  3. 12 remaining items

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions