diff --git a/helm/bundles/cortex-cinder/Chart.yaml b/helm/bundles/cortex-cinder/Chart.yaml index c859282c8..9bc6899c8 100644 --- a/helm/bundles/cortex-cinder/Chart.yaml +++ b/helm/bundles/cortex-cinder/Chart.yaml @@ -5,7 +5,7 @@ apiVersion: v2 name: cortex-cinder description: A Helm chart deploying Cortex for Cinder. type: application -version: 0.0.61 +version: 0.0.62 appVersion: 0.1.0 dependencies: # from: file://../../library/cortex-postgres @@ -16,12 +16,12 @@ dependencies: # from: file://../../library/cortex - name: cortex repository: oci://ghcr.io/cobaltcore-dev/cortex/charts - version: 0.0.48 + version: 0.0.49 alias: cortex-knowledge-controllers # from: file://../../library/cortex - name: cortex repository: oci://ghcr.io/cobaltcore-dev/cortex/charts - version: 0.0.48 + version: 0.0.49 alias: cortex-scheduling-controllers # Owner info adds a configmap to the kubernetes cluster with information on diff --git a/helm/bundles/cortex-crds/Chart.yaml b/helm/bundles/cortex-crds/Chart.yaml index 7a4672b1f..f97000078 100644 --- a/helm/bundles/cortex-crds/Chart.yaml +++ b/helm/bundles/cortex-crds/Chart.yaml @@ -5,13 +5,13 @@ apiVersion: v2 name: cortex-crds description: A Helm chart deploying Cortex CRDs. type: application -version: 0.0.61 +version: 0.0.62 appVersion: 0.1.0 dependencies: # from: file://../../library/cortex - name: cortex repository: oci://ghcr.io/cobaltcore-dev/cortex/charts - version: 0.0.48 + version: 0.0.49 # Owner info adds a configmap to the kubernetes cluster with information on # the service owner. This makes it easier to find out who to contact in case diff --git a/helm/bundles/cortex-ironcore/Chart.yaml b/helm/bundles/cortex-ironcore/Chart.yaml index cbb2574ea..7e6918997 100644 --- a/helm/bundles/cortex-ironcore/Chart.yaml +++ b/helm/bundles/cortex-ironcore/Chart.yaml @@ -5,13 +5,13 @@ apiVersion: v2 name: cortex-ironcore description: A Helm chart deploying Cortex for IronCore. type: application -version: 0.0.61 +version: 0.0.62 appVersion: 0.1.0 dependencies: # from: file://../../library/cortex - name: cortex repository: oci://ghcr.io/cobaltcore-dev/cortex/charts - version: 0.0.48 + version: 0.0.49 # Owner info adds a configmap to the kubernetes cluster with information on # the service owner. This makes it easier to find out who to contact in case diff --git a/helm/bundles/cortex-manila/Chart.yaml b/helm/bundles/cortex-manila/Chart.yaml index 849523e20..bac6774b7 100644 --- a/helm/bundles/cortex-manila/Chart.yaml +++ b/helm/bundles/cortex-manila/Chart.yaml @@ -5,7 +5,7 @@ apiVersion: v2 name: cortex-manila description: A Helm chart deploying Cortex for Manila. type: application -version: 0.0.61 +version: 0.0.62 appVersion: 0.1.0 dependencies: # from: file://../../library/cortex-postgres @@ -16,12 +16,12 @@ dependencies: # from: file://../../library/cortex - name: cortex repository: oci://ghcr.io/cobaltcore-dev/cortex/charts - version: 0.0.48 + version: 0.0.49 alias: cortex-knowledge-controllers # from: file://../../library/cortex - name: cortex repository: oci://ghcr.io/cobaltcore-dev/cortex/charts - version: 0.0.48 + version: 0.0.49 alias: cortex-scheduling-controllers # Owner info adds a configmap to the kubernetes cluster with information on diff --git a/helm/bundles/cortex-nova/Chart.yaml b/helm/bundles/cortex-nova/Chart.yaml index ebf4fdba9..9ea033123 100644 --- a/helm/bundles/cortex-nova/Chart.yaml +++ b/helm/bundles/cortex-nova/Chart.yaml @@ -5,7 +5,7 @@ apiVersion: v2 name: cortex-nova description: A Helm chart deploying Cortex for Nova. type: application -version: 0.0.61 +version: 0.0.62 appVersion: 0.1.0 dependencies: # from: file://../../library/cortex-postgres @@ -16,12 +16,12 @@ dependencies: # from: file://../../library/cortex - name: cortex repository: oci://ghcr.io/cobaltcore-dev/cortex/charts - version: 0.0.48 + version: 0.0.49 alias: cortex-knowledge-controllers # from: file://../../library/cortex - name: cortex repository: oci://ghcr.io/cobaltcore-dev/cortex/charts - version: 0.0.48 + version: 0.0.49 alias: cortex-scheduling-controllers # Owner info adds a configmap to the kubernetes cluster with information on diff --git a/helm/bundles/cortex-pods/Chart.yaml b/helm/bundles/cortex-pods/Chart.yaml index 5c998ec5b..5b62b5682 100644 --- a/helm/bundles/cortex-pods/Chart.yaml +++ b/helm/bundles/cortex-pods/Chart.yaml @@ -5,13 +5,13 @@ apiVersion: v2 name: cortex-pods description: A Helm chart deploying Cortex for Pods. type: application -version: 0.0.61 +version: 0.0.62 appVersion: 0.1.0 dependencies: # from: file://../../library/cortex - name: cortex repository: oci://ghcr.io/cobaltcore-dev/cortex/charts - version: 0.0.48 + version: 0.0.49 # Owner info adds a configmap to the kubernetes cluster with information on # the service owner. This makes it easier to find out who to contact in case diff --git a/helm/library/cortex/Chart.yaml b/helm/library/cortex/Chart.yaml index f82fe0576..b28f3c330 100644 --- a/helm/library/cortex/Chart.yaml +++ b/helm/library/cortex/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: cortex description: A Helm chart to distribute cortex. type: application -version: 0.0.48 -appVersion: "sha-f3c2ce54" +version: 0.0.49 +appVersion: "sha-7fda9569" icon: "https://example.com/icon.png" dependencies: [] diff --git a/internal/scheduling/cinder/e2e_checks.go b/internal/scheduling/cinder/e2e_checks.go index 511668c24..6b6c6715e 100644 --- a/internal/scheduling/cinder/e2e_checks.go +++ b/internal/scheduling/cinder/e2e_checks.go @@ -35,7 +35,7 @@ func checkCinderSchedulerReturnsValidHosts( Hosts: []api.ExternalSchedulerHost{}, Weights: map[string]float64{}, } - apiURL := "http://cortex-cinder-scheduler:8080/scheduler/cinder/external" + apiURL := "http://localhost:8080/scheduler/cinder/external" slog.Info("sending request to external scheduler", "apiURL", apiURL) requestBody := must.Return(json.Marshal(request)) diff --git a/internal/scheduling/manila/e2e_checks.go b/internal/scheduling/manila/e2e_checks.go index 466535b0e..a14d084c2 100644 --- a/internal/scheduling/manila/e2e_checks.go +++ b/internal/scheduling/manila/e2e_checks.go @@ -82,7 +82,7 @@ func checkManilaSchedulerReturnsValidHosts( Hosts: hosts, Weights: weights, } - apiURL := "http://cortex-manila-scheduler:8080/scheduler/manila/external" + apiURL := "http://localhost:8080/scheduler/manila/external" slog.Info("sending request to external scheduler", "apiURL", apiURL) requestBody := must.Return(json.Marshal(request)) diff --git a/internal/scheduling/nova/e2e_checks.go b/internal/scheduling/nova/e2e_checks.go index a8caca8ba..ad65a57b5 100644 --- a/internal/scheduling/nova/e2e_checks.go +++ b/internal/scheduling/nova/e2e_checks.go @@ -332,7 +332,7 @@ func checkNovaSchedulerReturnsValidHosts( req api.ExternalSchedulerRequest, ) []string { - apiURL := "http://cortex-nova-scheduler:8080/scheduler/nova/external" + apiURL := "http://localhost:8080/scheduler/nova/external" slog.Info("sending request to external scheduler", "apiURL", apiURL) requestBody := must.Return(json.Marshal(req)) diff --git a/internal/scheduling/reservations/commitments/api/info.go b/internal/scheduling/reservations/commitments/api/info.go index d8459c50b..b1a4fd776 100644 --- a/internal/scheduling/reservations/commitments/api/info.go +++ b/internal/scheduling/reservations/commitments/api/info.go @@ -7,6 +7,7 @@ import ( "context" "encoding/json" "fmt" + "math" "net/http" "strconv" "strings" @@ -75,12 +76,22 @@ func (api *HTTPAPI) recordInfoMetrics(statusCode int, startTime time.Time) { } // resourceAttributes holds the custom attributes for a resource in the info API response. +// Ratio values are in GiB per vCPU, matching the RAM resource unit (UnitGibibytes). type resourceAttributes struct { RamCoreRatio *uint64 `json:"ramCoreRatio,omitempty"` RamCoreRatioMin *uint64 `json:"ramCoreRatioMin,omitempty"` RamCoreRatioMax *uint64 `json:"ramCoreRatioMax,omitempty"` } +// mibToGiB converts a MiB pointer value to GiB, rounded to the nearest integer. Returns nil if v is nil. +func mibToGiB(v *uint64) *uint64 { + if v == nil { + return nil + } + gib := uint64(math.Round(float64(*v) / 1024)) + return &gib +} + // buildServiceInfo constructs the ServiceInfo response with metadata for all flavor groups. // For each flavor group, three resources are registered: // - _ram: RAM resource (unit = multiples of smallest flavor RAM, HandlesCommitments=true only if fixed ratio) @@ -110,11 +121,13 @@ func (api *HTTPAPI) buildServiceInfo(ctx context.Context, logger logr.Logger) (l } flavorListStr := strings.Join(flavorNames, ", ") - // Build attributes JSON with ratio info (shared across all resource types) + // Build attributes JSON with ratio info (shared across all resource types). + // Ratios are stored in MiB/vCPU in the knowledge CRD; convert to GiB/vCPU here + // so the values match the GiB unit used by the RAM resource. attrs := resourceAttributes{ - RamCoreRatio: groupData.RamCoreRatio, - RamCoreRatioMin: groupData.RamCoreRatioMin, - RamCoreRatioMax: groupData.RamCoreRatioMax, + RamCoreRatio: mibToGiB(groupData.RamCoreRatio), + RamCoreRatioMin: mibToGiB(groupData.RamCoreRatioMin), + RamCoreRatioMax: mibToGiB(groupData.RamCoreRatioMax), } attrsJSON, err := json.Marshal(attrs) if err != nil { @@ -146,13 +159,17 @@ func (api *HTTPAPI) buildServiceInfo(ctx context.Context, logger logr.Logger) (l // === 2. Cores Resource === coresResourceName := liquid.ResourceName(commitments.ResourceNameCores(groupName)) + coresTopology := liquid.AZAwareTopology + if resCfg.Cores.HandlesCommitments { + coresTopology = liquid.AZSeparatedTopology + } resources[coresResourceName] = liquid.ResourceInfo{ DisplayName: fmt.Sprintf( "CPU cores (usable by: %s)", flavorListStr, ), Unit: liquid.UnitNone, - Topology: liquid.AZAwareTopology, + Topology: coresTopology, NeedsResourceDemand: false, HasCapacity: resCfg.Cores.HasCapacity, HasQuota: resCfg.Cores.HasQuota, @@ -162,13 +179,17 @@ func (api *HTTPAPI) buildServiceInfo(ctx context.Context, logger logr.Logger) (l // === 3. Instances Resource === instancesResourceName := liquid.ResourceName(commitments.ResourceNameInstances(groupName)) + instancesTopology := liquid.AZAwareTopology + if resCfg.Instances.HandlesCommitments { + instancesTopology = liquid.AZSeparatedTopology + } resources[instancesResourceName] = liquid.ResourceInfo{ DisplayName: fmt.Sprintf( "instances (usable by: %s)", flavorListStr, ), Unit: liquid.UnitNone, - Topology: liquid.AZAwareTopology, + Topology: instancesTopology, NeedsResourceDemand: false, HasCapacity: resCfg.Instances.HasCapacity, HasQuota: resCfg.Instances.HasQuota, diff --git a/internal/scheduling/reservations/commitments/api/info_test.go b/internal/scheduling/reservations/commitments/api/info_test.go index e74964925..c7d946ddc 100644 --- a/internal/scheduling/reservations/commitments/api/info_test.go +++ b/internal/scheduling/reservations/commitments/api/info_test.go @@ -138,8 +138,6 @@ func TestHandleInfo_InvalidFlavorMemory(t *testing.T) { } func TestHandleInfo_ResourceFlagsFromConfig(t *testing.T) { - // Test that resource flags (HandlesCommitments, HasCapacity, HasQuota) are read from config, - // not derived from flavor group metadata. Both groups get resources regardless of ratio. scheme := runtime.NewScheme() if err := v1alpha1.AddToScheme(scheme); err != nil { t.Fatalf("failed to add scheme: %v", err) @@ -155,7 +153,9 @@ func TestHandleInfo_ResourceFlagsFromConfig(t *testing.T) { }, "largestFlavor": map[string]interface{}{"name": "hana_c8_m32", "vcpus": 8, "memoryMB": 32768, "diskGB": 100}, "smallestFlavor": map[string]interface{}{"name": "hana_c4_m16", "vcpus": 4, "memoryMB": 16384, "diskGB": 50}, - "ramCoreRatio": 4096, + // 4094 MiB/vCPU simulates real flavor RAM (4096 MiB nominal − 2 MiB video RAM). + // Truncating division gives 3; rounding gives 4. + "ramCoreRatio": 4094, }, { "name": "v2_variable", @@ -247,7 +247,7 @@ func TestHandleInfo_ResourceFlagsFromConfig(t *testing.T) { t.Error("hw_version_hana_fixed_ram: expected HasQuota=true (fixed ratio groups accept quotas)") } - // Test Cores resource: hw_version_hana_fixed_cores (always AZAwareTopology, no quota) + // Test Cores resource: hw_version_hana_fixed_cores (HandlesCommitments=false → AZAwareTopology) coresResource, ok := serviceInfo.Resources["hw_version_hana_fixed_cores"] if !ok { t.Fatal("expected hw_version_hana_fixed_cores resource to exist") @@ -265,7 +265,7 @@ func TestHandleInfo_ResourceFlagsFromConfig(t *testing.T) { t.Error("hw_version_hana_fixed_cores: expected HasQuota=false") } - // Test Instances resource: hw_version_hana_fixed_instances (always AZAwareTopology, no quota) + // Test Instances resource: hw_version_hana_fixed_instances (HandlesCommitments=false → AZAwareTopology) instancesResource, ok := serviceInfo.Resources["hw_version_hana_fixed_instances"] if !ok { t.Fatal("expected hw_version_hana_fixed_instances resource to exist") @@ -334,4 +334,122 @@ func TestHandleInfo_ResourceFlagsFromConfig(t *testing.T) { if v2InstancesResource.HasQuota { t.Error("hw_version_v2_variable_instances: expected HasQuota=false") } + + // Verify ratio attributes are converted from MiB to GiB. + // hana_fixed has ramCoreRatio=4096 MiB/vCPU → expect 4 GiB/vCPU. + checkAttrsRatio(t, "hw_version_hana_fixed_ram", ramResource.Attributes, 4, nil, nil) + // v2_variable has ramCoreRatioMin=2048 MiB/vCPU, ramCoreRatioMax=16384 MiB/vCPU → expect 2, 16 GiB/vCPU. + checkAttrsRatio(t, "hw_version_v2_variable_ram", v2RamResource.Attributes, 0, ptr(uint64(2)), ptr(uint64(16))) +} + +func TestHandleInfo_TopologyFollowsHandlesCommitments(t *testing.T) { + // Verifies that any resource with HandlesCommitments=true gets AZSeparatedTopology, + // regardless of whether it's RAM, Cores, or Instances. + scheme := runtime.NewScheme() + if err := v1alpha1.AddToScheme(scheme); err != nil { + t.Fatalf("failed to add scheme: %v", err) + } + + features := []map[string]interface{}{ + { + "name": "fg", + "flavors": []map[string]interface{}{{"name": "fg_c4_m16", "vcpus": 4, "memoryMB": 16384, "diskGB": 50}}, + "largestFlavor": map[string]interface{}{"name": "fg_c4_m16", "vcpus": 4, "memoryMB": 16384, "diskGB": 50}, + "smallestFlavor": map[string]interface{}{"name": "fg_c4_m16", "vcpus": 4, "memoryMB": 16384, "diskGB": 50}, + "ramCoreRatio": 4096, + }, + } + raw, err := v1alpha1.BoxFeatureList(features) + if err != nil { + t.Fatalf("failed to box features: %v", err) + } + knowledge := &v1alpha1.Knowledge{ + ObjectMeta: v1.ObjectMeta{Name: "flavor-groups"}, + Spec: v1alpha1.KnowledgeSpec{ + SchedulingDomain: v1alpha1.SchedulingDomainNova, + Extractor: v1alpha1.KnowledgeExtractorSpec{Name: "flavor_groups"}, + }, + Status: v1alpha1.KnowledgeStatus{ + Conditions: []v1.Condition{{Type: v1alpha1.KnowledgeConditionReady, Status: "True"}}, + Raw: raw, + LastContentChange: v1.Now(), + }, + } + + // All three resource types handle commitments. + cfg := commitments.DefaultAPIConfig() + cfg.FlavorGroupResourceConfig = map[string]commitments.FlavorGroupResourcesConfig{ + "fg": { + RAM: commitments.ResourceTypeConfig{HandlesCommitments: true, HasCapacity: true, HasQuota: true}, + Cores: commitments.ResourceTypeConfig{HandlesCommitments: true, HasCapacity: true, HasQuota: true}, + Instances: commitments.ResourceTypeConfig{HandlesCommitments: true, HasCapacity: true, HasQuota: true}, + }, + } + + k8sClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(knowledge).Build() + api := NewAPIWithConfig(k8sClient, cfg, nil) + + req := httptest.NewRequest(http.MethodGet, "/commitments/v1/info", http.NoBody) + w := httptest.NewRecorder() + api.HandleInfo(w, req) + + resp := w.Result() + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Fatalf("expected 200, got %d", resp.StatusCode) + } + + var serviceInfo liquid.ServiceInfo + if err := json.NewDecoder(resp.Body).Decode(&serviceInfo); err != nil { + t.Fatalf("failed to decode: %v", err) + } + + for _, resName := range []string{"hw_version_fg_ram", "hw_version_fg_cores", "hw_version_fg_instances"} { + res, ok := serviceInfo.Resources[liquid.ResourceName(resName)] + if !ok { + t.Fatalf("expected resource %s", resName) + } + if res.Topology != liquid.AZSeparatedTopology { + t.Errorf("%s: expected Topology=%q (HandlesCommitments=true), got %q", resName, liquid.AZSeparatedTopology, res.Topology) + } + } +} + +// ptr returns a pointer to v, for use in test assertions. +func ptr[T any](v T) *T { return &v } + +// checkAttrsRatio decodes the Attributes JSON of a resource and verifies the ratio fields +// are in GiB/vCPU. Pass ratioGiB=0 to skip the fixed-ratio check; pass nil min/max to skip range checks. +func checkAttrsRatio(t *testing.T, resName string, raw json.RawMessage, ratioGiB uint64, minGiB, maxGiB *uint64) { + t.Helper() + var attrs struct { + RamCoreRatio *uint64 `json:"ramCoreRatio"` + RamCoreRatioMin *uint64 `json:"ramCoreRatioMin"` + RamCoreRatioMax *uint64 `json:"ramCoreRatioMax"` + } + if err := json.Unmarshal(raw, &attrs); err != nil { + t.Fatalf("%s: failed to decode attributes: %v", resName, err) + } + if ratioGiB != 0 { + if attrs.RamCoreRatio == nil { + t.Errorf("%s: expected ramCoreRatio=%d GiB/vCPU, got nil", resName, ratioGiB) + } else if *attrs.RamCoreRatio != ratioGiB { + t.Errorf("%s: expected ramCoreRatio=%d GiB/vCPU, got %d", resName, ratioGiB, *attrs.RamCoreRatio) + } + } + if minGiB != nil { + if attrs.RamCoreRatioMin == nil { + t.Errorf("%s: expected ramCoreRatioMin=%d GiB/vCPU, got nil", resName, *minGiB) + } else if *attrs.RamCoreRatioMin != *minGiB { + t.Errorf("%s: expected ramCoreRatioMin=%d GiB/vCPU, got %d", resName, *minGiB, *attrs.RamCoreRatioMin) + } + } + if maxGiB != nil { + if attrs.RamCoreRatioMax == nil { + t.Errorf("%s: expected ramCoreRatioMax=%d GiB/vCPU, got nil", resName, *maxGiB) + } else if *attrs.RamCoreRatioMax != *maxGiB { + t.Errorf("%s: expected ramCoreRatioMax=%d GiB/vCPU, got %d", resName, *maxGiB, *attrs.RamCoreRatioMax) + } + } } diff --git a/internal/scheduling/reservations/commitments/api/report_capacity_test.go b/internal/scheduling/reservations/commitments/api/report_capacity_test.go index 638c060db..362f3000f 100644 --- a/internal/scheduling/reservations/commitments/api/report_capacity_test.go +++ b/internal/scheduling/reservations/commitments/api/report_capacity_test.go @@ -285,7 +285,7 @@ func TestCapacityCalculator(t *testing.T) { t.Run("CalculateCapacity reads capacity and usage from Ready CRD", func(t *testing.T) { knowledge := createTestFlavorGroupKnowledge(t) - crd := createTestFlavorGroupCapacity("test-group", "az-one", "test_c8_m32", 1000, 800, true) + crd := createTestFlavorGroupCapacity(1000, 800, true) fakeClient := fake.NewClientBuilder(). WithScheme(scheme). WithObjects(knowledge, crd). @@ -322,7 +322,7 @@ func TestCapacityCalculator(t *testing.T) { t.Run("CalculateCapacity returns zero capacity for missing CRD", func(t *testing.T) { knowledge := createTestFlavorGroupKnowledge(t) // CRD exists only for az-one; az-two has no CRD - crd := createTestFlavorGroupCapacity("test-group", "az-one", "test_c8_m32", 500, 400, true) + crd := createTestFlavorGroupCapacity(500, 400, true) fakeClient := fake.NewClientBuilder(). WithScheme(scheme). WithObjects(knowledge, crd). @@ -351,7 +351,7 @@ func TestCapacityCalculator(t *testing.T) { t.Run("CalculateCapacity omits usage for stale CRD (Ready=False)", func(t *testing.T) { knowledge := createTestFlavorGroupKnowledge(t) - crd := createTestFlavorGroupCapacity("test-group", "az-one", "test_c8_m32", 1000, 800, false) + crd := createTestFlavorGroupCapacity(1000, 800, false) fakeClient := fake.NewClientBuilder(). WithScheme(scheme). WithObjects(knowledge, crd). @@ -382,9 +382,41 @@ func TestCapacityCalculator(t *testing.T) { t.Error("expected usage to be absent (None) for stale CRD") } }) + + t.Run("CalculateCapacity omits resources with HasCapacity=false", func(t *testing.T) { + knowledge := createTestFlavorGroupKnowledge(t) + crd := createTestFlavorGroupCapacity(100, 80, true) + fakeClient := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(knowledge, crd). + WithStatusSubresource(crd). + Build() + + // Only RAM and Cores have capacity; Instances does not. + cfgNoInstances := commitments.APIConfig{ + FlavorGroupResourceConfig: map[string]commitments.FlavorGroupResourcesConfig{ + "*": { + RAM: commitments.ResourceTypeConfig{HasCapacity: true}, + Cores: commitments.ResourceTypeConfig{HasCapacity: true}, + Instances: commitments.ResourceTypeConfig{HasCapacity: false}, + }, + }, + } + calculator := commitments.NewCapacityCalculator(fakeClient, cfgNoInstances) + req := liquid.ServiceCapacityRequest{AllAZs: []liquid.AvailabilityZone{"az-one"}} + report, err := calculator.CalculateCapacity(context.Background(), req) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(report.Resources) != 2 { + t.Fatalf("expected 2 resources (ram, cores), got %d: %v", len(report.Resources), report.Resources) + } + if _, ok := report.Resources["hw_version_test-group_instances"]; ok { + t.Error("expected hw_version_test-group_instances to be absent (HasCapacity=false)") + } + }) } -// verifyPerAZMatchesRequest checks that perAZ entries match exactly the requested AZs. // This follows the same semantics as nova liquid: the response must contain // entries for all AZs in AllAZs, no more and no less. func verifyPerAZMatchesRequest(t *testing.T, res *liquid.ResourceCapacityReport, requestedAZs []liquid.AvailabilityZone) { @@ -443,7 +475,10 @@ func createEmptyFlavorGroupKnowledge() *v1alpha1.Knowledge { // createTestFlavorGroupCapacity creates a FlavorGroupCapacity CRD for testing. // totalSlots and placeableSlots are for the named smallest flavor entry. // ready controls whether the Ready condition is True or False. -func createTestFlavorGroupCapacity(group, az, smallestFlavorName string, totalSlots, placeableSlots int64, ready bool) *v1alpha1.FlavorGroupCapacity { +func createTestFlavorGroupCapacity(totalSlots, placeableSlots int64, ready bool) *v1alpha1.FlavorGroupCapacity { + const group = "test-group" + const az = "az-one" + const smallestFlavorName = "test_c8_m32" conditionStatus := v1.ConditionTrue if !ready { conditionStatus = v1.ConditionFalse diff --git a/internal/scheduling/reservations/commitments/e2e_checks.go b/internal/scheduling/reservations/commitments/e2e_checks.go index 758f5cf24..4895a8043 100644 --- a/internal/scheduling/reservations/commitments/e2e_checks.go +++ b/internal/scheduling/reservations/commitments/e2e_checks.go @@ -22,7 +22,7 @@ import ( const ( // Default URL for the commitments API endpoint. // This should match the service name in the helm chart. - defaultCommitmentsAPIURL = "http://cortex-nova-scheduler:8080" + defaultCommitmentsAPIURL = "http://localhost:8080" // defaultE2EProjectUUID is a well-known fake project UUID used when no ProjectID is configured. // It is intentionally not a real OpenStack project — commitments created under it self-expire. diff --git a/internal/scheduling/reservations/quota/config.go b/internal/scheduling/reservations/quota/config.go index b7314f595..706936872 100644 --- a/internal/scheduling/reservations/quota/config.go +++ b/internal/scheduling/reservations/quota/config.go @@ -19,6 +19,11 @@ type QuotaControllerConfig struct { // CRStateFilter defines which CommittedResource states to include // when summing cr_actual_usage. Default: ["confirmed", "guaranteed"] CRStateFilter []v1alpha1.CommitmentStatus `json:"crStateFilter"` + + // EnableHVDiff enables incremental TotalUsage updates via HV instance diffs. + // When false, only periodic full reconciles update TotalUsage (safer but slower convergence). + // Default: true. + EnableHVDiff *bool `json:"enableHVDiff,omitempty"` } // ApplyDefaults fills in any unset values with defaults. @@ -30,15 +35,28 @@ func (c *QuotaControllerConfig) ApplyDefaults() { if len(c.CRStateFilter) == 0 { c.CRStateFilter = defaults.CRStateFilter } + if c.EnableHVDiff == nil { + c.EnableHVDiff = defaults.EnableHVDiff + } +} + +// IsHVDiffEnabled returns whether incremental HV diff is enabled. +func (c *QuotaControllerConfig) IsHVDiffEnabled() bool { + if c.EnableHVDiff == nil { + return true // default: enabled + } + return *c.EnableHVDiff } // DefaultQuotaControllerConfig returns a default configuration. func DefaultQuotaControllerConfig() QuotaControllerConfig { + enableHVDiff := true return QuotaControllerConfig{ FullReconcileInterval: metav1.Duration{Duration: 5 * time.Minute}, CRStateFilter: []v1alpha1.CommitmentStatus{ v1alpha1.CommitmentStatusConfirmed, v1alpha1.CommitmentStatusGuaranteed, }, + EnableHVDiff: &enableHVDiff, } } diff --git a/internal/scheduling/reservations/quota/controller.go b/internal/scheduling/reservations/quota/controller.go index f374080bc..2aff199e3 100644 --- a/internal/scheduling/reservations/quota/controller.go +++ b/internal/scheduling/reservations/quota/controller.go @@ -435,6 +435,7 @@ func (c *QuotaController) accumulateAddedVM( delta.addIncrement(commitments.ResourceNameRAM(groupName), vm.AvailabilityZone, ramUnits) delta.addIncrement(commitments.ResourceNameCores(groupName), vm.AvailabilityZone, coresAmount) + delta.addIncrement(commitments.ResourceNameInstances(groupName), vm.AvailabilityZone, 1) } // isVMNewSinceLastReconcile checks if a VM was created after the last full reconcile. @@ -536,6 +537,7 @@ func (c *QuotaController) accumulateRemovedVM( delta.addDecrement(commitments.ResourceNameRAM(groupName), info.AvailabilityZone, ramUnits) delta.addDecrement(commitments.ResourceNameCores(groupName), info.AvailabilityZone, coresAmount) + delta.addDecrement(commitments.ResourceNameInstances(groupName), info.AvailabilityZone, 1) } // applyDeltaAndUpdateStatus applies batched deltas to ALL per-AZ ProjectQuota CRDs for a project. @@ -648,8 +650,12 @@ func (c *QuotaController) SetupWithManager(mgr ctrl.Manager) error { } // SetupHVWatcher sets up a separate controller to watch HV CRD changes -// for incremental TotalUsage updates. +// for incremental TotalUsage updates. Skipped if EnableHVDiff is false. func (c *QuotaController) SetupHVWatcher(mgr ctrl.Manager) error { + if !c.Config.IsHVDiffEnabled() { + log.Info("HV diff watcher disabled by config (enableHVDiff=false)") + return nil + } return ctrl.NewControllerManagedBy(mgr). Named("quota-hv-watcher"). WatchesRawSource(source.Kind( @@ -726,6 +732,7 @@ func (c *QuotaController) computeTotalUsage( ramResourceName := commitments.ResourceNameRAM(groupName) coresResourceName := commitments.ResourceNameCores(groupName) + instancesResourceName := commitments.ResourceNameInstances(groupName) ramUnits, coresAmount := vmResourceUnits(vm.Resources) @@ -748,6 +755,14 @@ func (c *QuotaController) computeTotalUsage( } coresUsage[vm.AvailabilityZone] += coresAmount result[vm.ProjectID][coresResourceName] = coresUsage + + // Accumulate instances usage for this project + AZ (1 per VM) + instancesUsage := result[vm.ProjectID][instancesResourceName] + if instancesUsage == nil { + instancesUsage = make(map[string]int64) + } + instancesUsage[vm.AvailabilityZone]++ + result[vm.ProjectID][instancesResourceName] = instancesUsage } return result diff --git a/internal/scheduling/reservations/quota/controller_test.go b/internal/scheduling/reservations/quota/controller_test.go index b5b724647..2becea2db 100644 --- a/internal/scheduling/reservations/quota/controller_test.go +++ b/internal/scheduling/reservations/quota/controller_test.go @@ -93,8 +93,8 @@ func TestComputeTotalUsage(t *testing.T) { result := ctrl.computeTotalUsage(vms, flavorToGroup, flavorGroups) - // project-a: hana_v2 in az-1: (32768+65536)/1024 = 96 GiB RAM, 8+16=24 cores - // project-a: hana_v2 in az-2: 32768/1024 = 32 GiB RAM, 8 cores + // project-a: hana_v2 in az-1: (32768+65536)/1024 = 96 GiB RAM, 8+16=24 cores, 2 instances + // project-a: hana_v2 in az-2: 32768/1024 = 32 GiB RAM, 8 cores, 1 instance projectA := result["project-a"] if projectA == nil { t.Fatal("expected project-a in results") @@ -116,7 +116,15 @@ func TestComputeTotalUsage(t *testing.T) { t.Errorf("expected project-a az-2 hana_v2_cores = 8, got %d", coresUsage["az-2"]) } - // project-b: general in az-1: 4096/1024 = 4 GiB RAM, 2 cores + instancesUsage := projectA["hw_version_hana_v2_instances"] + if instancesUsage["az-1"] != 2 { + t.Errorf("expected project-a az-1 hana_v2_instances = 2, got %d", instancesUsage["az-1"]) + } + if instancesUsage["az-2"] != 1 { + t.Errorf("expected project-a az-2 hana_v2_instances = 1, got %d", instancesUsage["az-2"]) + } + + // project-b: general in az-1: 4096/1024 = 4 GiB RAM, 2 cores, 1 instance projectB := result["project-b"] if projectB == nil { t.Fatal("expected project-b in results") @@ -127,6 +135,9 @@ func TestComputeTotalUsage(t *testing.T) { if projectB["hw_version_general_cores"]["az-1"] != 2 { t.Errorf("expected project-b az-1 general_cores = 2, got %d", projectB["hw_version_general_cores"]["az-1"]) } + if projectB["hw_version_general_instances"]["az-1"] != 1 { + t.Errorf("expected project-b az-1 general_instances = 1, got %d", projectB["hw_version_general_instances"]["az-1"]) + } // project-c: unknown flavor → not in results if _, exists := result["project-c"]; exists { @@ -560,6 +571,9 @@ func TestAccumulateAddedVM_KnownFlavor(t *testing.T) { if delta.increments["hw_version_hana_v2_cores"]["az-1"] != 8 { t.Errorf("expected cores increment = 8, got %d", delta.increments["hw_version_hana_v2_cores"]["az-1"]) } + if delta.increments["hw_version_hana_v2_instances"]["az-1"] != 1 { + t.Errorf("expected instances increment = 1, got %d", delta.increments["hw_version_hana_v2_instances"]["az-1"]) + } } // mockVMSource is a test helper for VMSource. diff --git a/internal/scheduling/reservations/quota/integration_test.go b/internal/scheduling/reservations/quota/integration_test.go index dbe174f69..d960ab396 100644 --- a/internal/scheduling/reservations/quota/integration_test.go +++ b/internal/scheduling/reservations/quota/integration_test.go @@ -47,27 +47,33 @@ func TestIntegration(t *testing.T) { // project-b: general az-1: 4096/1024 = 4 GiB, 2 cores ExpectedTotalUsage: map[string]map[string]map[string]int64{ "project-a": { - "hw_version_hana_v2_ram": {"az-1": 96, "az-2": 32}, - "hw_version_hana_v2_cores": {"az-1": 24, "az-2": 8}, - "hw_version_general_ram": {"az-1": 4}, - "hw_version_general_cores": {"az-1": 2}, + "hw_version_hana_v2_ram": {"az-1": 96, "az-2": 32}, + "hw_version_hana_v2_cores": {"az-1": 24, "az-2": 8}, + "hw_version_hana_v2_instances": {"az-1": 2, "az-2": 1}, + "hw_version_general_ram": {"az-1": 4}, + "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_instances": {"az-1": 1}, }, "project-b": { - "hw_version_general_ram": {"az-1": 4}, - "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_ram": {"az-1": 4}, + "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_instances": {"az-1": 1}, }, }, // No CRs -> PaygUsage == TotalUsage ExpectedPaygUsage: map[string]map[string]map[string]int64{ "project-a": { - "hw_version_hana_v2_ram": {"az-1": 96, "az-2": 32}, - "hw_version_hana_v2_cores": {"az-1": 24, "az-2": 8}, - "hw_version_general_ram": {"az-1": 4}, - "hw_version_general_cores": {"az-1": 2}, + "hw_version_hana_v2_ram": {"az-1": 96, "az-2": 32}, + "hw_version_hana_v2_cores": {"az-1": 24, "az-2": 8}, + "hw_version_hana_v2_instances": {"az-1": 2, "az-2": 1}, + "hw_version_general_ram": {"az-1": 4}, + "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_instances": {"az-1": 1}, }, "project-b": { - "hw_version_general_ram": {"az-1": 4}, - "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_ram": {"az-1": 4}, + "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_instances": {"az-1": 1}, }, }, }, @@ -94,10 +100,12 @@ func TestIntegration(t *testing.T) { Type: "full_reconcile", ExpectedTotalUsage: map[string]map[string]map[string]int64{ "project-a": { - "hw_version_hana_v2_ram": {"az-1": 96, "az-2": 32}, - "hw_version_hana_v2_cores": {"az-1": 24, "az-2": 8}, - "hw_version_general_ram": {"az-1": 4}, - "hw_version_general_cores": {"az-1": 2}, + "hw_version_hana_v2_ram": {"az-1": 96, "az-2": 32}, + "hw_version_hana_v2_cores": {"az-1": 24, "az-2": 8}, + "hw_version_hana_v2_instances": {"az-1": 2, "az-2": 1}, + "hw_version_general_ram": {"az-1": 4}, + "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_instances": {"az-1": 1}, }, }, // PaygUsage = TotalUsage - CRUsage @@ -106,10 +114,12 @@ func TestIntegration(t *testing.T) { // general: no CRs so PaygUsage == TotalUsage ExpectedPaygUsage: map[string]map[string]map[string]int64{ "project-a": { - "hw_version_hana_v2_ram": {"az-1": 94, "az-2": 32}, - "hw_version_hana_v2_cores": {"az-1": 14, "az-2": 8}, - "hw_version_general_ram": {"az-1": 4}, - "hw_version_general_cores": {"az-1": 2}, + "hw_version_hana_v2_ram": {"az-1": 94, "az-2": 32}, + "hw_version_hana_v2_cores": {"az-1": 14, "az-2": 8}, + "hw_version_hana_v2_instances": {"az-1": 2, "az-2": 1}, + "hw_version_general_ram": {"az-1": 4}, + "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_instances": {"az-1": 1}, }, }, }, @@ -129,10 +139,12 @@ func TestIntegration(t *testing.T) { Type: "full_reconcile", ExpectedTotalUsage: map[string]map[string]map[string]int64{ "project-a": { - "hw_version_hana_v2_ram": {"az-1": 96, "az-2": 32}, - "hw_version_hana_v2_cores": {"az-1": 24, "az-2": 8}, - "hw_version_general_ram": {"az-1": 4}, - "hw_version_general_cores": {"az-1": 2}, + "hw_version_hana_v2_ram": {"az-1": 96, "az-2": 32}, + "hw_version_hana_v2_cores": {"az-1": 24, "az-2": 8}, + "hw_version_hana_v2_instances": {"az-1": 2, "az-2": 1}, + "hw_version_general_ram": {"az-1": 4}, + "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_instances": {"az-1": 1}, }, }, }, @@ -163,10 +175,12 @@ func TestIntegration(t *testing.T) { // +32 GiB RAM (32768/1024), +8 cores ExpectedTotalUsage: map[string]map[string]map[string]int64{ "project-a": { - "hw_version_hana_v2_ram": {"az-1": 128, "az-2": 32}, - "hw_version_hana_v2_cores": {"az-1": 32, "az-2": 8}, - "hw_version_general_ram": {"az-1": 4}, - "hw_version_general_cores": {"az-1": 2}, + "hw_version_hana_v2_ram": {"az-1": 128, "az-2": 32}, + "hw_version_hana_v2_cores": {"az-1": 32, "az-2": 8}, + "hw_version_hana_v2_instances": {"az-1": 3, "az-2": 1}, + "hw_version_general_ram": {"az-1": 4}, + "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_instances": {"az-1": 1}, }, }, }, @@ -186,10 +200,12 @@ func TestIntegration(t *testing.T) { Type: "full_reconcile", ExpectedTotalUsage: map[string]map[string]map[string]int64{ "project-a": { - "hw_version_hana_v2_ram": {"az-1": 96, "az-2": 32}, - "hw_version_hana_v2_cores": {"az-1": 24, "az-2": 8}, - "hw_version_general_ram": {"az-1": 4}, - "hw_version_general_cores": {"az-1": 2}, + "hw_version_hana_v2_ram": {"az-1": 96, "az-2": 32}, + "hw_version_hana_v2_cores": {"az-1": 24, "az-2": 8}, + "hw_version_hana_v2_instances": {"az-1": 2, "az-2": 1}, + "hw_version_general_ram": {"az-1": 4}, + "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_instances": {"az-1": 1}, }, }, }, @@ -203,10 +219,12 @@ func TestIntegration(t *testing.T) { // Should NOT increment -- vm-1 CreatedAt is 2025-12-01 which is before reconcile time ExpectedTotalUsage: map[string]map[string]map[string]int64{ "project-a": { - "hw_version_hana_v2_ram": {"az-1": 96, "az-2": 32}, - "hw_version_hana_v2_cores": {"az-1": 24, "az-2": 8}, - "hw_version_general_ram": {"az-1": 4}, - "hw_version_general_cores": {"az-1": 2}, + "hw_version_hana_v2_ram": {"az-1": 96, "az-2": 32}, + "hw_version_hana_v2_cores": {"az-1": 24, "az-2": 8}, + "hw_version_hana_v2_instances": {"az-1": 2, "az-2": 1}, + "hw_version_general_ram": {"az-1": 4}, + "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_instances": {"az-1": 1}, }, }, }, @@ -239,10 +257,12 @@ func TestIntegration(t *testing.T) { Type: "full_reconcile", ExpectedTotalUsage: map[string]map[string]map[string]int64{ "project-a": { - "hw_version_hana_v2_ram": {"az-1": 96, "az-2": 32}, - "hw_version_hana_v2_cores": {"az-1": 24, "az-2": 8}, - "hw_version_general_ram": {"az-1": 4}, - "hw_version_general_cores": {"az-1": 2}, + "hw_version_hana_v2_ram": {"az-1": 96, "az-2": 32}, + "hw_version_hana_v2_cores": {"az-1": 24, "az-2": 8}, + "hw_version_hana_v2_instances": {"az-1": 2, "az-2": 1}, + "hw_version_general_ram": {"az-1": 4}, + "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_instances": {"az-1": 1}, }, }, }, @@ -263,10 +283,12 @@ func TestIntegration(t *testing.T) { // Decrement: -32 GiB RAM, -8 cores in az-1 ExpectedTotalUsage: map[string]map[string]map[string]int64{ "project-a": { - "hw_version_hana_v2_ram": {"az-1": 64, "az-2": 32}, - "hw_version_hana_v2_cores": {"az-1": 16, "az-2": 8}, - "hw_version_general_ram": {"az-1": 4}, - "hw_version_general_cores": {"az-1": 2}, + "hw_version_hana_v2_ram": {"az-1": 64, "az-2": 32}, + "hw_version_hana_v2_cores": {"az-1": 16, "az-2": 8}, + "hw_version_hana_v2_instances": {"az-1": 1, "az-2": 1}, + "hw_version_general_ram": {"az-1": 4}, + "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_instances": {"az-1": 1}, }, }, }, @@ -288,10 +310,12 @@ func TestIntegration(t *testing.T) { Type: "full_reconcile", ExpectedTotalUsage: map[string]map[string]map[string]int64{ "project-a": { - "hw_version_hana_v2_ram": {"az-1": 96, "az-2": 32}, - "hw_version_hana_v2_cores": {"az-1": 24, "az-2": 8}, - "hw_version_general_ram": {"az-1": 4}, - "hw_version_general_cores": {"az-1": 2}, + "hw_version_hana_v2_ram": {"az-1": 96, "az-2": 32}, + "hw_version_hana_v2_cores": {"az-1": 24, "az-2": 8}, + "hw_version_hana_v2_instances": {"az-1": 2, "az-2": 1}, + "hw_version_general_ram": {"az-1": 4}, + "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_instances": {"az-1": 1}, }, }, }, @@ -309,10 +333,12 @@ func TestIntegration(t *testing.T) { // vm-1: IsServerActive=true, so NOT decremented ExpectedTotalUsage: map[string]map[string]map[string]int64{ "project-a": { - "hw_version_hana_v2_ram": {"az-1": 96, "az-2": 32}, - "hw_version_hana_v2_cores": {"az-1": 24, "az-2": 8}, - "hw_version_general_ram": {"az-1": 4}, - "hw_version_general_cores": {"az-1": 2}, + "hw_version_hana_v2_ram": {"az-1": 96, "az-2": 32}, + "hw_version_hana_v2_cores": {"az-1": 24, "az-2": 8}, + "hw_version_hana_v2_instances": {"az-1": 2, "az-2": 1}, + "hw_version_general_ram": {"az-1": 4}, + "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_instances": {"az-1": 1}, }, }, }, @@ -336,10 +362,12 @@ func TestIntegration(t *testing.T) { Type: "full_reconcile", ExpectedPaygUsage: map[string]map[string]map[string]int64{ "project-a": { - "hw_version_hana_v2_ram": {"az-1": 95, "az-2": 32}, // 96-1=95 - "hw_version_hana_v2_cores": {"az-1": 24, "az-2": 8}, - "hw_version_general_ram": {"az-1": 4}, - "hw_version_general_cores": {"az-1": 2}, + "hw_version_hana_v2_ram": {"az-1": 95, "az-2": 32}, // 96-1=95 + "hw_version_hana_v2_cores": {"az-1": 24, "az-2": 8}, + "hw_version_hana_v2_instances": {"az-1": 2, "az-2": 1}, + "hw_version_general_ram": {"az-1": 4}, + "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_instances": {"az-1": 1}, }, }, }, @@ -350,10 +378,12 @@ func TestIntegration(t *testing.T) { UsedAmount: 3, ExpectedPaygUsage: map[string]map[string]map[string]int64{ "project-a": { - "hw_version_hana_v2_ram": {"az-1": 93, "az-2": 32}, // 96-3=93 - "hw_version_hana_v2_cores": {"az-1": 24, "az-2": 8}, - "hw_version_general_ram": {"az-1": 4}, - "hw_version_general_cores": {"az-1": 2}, + "hw_version_hana_v2_ram": {"az-1": 93, "az-2": 32}, // 96-3=93 + "hw_version_hana_v2_cores": {"az-1": 24, "az-2": 8}, + "hw_version_hana_v2_instances": {"az-1": 2, "az-2": 1}, + "hw_version_general_ram": {"az-1": 4}, + "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_instances": {"az-1": 1}, }, }, }, @@ -402,14 +432,17 @@ func TestIntegration(t *testing.T) { Type: "full_reconcile", ExpectedTotalUsage: map[string]map[string]map[string]int64{ "project-a": { - "hw_version_hana_v2_ram": {"az-1": 96, "az-2": 32}, - "hw_version_hana_v2_cores": {"az-1": 24, "az-2": 8}, - "hw_version_general_ram": {"az-1": 4}, - "hw_version_general_cores": {"az-1": 2}, + "hw_version_hana_v2_ram": {"az-1": 96, "az-2": 32}, + "hw_version_hana_v2_cores": {"az-1": 24, "az-2": 8}, + "hw_version_hana_v2_instances": {"az-1": 2, "az-2": 1}, + "hw_version_general_ram": {"az-1": 4}, + "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_instances": {"az-1": 1}, }, "project-b": { - "hw_version_general_ram": {"az-1": 4}, - "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_ram": {"az-1": 4}, + "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_instances": {"az-1": 1}, }, }, }, @@ -418,14 +451,17 @@ func TestIntegration(t *testing.T) { Type: "full_reconcile", ExpectedTotalUsage: map[string]map[string]map[string]int64{ "project-a": { - "hw_version_hana_v2_ram": {"az-1": 96, "az-2": 32}, - "hw_version_hana_v2_cores": {"az-1": 24, "az-2": 8}, - "hw_version_general_ram": {"az-1": 4}, - "hw_version_general_cores": {"az-1": 2}, + "hw_version_hana_v2_ram": {"az-1": 96, "az-2": 32}, + "hw_version_hana_v2_cores": {"az-1": 24, "az-2": 8}, + "hw_version_hana_v2_instances": {"az-1": 2, "az-2": 1}, + "hw_version_general_ram": {"az-1": 4}, + "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_instances": {"az-1": 1}, }, "project-b": { - "hw_version_general_ram": {"az-1": 4}, - "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_ram": {"az-1": 4}, + "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_instances": {"az-1": 1}, }, }, }, @@ -450,10 +486,12 @@ func TestIntegration(t *testing.T) { // PaygUsage == TotalUsage because pending CRs are excluded ExpectedPaygUsage: map[string]map[string]map[string]int64{ "project-a": { - "hw_version_hana_v2_ram": {"az-1": 96, "az-2": 32}, - "hw_version_hana_v2_cores": {"az-1": 24, "az-2": 8}, - "hw_version_general_ram": {"az-1": 4}, - "hw_version_general_cores": {"az-1": 2}, + "hw_version_hana_v2_ram": {"az-1": 96, "az-2": 32}, + "hw_version_hana_v2_cores": {"az-1": 24, "az-2": 8}, + "hw_version_hana_v2_instances": {"az-1": 2, "az-2": 1}, + "hw_version_general_ram": {"az-1": 4}, + "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_instances": {"az-1": 1}, }, }, }, @@ -473,10 +511,12 @@ func TestIntegration(t *testing.T) { Type: "full_reconcile", ExpectedTotalUsage: map[string]map[string]map[string]int64{ "project-a": { - "hw_version_hana_v2_ram": {"az-1": 96, "az-2": 32}, - "hw_version_hana_v2_cores": {"az-1": 24, "az-2": 8}, - "hw_version_general_ram": {"az-1": 4}, - "hw_version_general_cores": {"az-1": 2}, + "hw_version_hana_v2_ram": {"az-1": 96, "az-2": 32}, + "hw_version_hana_v2_cores": {"az-1": 24, "az-2": 8}, + "hw_version_hana_v2_instances": {"az-1": 2, "az-2": 1}, + "hw_version_general_ram": {"az-1": 4}, + "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_instances": {"az-1": 1}, }, }, }, @@ -508,10 +548,12 @@ func TestIntegration(t *testing.T) { // TotalUsage now has phantom's contribution (drift) ExpectedTotalUsage: map[string]map[string]map[string]int64{ "project-a": { - "hw_version_hana_v2_ram": {"az-1": 128, "az-2": 32}, // 96+32 drift - "hw_version_hana_v2_cores": {"az-1": 32, "az-2": 8}, // 24+8 drift - "hw_version_general_ram": {"az-1": 4}, - "hw_version_general_cores": {"az-1": 2}, + "hw_version_hana_v2_ram": {"az-1": 128, "az-2": 32}, // 96+32 drift + "hw_version_hana_v2_cores": {"az-1": 32, "az-2": 8}, // 24+8 drift + "hw_version_hana_v2_instances": {"az-1": 3, "az-2": 1}, + "hw_version_general_ram": {"az-1": 4}, + "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_instances": {"az-1": 1}, }, }, }, @@ -522,10 +564,12 @@ func TestIntegration(t *testing.T) { OverrideVMs: baseVMsPtr(), ExpectedTotalUsage: map[string]map[string]map[string]int64{ "project-a": { - "hw_version_hana_v2_ram": {"az-1": 96, "az-2": 32}, // corrected - "hw_version_hana_v2_cores": {"az-1": 24, "az-2": 8}, // corrected - "hw_version_general_ram": {"az-1": 4}, - "hw_version_general_cores": {"az-1": 2}, + "hw_version_hana_v2_ram": {"az-1": 96, "az-2": 32}, // corrected + "hw_version_hana_v2_cores": {"az-1": 24, "az-2": 8}, // corrected + "hw_version_hana_v2_instances": {"az-1": 2, "az-2": 1}, + "hw_version_general_ram": {"az-1": 4}, + "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_instances": {"az-1": 1}, }, }, }, @@ -561,14 +605,17 @@ func TestIntegration(t *testing.T) { Type: "full_reconcile", ExpectedTotalUsage: map[string]map[string]map[string]int64{ "project-a": { - "hw_version_hana_v2_ram": {"az-1": 96, "az-2": 32}, - "hw_version_hana_v2_cores": {"az-1": 24, "az-2": 8}, - "hw_version_general_ram": {"az-1": 4}, - "hw_version_general_cores": {"az-1": 2}, + "hw_version_hana_v2_ram": {"az-1": 96, "az-2": 32}, + "hw_version_hana_v2_cores": {"az-1": 24, "az-2": 8}, + "hw_version_hana_v2_instances": {"az-1": 2, "az-2": 1}, + "hw_version_general_ram": {"az-1": 4}, + "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_instances": {"az-1": 1}, }, "project-b": { - "hw_version_general_ram": {"az-1": 4}, - "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_ram": {"az-1": 4}, + "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_instances": {"az-1": 1}, }, }, }, @@ -598,10 +645,12 @@ func TestIntegration(t *testing.T) { ), ExpectedTotalUsage: map[string]map[string]map[string]int64{ "project-a": { - "hw_version_hana_v2_ram": {"az-1": 128, "az-2": 32}, - "hw_version_hana_v2_cores": {"az-1": 32, "az-2": 8}, - "hw_version_general_ram": {"az-1": 4}, - "hw_version_general_cores": {"az-1": 2}, + "hw_version_hana_v2_ram": {"az-1": 128, "az-2": 32}, + "hw_version_hana_v2_cores": {"az-1": 32, "az-2": 8}, + "hw_version_hana_v2_instances": {"az-1": 3, "az-2": 1}, + "hw_version_general_ram": {"az-1": 4}, + "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_instances": {"az-1": 1}, }, }, }, @@ -629,8 +678,9 @@ func TestIntegration(t *testing.T) { ), ExpectedTotalUsage: map[string]map[string]map[string]int64{ "project-b": { - "hw_version_general_ram": {"az-1": 8}, // 4+4 drift - "hw_version_general_cores": {"az-1": 4}, // 2+2 drift + "hw_version_general_ram": {"az-1": 8}, // 4+4 drift + "hw_version_general_cores": {"az-1": 4}, // 2+2 drift + "hw_version_general_instances": {"az-1": 2}, // 1+1 drift }, }, }, @@ -663,10 +713,12 @@ func TestIntegration(t *testing.T) { ), ExpectedTotalUsage: map[string]map[string]map[string]int64{ "project-a": { - "hw_version_hana_v2_ram": {"az-1": 96, "az-2": 32}, // 128-32=96 - "hw_version_hana_v2_cores": {"az-1": 24, "az-2": 8}, // 32-8=24 - "hw_version_general_ram": {"az-1": 4}, - "hw_version_general_cores": {"az-1": 2}, + "hw_version_hana_v2_ram": {"az-1": 96, "az-2": 32}, // 128-32=96 + "hw_version_hana_v2_cores": {"az-1": 24, "az-2": 8}, // 32-8=24 + "hw_version_hana_v2_instances": {"az-1": 2, "az-2": 1}, + "hw_version_general_ram": {"az-1": 4}, + "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_instances": {"az-1": 1}, }, }, }, @@ -692,14 +744,17 @@ func TestIntegration(t *testing.T) { }, ExpectedTotalUsage: map[string]map[string]map[string]int64{ "project-a": { - "hw_version_hana_v2_ram": {"az-1": 128, "az-2": 32}, // corrected up - "hw_version_hana_v2_cores": {"az-1": 32, "az-2": 8}, // corrected up - "hw_version_general_ram": {"az-1": 4}, - "hw_version_general_cores": {"az-1": 2}, + "hw_version_hana_v2_ram": {"az-1": 128, "az-2": 32}, // corrected up + "hw_version_hana_v2_cores": {"az-1": 32, "az-2": 8}, // corrected up + "hw_version_hana_v2_instances": {"az-1": 3, "az-2": 1}, + "hw_version_general_ram": {"az-1": 4}, + "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_instances": {"az-1": 1}, }, "project-b": { - "hw_version_general_ram": {"az-1": 4}, // corrected down - "hw_version_general_cores": {"az-1": 2}, // corrected down + "hw_version_general_ram": {"az-1": 4}, // corrected down + "hw_version_general_cores": {"az-1": 2}, // corrected down + "hw_version_general_instances": {"az-1": 1}, }, }, }, @@ -730,10 +785,12 @@ func TestIntegration(t *testing.T) { // vm-1 migrated, NOT decremented -- totals unchanged ExpectedTotalUsage: map[string]map[string]map[string]int64{ "project-a": { - "hw_version_hana_v2_ram": {"az-1": 128, "az-2": 32}, - "hw_version_hana_v2_cores": {"az-1": 32, "az-2": 8}, - "hw_version_general_ram": {"az-1": 4}, - "hw_version_general_cores": {"az-1": 2}, + "hw_version_hana_v2_ram": {"az-1": 128, "az-2": 32}, + "hw_version_hana_v2_cores": {"az-1": 32, "az-2": 8}, + "hw_version_hana_v2_instances": {"az-1": 3, "az-2": 1}, + "hw_version_general_ram": {"az-1": 4}, + "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_instances": {"az-1": 1}, }, }, }, @@ -743,14 +800,17 @@ func TestIntegration(t *testing.T) { Type: "full_reconcile", ExpectedTotalUsage: map[string]map[string]map[string]int64{ "project-a": { - "hw_version_hana_v2_ram": {"az-1": 128, "az-2": 32}, - "hw_version_hana_v2_cores": {"az-1": 32, "az-2": 8}, - "hw_version_general_ram": {"az-1": 4}, - "hw_version_general_cores": {"az-1": 2}, + "hw_version_hana_v2_ram": {"az-1": 128, "az-2": 32}, + "hw_version_hana_v2_cores": {"az-1": 32, "az-2": 8}, + "hw_version_hana_v2_instances": {"az-1": 3, "az-2": 1}, + "hw_version_general_ram": {"az-1": 4}, + "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_instances": {"az-1": 1}, }, "project-b": { - "hw_version_general_ram": {"az-1": 4}, - "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_ram": {"az-1": 4}, + "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_instances": {"az-1": 1}, }, }, }, @@ -770,18 +830,22 @@ func TestIntegration(t *testing.T) { // Only az-1 data should be written (az-2 CRD doesn't exist) ExpectedTotalUsage: map[string]map[string]map[string]int64{ "project-a": { - "hw_version_hana_v2_ram": {"az-1": 96}, - "hw_version_hana_v2_cores": {"az-1": 24}, - "hw_version_general_ram": {"az-1": 4}, - "hw_version_general_cores": {"az-1": 2}, + "hw_version_hana_v2_ram": {"az-1": 96}, + "hw_version_hana_v2_cores": {"az-1": 24}, + "hw_version_hana_v2_instances": {"az-1": 2}, + "hw_version_general_ram": {"az-1": 4}, + "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_instances": {"az-1": 1}, }, }, ExpectedPaygUsage: map[string]map[string]map[string]int64{ "project-a": { - "hw_version_hana_v2_ram": {"az-1": 96}, - "hw_version_hana_v2_cores": {"az-1": 24}, - "hw_version_general_ram": {"az-1": 4}, - "hw_version_general_cores": {"az-1": 2}, + "hw_version_hana_v2_ram": {"az-1": 96}, + "hw_version_hana_v2_cores": {"az-1": 24}, + "hw_version_hana_v2_instances": {"az-1": 2}, + "hw_version_general_ram": {"az-1": 4}, + "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_instances": {"az-1": 1}, }, }, }, @@ -812,14 +876,17 @@ func TestIntegration(t *testing.T) { // Verify TotalUsage is correctly computed from VMs ExpectedTotalUsage: map[string]map[string]map[string]int64{ "project-a": { - "hw_version_hana_v2_ram": {"az-1": 96, "az-2": 32}, - "hw_version_hana_v2_cores": {"az-1": 24, "az-2": 8}, - "hw_version_general_ram": {"az-1": 4}, - "hw_version_general_cores": {"az-1": 2}, + "hw_version_hana_v2_ram": {"az-1": 96, "az-2": 32}, + "hw_version_hana_v2_cores": {"az-1": 24, "az-2": 8}, + "hw_version_hana_v2_instances": {"az-1": 2, "az-2": 1}, + "hw_version_general_ram": {"az-1": 4}, + "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_instances": {"az-1": 1}, }, "project-b": { - "hw_version_general_ram": {"az-1": 4}, - "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_ram": {"az-1": 4}, + "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_instances": {"az-1": 1}, }, }, // Verify PaygUsage = TotalUsage - CRUsage per AZ @@ -827,14 +894,17 @@ func TestIntegration(t *testing.T) { // az-2: hana_v2_ram: 32-3=29, hana_v2_cores: 8-0=8 ExpectedPaygUsage: map[string]map[string]map[string]int64{ "project-a": { - "hw_version_hana_v2_ram": {"az-1": 91, "az-2": 29}, - "hw_version_hana_v2_cores": {"az-1": 20, "az-2": 8}, - "hw_version_general_ram": {"az-1": 4}, - "hw_version_general_cores": {"az-1": 2}, + "hw_version_hana_v2_ram": {"az-1": 91, "az-2": 29}, + "hw_version_hana_v2_cores": {"az-1": 20, "az-2": 8}, + "hw_version_hana_v2_instances": {"az-1": 2, "az-2": 1}, + "hw_version_general_ram": {"az-1": 4}, + "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_instances": {"az-1": 1}, }, "project-b": { - "hw_version_general_ram": {"az-1": 4}, - "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_ram": {"az-1": 4}, + "hw_version_general_cores": {"az-1": 2}, + "hw_version_general_instances": {"az-1": 1}, }, }, },