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
174 changes: 174 additions & 0 deletions api/v1alpha1/committed_resource_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// Copyright SAP SE
// SPDX-License-Identifier: Apache-2.0

package v1alpha1

import (
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// Only confirmed and guaranteed commitments result in active Reservation slots.
type CommitmentStatus string

const (
// CommitmentStatusPlanned: StartTime not yet reached; no resources guaranteed, no Reservation slots yet.
CommitmentStatusPlanned CommitmentStatus = "planned"
// CommitmentStatusPending: StartTime reached; no resources guaranteed, no Reservation slots yet.
CommitmentStatusPending CommitmentStatus = "pending"
// CommitmentStatusGuaranteed: StartTime not reached yet; resources are guaranteed latest starting from StartTime, Reservation slots in sync.
CommitmentStatusGuaranteed CommitmentStatus = "guaranteed"
// CommitmentStatusConfirmed: StartTime reached; resources are guaranteed, Reservation slots in sync.
CommitmentStatusConfirmed CommitmentStatus = "confirmed"
// CommitmentStatusSuperseded: replaced by another commitment; no resources guaranteed, Reservation slots removed.
CommitmentStatusSuperseded CommitmentStatus = "superseded"
// CommitmentStatusExpired: past EndTime; no resources guaranteed, Reservation slots removed.
CommitmentStatusExpired CommitmentStatus = "expired"
)

// CommittedResourceType identifies the kind of resource a commitment covers.
type CommittedResourceType string

const (
// CommittedResourceTypeMemory: RAM commitment; drives flavor-based Reservation slot creation.
CommittedResourceTypeMemory CommittedResourceType = "memory"
// CommittedResourceTypeCores: CPU core commitment; verified arithmetically, no Reservation slots created.
CommittedResourceTypeCores CommittedResourceType = "cores"
)

// CommittedResourceSpec defines the desired state of CommittedResource,
type CommittedResourceSpec struct {
// UUID of the commitment this resource corresponds to.
// +kubebuilder:validation:Required
CommitmentUUID string `json:"commitmentUUID"`

// SchedulingDomain specifies the scheduling domain for this committed resource (e.g., "nova", "ironcore").
// +kubebuilder:validation:Optional
SchedulingDomain SchedulingDomain `json:"schedulingDomain,omitempty"`

// FlavorGroupName identifies the flavor group this commitment targets, e.g. "kvm_v2_hana_s".
// +kubebuilder:validation:Required
FlavorGroupName string `json:"flavorGroupName"`

// ResourceType identifies the kind of resource committed: memory drives Reservation slots; cores uses an arithmetic check only.
// +kubebuilder:validation:Enum=memory;cores
// +kubebuilder:validation:Required
ResourceType CommittedResourceType `json:"resourceType"`

// Amount is the total committed quantity.
// memory: MiB expressed in K8s binary SI notation (e.g. "1280Gi", "640Mi").
// cores: integer core count (e.g. "40").
// +kubebuilder:validation:Required
Amount resource.Quantity `json:"amount"`
Comment on lines +58 to +62
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we not work in multiples of smallest flavor here?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CRD should be self contained, so not depending implicitly on e.g. flavor group definition, also considering that we were told the groups are likely to change afterwards. So the Liquid API will do the translation and writes the raw amount here

Comment on lines +58 to +62
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, does this mean that we would create multiple committed resources for one reservation so we can keep track of both cpu cores and RAM?

I would have expected something more like this, but maybe I am off-track here:

	// Example:
	// cpu: "40"
	// memory: "1280Gi"
	// +kubebuilder:validation:Required
	Resources corev1.ResourceList `json:"resources"`

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With Limes we have always one resource per commitment. So we have e.g. two commitment for the same project, when the user wants to specify memory and cpu for a GP group. Still our controller can read both related CR CRDs to do more advanced flavor selection.. considering the CPU cores the user has in mind while choosing the right flavor sizes for the reservations of the memory cr crd.
I also thought about using a ResourceList with always using a single resource in there.. but it feels like doing optimization for a use case we dont have atm.

Comment on lines +58 to +62
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect generated CRDs and consumers for amount validation.
# Expected: Either the generated CommittedResource CRD has amount positivity/type validation,
# or Go consumers reject non-positive amounts before using Spec.Amount.Value().

set -euo pipefail

echo "Generated CRD snippets containing amount validation:"
fd -i 'committedresource|crd|cortex' --extension yaml --extension yml --extension json \
  --exec rg -n -C5 'amount:|x-kubernetes-validations|minimum|pattern' {}

echo
echo "Go consumers of Spec.Amount:"
rg -n -C5 --type=go '\.Spec\.Amount|Amount\.Value\(|Amount\.Sign\(|Amount\.MilliValue\('

Repository: cobaltcore-dev/cortex

Length of output: 21606


🏁 Script executed:

find . -name "*committed*" -type f | grep -E "\.(go|yaml|yml)$"

Repository: cobaltcore-dev/cortex

Length of output: 107


🏁 Script executed:

# Check the committed_resource_types.go file for kubebuilder validation markers
cat -n api/v1alpha1/committed_resource_types.go | head -100

Repository: cobaltcore-dev/cortex

Length of output: 5270


🏁 Script executed:

# Look for the consumer code
cat -n internal/scheduling/reservations/commitments/state.go | sed -n '200,250p'

Repository: cobaltcore-dev/cortex

Length of output: 2122


🏁 Script executed:

# Search for admission webhooks and validation rules
rg -n "Amount|validation.*positive|minimum.*Amount" --type=go | grep -i "commit\|valid\|webhook"

Repository: cobaltcore-dev/cortex

Length of output: 8691


🏁 Script executed:

# Check if there are any validation webhook implementations
fd -type f -name "*.go" | xargs rg -l "ValidatingWebhook|Validate" | head -20

Repository: cobaltcore-dev/cortex

Length of output: 1333


🏁 Script executed:

# Search for kubebuilder CEL validation rules (x-kubernetes-validations)
rg -n "x-kubernetes-validations|kubebuilder:validation:Minimum" api/

Repository: cobaltcore-dev/cortex

Length of output: 47


Add validation to reject non-positive committed amounts.

Amount currently accepts zero, negative quantities (e.g., -1Gi), and the CRD has no validation rules enforcing positivity. The consumer in internal/scheduling/reservations/commitments/state.go:226 forwards cr.Spec.Amount.Value() to CommitmentState.TotalMemoryBytes without validation, allowing invalid amounts to reach scheduling state.

Add +kubebuilder:validation:Minimum=1 or a CEL validation rule to the Amount field to reject non-positive values at the API boundary, or validate in the consumer before constructing CommitmentState.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/v1alpha1/committed_resource_types.go` around lines 57 - 61, The Amount
field on the CRD currently allows zero/negative quantities; add API-level
validation to reject non-positive values by annotating the Amount field
(resource.Quantity `Amount`) with a kubebuilder validation such as
+kubebuilder:validation:Minimum=1 or a CEL rule enforcing >0, and/or add a
defensive check in the consumer path (the code that reads cr.Spec.Amount.Value()
in internal/scheduling/reservations/commitments/state.go before calling
CommitmentState.TotalMemoryBytes) to return an error if Amount.Value() <= 0 so
invalid CRs cannot reach scheduling state.


// AvailabilityZone specifies the availability zone for this commitment.
// +kubebuilder:validation:Required
AvailabilityZone string `json:"availabilityZone"`

// ProjectID of the OpenStack project this commitment belongs to.
// +kubebuilder:validation:Required
ProjectID string `json:"projectID"`

// DomainID of the OpenStack domain this commitment belongs to.
// +kubebuilder:validation:Required
DomainID string `json:"domainID"`

// StartTime is the activation time for Reservation slots.
// Nil for guaranteed commitments (slots are active from creation); set to ConfirmedAt for confirmed ones.
// +kubebuilder:validation:Optional
StartTime *metav1.Time `json:"startTime,omitempty"`

// EndTime is when Reservation slots expire. Nil for unbounded commitments with no expiry.
// +kubebuilder:validation:Optional
EndTime *metav1.Time `json:"endTime,omitempty"`

// ConfirmedAt is when the commitment was confirmed.
// +kubebuilder:validation:Optional
ConfirmedAt *metav1.Time `json:"confirmedAt,omitempty"`

// State is the lifecycle state of the commitment.
// +kubebuilder:validation:Enum=planned;pending;guaranteed;confirmed;superseded;expired
// +kubebuilder:validation:Required
State CommitmentStatus `json:"state"`
Comment on lines +89 to +92
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check whether all FromCommittedResource call sites filter Spec.State.
# Expected: Each call path should restrict processing to CommitmentStatusGuaranteed/Confirmed
# before active CommitmentState objects are produced.

set -euo pipefail

echo "FromCommittedResource call sites:"
rg -n -C8 --type=go '\bFromCommittedResource\s*\('

echo
echo "State checks in Go code:"
rg -n -C5 --type=go 'CommitmentStatus(Guaranteed|Confirmed|Planned|Pending|Superseded|Expired)|\.Spec\.State'

Repository: cobaltcore-dev/cortex

Length of output: 5131


🏁 Script executed:

# Get the full FromCommittedResource function implementation
sed -n '210,250p' internal/scheduling/reservations/commitments/state.go

Repository: cobaltcore-dev/cortex

Length of output: 1519


🏁 Script executed:

# Find all call sites of FromCommittedResource
rg -n '\bFromCommittedResource\b' --type=go

Repository: cobaltcore-dev/cortex

Length of output: 343


🏁 Script executed:

# Search for FromCommittedResource with various patterns
rg 'FromCommittedResource' --type=go -A 2 -B 2

echo "---"
echo "Search for any references to the function (without parentheses):"
rg 'FromCommittedResource' --type=go

Repository: cobaltcore-dev/cortex

Length of output: 1214


🏁 Script executed:

# Search for patterns where CommittedResource is processed
rg 'CommittedResource' --type=go | grep -E '(func|switch|if|case).*Spec\.State|State.*:=' | head -20

Repository: cobaltcore-dev/cortex

Length of output: 47


🏁 Script executed:

# Search for CommitmentState usage (the return type of FromCommittedResource)
rg 'CommitmentState' --type=go | head -30

Repository: cobaltcore-dev/cortex

Length of output: 4280


🏁 Script executed:

# Look for controllers or reconcilers handling CommittedResource
rg 'CommittedResource' --type=go -l | xargs -I {} grep -l 'controller\|reconcil' {} 2>/dev/null

Repository: cobaltcore-dev/cortex

Length of output: 1358


🏁 Script executed:

# Check for TODO, FIXME, or WIP markers related to commitments
rg -i '(TODO|FIXME|WIP).*commitment|commitment.*(TODO|FIXME|WIP)' --type=go

Repository: cobaltcore-dev/cortex

Length of output: 391


🏁 Script executed:

# Check syncer.go for FromCommittedResource usage and state filtering
cat -n internal/scheduling/reservations/commitments/syncer.go | head -150

Repository: cobaltcore-dev/cortex

Length of output: 5977


🏁 Script executed:

# Check controller.go to see how CommittedResource is processed
rg -n 'FromCommittedResource|Spec\.State' internal/scheduling/reservations/commitments/controller.go -A 5 -B 5

Repository: cobaltcore-dev/cortex

Length of output: 47


🏁 Script executed:

# Search for CommittedResource CRD creation and usage in controller
grep -n 'CommittedResource' internal/scheduling/reservations/commitments/controller.go | head -20

Repository: cobaltcore-dev/cortex

Length of output: 1481


🏁 Script executed:

# Search for where CommittedResource objects flow
rg 'v1alpha1\.CommittedResource|CommittedResource\{' --type=go -B 3 -A 3 | head -80

Repository: cobaltcore-dev/cortex

Length of output: 6861


🏁 Script executed:

# Check if FromCommittedResource might be called indirectly or is simply unused
rg '&CommitmentState\{|CommitmentState\{' --type=go | grep -v test | head -20

Repository: cobaltcore-dev/cortex

Length of output: 601


🏁 Script executed:

# Search for any registration of FromCommittedResource as a function pointer
rg 'FromCommittedResource' --type=go

# Check the entire state.go file for context on FromCommittedResource
wc -l internal/scheduling/reservations/commitments/state.go

# Look for any interface definitions or function type assignments
rg 'func\s*\(' --type=go internal/scheduling/reservations/commitments/state.go | grep -A 1 -B 1 'CommittedResource'

Repository: cobaltcore-dev/cortex

Length of output: 393


🏁 Script executed:

# Check if there's a controller.go that reconciles CommittedResource objects
grep -n 'CommittedResource' internal/scheduling/reservations/commitments/controller.go | head -5

# Look at the full picture: where are CommittedResource objects coming from?
rg 'watch.*CommittedResource|For.*CommittedResource' --type=go

Repository: cobaltcore-dev/cortex

Length of output: 1174


Add state filtering to FromCommittedResource before function is called.

The function FromCommittedResource at internal/scheduling/reservations/commitments/state.go:212 currently converts any CommittedResource to CommitmentState without validating the Spec.State field. The API specification clearly requires that only guaranteed and confirmed commitments create active Reservation slots. Although this function has no current call sites, it should enforce this constraint to prevent misuse when integrated.

Add a guard to reject planned, pending, superseded, and expired states:

if cr.Spec.State != v1alpha1.CommitmentStatusGuaranteed && cr.Spec.State != v1alpha1.CommitmentStatusConfirmed {
    return nil, errors.New("only guaranteed and confirmed commitments are supported")
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/v1alpha1/committed_resource_types.go` around lines 88 - 91, Add a state
guard inside the FromCommittedResource function to reject non-active commitment
states: check the incoming CommittedResource's cr.Spec.State and return an error
unless it equals v1alpha1.CommitmentStatusGuaranteed or
v1alpha1.CommitmentStatusConfirmed; this ensures FromCommittedResource only
converts guaranteed/confirmed commitments into CommitmentState and rejects
planned, pending, superseded, and expired values.

}

// CommittedResourceStatus defines the observed state of CommittedResource.
type CommittedResourceStatus struct {
// AcceptedAmount is the quantity the controller last successfully provisioned as Reservation slots.
// Nil if the spec has never been successfully reconciled.
// +kubebuilder:validation:Optional
AcceptedAmount *resource.Quantity `json:"acceptedAmount,omitempty"`

// AcceptedAt is when the controller last successfully reconciled the spec into Reservation slots.
// +kubebuilder:validation:Optional
AcceptedAt *metav1.Time `json:"acceptedAt,omitempty"`

// LastChanged is when the spec was last written by the syncer.
// When AcceptedAt is older than LastChanged, the controller has pending work.
// +kubebuilder:validation:Optional
LastChanged *metav1.Time `json:"lastChanged,omitempty"`

// LastReconcileAt is when the controller last ran its reconcile loop for this resource.
// +kubebuilder:validation:Optional
LastReconcileAt *metav1.Time `json:"lastReconcileAt,omitempty"`

// AssignedVMs holds the UUIDs of VMs deterministically assigned to this committed resource.
// Populated by the usage reconciler; used to compute UsedAmount and drive the quota controller.
// +kubebuilder:validation:Optional
AssignedVMs []string `json:"assignedVMs,omitempty"`

// UsedAmount is the sum of assigned VM resources expressed in the same units as Spec.Amount.
// Populated by the usage reconciler.
// +kubebuilder:validation:Optional
UsedAmount *resource.Quantity `json:"usedAmount,omitempty"`

// LastUsageReconcileAt is when the usage reconciler last updated AssignedVMs and UsedAmount.
// +kubebuilder:validation:Optional
LastUsageReconcileAt *metav1.Time `json:"lastUsageReconcileAt,omitempty"`

// Conditions holds the current status conditions.
// +kubebuilder:validation:Optional
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Cluster
// +kubebuilder:printcolumn:name="Project",type="string",JSONPath=".spec.projectID"
// +kubebuilder:printcolumn:name="FlavorGroup",type="string",JSONPath=".spec.flavorGroupName"
// +kubebuilder:printcolumn:name="ResourceType",type="string",JSONPath=".spec.resourceType"
// +kubebuilder:printcolumn:name="AZ",type="string",JSONPath=".spec.availabilityZone"
// +kubebuilder:printcolumn:name="Amount",type="string",JSONPath=".spec.amount"
// +kubebuilder:printcolumn:name="AcceptedAmount",type="string",JSONPath=".status.acceptedAmount"
// +kubebuilder:printcolumn:name="UsedAmount",type="string",JSONPath=".status.usedAmount"
// +kubebuilder:printcolumn:name="State",type="string",JSONPath=".spec.state"
// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status"
// +kubebuilder:printcolumn:name="StartTime",type="date",JSONPath=".spec.startTime",priority=1
// +kubebuilder:printcolumn:name="EndTime",type="date",JSONPath=".spec.endTime",priority=1

// CommittedResource is the Schema for the committedresources API
type CommittedResource struct {
metav1.TypeMeta `json:",inline"`

// +optional
metav1.ObjectMeta `json:"metadata,omitempty,omitzero"`

// +required
Spec CommittedResourceSpec `json:"spec"`

// +optional
Status CommittedResourceStatus `json:"status,omitempty,omitzero"`
}

// +kubebuilder:object:root=true

// CommittedResourceList contains a list of CommittedResource
type CommittedResourceList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []CommittedResource `json:"items"`
}

func init() {
SchemeBuilder.Register(&CommittedResource{}, &CommittedResourceList{})
}
Loading
Loading