Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func (api *HTTPAPI) HandleReportCapacity(w http.ResponseWriter, r *http.Request)

// Calculate capacity
calculator := NewCapacityCalculator(api.client)
report, err := calculator.CalculateCapacity(ctx)
report, err := calculator.CalculateCapacity(ctx, req)
if err != nil {
logger.Error(err, "failed to calculate capacity")
statusCode = http.StatusInternalServerError
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,10 @@ func TestCapacityCalculator(t *testing.T) {
Build()

calculator := NewCapacityCalculator(fakeClient)
_, err := calculator.CalculateCapacity(context.Background())
req := liquid.ServiceCapacityRequest{
AllAZs: []liquid.AvailabilityZone{"az-one", "az-two"},
}
_, err := calculator.CalculateCapacity(context.Background(), req)
if err == nil {
t.Fatal("Expected error when flavor groups knowledge doesn't exist, got nil")
}
Expand All @@ -154,7 +157,10 @@ func TestCapacityCalculator(t *testing.T) {
Build()

calculator := NewCapacityCalculator(fakeClient)
report, err := calculator.CalculateCapacity(context.Background())
req := liquid.ServiceCapacityRequest{
AllAZs: []liquid.AvailabilityZone{"az-one", "az-two"},
}
report, err := calculator.CalculateCapacity(context.Background(), req)
if err != nil {
t.Fatalf("Expected no error, got: %v", err)
}
Expand All @@ -168,8 +174,8 @@ func TestCapacityCalculator(t *testing.T) {
}
})

t.Run("CalculateCapacity returns empty perAZ when no HostDetails exist", func(t *testing.T) {
// Create a flavor group knowledge without host details
t.Run("CalculateCapacity returns perAZ entries for all AZs from request", func(t *testing.T) {
// Create a flavor group knowledge
flavorGroupKnowledge := createTestFlavorGroupKnowledge(t, "test-group")

fakeClient := fake.NewClientBuilder().
Expand All @@ -178,7 +184,11 @@ func TestCapacityCalculator(t *testing.T) {
Build()

calculator := NewCapacityCalculator(fakeClient)
report, err := calculator.CalculateCapacity(context.Background())
// Request specifies the AZs that must be in the report
req := liquid.ServiceCapacityRequest{
AllAZs: []liquid.AvailabilityZone{"qa-de-1a", "qa-de-1b", "qa-de-1d"},
}
report, err := calculator.CalculateCapacity(context.Background(), req)
if err != nil {
t.Fatalf("Expected no error, got: %v", err)
}
Expand All @@ -188,31 +198,46 @@ func TestCapacityCalculator(t *testing.T) {
t.Fatalf("Expected 3 resources (_ram, _cores, _instances), got %d", len(report.Resources))
}

// Check RAM resource
// Check RAM resource has entries for all requested AZs
ramResource := report.Resources[liquid.ResourceName("hw_version_test-group_ram")]
if ramResource == nil {
t.Fatal("Expected hw_version_test-group_ram resource to exist")
}
if len(ramResource.PerAZ) != 0 {
t.Errorf("Expected 0 AZs for RAM resource, got %d", len(ramResource.PerAZ))
if len(ramResource.PerAZ) != 3 {
t.Errorf("Expected 3 AZs for RAM resource, got %d", len(ramResource.PerAZ))
}
for _, az := range req.AllAZs {
if _, ok := ramResource.PerAZ[az]; !ok {
t.Errorf("Expected RAM resource to have entry for AZ %s", az)
}
}

// Check Cores resource
// Check Cores resource has entries for all requested AZs
coresResource := report.Resources[liquid.ResourceName("hw_version_test-group_cores")]
if coresResource == nil {
t.Fatal("Expected hw_version_test-group_cores resource to exist")
}
if len(coresResource.PerAZ) != 0 {
t.Errorf("Expected 0 AZs for Cores resource, got %d", len(coresResource.PerAZ))
if len(coresResource.PerAZ) != 3 {
t.Errorf("Expected 3 AZs for Cores resource, got %d", len(coresResource.PerAZ))
}
for _, az := range req.AllAZs {
if _, ok := coresResource.PerAZ[az]; !ok {
t.Errorf("Expected Cores resource to have entry for AZ %s", az)
}
}

// Check Instances resource
// Check Instances resource has entries for all requested AZs
instancesResource := report.Resources[liquid.ResourceName("hw_version_test-group_instances")]
if instancesResource == nil {
t.Fatal("Expected hw_version_test-group_instances resource to exist")
}
if len(instancesResource.PerAZ) != 0 {
t.Errorf("Expected 0 AZs for Instances resource, got %d", len(instancesResource.PerAZ))
if len(instancesResource.PerAZ) != 3 {
t.Errorf("Expected 3 AZs for Instances resource, got %d", len(instancesResource.PerAZ))
}
for _, az := range req.AllAZs {
if _, ok := instancesResource.PerAZ[az]; !ok {
t.Errorf("Expected Instances resource to have entry for AZ %s", az)
}
}
})
}
Expand Down
59 changes: 7 additions & 52 deletions internal/scheduling/reservations/commitments/capacity.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@
import (
"context"
"fmt"
"sort"

"github.com/cobaltcore-dev/cortex/api/v1alpha1"
"github.com/cobaltcore-dev/cortex/internal/knowledge/extractor/plugins/compute"
"github.com/cobaltcore-dev/cortex/internal/scheduling/reservations"
. "github.com/majewsky/gg/option"
Expand All @@ -28,7 +26,8 @@
// CalculateCapacity computes per-AZ capacity for all flavor groups.
// For each flavor group, three resources are reported: _ram, _cores, _instances.
// All flavor groups are included, not just those with fixed RAM/core ratio.
func (c *CapacityCalculator) CalculateCapacity(ctx context.Context) (liquid.ServiceCapacityReport, error) {
// The request provides the list of all AZs from Limes that must be included in the report.
func (c *CapacityCalculator) CalculateCapacity(ctx context.Context, req liquid.ServiceCapacityRequest) (liquid.ServiceCapacityReport, error) {
// Get all flavor groups from Knowledge CRDs
knowledge := &reservations.FlavorGroupKnowledgeClient{Client: c.client}
flavorGroups, err := knowledge.GetAllFlavorGroups(ctx, nil)
Expand All @@ -52,7 +51,7 @@
// All flavor groups are included in capacity reporting (not just those with fixed ratio).

// Calculate per-AZ capacity (placeholder: capacity=0 for all resources)
azCapacity, err := c.calculateAZCapacity(ctx, groupName, groupData)
azCapacity, err := c.calculateAZCapacity(ctx, groupName, groupData, req.AllAZs)
if err != nil {
return liquid.ServiceCapacityReport{}, fmt.Errorf("failed to calculate capacity for %s: %w", groupName, err)
}
Expand Down Expand Up @@ -101,15 +100,11 @@
}

func (c *CapacityCalculator) calculateAZCapacity(
ctx context.Context,
_ context.Context,
_ string, // groupName - reserved for future use
_ compute.FlavorGroupFeature, // groupData - reserved for future use
allAZs []liquid.AvailabilityZone, // list of all AZs from Limes request
) (map[liquid.AvailabilityZone]*liquid.AZResourceCapacityReport, error) {

Check failure on line 107 in internal/scheduling/reservations/commitments/capacity.go

View workflow job for this annotation

GitHub Actions / CodeQL

(*CapacityCalculator).calculateAZCapacity - result 1 (error) is always nil (unparam)

Check failure on line 107 in internal/scheduling/reservations/commitments/capacity.go

View workflow job for this annotation

GitHub Actions / Checks

(*CapacityCalculator).calculateAZCapacity - result 1 (error) is always nil (unparam)
// Get list of availability zones from HostDetails Knowledge
azs, err := c.getAvailabilityZones(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get availability zones: %w", err)
}

// Create report entry for each AZ with placeholder capacity=0.
//
Expand All @@ -123,52 +118,12 @@
// TODO: Calculate actual capacity from Reservation CRDs or host resources
// TODO: Calculate actual usage from VM allocations
result := make(map[liquid.AvailabilityZone]*liquid.AZResourceCapacityReport)
for _, az := range azs {
result[liquid.AvailabilityZone(az)] = &liquid.AZResourceCapacityReport{
for _, az := range allAZs {
result[az] = &liquid.AZResourceCapacityReport{
Capacity: 0, // Placeholder: capacity=0 until actual calculation is implemented
Usage: Some[uint64](0), // Placeholder: usage=0 until actual calculation is implemented
}
}

return result, nil
}

func (c *CapacityCalculator) getAvailabilityZones(ctx context.Context) ([]string, error) {
// List all Knowledge CRDs to find host-details knowledge
var knowledgeList v1alpha1.KnowledgeList
if err := c.client.List(ctx, &knowledgeList); err != nil {
return nil, fmt.Errorf("failed to list Knowledge CRDs: %w", err)
}

// Find host-details knowledge and extract AZs
azSet := make(map[string]struct{})
for _, knowledge := range knowledgeList.Items {
// Look for host-details extractor
if knowledge.Spec.Extractor.Name != "host_details" {
continue
}

// Parse features from Raw data
features, err := v1alpha1.UnboxFeatureList[compute.HostDetails](knowledge.Status.Raw)
if err != nil {
// Skip if we can't parse this knowledge
continue
}

// Collect unique AZ names
for _, feature := range features {
if feature.AvailabilityZone != "" {
azSet[feature.AvailabilityZone] = struct{}{}
}
}
}

// Convert set to sorted slice
azs := make([]string, 0, len(azSet))
for az := range azSet {
azs = append(azs, az)
}
sort.Strings(azs)

return azs, nil
}
Loading