From 5b254484d5e43e8446fc8278504b71a5857e504a Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Thu, 15 Oct 2026 09:18:54 +0200 Subject: [PATCH 1/2] feat(ladder): expose scoped EC2 commitment rates Reuse the strict CE rate client through an optional ladder capability and an explicit AWS configuration factory. Keep unsupported layers on Hold. Pin the provider to the published shared allocation contract and cover the public SDK path, scoped pagination, tranche rates and repeat-run netting. --- pkg/ladder/capability.go | 6 + providers/aws/go.mod | 2 +- providers/aws/go.sum | 4 +- providers/aws/ladder/commitment_rates.go | 46 +++++ .../aws/ladder/commitment_rates_sdk_test.go | 188 ++++++++++++++++++ providers/aws/ladder/commitment_rates_test.go | 119 +++++++++++ providers/aws/ladder/factory.go | 16 +- providers/aws/ladder/ladder.go | 1 + 8 files changed, 373 insertions(+), 9 deletions(-) create mode 100644 providers/aws/ladder/commitment_rates.go create mode 100644 providers/aws/ladder/commitment_rates_sdk_test.go create mode 100644 providers/aws/ladder/commitment_rates_test.go diff --git a/pkg/ladder/capability.go b/pkg/ladder/capability.go index 9206ac8c..769a5130 100644 --- a/pkg/ladder/capability.go +++ b/pkg/ladder/capability.go @@ -6,6 +6,12 @@ import ( "github.com/LeanerCloud/cloud-commitments-go/pkg/common" ) +// CommitmentRateCapability optionally supplies new-purchase commitment dollars +// per covered on-demand dollar. Missing layers in the partial map are intentional. +type CommitmentRateCapability interface { + GetCommitmentRates(context.Context, Scope, Term, PaymentOption) (map[LayerType]float64, error) +} + // LadderCapability is implemented by each cloud provider to give the ladder // engine a uniform interface for querying commitment state and executing // purchases. Implementations live in providers/ and are injected into the diff --git a/providers/aws/go.mod b/providers/aws/go.mod index 5ff1a584..cc346fa8 100644 --- a/providers/aws/go.mod +++ b/providers/aws/go.mod @@ -3,7 +3,7 @@ module github.com/LeanerCloud/cloud-commitments-go/providers/aws go 1.26.9 require ( - github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261013182117-cdcdc7fbcc32 + github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261014214001-29adce1c3883 github.com/aws/aws-sdk-go-v2 v1.41.5 github.com/aws/aws-sdk-go-v2/config v1.29.12 github.com/aws/aws-sdk-go-v2/credentials v1.17.65 diff --git a/providers/aws/go.sum b/providers/aws/go.sum index 0598ad2d..6ca9bd3b 100644 --- a/providers/aws/go.sum +++ b/providers/aws/go.sum @@ -1,5 +1,5 @@ -github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261013182117-cdcdc7fbcc32 h1:IxqJVVAA26JAS9BtKvNFjn2zPN/LuzPFP5y4O417JS8= -github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261013182117-cdcdc7fbcc32/go.mod h1:8RTmzPazA8IeVZfn7wNaU7ILx0VK5eHZQozO21Mvtq8= +github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261014214001-29adce1c3883 h1:SZuUVNZD0s/LHuI2e3hr3Pd14AuJgTHvXyShicoW4kA= +github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261014214001-29adce1c3883/go.mod h1:8RTmzPazA8IeVZfn7wNaU7ILx0VK5eHZQozO21Mvtq8= github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY= github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= github.com/aws/aws-sdk-go-v2/config v1.29.12 h1:Y/2a+jLPrPbHpFkpAAYkVEtJmxORlXoo5k2g1fa2sUo= diff --git a/providers/aws/ladder/commitment_rates.go b/providers/aws/ladder/commitment_rates.go new file mode 100644 index 00000000..f7908d1e --- /dev/null +++ b/providers/aws/ladder/commitment_rates.go @@ -0,0 +1,46 @@ +package ladder + +import ( + "context" + "errors" + "fmt" + + "github.com/aws/aws-sdk-go-v2/service/costexplorer/types" + + "github.com/LeanerCloud/cloud-commitments-go/pkg/ladder" + "github.com/LeanerCloud/cloud-commitments-go/providers/aws/recommendations" +) + +type spCommitmentRateSource interface { + GetSPCommitmentRate(context.Context, recommendations.SPCommitmentRateParams) (float64, error) +} + +// GetCommitmentRates leaves Compute absent pending the caller's sizing policy; +// regional EC2 Instance recommendations are not a Compute pricing proxy. +func (a *AWSLadder) GetCommitmentRates(ctx context.Context, scope ladder.Scope, term ladder.Term, payment ladder.PaymentOption) (map[ladder.LayerType]float64, error) { + if err := a.validateScope(scope); err != nil { + return nil, err + } + if err := term.Validate(); err != nil { + return nil, fmt.Errorf("GetCommitmentRates: %w", err) + } + if err := payment.Validate(); err != nil { + return nil, fmt.Errorf("GetCommitmentRates: %w", err) + } + if a.spRates == nil { + return nil, errors.New("GetCommitmentRates: Savings Plan rate source not wired") + } + rate, err := a.spRates.GetSPCommitmentRate(ctx, recommendations.SPCommitmentRateParams{ + PlanType: types.SupportedSavingsPlansTypeEc2InstanceSp, + AccountID: a.cfg.AccountID, Region: a.cfg.Region, + Term: string(term), PaymentOption: string(payment), + LookbackPeriod: fmt.Sprintf("%dd", a.cfg.lookbackDays()), + }) + if errors.Is(err, recommendations.ErrNoSPCommitmentRate) { + return map[ladder.LayerType]float64{}, nil + } + if err != nil { + return nil, fmt.Errorf("GetCommitmentRates: %w", err) + } + return map[ladder.LayerType]float64{ladder.LayerEC2InstanceSP: rate}, nil +} diff --git a/providers/aws/ladder/commitment_rates_sdk_test.go b/providers/aws/ladder/commitment_rates_sdk_test.go new file mode 100644 index 00000000..405dbd80 --- /dev/null +++ b/providers/aws/ladder/commitment_rates_sdk_test.go @@ -0,0 +1,188 @@ +package ladder_test + +import ( + "context" + "encoding/json" + "fmt" + "math/big" + "net/http" + "net/http/httptest" + "net/url" + "sync/atomic" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/LeanerCloud/cloud-commitments-go/pkg/common" + "github.com/LeanerCloud/cloud-commitments-go/pkg/ladder" + awsladder "github.com/LeanerCloud/cloud-commitments-go/providers/aws/ladder" +) + +type ceLoopback struct { + endpoint *url.URL + transport http.RoundTripper +} + +func (c ceLoopback) RoundTrip(req *http.Request) (*http.Response, error) { + if req.URL.Scheme != "https" || req.URL.Host != "ce.us-east-1.amazonaws.com" { + return nil, fmt.Errorf("fixture rejects host %s", req.URL.Host) + } + local := req.Clone(req.Context()) + local.URL.Scheme, local.URL.Host = c.endpoint.Scheme, c.endpoint.Host + local.Host = c.endpoint.Host + return c.transport.RoundTrip(local) +} + +// These CE-shaped responses are constructed fixtures, not recorded billing data. +func rateDetail(account, region, family, cost, savings string) string { + return fmt.Sprintf(`{"AccountId":%q,"SavingsPlansDetails":{"Region":%q,"InstanceFamily":%q},"EstimatedSPCost":%q,"EstimatedSavingsAmount":%q,"EstimatedAverageUtilization":"100"}`, + account, region, family, cost, savings) +} + +func ratePage(details, token string) string { + return fmt.Sprintf(`{"NextPageToken":%q,"SavingsPlansPurchaseRecommendation":{"SavingsPlansPurchaseRecommendationDetails":[%s]}}`, token, details) +} + +func sdkRateLadder(t *testing.T, pages []string) *awsladder.AWSLadder { + t.Helper() + var calls atomic.Int32 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + page := int(calls.Add(1)) - 1 + assert.Equal(t, http.MethodPost, r.Method) + assert.Equal(t, "AWSInsightsIndexService.GetSavingsPlansPurchaseRecommendation", r.Header.Get("X-Amz-Target")) + var input map[string]any + if !assert.NoError(t, json.NewDecoder(r.Body).Decode(&input)) { + w.WriteHeader(http.StatusBadRequest) + return + } + want := map[string]any{"SavingsPlansType": "EC2_INSTANCE_SP", "AccountScope": "LINKED", + "TermInYears": "THREE_YEARS", "PaymentOption": "PARTIAL_UPFRONT", "LookbackPeriodInDays": "SEVEN_DAYS"} + if page > 0 { + want["NextPageToken"] = "second" + } + assert.Equal(t, want, input) + if page >= len(pages) { + t.Error("unexpected continuation request") + w.WriteHeader(http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", "application/x-amz-json-1.1") + _, err := w.Write([]byte(pages[page])) + assert.NoError(t, err) + })) + t.Cleanup(func() { + srv.Close() + assert.Equal(t, int32(len(pages)), calls.Load()) + }) + endpoint, err := url.Parse(srv.URL) + require.NoError(t, err) + awsCfg := aws.Config{Region: "us-west-2", RetryMaxAttempts: 1, + Credentials: credentials.NewStaticCredentialsProvider("synthetic", "synthetic", ""), + HTTPClient: &http.Client{Transport: ceLoopback{endpoint: endpoint, transport: srv.Client().Transport}}, + } + a, err := awsladder.NewFromConfig(awsCfg, awsladder.Config{Region: "eu-west-1", AccountID: "fixture-account", LookbackDays: 7}) + require.NoError(t, err) + assert.Equal(t, "us-west-2", awsCfg.Region) + return a +} + +func TestCommitmentRates_PublicSDKConsumer(t *testing.T) { + a := sdkRateLadder(t, []string{ + ratePage(rateDetail("fixture-account", "eu-west-1", "m5", "10", "10")+","+ + rateDetail("other-fixture-account", "eu-west-1", "m5", "999", "1")+","+ + rateDetail("fixture-account", "us-east-1", "m5", "999", "1"), "second"), + ratePage(rateDetail("fixture-account", "eu-west-1", "c5", "60", "20"), ""), + }) + scope := ladder.Scope{Provider: common.ProviderAWS, AccountID: "fixture-account"} + rates, err := a.GetCommitmentRates(context.Background(), scope, ladder.Term3Year, ladder.PaymentPartialUpfront) + require.NoError(t, err) + require.Equal(t, map[ladder.LayerType]float64{ladder.LayerEC2InstanceSP: 0.7}, rates) + in := consumerInput(a, scope, rates) + result, err := ladder.Allocate(in) + require.NoError(t, err) + require.Len(t, result.Allocations, 1) + allocation := result.Allocations[0] + assert.Equal(t, ladder.LayerEC2InstanceSP, allocation.Layer) + assert.Equal(t, 0, allocation.OnDemandEquivalentUSDPerHour.Cmp(big.NewRat(10, 1))) + assert.InDelta(t, 7, ratFloat(allocation.GapUSDPerHour), 1e-12) + assertMissingRateHold(t, result, ladder.LayerComputeSP) + tranches, err := ladder.BuildTranches(&ladder.TrancheInput{ + Config: &in.Config, RunID: "synthetic-run", Term: ladder.Term3Year, + PaymentOption: ladder.PaymentPartialUpfront, NewID: func() string { return "synthetic-tranche" }, + Now: in.Now, Allocations: result.Allocations, + }) + require.NoError(t, err) + require.Len(t, tranches.Tranches, 1) + tr := tranches.Tranches[0] + assert.Equal(t, allocation.GapUSDPerHour.RatString(), tr.AmountUSDPerHour) + assert.Equal(t, allocation.Rate.RatString(), tr.CommitmentRate) + storedAmount, ok := new(big.Rat).SetString(tr.AmountUSDPerHour) + require.True(t, ok) + storedRate, ok := new(big.Rat).SetString(tr.CommitmentRate) + require.True(t, ok) + in.InFlight = []ladder.InFlightTranche{{Layer: tr.Layer, CommitmentUSDPerHour: ratFloat(storedAmount), Rate: ratFloat(storedRate)}} + replay, err := ladder.Allocate(in) + require.NoError(t, err) + assert.True(t, replay.IsNoOp()) + assertMissingRateHold(t, replay, ladder.LayerComputeSP) +} + +func consumerInput(a *awsladder.AWSLadder, scope ladder.Scope, rates map[ladder.LayerType]float64) *ladder.AllocationInput { + zero, low, stable := 0.0, 20.0, 10.0 + states := make(map[ladder.LayerType]ladder.LayerState) + for _, spec := range a.SupportedLayers() { + states[spec.Type] = ladder.LayerState{Layer: spec.Type, ExistingUSDPerHour: &zero, ExpiringUSDPerHour: &zero} + } + return &ladder.AllocationInput{ + Config: ladder.LadderConfig{Scope: scope, Mode: ladder.ModeEmailApproval, Cadence: ladder.CadenceWeekly, + TargetCoveragePct: 100, BaselinePercentile: 5, LookbackDays: 7, MaxActionsPerRun: 10, + BufferUtilizationThresholdPct: ladder.DefaultBufferUtilizationThresholdPct, + Ramp: ladder.RampSchedule{Steps: []ladder.RampStep{{AfterDays: 0, Fraction: 1}}}}, + Layers: a.SupportedLayers(), LayerStates: states, CommitmentRates: rates, InFlight: []ladder.InFlightTranche{}, + Baseline: ladder.UsageBaseline{LowWaterUSDPerHour: &low, StableUSDPerHour: &stable}, + Now: time.Date(2026, 10, 15, 0, 0, 0, 0, time.UTC), DataSources: []string{"constructed-ce-loopback"}, + } +} + +func ratFloat(r *big.Rat) float64 { f, _ := r.Float64(); return f } + +func assertMissingRateHold(t *testing.T, result *ladder.AllocateResult, layer ladder.LayerType) { + t.Helper() + for _, hold := range result.Holds { + if hold.Layer == layer && hold.Action == ladder.ActionHold { + assert.Contains(t, hold.Rationale, "no commitment rate") + return + } + } + t.Errorf("missing rate Hold for %s: %+v", layer, result) +} + +func TestCommitmentRates_SDKMissingAndMalformed(t *testing.T) { + for _, cost := range []string{"", "bad"} { + t.Run("cost="+cost, func(t *testing.T) { + detail := rateDetail("fixture-account", "eu-west-1", "m5", cost, "3") + if cost == "" { + detail = `{"AccountId":"fixture-account","SavingsPlansDetails":{"Region":"eu-west-1"}}` + } + a := sdkRateLadder(t, []string{ratePage(detail, "")}) + scope := ladder.Scope{Provider: common.ProviderAWS, AccountID: "fixture-account"} + rates, err := a.GetCommitmentRates(context.Background(), scope, ladder.Term3Year, ladder.PaymentPartialUpfront) + if cost != "" { + require.ErrorContains(t, err, "EstimatedSPCost") + assert.Nil(t, rates) + return + } + require.NoError(t, err) + require.NotNil(t, rates) + assert.Empty(t, rates) + result, err := ladder.Allocate(consumerInput(a, scope, rates)) + require.NoError(t, err) + assert.True(t, result.IsNoOp()) + assertMissingRateHold(t, result, ladder.LayerEC2InstanceSP) + }) + } +} diff --git a/providers/aws/ladder/commitment_rates_test.go b/providers/aws/ladder/commitment_rates_test.go new file mode 100644 index 00000000..261477b3 --- /dev/null +++ b/providers/aws/ladder/commitment_rates_test.go @@ -0,0 +1,119 @@ +package ladder + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/costexplorer/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/LeanerCloud/cloud-commitments-go/pkg/common" + "github.com/LeanerCloud/cloud-commitments-go/pkg/ladder" + "github.com/LeanerCloud/cloud-commitments-go/providers/aws/recommendations" +) + +type fakeSPRateSource struct { + err error + params []recommendations.SPCommitmentRateParams +} + +func TestNewFromConfig_ValidatesAndScopesClients(t *testing.T) { + t.Parallel() + for _, cfg := range []Config{{AccountID: testScope().AccountID}, {Region: "eu-west-1"}} { + a, err := NewFromConfig(aws.Config{}, cfg) + require.Error(t, err) + assert.Nil(t, a) + } + awsCfg := aws.Config{Region: "us-west-2"} + a, err := NewFromConfig(awsCfg, Config{Region: "eu-west-1", AccountID: testScope().AccountID}) + require.NoError(t, err) + assert.Equal(t, "us-west-2", awsCfg.Region) + assert.Equal(t, "eu-west-1", a.cfg.Region) + assert.Equal(t, "eu-west-1", a.sps.(*spListerAdapter).region) + require.NotNil(t, a.spRates) +} + +func (f *fakeSPRateSource) GetSPCommitmentRate(_ context.Context, p recommendations.SPCommitmentRateParams) (float64, error) { + f.params = append(f.params, p) + return 0.7, f.err +} + +func TestGetCommitmentRates_ValidatesBeforeSource(t *testing.T) { + t.Parallel() + for _, tc := range []struct { + name string + scope ladder.Scope + term ladder.Term + payment ladder.PaymentOption + }{ + {"provider", ladder.Scope{Provider: common.ProviderAzure, AccountID: testScope().AccountID}, ladder.Term1Year, ladder.PaymentNoUpfront}, + {"account", ladder.Scope{Provider: common.ProviderAWS, AccountID: "wrong"}, ladder.Term1Year, ladder.PaymentNoUpfront}, + {"term", testScope(), ladder.Term("bad"), ladder.PaymentNoUpfront}, + {"payment", testScope(), ladder.Term1Year, ladder.PaymentOption("bad")}, + } { + t.Run(tc.name, func(t *testing.T) { + f := &fakeSPRateSource{} + a := &AWSLadder{cfg: Config{Region: "us-east-1", AccountID: testScope().AccountID}, spRates: f} + rates, err := a.GetCommitmentRates(context.Background(), tc.scope, tc.term, tc.payment) + require.Error(t, err) + assert.Nil(t, rates) + assert.Empty(t, f.params) + }) + } +} + +func TestGetCommitmentRates_ForwardsSelection(t *testing.T) { + t.Parallel() + for _, days := range []int{0, 7, 60} { + t.Run(fmt.Sprint(days), func(t *testing.T) { + f := &fakeSPRateSource{} + a := &AWSLadder{cfg: Config{Region: "eu-west-1", AccountID: testScope().AccountID, LookbackDays: days}, spRates: f} + rates, err := a.GetCommitmentRates(context.Background(), testScope(), ladder.Term3Year, ladder.PaymentPartialUpfront) + require.NoError(t, err) + assert.Equal(t, map[ladder.LayerType]float64{ladder.LayerEC2InstanceSP: 0.7}, rates) + if days == 0 { + days = DefaultLookbackDays + } + assert.Equal(t, []recommendations.SPCommitmentRateParams{{ + PlanType: types.SupportedSavingsPlansTypeEc2InstanceSp, + AccountID: testScope().AccountID, Region: "eu-west-1", + Term: "3yr", PaymentOption: "partial-upfront", LookbackPeriod: fmt.Sprintf("%dd", days), + }}, f.params) + }) + } +} + +func TestGetCommitmentRates_AbsenceAndErrors(t *testing.T) { + t.Parallel() + hard := errors.New("source failed") + for _, tc := range []struct { + name string + source spCommitmentRateSource + want error + }{ + {"unwired", nil, nil}, + {"absent", &fakeSPRateSource{err: fmt.Errorf("wrapped: %w", recommendations.ErrNoSPCommitmentRate)}, nil}, + {"hard", &fakeSPRateSource{err: hard}, hard}, + } { + t.Run(tc.name, func(t *testing.T) { + a := newTestLadder(t, &fakeRILister{}, &fakeSPLister{}, &fakeCoverageSource{}, &fakeUtilizationSource{}) + a.spRates = tc.source + rates, err := a.GetCommitmentRates(context.Background(), testScope(), ladder.Term1Year, ladder.PaymentNoUpfront) + if tc.source == nil { + require.ErrorContains(t, err, "not wired") + assert.Nil(t, rates) + } else if tc.want != nil { + require.ErrorIs(t, err, tc.want) + assert.Nil(t, rates) + } else { + require.NoError(t, err) + require.NotNil(t, rates) + assert.Empty(t, rates) + } + }) + } +} diff --git a/providers/aws/ladder/factory.go b/providers/aws/ladder/factory.go index e132409c..d05d66b7 100644 --- a/providers/aws/ladder/factory.go +++ b/providers/aws/ladder/factory.go @@ -60,6 +60,13 @@ func NewFromAWSConfig(ctx context.Context, region, accountID string) (pkgladder. if err != nil { return nil, fmt.Errorf("awsladder.NewFromAWSConfig: load AWS config: %w", err) } + return NewFromConfig(awsCfg, Config{Region: region, AccountID: accountID}) +} + +// NewFromConfig accepts resolved credentials and transport configuration for +// callers that already own the account's AWS configuration. +func NewFromConfig(awsCfg aws.Config, cfg Config) (*AWSLadder, error) { + awsCfg.Region = cfg.Region // recommendations.Client satisfies riCoverageSource and utilizationSource // directly, and is the underlying client for the on-demand series and SP @@ -74,14 +81,10 @@ func NewFromAWSConfig(ctx context.Context, region, accountID string) (pkgladder. // need the offering and purchase methods the full API exposes). spSDKClient := sdksp.NewFromConfig(awsCfg) - cfg := Config{ - Region: region, - AccountID: accountID, - } l, err := New( cfg, ec2Client, // riLister - &spListerAdapter{api: spSDKClient, region: region}, // spLister (region-scoped) + &spListerAdapter{api: spSDKClient, region: cfg.Region}, // spLister (region-scoped) recoClient, // riCoverageSource &onDemandSeriesAdapter{client: recoClient}, // onDemandSeriesSource recoClient, // utilizationSource @@ -89,8 +92,9 @@ func NewFromAWSConfig(ctx context.Context, region, accountID string) (pkgladder. &spUtilizationAdapter{client: recoClient}, // spUtilizationSource ) if err != nil { - return nil, fmt.Errorf("awsladder.NewFromAWSConfig: %w", err) + return nil, fmt.Errorf("awsladder.NewFromConfig: %w", err) } + l.spRates = recoClient return l, nil } diff --git a/providers/aws/ladder/ladder.go b/providers/aws/ladder/ladder.go index d9065871..df8fa5b6 100644 --- a/providers/aws/ladder/ladder.go +++ b/providers/aws/ladder/ladder.go @@ -93,6 +93,7 @@ type AWSLadder struct { riPurchase riPurchaser // write side; nil until WithWriteSide is called spPurchase spPurchaser // write side; nil until WithWriteSide is called exchange exchangeRunner // write side; nil until WithWriteSide is called + spRates spCommitmentRateSource cfg Config } From 8612582b6e7ce6b584a43e96bcd07600fac2adcc Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Thu, 15 Oct 2026 09:58:22 +0200 Subject: [PATCH 2/2] fix(aws): expose rate interface through shared minimum (#405) Select the published shared rate interface as the AWS module minimum so a normal provider-only installation exposes the optional capability. Refs #404 and #349. --- providers/aws/go.mod | 2 +- providers/aws/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/providers/aws/go.mod b/providers/aws/go.mod index cc346fa8..f0a8a74d 100644 --- a/providers/aws/go.mod +++ b/providers/aws/go.mod @@ -3,7 +3,7 @@ module github.com/LeanerCloud/cloud-commitments-go/providers/aws go 1.26.9 require ( - github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261014214001-29adce1c3883 + github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261015071854-5b254484d5e4 github.com/aws/aws-sdk-go-v2 v1.41.5 github.com/aws/aws-sdk-go-v2/config v1.29.12 github.com/aws/aws-sdk-go-v2/credentials v1.17.65 diff --git a/providers/aws/go.sum b/providers/aws/go.sum index 6ca9bd3b..779dd739 100644 --- a/providers/aws/go.sum +++ b/providers/aws/go.sum @@ -1,5 +1,5 @@ -github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261014214001-29adce1c3883 h1:SZuUVNZD0s/LHuI2e3hr3Pd14AuJgTHvXyShicoW4kA= -github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261014214001-29adce1c3883/go.mod h1:8RTmzPazA8IeVZfn7wNaU7ILx0VK5eHZQozO21Mvtq8= +github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261015071854-5b254484d5e4 h1:qI0IihoIaQ0MFgj9hXF0aGyokniwwYS938PrNmMVbGs= +github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20261015071854-5b254484d5e4/go.mod h1:8RTmzPazA8IeVZfn7wNaU7ILx0VK5eHZQozO21Mvtq8= github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY= github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= github.com/aws/aws-sdk-go-v2/config v1.29.12 h1:Y/2a+jLPrPbHpFkpAAYkVEtJmxORlXoo5k2g1fa2sUo=