From d1267bdfaba4c8f2116b0a200d79619f14564d0d Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Sat, 7 Nov 2026 23:02:33 +0100 Subject: [PATCH] fix(rebuy): widen the scope probe start by two days and note the lookback cap Recommendations can be about a day old, so their window can start before a probe that begins exactly N days back. The probe now starts two days earlier, which can only refuse more. The widened window (365+2 days) stays inside Cost Explorer's roughly 13 month range. The 365 day cap on --coverage-lookback-days applies to every --target-coverage run; note it in the changelog. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 4 ++++ cmd/rebuy_scope_command_test.go | 2 +- cmd/rebuy_scope_probe.go | 6 +++++- cmd/rebuy_scope_probe_test.go | 2 +- docs/cli/purchase-safety.md | 2 +- 5 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac49ae1d3..d14388064 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -169,6 +169,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). ### Changed +- `--coverage-lookback-days` is now capped at 365 for every `--target-coverage` + run, not only gated rebuy runs; values from 366 up to about 395, previously + accepted, are now rejected. The gated rebuy scope probe also starts two days + earlier than the longer lookback window, so it can only refuse more. - Bump cloud-commitments-go to dcb5ba0: Azure reservation purchases use one shared helper with aligned guards, error messages and display names, and hold the shared concurrency slot per call instead of per batch; the AWS provider diff --git a/cmd/rebuy_scope_command_test.go b/cmd/rebuy_scope_command_test.go index d727f0335..4e73713e0 100644 --- a/cmd/rebuy_scope_command_test.go +++ b/cmd/rebuy_scope_command_test.go @@ -146,7 +146,7 @@ func (p *completenessProxy) linkedAccountCoverage(body []byte, ceService, foreig wantDays = 60 foreignVisible = end.Sub(start) > 30*24*time.Hour } - require.Equal(p.t, time.Duration(wantDays)*24*time.Hour, end.Sub(start)) + require.Equal(p.t, time.Duration(wantDays+2)*24*time.Hour, end.Sub(start)) // Cost Explorer rejects an And with fewer than two operands, so one filter // must be a bare dimension (as in cloud-commitments-go's CE filter builders). filters := make(map[string][]string) diff --git a/cmd/rebuy_scope_probe.go b/cmd/rebuy_scope_probe.go index 60df38093..6c220bd4e 100644 --- a/cmd/rebuy_scope_probe.go +++ b/cmd/rebuy_scope_probe.go @@ -112,6 +112,10 @@ func recLookbackDays(period string) (int, error) { return 0, refuseRebuyScope("cannot read --rec-lookback-period %q, so the scope window is unknown; use 7d, 30d or 60d, or omit --rebuy-window-days", period) } +// rebuyProbeStartMarginDays: recommendations can be about a day old, so their +// window can start before a probe that begins exactly N days back. +const rebuyProbeStartMarginDays = 2 + // rebuyProbeInput builds the probe request: the larger of the coverage and // recommendation lookback windows, so an account that contributed usage to // either the recommendations or the coverage demand is seen (Metrics "Hour" as @@ -124,7 +128,7 @@ func rebuyProbeInput(ceService string, cfg Config) (*costexplorer.GetReservation return nil, err } end := time.Now().UTC() - start := end.AddDate(0, 0, -max(coverageLookbackDays(cfg), recDays)) + start := end.AddDate(0, 0, -(max(coverageLookbackDays(cfg), recDays) + rebuyProbeStartMarginDays)) filter := &types.Expression{Dimensions: &types.DimensionValues{Key: types.DimensionService, Values: []string{ceService}}} if len(cfg.Regions) > 0 { filter = &types.Expression{And: []types.Expression{ diff --git a/cmd/rebuy_scope_probe_test.go b/cmd/rebuy_scope_probe_test.go index eaf142aac..a2d2a5ef3 100644 --- a/cmd/rebuy_scope_probe_test.go +++ b/cmd/rebuy_scope_probe_test.go @@ -181,7 +181,7 @@ func TestProbeRequestShape(t *testing.T) { require.NoError(t, err) end, err := time.Parse(time.DateOnly, aws.ToString(fake.inputs[0].TimePeriod.End)) require.NoError(t, err) - assert.Equal(t, time.Duration(tc.wantDays)*24*time.Hour, end.Sub(start), "coverage %d rec %q", tc.coverage, tc.rec) + assert.Equal(t, time.Duration(tc.wantDays+2)*24*time.Hour, end.Sub(start), "coverage %d rec %q", tc.coverage, tc.rec) } }) t.Run("an unparsable recommendation lookback refuses before any request", func(t *testing.T) { diff --git a/docs/cli/purchase-safety.md b/docs/cli/purchase-safety.md index ad0139173..57c04e4f7 100644 --- a/docs/cli/purchase-safety.md +++ b/docs/cli/purchase-safety.md @@ -119,7 +119,7 @@ The run exits 1 and writes no CSV, on dry runs and `--purchase` runs alike, in t - the credentials belong to a member account of an AWS Organization. Replacements bought centrally are not visible to a member account, so its expiry netting could buy them again. Gated rebuy runs from a standalone account, or from a management account whose member accounts have no in-scope usage; - a recommendation row belongs to another account; -- a Cost Explorer coverage query (grouped by linked account, over the longer of `--coverage-lookback-days` and `--rec-lookback-period`, with the same service and, when `--regions` is set, region filters as sizing) shows usage in another account, so recommendations hidden behind your own account's rows are caught too; +- a Cost Explorer coverage query (grouped by linked account, over the longer of `--coverage-lookback-days` and `--rec-lookback-period` plus a two-day margin, with the same service and, when `--regions` is set, region filters as sizing) shows usage in another account, so recommendations hidden behind your own account's rows are caught too; - the check itself could not run: an unrecognized `--rec-lookback-period`, the STS identity lookup, the `organizations:DescribeOrganization` role check, the Cost Explorer scope query or the RI inventory read failed. Before this check, a failed RI inventory read on a dry run only warned and sized without the expiry netting; now it exits 1. A scope refusal names the other account and the service. To recover, run from a standalone account or from a management account whose member accounts have no in-scope usage, or omit `--rebuy-window-days`. A check failure says "the scope check could not run" and the cause: fix access or retry, or omit `--rebuy-window-days`. A failed role check also asks you to grant `organizations:DescribeOrganization` (read-only); see [cloud-setup.md](cloud-setup.md#aws-permissions-for---rebuy-window-days). A Cost Explorer AccessDenied also asks you to grant `ce:GetReservationCoverage` or enable Cost Explorer access for the account. A Cost Explorer failure while building the coverage map on a dry run still only warns, as described above. `--input-csv` runs never apply rebuy.