Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion internal/scheduling/reservations/commitments/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package commitments
import (
"context"
"net/http"
"strings"
"sync"

"github.com/cobaltcore-dev/cortex/internal/scheduling/nova"
Expand Down Expand Up @@ -57,10 +58,23 @@ func (api *HTTPAPI) Init(mux *http.ServeMux, registry prometheus.Registerer, log
mux.HandleFunc("/commitments/v1/change-commitments", api.HandleChangeCommitments)
mux.HandleFunc("/commitments/v1/report-capacity", api.HandleReportCapacity)
mux.HandleFunc("/commitments/v1/info", api.HandleInfo)
mux.HandleFunc("/commitments/v1/projects/", api.HandleReportUsage) // matches /commitments/v1/projects/:project_id/report-usage
mux.HandleFunc("/commitments/v1/projects/", api.handleProjectEndpoint) // routes to report-usage or quota

log.Info("commitments API initialized",
"changeCommitmentsEnabled", api.config.EnableChangeCommitmentsAPI,
"reportUsageEnabled", api.config.EnableReportUsageAPI,
"reportCapacityEnabled", api.config.EnableReportCapacityAPI)
}

// handleProjectEndpoint routes /commitments/v1/projects/:project_id/... requests to the appropriate handler.
func (api *HTTPAPI) handleProjectEndpoint(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
switch {
case strings.HasSuffix(path, "/report-usage"):
api.HandleReportUsage(w, r)
case strings.HasSuffix(path, "/quota"):
api.HandleQuota(w, r)
default:
http.Error(w, "Not found", http.StatusNotFound)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func sortedKeys[K ~string, V any](m map[K]V) []K {
return keys
}

// implements POST /v1/change-commitments from Limes LIQUID API:
// implements POST /commitments/v1/change-commitments from Limes LIQUID API:
// See: https://github.com/sapcc/go-api-declarations/blob/main/liquid/commitment.go
// See: https://pkg.go.dev/github.com/sapcc/go-api-declarations/liquid
//
Expand Down Expand Up @@ -69,7 +69,7 @@ func (api *HTTPAPI) HandleChangeCommitments(w http.ResponseWriter, r *http.Reque
defer api.changeMutex.Unlock()

ctx := reservations.WithGlobalRequestID(context.Background(), "committed-resource-"+requestID)
logger := LoggerFromContext(ctx).WithValues("component", "api", "endpoint", "/v1/change-commitments")
logger := LoggerFromContext(ctx).WithValues("component", "api", "endpoint", "/commitments/v1/change-commitments")

// Only accept POST method
if r.Method != http.MethodPost {
Expand Down
6 changes: 3 additions & 3 deletions internal/scheduling/reservations/commitments/api_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import (
// errInternalServiceInfo indicates an internal error while building service info (e.g., invalid unit configuration)
var errInternalServiceInfo = errors.New("internal error building service info")

// handles GET /v1/info requests from Limes:
// handles GET /commitments/v1/info requests from Limes:
// See: https://github.com/sapcc/go-api-declarations/blob/main/liquid/commitment.go
// See: https://pkg.go.dev/github.com/sapcc/go-api-declarations/liquid
func (api *HTTPAPI) HandleInfo(w http.ResponseWriter, r *http.Request) {
Expand All @@ -38,7 +38,7 @@ func (api *HTTPAPI) HandleInfo(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Request-ID", requestID)

ctx := reservations.WithGlobalRequestID(r.Context(), "committed-resource-"+requestID)
logger := LoggerFromContext(ctx).WithValues("component", "api", "endpoint", "/v1/info")
logger := LoggerFromContext(ctx).WithValues("component", "api", "endpoint", "/commitments/v1/info")

// Only accept GET method
if r.Method != http.MethodGet {
Expand Down Expand Up @@ -152,7 +152,7 @@ func (api *HTTPAPI) buildServiceInfo(ctx context.Context, logger logr.Logger) (l
Unit: unit, // Non-standard unit: multiples of smallest flavor RAM
Topology: liquid.AZAwareTopology, // Commitments are per-AZ
NeedsResourceDemand: false, // Capacity planning out of scope for now
HasCapacity: handlesCommitments, // We report capacity via /v1/report-capacity only for groups that accept commitments
HasCapacity: handlesCommitments, // We report capacity via /commitments/v1/report-capacity only for groups that accept commitments
HasQuota: false, // No quota enforcement as of now
HandlesCommitments: handlesCommitments, // Only for groups with fixed RAM/core ratio
Attributes: attrsJSON,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func TestHandleInfo_KnowledgeNotReady(t *testing.T) {

api := NewAPI(k8sClient)

req := httptest.NewRequest(http.MethodGet, "/v1/info", http.NoBody)
req := httptest.NewRequest(http.MethodGet, "/commitments/v1/info", http.NoBody)
w := httptest.NewRecorder()

api.HandleInfo(w, req)
Expand Down Expand Up @@ -63,7 +63,7 @@ func TestHandleInfo_MethodNotAllowed(t *testing.T) {
api := NewAPI(k8sClient)

// Use POST instead of GET
req := httptest.NewRequest(http.MethodPost, "/v1/info", http.NoBody)
req := httptest.NewRequest(http.MethodPost, "/commitments/v1/info", http.NoBody)
w := httptest.NewRecorder()

api.HandleInfo(w, req)
Expand Down Expand Up @@ -124,7 +124,7 @@ func TestHandleInfo_InvalidFlavorMemory(t *testing.T) {

api := NewAPI(k8sClient)

req := httptest.NewRequest(http.MethodGet, "/v1/info", http.NoBody)
req := httptest.NewRequest(http.MethodGet, "/commitments/v1/info", http.NoBody)
w := httptest.NewRecorder()
api.HandleInfo(w, req)

Expand Down Expand Up @@ -197,7 +197,7 @@ func TestHandleInfo_HasCapacityEqualsHandlesCommitments(t *testing.T) {

api := NewAPI(k8sClient)

req := httptest.NewRequest(http.MethodGet, "/v1/info", http.NoBody)
req := httptest.NewRequest(http.MethodGet, "/commitments/v1/info", http.NoBody)
w := httptest.NewRecorder()
api.HandleInfo(w, req)

Expand Down
40 changes: 40 additions & 0 deletions internal/scheduling/reservations/commitments/api_quota.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright SAP SE
// SPDX-License-Identifier: Apache-2.0

package commitments

import (
"net/http"

"github.com/google/uuid"
)

// HandleQuota implements PUT /commitments/v1/projects/:project_id/quota from Limes LIQUID API.
// See: https://pkg.go.dev/github.com/sapcc/go-api-declarations/liquid
//
// This is a no-op endpoint that accepts quota requests but doesn't store them.
// Cortex does not enforce quotas for committed resources - quota enforcement
// happens through commitment validation at change-commitments time.
// The endpoint exists for API compatibility with the LIQUID specification.
func (api *HTTPAPI) HandleQuota(w http.ResponseWriter, r *http.Request) {
// Extract or generate request ID for tracing
requestID := r.Header.Get("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
}
w.Header().Set("X-Request-ID", requestID)

log := baseLog.WithValues("requestID", requestID, "endpoint", "quota")

if r.Method != http.MethodPut {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}

// No-op: Accept the quota request but don't store it
// Cortex handles capacity through commitments, not quotas
log.V(1).Info("received quota request (no-op)", "path", r.URL.Path)

// Return 204 No Content as expected by the LIQUID API
w.WriteHeader(http.StatusNoContent)
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,9 @@ func TestHandleReportCapacity(t *testing.T) {
if err != nil {
t.Fatal(err)
}
req = httptest.NewRequest(tt.method, "/v1/report-capacity", bytes.NewReader(bodyBytes))
req = httptest.NewRequest(tt.method, "/commitments/v1/report-capacity", bytes.NewReader(bodyBytes))
} else {
req = httptest.NewRequest(tt.method, "/v1/report-capacity", http.NoBody)
req = httptest.NewRequest(tt.method, "/commitments/v1/report-capacity", http.NoBody)
}
req = req.WithContext(context.Background())

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,15 +99,16 @@ func (api *HTTPAPI) recordUsageMetrics(statusCode int, startTime time.Time) {
}

// extractProjectIDFromPath extracts the project UUID from the URL path.
// Expected path format: /commitments/v1/projects/:project_id/report-usage
// Expected path format: /commitments/v1/projects/:project_id/<endpoint>
// where <endpoint> is "report-usage" or "quota"
func extractProjectIDFromPath(path string) (string, error) {
// Path: /commitments/v1/projects/<uuid>/report-usage
// Path: /commitments/v1/projects/<uuid>/<endpoint>
parts := strings.Split(strings.Trim(path, "/"), "/")
// Expected: ["v1", "commitments", "projects", "<uuid>", "report-usage"]
// Expected: ["commitments", "v1", "projects", "<uuid>", "<endpoint>"]
if len(parts) < 5 {
return "", fmt.Errorf("path too short: %s", path)
}
if parts[2] != "projects" || parts[4] != "report-usage" {
if parts[0] != "commitments" || parts[1] != "v1" || parts[2] != "projects" {
return "", fmt.Errorf("unexpected path format: %s", path)
}
projectID := parts[3]
Expand Down
Loading