@@ -6,20 +6,29 @@ package commitments
66import (
77 "context"
88 "encoding/json"
9+ "errors"
910 "fmt"
1011 "net/http"
12+ "strconv"
1113 "strings"
14+ "time"
1215
1316 "github.com/cobaltcore-dev/cortex/internal/scheduling/reservations"
1417 "github.com/go-logr/logr"
1518 "github.com/google/uuid"
1619 liquid "github.com/sapcc/go-api-declarations/liquid"
1720)
1821
22+ // errInternalServiceInfo indicates an internal error while building service info (e.g., invalid unit configuration)
23+ var errInternalServiceInfo = errors .New ("internal error building service info" )
24+
1925// handles GET /v1/info requests from Limes:
2026// See: https://github.com/sapcc/go-api-declarations/blob/main/liquid/commitment.go
2127// See: https://pkg.go.dev/github.com/sapcc/go-api-declarations/liquid
2228func (api * HTTPAPI ) HandleInfo (w http.ResponseWriter , r * http.Request ) {
29+ startTime := time .Now ()
30+ statusCode := http .StatusOK
31+
2332 // Extract or generate request ID for tracing
2433 requestID := r .Header .Get ("X-Request-ID" )
2534 if requestID == "" {
@@ -33,7 +42,9 @@ func (api *HTTPAPI) HandleInfo(w http.ResponseWriter, r *http.Request) {
3342
3443 // Only accept GET method
3544 if r .Method != http .MethodGet {
36- http .Error (w , "Method not allowed" , http .StatusMethodNotAllowed )
45+ statusCode = http .StatusMethodNotAllowed
46+ http .Error (w , "Method not allowed" , statusCode )
47+ api .recordInfoMetrics (statusCode , startTime )
3748 return
3849 }
3950
@@ -42,20 +53,35 @@ func (api *HTTPAPI) HandleInfo(w http.ResponseWriter, r *http.Request) {
4253 // Build info response
4354 info , err := api .buildServiceInfo (ctx , logger )
4455 if err != nil {
45- // Use Info level for expected conditions like knowledge not being ready yet
46- logger .Info ("service info not available yet" , "error" , err .Error ())
47- http .Error (w , "Service temporarily unavailable: " + err .Error (),
48- http .StatusServiceUnavailable )
56+ if errors .Is (err , errInternalServiceInfo ) {
57+ logger .Error (err , "internal error building service info" )
58+ statusCode = http .StatusInternalServerError
59+ http .Error (w , "Internal server error: " + err .Error (), statusCode )
60+ } else {
61+ // Use Info level for expected conditions like knowledge not being ready yet
62+ logger .Info ("service info not available yet" , "error" , err .Error ())
63+ statusCode = http .StatusServiceUnavailable
64+ http .Error (w , "Service temporarily unavailable: " + err .Error (), statusCode )
65+ }
66+ api .recordInfoMetrics (statusCode , startTime )
4967 return
5068 }
5169
5270 // Return response
5371 w .Header ().Set ("Content-Type" , "application/json" )
54- w .WriteHeader (http . StatusOK )
72+ w .WriteHeader (statusCode )
5573 if err := json .NewEncoder (w ).Encode (info ); err != nil {
5674 logger .Error (err , "failed to encode service info" )
57- return
5875 }
76+ api .recordInfoMetrics (statusCode , startTime )
77+ }
78+
79+ // recordInfoMetrics records Prometheus metrics for an info API request.
80+ func (api * HTTPAPI ) recordInfoMetrics (statusCode int , startTime time.Time ) {
81+ duration := time .Since (startTime ).Seconds ()
82+ statusCodeStr := strconv .Itoa (statusCode )
83+ api .infoMonitor .requestCounter .WithLabelValues (statusCodeStr ).Inc ()
84+ api .infoMonitor .requestDuration .WithLabelValues (statusCodeStr ).Observe (duration )
5985}
6086
6187// resourceAttributes holds the custom attributes for a resource in the info API response.
@@ -108,9 +134,22 @@ func (api *HTTPAPI) buildServiceInfo(ctx context.Context, logger logr.Logger) (l
108134 attrsJSON = nil
109135 }
110136
137+ // Build unit from smallest flavor memory (e.g., "131072 MiB" for 128 GiB)
138+ // Validate memory is positive to avoid panic in MultiplyBy (which panics on factor=0)
139+ if groupData .SmallestFlavor .MemoryMB == 0 {
140+ return liquid.ServiceInfo {}, fmt .Errorf ("%w: flavor group %q has invalid smallest flavor with memoryMB=0" ,
141+ errInternalServiceInfo , groupName )
142+ }
143+ unit , err := liquid .UnitMebibytes .MultiplyBy (groupData .SmallestFlavor .MemoryMB )
144+ if err != nil {
145+ // Note: This error only occurs on uint64 overflow, which is unrealistic for memory values
146+ return liquid.ServiceInfo {}, fmt .Errorf ("%w: failed to create unit for flavor group %q: %w" ,
147+ errInternalServiceInfo , groupName , err )
148+ }
149+
111150 resources [resourceName ] = liquid.ResourceInfo {
112151 DisplayName : displayName ,
113- Unit : liquid . UnitNone , // Countable : multiples of smallest flavor instances
152+ Unit : unit , // Non-standard unit : multiples of smallest flavor RAM
114153 Topology : liquid .AZAwareTopology , // Commitments are per-AZ
115154 NeedsResourceDemand : false , // Capacity planning out of scope for now
116155 HasCapacity : handlesCommitments , // We report capacity via /v1/report-capacity only for groups that accept commitments
0 commit comments