Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion cmd/rebuy_scope_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 5 additions & 1 deletion cmd/rebuy_scope_probe.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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{
Expand Down
2 changes: 1 addition & 1 deletion cmd/rebuy_scope_probe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion docs/cli/purchase-safety.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading