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
10 changes: 5 additions & 5 deletions cmd/manager/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,11 @@ func main() {
detectorPipelineMonitor := schedulinglib.NewDetectorPipelineMonitor()
metrics.Registry.MustRegister(&detectorPipelineMonitor)

// Initialize commitments API for LIQUID interface (Postgres-backed usage reporting).
commitmentsConfig := conf.GetConfigOrDie[commitments.Config]()
commitmentsAPI := commitments.NewAPIWithConfig(multiclusterClient, commitmentsConfig, nil)
commitmentsAPI.Init(mux, metrics.Registry, ctrl.Log.WithName("commitments-api"))

if slices.Contains(mainConfig.EnabledControllers, "nova-pipeline-controllers") {
// Filter-weigher pipeline controller setup.
filterWeigherController := &nova.FilterWeigherPipelineController{
Expand Down Expand Up @@ -373,11 +378,6 @@ func main() {
os.Exit(1)
}

// Initialize commitments API for LIQUID interface (with Nova client for usage reporting)
commitmentsConfig := conf.GetConfigOrDie[commitments.Config]()
commitmentsAPI := commitments.NewAPIWithConfig(multiclusterClient, commitmentsConfig, novaClient)
commitmentsAPI.Init(mux, metrics.Registry, ctrl.Log.WithName("commitments-api"))

deschedulingsController := &nova.DetectorPipelineController{
Monitor: detectorPipelineMonitor,
Breaker: &nova.DetectorCycleBreaker{NovaClient: novaClient},
Expand Down
3 changes: 3 additions & 0 deletions helm/bundles/cortex-nova/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ cortex: &cortex
keystoneSecretRef:
name: cortex-nova-openstack-keystone
namespace: default
databaseSecretRef:
name: cortex-nova-postgres
namespace: default
# ssoSecretRef:
# name: cortex-nova-openstack-sso
# namespace: default
Expand Down
42 changes: 33 additions & 9 deletions internal/scheduling/reservations/commitments/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,38 @@ import (
"strings"
"sync"

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

// UsageNovaClient is a minimal interface for the Nova client needed by the usage API.
// This allows for easy mocking in tests without implementing the full NovaClient interface.
type UsageNovaClient interface {
ListProjectServers(ctx context.Context, projectID string) ([]nova.ServerDetail, error)
// 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
}

// HTTPAPI implements Limes LIQUID commitment validation endpoints.
type HTTPAPI struct {
client client.Client
config Config
novaClient UsageNovaClient
usageDB UsageDBClient
Comment thread
coderabbitai[bot] marked this conversation as resolved.
monitor ChangeCommitmentsAPIMonitor
usageMonitor ReportUsageAPIMonitor
capacityMonitor ReportCapacityAPIMonitor
Expand All @@ -38,11 +53,20 @@ func NewAPI(client client.Client) *HTTPAPI {
return NewAPIWithConfig(client, DefaultConfig(), nil)
}

func NewAPIWithConfig(client client.Client, config Config, novaClient UsageNovaClient) *HTTPAPI {
// NewAPIWithConfig creates an HTTPAPI. If usageDB is nil and config.DatabaseSecretRef
// is set, a lazy-connecting PostgresReader-backed client is created automatically.
func NewAPIWithConfig(k8sClient client.Client, config Config, usageDB UsageDBClient) *HTTPAPI {
if usageDB == nil && config.DatabaseSecretRef != nil {
reader := &external.PostgresReader{
Client: k8sClient,
DatabaseSecretRef: *config.DatabaseSecretRef,
}
usageDB = NewDBUsageClient(reader)
}
return &HTTPAPI{
client: client,
client: k8sClient,
config: config,
novaClient: novaClient,
usageDB: usageDB,
monitor: NewChangeCommitmentsAPIMonitor(),
usageMonitor: NewReportUsageAPIMonitor(),
capacityMonitor: NewReportCapacityAPIMonitor(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func (api *HTTPAPI) HandleReportUsage(w http.ResponseWriter, r *http.Request) {
}

// Use UsageCalculator to build usage report
calculator := NewUsageCalculator(api.client, api.novaClient)
calculator := NewUsageCalculator(api.client, api.usageDB)
report, err := calculator.CalculateUsage(r.Context(), log, projectID, req.AllAZs)
if err != nil {
log.Error(err, "failed to calculate usage report", "projectID", projectID)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import (

"github.com/cobaltcore-dev/cortex/api/v1alpha1"
"github.com/cobaltcore-dev/cortex/internal/knowledge/extractor/plugins/compute"
"github.com/cobaltcore-dev/cortex/internal/scheduling/nova"
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 @@ -432,42 +431,41 @@ type ExpectedVMUsage struct {
}

// ============================================================================
// Mock Nova Client
// Mock UsageDBClient
// ============================================================================

type mockUsageNovaClient struct {
servers map[string][]nova.ServerDetail // projectID -> servers
err error
type mockUsageDBClient struct {
rows map[string][]VMRow // projectID -> rows
err error
}

func newMockUsageNovaClient() *mockUsageNovaClient {
return &mockUsageNovaClient{
servers: make(map[string][]nova.ServerDetail),
func newMockUsageDBClient() *mockUsageDBClient {
return &mockUsageDBClient{
rows: make(map[string][]VMRow),
}
}

func (m *mockUsageNovaClient) ListProjectServers(_ context.Context, projectID string) ([]nova.ServerDetail, error) {
func (m *mockUsageDBClient) ListProjectVMs(_ context.Context, projectID string) ([]VMRow, error) {
if m.err != nil {
return nil, m.err
}
return m.servers[projectID], nil
return m.rows[projectID], nil
}

func (m *mockUsageNovaClient) addVM(vm *TestVMUsage) {
server := nova.ServerDetail{
ID: vm.UUID,
Name: vm.UUID,
Status: "ACTIVE",
TenantID: vm.ProjectID,
Created: vm.CreatedAt.Format(time.RFC3339),
AvailabilityZone: vm.AZ,
Hypervisor: vm.Host,
FlavorName: vm.Flavor.Name,
FlavorRAM: uint64(vm.Flavor.MemoryMB), //nolint:gosec
FlavorVCPUs: uint64(vm.Flavor.VCPUs), //nolint:gosec
FlavorDisk: vm.Flavor.DiskGB,
}
m.servers[vm.ProjectID] = append(m.servers[vm.ProjectID], server)
func (m *mockUsageDBClient) addVM(vm *TestVMUsage) {
row := VMRow{
ID: vm.UUID,
Name: vm.UUID,
Status: "ACTIVE",
Created: vm.CreatedAt.Format(time.RFC3339),
AZ: vm.AZ,
Hypervisor: vm.Host,
FlavorName: vm.Flavor.Name,
FlavorRAM: uint64(vm.Flavor.MemoryMB), //nolint:gosec
FlavorVCPUs: uint64(vm.Flavor.VCPUs), //nolint:gosec
FlavorDisk: vm.Flavor.DiskGB,
}
m.rows[vm.ProjectID] = append(m.rows[vm.ProjectID], row)
}

// ============================================================================
Expand All @@ -478,7 +476,7 @@ type UsageTestEnv struct {
T *testing.T
Scheme *runtime.Scheme
K8sClient client.Client
NovaClient *mockUsageNovaClient
DBClient *mockUsageDBClient
FlavorGroups FlavorGroupsKnowledge
HTTPServer *httptest.Server
API *HTTPAPI
Expand Down Expand Up @@ -530,14 +528,14 @@ func newUsageTestEnv(
}).
Build()

// Create mock Nova client with VMs
novaClient := newMockUsageNovaClient()
// Create mock DB client with VMs
dbClient := newMockUsageDBClient()
for _, vm := range vms {
novaClient.addVM(vm)
dbClient.addVM(vm)
}

// Create API with mock Nova client
api := NewAPIWithConfig(k8sClient, DefaultConfig(), novaClient)
// Create API with mock DB client
api := NewAPIWithConfig(k8sClient, DefaultConfig(), dbClient)
mux := http.NewServeMux()
registry := prometheus.NewRegistry()
api.Init(mux, registry, log.Log)
Expand All @@ -547,7 +545,7 @@ func newUsageTestEnv(
T: t,
Scheme: scheme,
K8sClient: k8sClient,
NovaClient: novaClient,
DBClient: dbClient,
FlavorGroups: flavorGroups,
HTTPServer: httpServer,
API: api,
Expand Down
Loading
Loading