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/>`, 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=