diff --git a/cmd/multi_service_helpers.go b/cmd/multi_service_helpers.go
index 5770ed59e..387508d00 100644
--- a/cmd/multi_service_helpers.go
+++ b/cmd/multi_service_helpers.go
@@ -2,6 +2,7 @@ package main
import (
"context"
+ "errors"
"fmt"
"log"
"sort"
@@ -108,7 +109,10 @@ func getAllAWSRegionsWithClient(ctx context.Context, ec2Client EC2ClientInterfac
// discoverRegionsForService discovers regions that have recommendations for a specific service.
func discoverRegionsForService(ctx context.Context, client provider.RecommendationsClient, service common.ServiceType) ([]string, error) {
recs, err := client.GetRecommendationsForService(ctx, service)
- if partial := azureprovider.AsPartialSubscriptionFailure(err); partial != nil {
+ var incomplete *recommendations.IncompleteRecommendationsError
+ if errors.As(err, &incomplete) {
+ AppLogger.Printf(" ⚠️ Region discovery incomplete: %v\n", incomplete)
+ } else if partial := azureprovider.AsPartialSubscriptionFailure(err); partial != nil {
// Region discovery is best-effort: the subscriptions that answered
// still tell us where to look. Report the gap rather than dropping
// the discovered regions or failing outright.
@@ -463,6 +467,11 @@ func fetchRecommendationsForRegion(
}
recs, err := recClient.GetRecommendations(ctx, ¶ms)
+ var incomplete *recommendations.IncompleteRecommendationsError
+ if errors.As(err, &incomplete) {
+ AppLogger.Printf(" ⚠️ %v\n", incomplete)
+ return recs
+ }
if partial := azureprovider.AsPartialSubscriptionFailure(err); partial != nil {
// Keep the subscriptions that did answer, but say plainly that the
// sweep was incomplete: without this the operator would read a short
diff --git a/cmd/recommendation_completeness_proxy_test.go b/cmd/recommendation_completeness_proxy_test.go
new file mode 100644
index 000000000..ec8a71b5e
--- /dev/null
+++ b/cmd/recommendation_completeness_proxy_test.go
@@ -0,0 +1,198 @@
+package main
+
+import (
+ "bufio"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/tls"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/pem"
+ "errors"
+ "fmt"
+ "io"
+ "math/big"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+)
+
+type completenessProxy struct {
+ t *testing.T
+ mu sync.Mutex
+ handlers sync.WaitGroup
+ regions string
+ details string
+ requests map[string]int
+ cert tls.Certificate
+}
+
+func newCompletenessProxy(t *testing.T, dir, regions, details string) (*completenessProxy, string, string) {
+ t.Helper()
+ key, err := rsa.GenerateKey(rand.Reader, 2048)
+ require.NoError(t, err)
+ template := &x509.Certificate{
+ SerialNumber: big.NewInt(54), Subject: pkix.Name{CommonName: "recommendation fixture"},
+ DNSNames: []string{"ce.us-east-1.amazonaws.com", "ec2.us-east-1.amazonaws.com", "rds.us-east-1.amazonaws.com"},
+ NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(time.Hour),
+ IsCA: true, BasicConstraintsValid: true,
+ KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
+ ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
+ }
+ der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
+ require.NoError(t, err)
+ certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
+ caPath := filepath.Join(dir, "ca.pem")
+ require.NoError(t, os.WriteFile(caPath, certPEM, 0600))
+ fixture := &completenessProxy{t: t, regions: regions, details: details, requests: make(map[string]int),
+ cert: tls.Certificate{Certificate: [][]byte{der}, PrivateKey: key}}
+ server := httptest.NewServer(http.HandlerFunc(fixture.serveConnect))
+ t.Cleanup(func() {
+ server.Close()
+ fixture.handlers.Wait()
+ })
+ return fixture, server.URL, caPath
+}
+
+func (p *completenessProxy) serveConnect(w http.ResponseWriter, r *http.Request) {
+ p.handlers.Add(1)
+ defer p.handlers.Done()
+ allowed := r.Host == "ce.us-east-1.amazonaws.com:443" || r.Host == "ec2.us-east-1.amazonaws.com:443" || r.Host == "rds.us-east-1.amazonaws.com:443"
+ if r.Method != http.MethodConnect || !allowed {
+ p.t.Errorf("unexpected CONNECT %s %s", r.Method, r.Host)
+ http.Error(w, "forbidden", http.StatusForbidden)
+ return
+ }
+ conn, _, err := w.(http.Hijacker).Hijack()
+ if err != nil {
+ p.t.Error(err)
+ return
+ }
+ defer conn.Close()
+ if err := conn.SetDeadline(time.Now().Add(40 * time.Second)); err != nil {
+ p.t.Error(err)
+ return
+ }
+ if _, err := fmt.Fprint(conn, "HTTP/1.1 200 Connection Established\r\n\r\n"); err != nil {
+ p.t.Error(err)
+ return
+ }
+ secure := tls.Server(conn, &tls.Config{Certificates: []tls.Certificate{p.cert}, MinVersion: tls.VersionTLS12})
+ if err := secure.Handshake(); err != nil {
+ p.t.Errorf("TLS handshake: %v", err)
+ return
+ }
+ reader := bufio.NewReader(secure)
+ for {
+ req, err := http.ReadRequest(reader)
+ if errors.Is(err, io.EOF) {
+ return
+ }
+ if err != nil {
+ p.t.Errorf("read request: %v", err)
+ return
+ }
+ if req.Host != strings.TrimSuffix(r.Host, ":443") {
+ p.t.Errorf("tunnel host mismatch: %s", req.Host)
+ return
+ }
+ response := p.respond(req)
+ err = response.Write(secure)
+ _ = response.Body.Close()
+ if err != nil {
+ p.t.Errorf("write response: %v", err)
+ return
+ }
+ }
+}
+
+func (p *completenessProxy) respond(req *http.Request) *http.Response {
+ body, err := io.ReadAll(req.Body)
+ _ = req.Body.Close()
+ if err != nil {
+ p.t.Error(err)
+ }
+ op := req.Header.Get("X-Amz-Target")
+ if op == "" {
+ values, parseErr := url.ParseQuery(string(body))
+ if parseErr != nil {
+ p.t.Error(parseErr)
+ }
+ op = values.Get("Action")
+ }
+ p.mu.Lock()
+ p.requests[op]++
+ call := p.requests[op]
+ p.mu.Unlock()
+ status, contentType, payload := p.operation(req.Host, op, call)
+ return &http.Response{StatusCode: status, ProtoMajor: 1, ProtoMinor: 1,
+ Header: http.Header{"Content-Type": []string{contentType}},
+ Body: io.NopCloser(strings.NewReader(payload)), ContentLength: int64(len(payload))}
+}
+
+func (p *completenessProxy) operation(host, op string, call int) (int, string, string) {
+ if host == "ce.us-east-1.amazonaws.com" && op == "AWSInsightsIndexService.GetReservationCoverage" {
+ return 200, "application/x-amz-json-1.1", `{"CoveragesByTime":[]}`
+ }
+ if host == "ce.us-east-1.amazonaws.com" && op == "AWSInsightsIndexService.GetReservationPurchaseRecommendation" {
+ if p.details == "api-error" {
+ return 400, "application/x-amz-json-1.1", `{"__type":"AccessDeniedException","message":"fixture denied recommendations"}`
+ }
+ const valid = `{"RecommendedNumberOfInstancesToPurchase":"2","EstimatedMonthlySavingsAmount":"10",
+ "EstimatedMonthlyOnDemandCost":"30","InstanceDetails":{"RDSInstanceDetails":{
+ "InstanceType":"db.t3.medium","Region":"us-east-1","DeploymentOption":"Single-AZ"}}}`
+ const invalid = `{"RecommendedNumberOfInstancesToPurchase":"not-a-number"}`
+ details := map[string]string{
+ "valid": valid,
+ "mixed": valid + "," + invalid,
+ "all-invalid": invalid,
+ "empty": "",
+ }
+ return 200, "application/x-amz-json-1.1", `{"Recommendations":[{"RecommendationDetails":[` + details[p.details] + `]}]}`
+ }
+ if host == "ec2.us-east-1.amazonaws.com" && op == "DescribeRegions" {
+ if p.regions == "fallback" && call == 2 {
+ return 400, "text/xml", `UnauthorizedOperationfixture denied region discovery`
+ }
+ return 200, "text/xml", `- us-east-1
`
+ }
+ responses := map[string]string{
+ "DescribeDBInstances": "DBInstances",
+ "DescribeDBMajorEngineVersions": "DBMajorEngineVersions",
+ "DescribeReservedDBInstances": "ReservedDBInstances",
+ }
+ if element, ok := responses[op]; ok && host == "rds.us-east-1.amazonaws.com" {
+ return 200, "text/xml", fmt.Sprintf(`<%sResponse xmlns="http://rds.amazonaws.com/doc/2014-10-31/"><%sResult><%s/>%sResult>%sResponse>`, op, op, element, op, op)
+ }
+ p.t.Errorf("unexpected operation %s %s", host, op)
+ return 403, "text/plain", "fixture rejected operation"
+}
+
+func (p *completenessProxy) assertRequests(t *testing.T) {
+ t.Helper()
+ p.handlers.Wait()
+ p.mu.Lock()
+ defer p.mu.Unlock()
+ ceCalls := 1
+ if p.regions == "fallback" {
+ ceCalls = 6
+ if p.details == "valid" || p.details == "mixed" {
+ ceCalls++
+ }
+ }
+ require.Equal(t, ceCalls, p.requests["AWSInsightsIndexService.GetReservationPurchaseRecommendation"], "operations=%v", p.requests)
+ wantRegions := 2
+ if p.regions == "explicit" {
+ wantRegions = 1
+ }
+ require.Equal(t, wantRegions, p.requests["DescribeRegions"], "region calls")
+ t.Logf("actual root command, config loader, SDK and CSV; synthetic operations=%v", p.requests)
+}
diff --git a/cmd/recommendation_completeness_test.go b/cmd/recommendation_completeness_test.go
new file mode 100644
index 000000000..1292cd3fe
--- /dev/null
+++ b/cmd/recommendation_completeness_test.go
@@ -0,0 +1,179 @@
+package main
+
+import (
+ "bytes"
+ "context"
+ "crypto/tls"
+ "crypto/x509"
+ "encoding/csv"
+ "errors"
+ "fmt"
+ "net/http"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/LeanerCloud/cloud-commitments-go/pkg/common"
+ "github.com/LeanerCloud/cloud-commitments-go/providers/aws/recommendations"
+ "github.com/stretchr/testify/mock"
+ "github.com/stretchr/testify/require"
+)
+
+func TestRecommendationCompletenessCommand(t *testing.T) {
+ if os.Getenv("CUDLY_COMPLETENESS_CHILD") == "1" {
+ runCompletenessCommandChild(t)
+ return
+ }
+ for _, regionMode := range []string{"explicit", "default", "fallback"} {
+ for _, details := range []string{"valid", "mixed", "all-invalid", "empty", "api-error"} {
+ t.Run(regionMode+"/"+details, func(t *testing.T) {
+ dir := t.TempDir()
+ fixture, proxyURL, caPath := newCompletenessProxy(t, dir, regionMode, details)
+ output := filepath.Join(dir, "recommendations.csv")
+ ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
+ defer cancel()
+ child := exec.CommandContext(ctx, os.Args[0], "-test.run=^TestRecommendationCompletenessCommand$", "-test.v")
+ child.Env = []string{
+ "PATH=/usr/bin:/bin", "HOME=" + dir, "TMPDIR=" + dir,
+ "CUDLY_COMPLETENESS_CHILD=1", "CUDLY_COMPLETENESS_OUTPUT=" + output,
+ "CUDLY_COMPLETENESS_AUDIT=" + filepath.Join(dir, "audit.jsonl"),
+ "CUDLY_COMPLETENESS_REGIONS=" + regionMode,
+ "AWS_ACCESS_KEY_ID=synthetic", "AWS_SECRET_ACCESS_KEY=synthetic", "AWS_REGION=us-east-1",
+ "AWS_EC2_METADATA_DISABLED=true", "AWS_MAX_ATTEMPTS=1",
+ "AWS_CONFIG_FILE=" + filepath.Join(dir, "absent-config"),
+ "AWS_SHARED_CREDENTIALS_FILE=" + filepath.Join(dir, "absent-credentials"),
+ "HTTPS_PROXY=" + proxyURL, "HTTP_PROXY=" + proxyURL, "AWS_CA_BUNDLE=" + caPath,
+ }
+ var stdout, stderr bytes.Buffer
+ child.Stdout, child.Stderr = &stdout, &stderr
+ require.NoError(t, child.Run(), "stdout: %s\nstderr: %s", &stdout, &stderr)
+ fixture.assertRequests(t)
+ logs := stdout.String() + stderr.String()
+ for _, failure := range []string{"Could not check", "Failed to query", "request send failed", "certificate", "TLS handshake"} {
+ require.NotContains(t, logs, failure, "unexpected ancillary error")
+ }
+ assertCompletenessDiagnostics(t, regionMode, details, stdout.String(), stderr.String())
+ assertCompletenessCSV(t, output, details, logs)
+ })
+ }
+ }
+}
+
+func runCompletenessCommandChild(t *testing.T) {
+ t.Helper()
+ ca, err := os.ReadFile(os.Getenv("AWS_CA_BUNDLE"))
+ require.NoError(t, err)
+ roots := x509.NewCertPool()
+ require.True(t, roots.AppendCertsFromPEM(ca), "invalid fixture CA")
+ transport := http.DefaultTransport.(*http.Transport).Clone()
+ transport.TLSClientConfig = &tls.Config{RootCAs: roots, MinVersion: tls.VersionTLS12}
+ http.DefaultTransport = transport
+ t.Cleanup(transport.CloseIdleConnections)
+ args := []string{"--services", "rds", "--term", "1", "--include-extended-support", "--coverage", "100",
+ "--output", os.Getenv("CUDLY_COMPLETENESS_OUTPUT"), "--audit-log", os.Getenv("CUDLY_COMPLETENESS_AUDIT")}
+ if os.Getenv("CUDLY_COMPLETENESS_REGIONS") == "explicit" {
+ args = append(args, "--regions", "us-east-1")
+ }
+ rootCmd.SetArgs(args)
+ require.NoError(t, rootCmd.Execute())
+}
+
+func assertCompletenessDiagnostics(t *testing.T, regionMode, details, stdout, stderr string) {
+ t.Helper()
+ malformed := details == "mixed" || details == "all-invalid"
+ if malformed {
+ count := 1
+ if regionMode == "fallback" {
+ count = 6
+ }
+ want := fmt.Sprintf("incomplete AWS recommendations: %d failed details, 0 failed scopes:", count)
+ for _, diagnostic := range []string{want, "not-a-number", "detail"} {
+ require.Contains(t, stdout, diagnostic, "stderr: %s", stderr)
+ }
+ if regionMode == "fallback" {
+ require.Contains(t, stdout, "Region discovery incomplete")
+ }
+ } else {
+ require.NotContains(t, stdout, "incomplete AWS recommendations")
+ }
+ if details == "api-error" {
+ require.Contains(t, stdout+stderr, "fixture denied recommendations")
+ } else {
+ require.NotContains(t, stdout+stderr, "Failed to fetch recommendations", "partial collection treated as fatal")
+ require.NotContains(t, stdout+stderr, "failed to discover regions", "partial collection treated as fatal")
+ }
+}
+
+func assertCompletenessCSV(t *testing.T, output, details, logs string) {
+ t.Helper()
+ data, err := os.ReadFile(output)
+ if details != "valid" && details != "mixed" {
+ require.ErrorIs(t, err, os.ErrNotExist, "unexpected CSV: %s\n%s", data, logs)
+ require.NotContains(t, logs, "CSV report written", "false CSV success for %s", details)
+ return
+ }
+ require.NoError(t, err, "CSV: %s", logs)
+ rows, err := csv.NewReader(bytes.NewReader(data)).ReadAll()
+ require.NoError(t, err)
+ require.Len(t, rows, 3, "want header, exact surviving RDS row and total: %s", logs)
+ require.Equal(t, "TOTAL", rows[2][0])
+ columns := make(map[string]string)
+ for i, header := range rows[0] {
+ columns[header] = rows[1][i]
+ }
+ for field, want := range map[string]string{"Service": "rds", "Region": "us-east-1", "ResourceType": "db.t3.medium", "Count": "2"} {
+ require.Equal(t, want, columns[field], "CSV %s; row=%v", field, rows[1])
+ }
+ require.Contains(t, logs, "CSV report written")
+}
+
+func TestRecommendationCompletenessHelpers(t *testing.T) {
+ partial := &recommendations.IncompleteRecommendationsError{
+ FailedDetails: 2, FailedScopes: 1,
+ Causes: []error{errors.New("rds block 0 detail 1: invalid quantity"), errors.New("rds block 1 detail 0: invalid cost"), errors.New("rds 3yr AllUpfront: denied")},
+ }
+ survivors := []common.Recommendation{{Service: common.ServiceRDS, Region: "us-east-1", Count: 2}}
+ for _, tc := range []struct {
+ name string
+ err error
+ recs []common.Recommendation
+ keep bool
+ }{
+ {"partial", fmt.Errorf("wrapped: %w", partial), survivors, true},
+ {"all-invalid", partial, nil, true},
+ {"ordinary", errors.New("API failed"), survivors, false},
+ {"canceled", context.Canceled, survivors, false},
+ {"deadline", context.DeadlineExceeded, survivors, false},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ var output bytes.Buffer
+ previous := AppLogger.Writer()
+ AppLogger.SetOutput(&output)
+ t.Cleanup(func() { AppLogger.SetOutput(previous) })
+ ctx := context.Background()
+ client := &MockRecommendationsClient{}
+ client.On("GetRecommendationsForService", ctx, common.ServiceRDS).Return(tc.recs, tc.err)
+ client.On("GetRecommendations", ctx, mock.Anything).Return(tc.recs, tc.err)
+ regions, err := discoverRegionsForService(ctx, client, common.ServiceRDS)
+ got := fetchRecommendationsForRegion(ctx, client, common.ServiceRDS, "us-east-1", Config{TermYears: 1})
+ if !tc.keep {
+ require.ErrorIs(t, err, tc.err)
+ require.Nil(t, regions)
+ require.Nil(t, got)
+ require.NotContains(t, output.String(), "incomplete AWS recommendations")
+ } else {
+ require.NoError(t, err)
+ require.Equal(t, tc.recs, got)
+ require.Len(t, regions, len(tc.recs))
+ require.Equal(t, 2, strings.Count(output.String(), "2 failed details, 1 failed scopes"))
+ for _, cause := range partial.Causes {
+ require.Equal(t, 2, strings.Count(output.String(), cause.Error()))
+ }
+ }
+ client.AssertExpectations(t)
+ })
+ }
+}
diff --git a/go.mod b/go.mod
index ebb36821a..e8166a8a4 100644
--- a/go.mod
+++ b/go.mod
@@ -47,7 +47,7 @@ require (
github.com/aws/smithy-go v1.24.2 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
- github.com/go-logr/logr v1.4.3 // indirect
+ github.com/go-logr/logr v1.4.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/google/s2a-go v0.1.9 // indirect
@@ -61,9 +61,9 @@ require (
github.com/stretchr/objx v0.5.3 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
- go.opentelemetry.io/otel v1.44.0 // indirect
- go.opentelemetry.io/otel/metric v1.44.0 // indirect
- go.opentelemetry.io/otel/trace v1.44.0 // indirect
+ go.opentelemetry.io/otel v1.45.0 // indirect
+ go.opentelemetry.io/otel/metric v1.45.0 // indirect
+ go.opentelemetry.io/otel/trace v1.45.0 // indirect
golang.org/x/crypto v0.56.0 // indirect
golang.org/x/net v0.58.0 // indirect
golang.org/x/oauth2 v0.36.0
@@ -82,8 +82,8 @@ require (
require (
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2 v2.2.0
- github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20260928214714-ce9513612901
- github.com/LeanerCloud/cloud-commitments-go/providers/aws v0.0.0-20260928214714-ce9513612901
+ github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20260929105827-b3b4cb5e3d80
+ github.com/LeanerCloud/cloud-commitments-go/providers/aws v0.0.0-20261002152209-006ef5c8d0a2
github.com/LeanerCloud/cloud-commitments-go/providers/azure v0.0.0-20260928214714-ce9513612901
github.com/LeanerCloud/cloud-commitments-go/providers/gcp v0.0.0-20260928214714-ce9513612901
github.com/aws/aws-sdk-go-v2/service/organizations v1.45.3
@@ -126,6 +126,6 @@ require (
github.com/std-uritemplate/std-uritemplate/go/v2 v2.0.3 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.44.0 // indirect
- go.opentelemetry.io/otel/sdk v1.44.0 // indirect
- go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect
+ go.opentelemetry.io/otel/sdk v1.45.0 // indirect
+ go.opentelemetry.io/otel/sdk/metric v1.45.0 // indirect
)
diff --git a/go.sum b/go.sum
index e9ca132e1..b0f0bd421 100644
--- a/go.sum
+++ b/go.sum
@@ -78,10 +78,10 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
-github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20260928214714-ce9513612901 h1:JWQwkwshjBoEuoIuFK1KHh70yZDYQTrm5GRloD6ojjU=
-github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20260928214714-ce9513612901/go.mod h1:ApWBliDXe099f3oDXBz41K/I9v4bHvn1dG/BGoRmHlw=
-github.com/LeanerCloud/cloud-commitments-go/providers/aws v0.0.0-20260928214714-ce9513612901 h1:18MxQd7+ZQ+Z2uNbZfpIJQ00JWdSdTVSmVWBzg4R0DA=
-github.com/LeanerCloud/cloud-commitments-go/providers/aws v0.0.0-20260928214714-ce9513612901/go.mod h1:tIUCLVX6utdBPiP6dAjY7GUfwIQngeSMK5A9aRqe6gA=
+github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20260929105827-b3b4cb5e3d80 h1:wVKlMokfaME/Lw525Qz3F155R3nY4VmuaIyh+6d4sCU=
+github.com/LeanerCloud/cloud-commitments-go/pkg v0.0.0-20260929105827-b3b4cb5e3d80/go.mod h1:ApWBliDXe099f3oDXBz41K/I9v4bHvn1dG/BGoRmHlw=
+github.com/LeanerCloud/cloud-commitments-go/providers/aws v0.0.0-20261002152209-006ef5c8d0a2 h1:rYxXq0G0O2lkf8AB1ni9m4pVhIdb6Z0y52sTxS1RKIA=
+github.com/LeanerCloud/cloud-commitments-go/providers/aws v0.0.0-20261002152209-006ef5c8d0a2/go.mod h1:d4nsy61/Ptib0SxqMg0ble3yksVm3+QbtUtmGja82PA=
github.com/LeanerCloud/cloud-commitments-go/providers/azure v0.0.0-20260928214714-ce9513612901 h1:iSdHYdmGUjcjtSmgGZxptBzDuLrJ9KR2mh1/x7FNvSY=
github.com/LeanerCloud/cloud-commitments-go/providers/azure v0.0.0-20260928214714-ce9513612901/go.mod h1:zgCL/ozOkcZUDbEC7a2UwW+6MPcLoBlEU2WUZ0TORUs=
github.com/LeanerCloud/cloud-commitments-go/providers/gcp v0.0.0-20260928214714-ce9513612901 h1:i6OwXLUheudN3GfwnYXdKuEq8vPdE9qkNJ+r71LRLKA=
@@ -152,8 +152,8 @@ github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
-github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
+github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
@@ -229,20 +229,20 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.6
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
-go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
-go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
+go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU=
+go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w=
-go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
-go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
-go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA=
-go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk=
-go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
-go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
-go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
-go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
-go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
-go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
+go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M=
+go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s=
+go.opentelemetry.io/otel/metric/x v0.67.0 h1:PcicCNZFkZ4bXfSooXdo3WN7RBOVOtjVdo1wD358Uns=
+go.opentelemetry.io/otel/metric/x v0.67.0/go.mod h1:FBjCWZe6wgcqxcMtjdGiClDKXb2YxxXii0CXftE4QtI=
+go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw=
+go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA=
+go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o=
+go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA=
+go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag=
+go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc=
golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=