diff --git a/api/v1alpha1/decision_types.go b/api/v1alpha1/decision_types.go index c3f02de1e..d72c02ab5 100644 --- a/api/v1alpha1/decision_types.go +++ b/api/v1alpha1/decision_types.go @@ -10,90 +10,52 @@ import ( ) type DecisionSpec struct { - // SchedulingDomain defines in which scheduling domain this decision - // was or is processed (e.g., nova, cinder, manila). - SchedulingDomain SchedulingDomain `json:"schedulingDomain"` - - // A reference to the pipeline that should be used for this decision. - // This reference can be used to look up the pipeline definition and its - // scheduler step configuration for additional context. - PipelineRef corev1.ObjectReference `json:"pipelineRef"` - - // An identifier for the underlying resource to be scheduled. - // For example, this can be the UUID of a nova instance or cinder volume. - // This can be used to correlate multiple decisions for the same resource. - ResourceID string `json:"resourceID"` + // Reference to the resource being scheduled (e.g., VM, Pod, Machine). + // This will typically reference the VM CRD in the future. + ResourceRef corev1.ObjectReference `json:"resourceRef"` + // DEPRECATED: Transition field - will be moved to VM CRD. // If the type is "nova", this field contains the raw nova decision request. // +kubebuilder:validation:Optional NovaRaw *runtime.RawExtension `json:"novaRaw,omitempty"` - // If the type is "cinder", this field contains the raw cinder decision request. - // +kubebuilder:validation:Optional - CinderRaw *runtime.RawExtension `json:"cinderRaw,omitempty"` - // If the type is "manila", this field contains the raw manila decision request. - // +kubebuilder:validation:Optional - ManilaRaw *runtime.RawExtension `json:"manilaRaw,omitempty"` - // If the type is "machine", this field contains the machine reference. - // +kubebuilder:validation:Optional - MachineRef *corev1.ObjectReference `json:"machineRef,omitempty"` - // If the type is "pod", this field contains the pod reference. - // +kubebuilder:validation:Optional - PodRef *corev1.ObjectReference `json:"podRef,omitempty"` -} - -type StepResult struct { - // object reference to the scheduler step. - StepName string `json:"stepName"` - // Activations of the step for each host. - Activations map[string]float64 `json:"activations"` } -type DecisionResult struct { - // Raw input weights to the pipeline. - // +kubebuilder:validation:Optional - RawInWeights map[string]float64 `json:"rawInWeights"` - // Normalized input weights to the pipeline. - // +kubebuilder:validation:Optional - NormalizedInWeights map[string]float64 `json:"normalizedInWeights"` - // Outputs of the decision pipeline including the activations used - // to make the final ordering of compute hosts. - // +kubebuilder:validation:Optional - StepResults []StepResult `json:"stepResults,omitempty"` - // Aggregated output weights from the pipeline. - // +kubebuilder:validation:Optional - AggregatedOutWeights map[string]float64 `json:"aggregatedOutWeights"` - // Final ordered list of hosts from most preferred to least preferred. - // +kubebuilder:validation:Optional - OrderedHosts []string `json:"orderedHosts,omitempty"` - // The first element of the ordered hosts is considered the target host. - // +kubebuilder:validation:Optional - TargetHost *string `json:"targetHost,omitempty"` +// PlacementRecord tracks a single placement decision for a resource. +// This is used to build a history of where a resource has been scheduled +// and when, enabling migration rate limiting and cycle prevention. +type PlacementRecord struct { + // The host where the resource was placed. + Host string `json:"host"` + // When this placement decision was made. + Timestamp metav1.Time `json:"timestamp"` + // The reason for this placement (e.g., "InitialPlacement", "Migration", "Rescheduling", "Descheduling"). + Reason string `json:"reason"` + // Optional reference to the pipeline that made this decision. + // +kubebuilder:validation:Optional + PipelineRef *corev1.ObjectReference `json:"pipelineRef,omitempty"` } const ( // The decision was successfully processed. DecisionConditionReady = "Ready" + // The resource could not be scheduled to any host. + DecisionConditionUnschedulable = "Unschedulable" ) type DecisionStatus struct { - // The result of this decision. - // +kubebuilder:validation:Optional - Result *DecisionResult `json:"result,omitempty"` - - // If there were previous decisions for the underlying resource, they can - // be resolved here to provide historical context for the decision. - // +kubebuilder:validation:Optional - History *[]corev1.ObjectReference `json:"history,omitempty"` - - // The number of decisions that preceded this one for the same resource. + // PlacementHistory tracks the last N placement decisions for this resource. + // This provides a data basis for migration rate limiting and cycle prevention. + // Detailed explanations are communicated via Kubernetes Events. // +kubebuilder:validation:Optional - Precedence *int `json:"precedence,omitempty"` + // +kubebuilder:validation:MaxItems=50 + PlacementHistory []PlacementRecord `json:"placementHistory,omitempty"` - // A human-readable explanation of the decision result. + // CurrentHost is the current/target host for the resource. // +kubebuilder:validation:Optional - Explanation string `json:"explanation,omitempty"` + CurrentHost *string `json:"currentHost,omitempty"` // The current status conditions of the decision. + // Use events (kubectl describe) for detailed scheduling explanations. // +kubebuilder:validation:Optional Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` } @@ -101,16 +63,15 @@ type DecisionStatus struct { // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:resource:scope=Cluster -// +kubebuilder:printcolumn:name="Domain",type="string",JSONPath=".spec.schedulingDomain" -// +kubebuilder:printcolumn:name="Resource ID",type="string",JSONPath=".spec.resourceID" -// +kubebuilder:printcolumn:name="#",type="string",JSONPath=".status.precedence" -// +kubebuilder:printcolumn:name="Created",type="date",JSONPath=".metadata.creationTimestamp" -// +kubebuilder:printcolumn:name="Pipeline",type="string",JSONPath=".spec.pipelineRef.name" -// +kubebuilder:printcolumn:name="TargetHost",type="string",JSONPath=".status.result.targetHost" +// +kubebuilder:printcolumn:name="Resource",type="string",JSONPath=".spec.resourceRef.name" +// +kubebuilder:printcolumn:name="CurrentHost",type="string",JSONPath=".status.currentHost" +// +kubebuilder:printcolumn:name="Placements",type="integer",JSONPath=".status.placementHistory[*].host" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" // +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status" -// +kubebuilder:selectablefield:JSONPath=".spec.resourceID" -// Decision is the Schema for the decisions API +// Decision tracks placement decisions and history for scheduled resources. +// It provides transparency (via events) and a data basis (via placement history) +// for scheduling decisions, migration rate limiting, and cycle prevention. type Decision struct { metav1.TypeMeta `json:",inline"` diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 2551ef3ea..63007b507 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -194,88 +194,15 @@ func (in *DecisionList) DeepCopyObject() runtime.Object { return nil } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DecisionResult) DeepCopyInto(out *DecisionResult) { - *out = *in - if in.RawInWeights != nil { - in, out := &in.RawInWeights, &out.RawInWeights - *out = make(map[string]float64, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } - if in.NormalizedInWeights != nil { - in, out := &in.NormalizedInWeights, &out.NormalizedInWeights - *out = make(map[string]float64, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } - if in.StepResults != nil { - in, out := &in.StepResults, &out.StepResults - *out = make([]StepResult, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - if in.AggregatedOutWeights != nil { - in, out := &in.AggregatedOutWeights, &out.AggregatedOutWeights - *out = make(map[string]float64, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } - if in.OrderedHosts != nil { - in, out := &in.OrderedHosts, &out.OrderedHosts - *out = make([]string, len(*in)) - copy(*out, *in) - } - if in.TargetHost != nil { - in, out := &in.TargetHost, &out.TargetHost - *out = new(string) - **out = **in - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DecisionResult. -func (in *DecisionResult) DeepCopy() *DecisionResult { - if in == nil { - return nil - } - out := new(DecisionResult) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DecisionSpec) DeepCopyInto(out *DecisionSpec) { *out = *in - out.PipelineRef = in.PipelineRef + out.ResourceRef = in.ResourceRef if in.NovaRaw != nil { in, out := &in.NovaRaw, &out.NovaRaw *out = new(runtime.RawExtension) (*in).DeepCopyInto(*out) } - if in.CinderRaw != nil { - in, out := &in.CinderRaw, &out.CinderRaw - *out = new(runtime.RawExtension) - (*in).DeepCopyInto(*out) - } - if in.ManilaRaw != nil { - in, out := &in.ManilaRaw, &out.ManilaRaw - *out = new(runtime.RawExtension) - (*in).DeepCopyInto(*out) - } - if in.MachineRef != nil { - in, out := &in.MachineRef, &out.MachineRef - *out = new(v1.ObjectReference) - **out = **in - } - if in.PodRef != nil { - in, out := &in.PodRef, &out.PodRef - *out = new(v1.ObjectReference) - **out = **in - } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DecisionSpec. @@ -291,23 +218,16 @@ func (in *DecisionSpec) DeepCopy() *DecisionSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DecisionStatus) DeepCopyInto(out *DecisionStatus) { *out = *in - if in.Result != nil { - in, out := &in.Result, &out.Result - *out = new(DecisionResult) - (*in).DeepCopyInto(*out) - } - if in.History != nil { - in, out := &in.History, &out.History - *out = new([]v1.ObjectReference) - if **in != nil { - in, out := *in, *out - *out = make([]v1.ObjectReference, len(*in)) - copy(*out, *in) + if in.PlacementHistory != nil { + in, out := &in.PlacementHistory, &out.PlacementHistory + *out = make([]PlacementRecord, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) } } - if in.Precedence != nil { - in, out := &in.Precedence, &out.Precedence - *out = new(int) + if in.CurrentHost != nil { + in, out := &in.CurrentHost, &out.CurrentHost + *out = new(string) **out = **in } if in.Conditions != nil { @@ -1007,6 +927,27 @@ func (in *PlacementDatasource) DeepCopy() *PlacementDatasource { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PlacementRecord) DeepCopyInto(out *PlacementRecord) { + *out = *in + in.Timestamp.DeepCopyInto(&out.Timestamp) + if in.PipelineRef != nil { + in, out := &in.PipelineRef, &out.PipelineRef + *out = new(v1.ObjectReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlacementRecord. +func (in *PlacementRecord) DeepCopy() *PlacementRecord { + if in == nil { + return nil + } + out := new(PlacementRecord) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PrometheusDatasource) DeepCopyInto(out *PrometheusDatasource) { *out = *in @@ -1172,28 +1113,6 @@ func (in *ReservationStatus) DeepCopy() *ReservationStatus { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *StepResult) DeepCopyInto(out *StepResult) { - *out = *in - if in.Activations != nil { - in, out := &in.Activations, &out.Activations - *out = make(map[string]float64, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StepResult. -func (in *StepResult) DeepCopy() *StepResult { - if in == nil { - return nil - } - out := new(StepResult) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *WeigherSpec) DeepCopyInto(out *WeigherSpec) { *out = *in diff --git a/helm/library/cortex/files/crds/cortex.cloud_decisions.yaml b/helm/library/cortex/files/crds/cortex.cloud_decisions.yaml index 3332a40e4..382ed5cf8 100644 --- a/helm/library/cortex/files/crds/cortex.cloud_decisions.yaml +++ b/helm/library/cortex/files/crds/cortex.cloud_decisions.yaml @@ -15,31 +15,28 @@ spec: scope: Cluster versions: - additionalPrinterColumns: - - jsonPath: .spec.schedulingDomain - name: Domain + - jsonPath: .spec.resourceRef.name + name: Resource type: string - - jsonPath: .spec.resourceID - name: Resource ID - type: string - - jsonPath: .status.precedence - name: '#' + - jsonPath: .status.currentHost + name: CurrentHost type: string + - jsonPath: .status.placementHistory[*].host + name: Placements + type: integer - jsonPath: .metadata.creationTimestamp - name: Created + name: Age type: date - - jsonPath: .spec.pipelineRef.name - name: Pipeline - type: string - - jsonPath: .status.result.targetHost - name: TargetHost - type: string - jsonPath: .status.conditions[?(@.type=='Ready')].status name: Ready type: string name: v1alpha1 schema: openAPIV3Schema: - description: Decision is the Schema for the decisions API + description: |- + Decision tracks placement decisions and history for scheduled resources. + It provides transparency (via events) and a data basis (via placement history) + for scheduling decisions, migration rate limiting, and cycle prevention. properties: apiVersion: description: |- @@ -61,113 +58,16 @@ spec: spec: description: spec defines the desired state of Decision properties: - cinderRaw: - description: If the type is "cinder", this field contains the raw - cinder decision request. - type: object - x-kubernetes-preserve-unknown-fields: true - machineRef: - description: If the type is "machine", this field contains the machine - reference. - properties: - apiVersion: - description: API version of the referent. - type: string - fieldPath: - description: |- - If referring to a piece of an object instead of an entire object, this string - should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. - For example, if the object reference is to a container within a pod, this would take on a value like: - "spec.containers{name}" (where "name" refers to the name of the container that triggered - the event) or if no container name is specified "spec.containers[2]" (container with - index 2 in this pod). This syntax is chosen only to have some well-defined way of - referencing a part of an object. - type: string - kind: - description: |- - Kind of the referent. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - namespace: - description: |- - Namespace of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ - type: string - resourceVersion: - description: |- - Specific resourceVersion to which this reference is made, if any. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency - type: string - uid: - description: |- - UID of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids - type: string - type: object - x-kubernetes-map-type: atomic - manilaRaw: - description: If the type is "manila", this field contains the raw - manila decision request. - type: object - x-kubernetes-preserve-unknown-fields: true novaRaw: - description: If the type is "nova", this field contains the raw nova - decision request. + description: |- + DEPRECATED: Transition field - will be moved to VM CRD. + If the type is "nova", this field contains the raw nova decision request. type: object x-kubernetes-preserve-unknown-fields: true - pipelineRef: + resourceRef: description: |- - A reference to the pipeline that should be used for this decision. - This reference can be used to look up the pipeline definition and its - scheduler step configuration for additional context. - properties: - apiVersion: - description: API version of the referent. - type: string - fieldPath: - description: |- - If referring to a piece of an object instead of an entire object, this string - should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. - For example, if the object reference is to a container within a pod, this would take on a value like: - "spec.containers{name}" (where "name" refers to the name of the container that triggered - the event) or if no container name is specified "spec.containers[2]" (container with - index 2 in this pod). This syntax is chosen only to have some well-defined way of - referencing a part of an object. - type: string - kind: - description: |- - Kind of the referent. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - namespace: - description: |- - Namespace of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ - type: string - resourceVersion: - description: |- - Specific resourceVersion to which this reference is made, if any. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency - type: string - uid: - description: |- - UID of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids - type: string - type: object - x-kubernetes-map-type: atomic - podRef: - description: If the type is "pod", this field contains the pod reference. + Reference to the resource being scheduled (e.g., VM, Pod, Machine). + This will typically reference the VM CRD in the future. properties: apiVersion: description: API version of the referent. @@ -209,27 +109,16 @@ spec: type: string type: object x-kubernetes-map-type: atomic - resourceID: - description: |- - An identifier for the underlying resource to be scheduled. - For example, this can be the UUID of a nova instance or cinder volume. - This can be used to correlate multiple decisions for the same resource. - type: string - schedulingDomain: - description: |- - SchedulingDomain defines in which scheduling domain this decision - was or is processed (e.g., nova, cinder, manila). - type: string required: - - pipelineRef - - resourceID - - schedulingDomain + - resourceRef type: object status: description: status defines the observed state of Decision properties: conditions: - description: The current status conditions of the decision. + description: |- + The current status conditions of the decision. + Use events (kubectl describe) for detailed scheduling explanations. items: description: Condition contains details for one aspect of the current state of this API Resource. @@ -285,116 +174,86 @@ spec: - type type: object type: array - explanation: - description: A human-readable explanation of the decision result. + currentHost: + description: CurrentHost is the current/target host for the resource. type: string - history: + placementHistory: description: |- - If there were previous decisions for the underlying resource, they can - be resolved here to provide historical context for the decision. + PlacementHistory tracks the last N placement decisions for this resource. + This provides a data basis for migration rate limiting and cycle prevention. + Detailed explanations are communicated via Kubernetes Events. items: - description: ObjectReference contains enough information to let - you inspect or modify the referred object. + description: |- + PlacementRecord tracks a single placement decision for a resource. + This is used to build a history of where a resource has been scheduled + and when, enabling migration rate limiting and cycle prevention. properties: - apiVersion: - description: API version of the referent. + host: + description: The host where the resource was placed. type: string - fieldPath: - description: |- - If referring to a piece of an object instead of an entire object, this string - should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. - For example, if the object reference is to a container within a pod, this would take on a value like: - "spec.containers{name}" (where "name" refers to the name of the container that triggered - the event) or if no container name is specified "spec.containers[2]" (container with - index 2 in this pod). This syntax is chosen only to have some well-defined way of - referencing a part of an object. - type: string - kind: - description: |- - Kind of the referent. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - namespace: - description: |- - Namespace of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ - type: string - resourceVersion: - description: |- - Specific resourceVersion to which this reference is made, if any. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + pipelineRef: + description: Optional reference to the pipeline that made this + decision. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + reason: + description: The reason for this placement (e.g., "InitialPlacement", + "Migration", "Rescheduling", "Descheduling"). type: string - uid: - description: |- - UID of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + timestamp: + description: When this placement decision was made. + format: date-time type: string + required: + - host + - reason + - timestamp type: object - x-kubernetes-map-type: atomic + maxItems: 50 type: array - precedence: - description: The number of decisions that preceded this one for the - same resource. - type: integer - result: - description: The result of this decision. - properties: - aggregatedOutWeights: - additionalProperties: - type: number - description: Aggregated output weights from the pipeline. - type: object - normalizedInWeights: - additionalProperties: - type: number - description: Normalized input weights to the pipeline. - type: object - orderedHosts: - description: Final ordered list of hosts from most preferred to - least preferred. - items: - type: string - type: array - rawInWeights: - additionalProperties: - type: number - description: Raw input weights to the pipeline. - type: object - stepResults: - description: |- - Outputs of the decision pipeline including the activations used - to make the final ordering of compute hosts. - items: - properties: - activations: - additionalProperties: - type: number - description: Activations of the step for each host. - type: object - stepName: - description: object reference to the scheduler step. - type: string - required: - - activations - - stepName - type: object - type: array - targetHost: - description: The first element of the ordered hosts is considered - the target host. - type: string - type: object type: object required: - spec type: object - selectableFields: - - jsonPath: .spec.resourceID served: true storage: true subresources: