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
3 changes: 2 additions & 1 deletion cmd/manager/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import (
"github.com/cobaltcore-dev/cortex/internal/scheduling/pods"
"github.com/cobaltcore-dev/cortex/internal/scheduling/reservations"
"github.com/cobaltcore-dev/cortex/internal/scheduling/reservations/commitments"
commitmentsapi "github.com/cobaltcore-dev/cortex/internal/scheduling/reservations/commitments/api"
"github.com/cobaltcore-dev/cortex/internal/scheduling/reservations/failover"
"github.com/cobaltcore-dev/cortex/pkg/conf"
"github.com/cobaltcore-dev/cortex/pkg/monitoring"
Expand Down Expand Up @@ -364,7 +365,7 @@ func main() {
if commitmentsConfig.DatasourceName != "" {
commitmentsUsageDB = commitments.NewDBUsageClient(multiclusterClient, commitmentsConfig.DatasourceName)
}
commitmentsAPI := commitments.NewAPIWithConfig(multiclusterClient, commitmentsConfig, commitmentsUsageDB)
commitmentsAPI := commitmentsapi.NewAPIWithConfig(multiclusterClient, commitmentsConfig, commitmentsUsageDB)
commitmentsAPI.Init(mux, metrics.Registry, ctrl.Log.WithName("commitments-api"))

if slices.Contains(mainConfig.EnabledControllers, "nova-pipeline-controllers") {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright SAP SE
// SPDX-License-Identifier: Apache-2.0

package commitments
package api

import (
"context"
Expand All @@ -15,6 +15,7 @@ import (

"github.com/cobaltcore-dev/cortex/api/v1alpha1"
"github.com/cobaltcore-dev/cortex/internal/scheduling/reservations"
commitments "github.com/cobaltcore-dev/cortex/internal/scheduling/reservations/commitments"
"github.com/go-logr/logr"
"github.com/google/uuid"
"github.com/sapcc/go-api-declarations/liquid"
Expand Down Expand Up @@ -69,7 +70,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", "/commitments/v1/change-commitments")
logger := commitments.LoggerFromContext(ctx).WithValues("component", "api", "endpoint", "/commitments/v1/change-commitments")

// Only accept POST method
if r.Method != http.MethodPost {
Expand Down Expand Up @@ -131,7 +132,7 @@ func (api *HTTPAPI) HandleChangeCommitments(w http.ResponseWriter, r *http.Reque
}

func (api *HTTPAPI) processCommitmentChanges(ctx context.Context, w http.ResponseWriter, logger logr.Logger, req liquid.CommitmentChangeRequest, resp *liquid.CommitmentChangeResponse) error {
manager := NewReservationManager(api.client)
manager := commitments.NewReservationManager(api.client)
requireRollback := false
failedCommitments := make(map[string]string) // commitmentUUID to reason for failure, for better response messages in case of rollback
creatorRequestID := reservations.GlobalRequestIDFromContext(ctx)
Expand Down Expand Up @@ -159,7 +160,7 @@ func (api *HTTPAPI) processCommitmentChanges(ctx context.Context, w http.Respons
return errors.New("version mismatch")
}

statesBefore := make(map[string]*CommitmentState) // map of commitmentID to existing state for rollback
statesBefore := make(map[string]*commitments.CommitmentState) // map of commitmentID to existing state for rollback
var reservationsToWatch []v1alpha1.Reservation

if req.DryRun {
Expand All @@ -173,7 +174,7 @@ ProcessLoop:
for _, resourceName := range sortedKeys(projectChanges.ByResource) {
resourceChanges := projectChanges.ByResource[resourceName]
// Validate resource name pattern (instances_group_*)
flavorGroupName, err := getFlavorGroupNameFromResource(string(resourceName))
flavorGroupName, err := commitments.GetFlavorGroupNameFromResource(string(resourceName))
if err != nil {
resp.RejectionReason = fmt.Sprintf("project with unknown resource name %s: %v", projectID, err)
requireRollback = true
Expand All @@ -189,8 +190,8 @@ ProcessLoop:
}

// Reject commitments for flavor groups that don't accept CRs
if !FlavorGroupAcceptsCommitments(&flavorGroup) {
resp.RejectionReason = FlavorGroupCommitmentRejectionReason(&flavorGroup)
if !commitments.FlavorGroupAcceptsCommitments(&flavorGroup) {
resp.RejectionReason = commitments.FlavorGroupCommitmentRejectionReason(&flavorGroup)
requireRollback = true
break ProcessLoop
}
Expand Down Expand Up @@ -221,16 +222,16 @@ ProcessLoop:
}
}

var stateBefore *CommitmentState
var stateBefore *commitments.CommitmentState
if len(existing_reservations.Items) == 0 {
stateBefore = &CommitmentState{
stateBefore = &commitments.CommitmentState{
CommitmentUUID: string(commitment.UUID),
ProjectID: string(projectID),
FlavorGroupName: flavorGroupName,
TotalMemoryBytes: 0,
}
} else {
stateBefore, err = FromReservations(existing_reservations.Items)
stateBefore, err = commitments.FromReservations(existing_reservations.Items)
if err != nil {
failedCommitments[string(commitment.UUID)] = "failed to parse existing commitment reservations"
logger.Info("failed to get existing state for commitment", "commitmentUUID", commitment.UUID, "error", err)
Expand All @@ -241,7 +242,7 @@ ProcessLoop:
statesBefore[string(commitment.UUID)] = stateBefore

// get desired state
stateDesired, err := FromChangeCommitmentTargetState(commitment, string(projectID), flavorGroupName, flavorGroup, string(req.AZ))
stateDesired, err := commitments.FromChangeCommitmentTargetState(commitment, string(projectID), flavorGroupName, flavorGroup, string(req.AZ))
if err != nil {
failedCommitments[string(commitment.UUID)] = err.Error()
logger.Info("failed to get desired state for commitment", "commitmentUUID", commitment.UUID, "error", err)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright SAP SE
// SPDX-License-Identifier: Apache-2.0

package commitments
package api

import (
"strconv"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright SAP SE
// SPDX-License-Identifier: Apache-2.0

package commitments
package api

import (
"github.com/prometheus/client_golang/prometheus"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright SAP SE
// SPDX-License-Identifier: Apache-2.0

package commitments
package api

import (
"encoding/json"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

//nolint:unparam,unused // test helper functions have fixed parameters for simplicity
package commitments
package api

import (
"bytes"
Expand All @@ -22,6 +22,7 @@ import (

"github.com/cobaltcore-dev/cortex/api/v1alpha1"
"github.com/cobaltcore-dev/cortex/internal/knowledge/extractor/plugins/compute"
commitments "github.com/cobaltcore-dev/cortex/internal/scheduling/reservations/commitments"
hv1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1"
"github.com/prometheus/client_golang/prometheus"
"github.com/sapcc/go-api-declarations/liquid"
Expand Down Expand Up @@ -444,8 +445,8 @@ func TestCommitmentChangeIntegration(t *testing.T) {
CommitmentRequest: newCommitmentRequest("az-a", false, 1234,
createCommitment("hw_version_hana_1_ram", "project-A", "uuid-disabled", "confirmed", 2),
),
CustomConfig: func() *Config {
cfg := DefaultConfig()
CustomConfig: func() *commitments.Config {
cfg := commitments.DefaultConfig()
cfg.EnableChangeCommitmentsAPI = false
return &cfg
}(),
Expand Down Expand Up @@ -498,8 +499,8 @@ func TestCommitmentChangeIntegration(t *testing.T) {
createCommitment("hw_version_hana_1_ram", "project-A", "uuid-timeout", "confirmed", 2),
),
// With 0ms timeout, the watch will timeout immediately before reservations become ready
CustomConfig: func() *Config {
cfg := DefaultConfig()
CustomConfig: func() *commitments.Config {
cfg := commitments.DefaultConfig()
cfg.ChangeAPIWatchReservationsTimeout = 0 * time.Millisecond
cfg.ChangeAPIWatchReservationsPollInterval = 100 * time.Millisecond
return &cfg
Expand Down Expand Up @@ -600,7 +601,7 @@ type CommitmentChangeTestCase struct {
ExpectedAPIResponse APIResponseExpectation
AvailableResources *AvailableResources // If nil, all reservations accepted without checks
EnvInfoVersion int64 // Override InfoVersion for version mismatch tests
CustomConfig *Config // Override default config for testing timeout behavior
CustomConfig *commitments.Config // Override default config for testing timeout behavior
}

// AvailableResources defines available memory per host (MB).
Expand Down Expand Up @@ -944,7 +945,7 @@ func newCommitmentTestEnv(
reservations []*v1alpha1.Reservation,
flavorGroups FlavorGroupsKnowledge,
resources *AvailableResources,
customConfig *Config,
customConfig *commitments.Config,
) *CommitmentTestEnv {

t.Helper()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,45 +1,27 @@
// Copyright SAP SE
// SPDX-License-Identifier: Apache-2.0

package commitments
package api

import (
"context"
"net/http"
"strings"
"sync"

commitments "github.com/cobaltcore-dev/cortex/internal/scheduling/reservations/commitments"
"github.com/go-logr/logr"
"github.com/prometheus/client_golang/prometheus"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
)

// UsageDBClient is the minimal interface for querying VM usage data from Postgres.
type UsageDBClient interface {
// ListProjectVMs returns all VMs for a project with their flavor data.
ListProjectVMs(ctx context.Context, projectID string) ([]VMRow, error)
}

// VMRow is the result of a joined server+flavor query from Postgres.
type VMRow struct {
ID string
Name string
Status string
Created string
AZ string
Hypervisor string
FlavorName string
FlavorRAM uint64
FlavorVCPUs uint64
FlavorDisk uint64
FlavorExtras string // JSON string of flavor extra_specs
}
var apiLog = ctrl.Log.WithName("committed-resource")

// HTTPAPI implements Limes LIQUID commitment validation endpoints.
type HTTPAPI struct {
client client.Client
config Config
usageDB UsageDBClient
config commitments.Config
usageDB commitments.UsageDBClient
monitor ChangeCommitmentsAPIMonitor
usageMonitor ReportUsageAPIMonitor
capacityMonitor ReportCapacityAPIMonitor
Expand All @@ -49,11 +31,11 @@ type HTTPAPI struct {
}

func NewAPI(client client.Client) *HTTPAPI {
return NewAPIWithConfig(client, DefaultConfig(), nil)
return NewAPIWithConfig(client, commitments.DefaultConfig(), nil)
}

// NewAPIWithConfig creates an HTTPAPI with the given config and optional usageDB client.
func NewAPIWithConfig(k8sClient client.Client, config Config, usageDB UsageDBClient) *HTTPAPI {
func NewAPIWithConfig(k8sClient client.Client, config commitments.Config, usageDB commitments.UsageDBClient) *HTTPAPI {
return &HTTPAPI{
client: k8sClient,
config: config,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright SAP SE
// SPDX-License-Identifier: Apache-2.0

package commitments
package api

import (
"context"
Expand All @@ -14,6 +14,7 @@ import (
"time"

"github.com/cobaltcore-dev/cortex/internal/scheduling/reservations"
commitments "github.com/cobaltcore-dev/cortex/internal/scheduling/reservations/commitments"
"github.com/go-logr/logr"
"github.com/google/uuid"
liquid "github.com/sapcc/go-api-declarations/liquid"
Expand All @@ -38,7 +39,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", "/commitments/v1/info")
logger := commitments.LoggerFromContext(ctx).WithValues("component", "api", "endpoint", "/commitments/v1/info")

// Only accept GET method
if r.Method != http.MethodGet {
Expand Down Expand Up @@ -113,7 +114,7 @@ func (api *HTTPAPI) buildServiceInfo(ctx context.Context, logger logr.Logger) (l
resources := make(map[liquid.ResourceName]liquid.ResourceInfo)
for groupName, groupData := range flavorGroups {
// Determine if this group accepts commitments (requires fixed RAM/core ratio)
handlesCommitments := FlavorGroupAcceptsCommitments(&groupData)
handlesCommitments := commitments.FlavorGroupAcceptsCommitments(&groupData)

// All flavor groups are registered for usage reporting.
// Only those with a fixed RAM/core ratio have HandlesCommitments=true.
Expand Down Expand Up @@ -143,7 +144,7 @@ func (api *HTTPAPI) buildServiceInfo(ctx context.Context, logger logr.Logger) (l
}

// === 1. RAM Resource ===
ramResourceName := liquid.ResourceName(ResourceNameRAM(groupName))
ramResourceName := liquid.ResourceName(commitments.ResourceNameRAM(groupName))
ramUnit, err := liquid.UnitMebibytes.MultiplyBy(groupData.SmallestFlavor.MemoryMB)
if err != nil {
// Note: This error only occurs on uint64 overflow, which is unrealistic for memory values
Expand All @@ -166,7 +167,7 @@ func (api *HTTPAPI) buildServiceInfo(ctx context.Context, logger logr.Logger) (l
}

// === 2. Cores Resource ===
coresResourceName := liquid.ResourceName(ResourceNameCores(groupName))
coresResourceName := liquid.ResourceName(commitments.ResourceNameCores(groupName))
resources[coresResourceName] = liquid.ResourceInfo{
DisplayName: fmt.Sprintf(
"CPU cores (usable by: %s)",
Expand All @@ -182,7 +183,7 @@ func (api *HTTPAPI) buildServiceInfo(ctx context.Context, logger logr.Logger) (l
}

// === 3. Instances Resource ===
instancesResourceName := liquid.ResourceName(ResourceNameInstances(groupName))
instancesResourceName := liquid.ResourceName(commitments.ResourceNameInstances(groupName))
resources[instancesResourceName] = liquid.ResourceInfo{
DisplayName: fmt.Sprintf(
"instances (usable by: %s)",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright SAP SE
// SPDX-License-Identifier: Apache-2.0

package commitments
package api

import (
"github.com/prometheus/client_golang/prometheus"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright SAP SE
// SPDX-License-Identifier: Apache-2.0

package commitments
package api

import (
"encoding/json"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright SAP SE
// SPDX-License-Identifier: Apache-2.0

package commitments
package api

import (
"net/http"
Expand All @@ -24,7 +24,7 @@ func (api *HTTPAPI) HandleQuota(w http.ResponseWriter, r *http.Request) {
}
w.Header().Set("X-Request-ID", requestID)

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

if r.Method != http.MethodPut {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright SAP SE
// SPDX-License-Identifier: Apache-2.0

package commitments
package api

import (
"encoding/json"
Expand All @@ -10,6 +10,7 @@ import (
"time"

"github.com/cobaltcore-dev/cortex/internal/scheduling/reservations"
commitments "github.com/cobaltcore-dev/cortex/internal/scheduling/reservations/commitments"
"github.com/google/uuid"
"github.com/sapcc/go-api-declarations/liquid"
)
Expand Down Expand Up @@ -38,7 +39,7 @@ func (api *HTTPAPI) HandleReportCapacity(w http.ResponseWriter, r *http.Request)
}

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

// Only accept POST method
if r.Method != http.MethodPost {
Expand All @@ -58,7 +59,7 @@ func (api *HTTPAPI) HandleReportCapacity(w http.ResponseWriter, r *http.Request)
}

// Calculate capacity
calculator := NewCapacityCalculator(api.client)
calculator := commitments.NewCapacityCalculator(api.client)
report, err := calculator.CalculateCapacity(ctx, req)
if err != nil {
logger.Error(err, "failed to calculate capacity")
Expand Down
Loading
Loading