Skip to content

fix: delegate on-demand pricing to truffle instead of fabricating a rate (#533) - #536

Merged
scttfrdmn merged 2 commits into
mainfrom
fix/533-delegate-pricing-to-truffle
Aug 19, 2026
Merged

fix: delegate on-demand pricing to truffle instead of fabricating a rate (#533)#536
scttfrdmn merged 2 commits into
mainfrom
fix/533-delegate-pricing-to-truffle

Conversation

@scttfrdmn

Copy link
Copy Markdown
Contributor

Summary

  • --cost-limit was enforced against a fabricated price, not the real on-demand rate. At launch, when PricePerHour wasn't already known, spawn called its own hand-rolled, uncached AWS Pricing API lookup (LookupEC2OnDemandPrice) and, on failure — which it always did for any principal onboarded via spawn onboard, since that policy granted no pricing:* action — fell back to a static per-family-size guess table (libs/pricing.GetEC2HourlyRate) missing every current-gen instance family (g6, g6e, p5, p5e, c7g, c8g, ...). Measured: g6e.xlarge guessed at $0.20/hr vs a real $1.861/hr (9.3x under); p5.48xlarge guessed at $9.60/hr vs a real $55.04/hr (5.7x under). That fabricated number is what gets written to spawn:price-per-hour and is what spored enforces --cost-limit against.
  • Pricing at launch now delegates entirely to truffle's OnDemandPriceWithSource (github.com/spore-host/truffle/pkg/aws) — the suite's pricing authority, with a real 24h cache and a static fallback that errors rather than guesses (truffle#114/test(agent): guard --on-complete fires regardless of job origin (#105) #115). spawn keeps no rate table of its own for enforcement anymore.
  • If truffle can't price the instance at all (neither live nor its own static table) and the launch requested --cost-limit, the launch now fails with an error naming the instance type, region, and limit — instead of silently leaving the cap unenforced. Without --cost-limit, an unpriceable instance still launches with PricePerHour left unset (spored's cap-check already treats 0 as "no cap enforced," never fabricated).
  • Removed spawn's own LookupEC2OnDemandPrice (dead code after the above). libs/pricing.GetEC2HourlyRate is intentionally left in place for the separate display-only estimate paths (--estimate-only's "On-demand:" line in cmd/launch_single.go, spawn cost in cmd/service.go/pkg/cost) — that's a companion display bug tracked separately, out of scope here per the issue.
  • Added pricing:GetProducts to spawn onboard's IAM policy (cmd/onboard.go) — a global, read-only action with no resource scoping — since the pricing lookup runs in-process under the launching principal's own credentials via truffle. Operational note: anyone onboarded before this change needs to re-run spawn onboard to pick this up.
  • Bumped github.com/spore-host/truffle v0.49.0 → v0.53.0. Diffed pkg/aws/pricing.go between the two versions — byte-identical, no signature changes to OnDemandPrice/OnDemandPriceWithSource/GetCapabilities. Skimmed truffle's CHANGELOG for v0.50.0–v0.53.0: additive fields (Capabilities), new discovery-path error-handling fixes, find/search UX changes — nothing affecting the call sites spawn already uses (cmd/slurm.go, cmd/task.go, cmd/launch_single.go, cmd/launch_sweep_quota.go, cmd/launch_preflight.go).
  • cmd/status.go's worst-case-cost display now reads the spawn:price-per-hour tag (via the existing pricePerHour helper) instead of independently re-deriving a rate, so the displayed ceiling can't drift from what --cost-limit is actually enforced against.

Notes on searches performed

  • Grepped for the "fabrication-asserting" test the issue expected around pricing_test.go:69-70 (unknown.xlargeexpected: 0.2). That test lives in the separate github.com/spore-host/libs module's pkg/pricing/pricing_test.go (a different repo/module), not in spawn — spawn itself had no pricing_test.go before this PR. Nothing to delete in this repo; libs/pricing.GetEC2HourlyRate and its test are untouched, since that package is still legitimately used for the display-only estimate paths noted above.

Test plan

  • New unit tests in pkg/aws/pricing_test.go (TestResolvePricePerHour_*) mock the truffle pricing call via a small onDemandPricer interface (no real AWS calls):
    • a successful lookup (live or truffle's own static source) populates PricePerHour with the real rate
    • a failed lookup + --cost-limit requested → error naming instance type/region/limit
    • a failed lookup + no --cost-limit → launch proceeds, PricePerHour stays 0
    • a caller-supplied PricePerHour is never overridden
  • Fail-without/pass-with verified by mutation: temporarily broke the cost-limit gate (cfg.CostLimit > 0cfg.CostLimit > 999999), confirmed 2 of the new tests failed, reverted, confirmed all pass.
  • make check clean (fmt, vet, staticcheck skip — not installed locally —, full short test suite across all packages).
  • go build ./... and go vet ./... clean after the truffle bump.

@codecov

codecov Bot commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.00000% with 6 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
pkg/aws/client.go 0.00% 2 Missing ⚠️
pkg/aws/pricing.go 90.00% 2 Missing ⚠️
cmd/onboard.go 0.00% 1 Missing ⚠️
cmd/status.go 0.00% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

…ate (#533)

--cost-limit was enforced against a fabricated price, not the real
on-demand rate. spawn's own hand-rolled AWS Pricing API lookup
(LookupEC2OnDemandPrice) had no cache and, on failure — which it always
did for a principal onboarded via `spawn onboard`, whose policy granted
no pricing:* action — fell back to a static per-family-size guess table
missing every current-gen instance family. Measured: g6e.xlarge guessed
at $0.20/hr against a real $1.861/hr; p5.48xlarge guessed at $9.60/hr
against a real $55.04/hr.

Pricing at launch now delegates to truffle's OnDemandPriceWithSource,
the suite's pricing authority (live Price List + 24h cache, static
fallback that errors rather than guesses). A launch that requested
--cost-limit and can't be priced at all now fails with a clear error
instead of silently leaving the cap unenforced; without --cost-limit an
unpriceable instance still launches with PricePerHour left unset.

Also:
- Add pricing:GetProducts to spawn onboard's IAM policy (required for
  the in-process truffle lookup to succeed for onboarded principals).
- Bump github.com/spore-host/truffle v0.49.0 -> v0.53.0 (pricing.go is
  byte-identical between the two; no signature changes to the pricing
  or GetCapabilities APIs spawn calls).
- cmd/status.go's worst-case-cost display now reads the
  spawn:price-per-hour tag instead of re-deriving a rate, so it can't
  drift from what --cost-limit is actually enforced against.
@scttfrdmn
scttfrdmn force-pushed the fix/533-delegate-pricing-to-truffle branch from 5847693 to 75007eb Compare August 19, 2026 22:24
lambda/alert-handler, lambda/autoscale-orchestrator, lambda/sweep-orchestrator,
and lambda/ttl-reaper each pin their own go.mod (replace ../..) and had drifted
out of sync with the root module's transitive AWS SDK versions from other
merges landing on main, failing govulncheck/Test in CI with "updates to
go.mod needed" without naming which module. Same nested-module trap as
lagotto#43 and the earlier x/mod bump on this same PR's rebase.
@scttfrdmn
scttfrdmn merged commit b189728 into main Aug 19, 2026
7 checks passed
@scttfrdmn
scttfrdmn deleted the fix/533-delegate-pricing-to-truffle branch August 19, 2026 22:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant