Skip to content

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

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

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