From d0495fb0c99ed8b548f0724df42c612f79cfec91 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Fri, 16 Oct 2026 03:41:23 +0200 Subject: [PATCH] feat(exchange)!: pass the source RI's availability zone to LookupOffering resolveOffering could not tell the offering lookup which zone a zonal convertible RI lives in, so the provider had no way to keep the target in that zone. RIMetadataInfo gains AvailabilityZone, and LookupOfferingFunc now takes a LookupOfferingParams struct carrying it alongside the existing fields. BREAKING CHANGE: LookupOfferingFunc takes LookupOfferingParams instead of six positional arguments; callers must also fill RIMetadataInfo.AvailabilityZone for zonal RIs. Closes #310 Co-Authored-By: Claude Opus 5.5 --- pkg/exchange/auto.go | 24 ++++++++++++++++++++++-- pkg/exchange/auto_test.go | 30 ++++++++++++++++++++++++++++-- 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/pkg/exchange/auto.go b/pkg/exchange/auto.go index ea935871..e8397f56 100644 --- a/pkg/exchange/auto.go +++ b/pkg/exchange/auto.go @@ -86,8 +86,20 @@ type RIExchangeConfig struct { LookbackDays int } +// LookupOfferingParams carries the target instance type and the source RI +// metadata a lookup needs. AvailabilityZone is the source RI's AZ and is empty +// for regional RIs. +type LookupOfferingParams struct { + InstanceType string + ProductDescription string + Tenancy string + Scope string + AvailabilityZone string + Duration int64 +} + // LookupOfferingFunc looks up a target offering ID for a given instance type and RI metadata. -type LookupOfferingFunc func(ctx context.Context, instanceType, productDesc, tenancy, scope string, duration int64) (string, error) +type LookupOfferingFunc func(ctx context.Context, p LookupOfferingParams) (string, error) // RunAutoExchangeParams holds all dependencies for RunAutoExchange. type RunAutoExchangeParams struct { @@ -123,6 +135,7 @@ type RIMetadataInfo struct { ProductDescription string InstanceTenancy string Scope string + AvailabilityZone string Duration int64 } @@ -284,7 +297,14 @@ func resolveOffering(ctx context.Context, params RunAutoExchangeParams, rec Resh } } - offeringID, err := params.LookupOffering(ctx, rec.TargetInstanceType, meta.ProductDescription, meta.InstanceTenancy, meta.Scope, meta.Duration) + offeringID, err := params.LookupOffering(ctx, LookupOfferingParams{ + InstanceType: rec.TargetInstanceType, + ProductDescription: meta.ProductDescription, + Tenancy: meta.InstanceTenancy, + Scope: meta.Scope, + AvailabilityZone: meta.AvailabilityZone, + Duration: meta.Duration, + }) if err != nil { logging.Warnf("offering lookup failed for %s -> %s: %v", rec.SourceRIID, rec.TargetInstanceType, err) return "", &SkippedRecommendation{ diff --git a/pkg/exchange/auto_test.go b/pkg/exchange/auto_test.go index ca944056..88f2482c 100644 --- a/pkg/exchange/auto_test.go +++ b/pkg/exchange/auto_test.go @@ -317,7 +317,7 @@ func defaultParams(store RIExchangeStore, client ExchangeClientInterface) RunAut return RunAutoExchangeParams{ Store: store, ExchangeClient: client, - LookupOffering: func(_ context.Context, _, _, _, _ string, _ int64) (string, error) { + LookupOffering: func(_ context.Context, _ LookupOfferingParams) (string, error) { return "offering-123", nil }, RIs: []RIInfo{ @@ -413,7 +413,7 @@ func TestRunAutoExchange_OfferingLookupFails(t *testing.T) { store := &mockExchangeStore{dailySpend: "0"} client := &mockExchangeClient{quoteResult: defaultQuote()} params := defaultParams(store, client) - params.LookupOffering = func(_ context.Context, _, _, _, _ string, _ int64) (string, error) { + params.LookupOffering = func(_ context.Context, _ LookupOfferingParams) (string, error) { return "", fmt.Errorf("no offering found") } @@ -1084,3 +1084,29 @@ func TestRunAutoExchange_SettlementFailure_StopsAfterFirstExchange(t *testing.T) assert.Equal(t, 1, client.executeCalls, "Execute must be called exactly once; second exchange must be halted") } + +func TestResolveOffering_ForwardsAvailabilityZone(t *testing.T) { + t.Parallel() + var got LookupOfferingParams + params := defaultParams(&mockExchangeStore{dailySpend: "0"}, &mockExchangeClient{}) + params.LookupOffering = func(_ context.Context, p LookupOfferingParams) (string, error) { + got = p + return "offering-az", nil + } + params.RIMetadata = map[string]RIMetadataInfo{ + "ri-001": { + ProductDescription: "Linux/UNIX", InstanceTenancy: "default", + Scope: "Availability Zone", AvailabilityZone: "us-east-1b", Duration: 31536000, + }, + } + rec := ReshapeRecommendation{SourceRIID: "ri-001", TargetInstanceType: "m5.2xlarge"} + + id, skipped := resolveOffering(context.Background(), params, rec) + + require.Nil(t, skipped) + assert.Equal(t, "offering-az", id) + assert.Equal(t, LookupOfferingParams{ + InstanceType: "m5.2xlarge", ProductDescription: "Linux/UNIX", Tenancy: "default", + Scope: "Availability Zone", AvailabilityZone: "us-east-1b", Duration: 31536000, + }, got) +}