diff --git a/api/v1alpha1/committed_resource_types.go b/api/v1alpha1/committed_resource_types.go new file mode 100644 index 000000000..5ed61a11a --- /dev/null +++ b/api/v1alpha1/committed_resource_types.go @@ -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"` + + // 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"` +} + +// 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{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 778c91710..d9daa7aab 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -9,9 +9,9 @@ package v1alpha1 import ( apiv1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1" - "k8s.io/api/core/v1" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) @@ -30,6 +30,33 @@ func (in *CinderDatasource) DeepCopy() *CinderDatasource { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CommittedResource) DeepCopyInto(out *CommittedResource) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommittedResource. +func (in *CommittedResource) DeepCopy() *CommittedResource { + if in == nil { + return nil + } + out := new(CommittedResource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CommittedResource) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CommittedResourceAllocation) DeepCopyInto(out *CommittedResourceAllocation) { *out = *in @@ -53,6 +80,38 @@ func (in *CommittedResourceAllocation) DeepCopy() *CommittedResourceAllocation { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CommittedResourceList) DeepCopyInto(out *CommittedResourceList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]CommittedResource, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommittedResourceList. +func (in *CommittedResourceList) DeepCopy() *CommittedResourceList { + if in == nil { + return nil + } + out := new(CommittedResourceList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CommittedResourceList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CommittedResourceReservationSpec) DeepCopyInto(out *CommittedResourceReservationSpec) { *out = *in @@ -97,6 +156,87 @@ func (in *CommittedResourceReservationStatus) DeepCopy() *CommittedResourceReser return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CommittedResourceSpec) DeepCopyInto(out *CommittedResourceSpec) { + *out = *in + out.Amount = in.Amount.DeepCopy() + if in.StartTime != nil { + in, out := &in.StartTime, &out.StartTime + *out = (*in).DeepCopy() + } + if in.EndTime != nil { + in, out := &in.EndTime, &out.EndTime + *out = (*in).DeepCopy() + } + if in.ConfirmedAt != nil { + in, out := &in.ConfirmedAt, &out.ConfirmedAt + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommittedResourceSpec. +func (in *CommittedResourceSpec) DeepCopy() *CommittedResourceSpec { + if in == nil { + return nil + } + out := new(CommittedResourceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CommittedResourceStatus) DeepCopyInto(out *CommittedResourceStatus) { + *out = *in + if in.AcceptedAmount != nil { + in, out := &in.AcceptedAmount, &out.AcceptedAmount + x := (*in).DeepCopy() + *out = &x + } + if in.AcceptedAt != nil { + in, out := &in.AcceptedAt, &out.AcceptedAt + *out = (*in).DeepCopy() + } + if in.LastChanged != nil { + in, out := &in.LastChanged, &out.LastChanged + *out = (*in).DeepCopy() + } + if in.LastReconcileAt != nil { + in, out := &in.LastReconcileAt, &out.LastReconcileAt + *out = (*in).DeepCopy() + } + if in.AssignedVMs != nil { + in, out := &in.AssignedVMs, &out.AssignedVMs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.UsedAmount != nil { + in, out := &in.UsedAmount, &out.UsedAmount + x := (*in).DeepCopy() + *out = &x + } + if in.LastUsageReconcileAt != nil { + in, out := &in.LastUsageReconcileAt, &out.LastUsageReconcileAt + *out = (*in).DeepCopy() + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommittedResourceStatus. +func (in *CommittedResourceStatus) DeepCopy() *CommittedResourceStatus { + if in == nil { + return nil + } + out := new(CommittedResourceStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CurrentDecision) DeepCopyInto(out *CurrentDecision) { *out = *in @@ -191,7 +331,7 @@ func (in *DatasourceSpec) DeepCopyInto(out *DatasourceSpec) { out.DatabaseSecretRef = in.DatabaseSecretRef if in.SSOSecretRef != nil { in, out := &in.SSOSecretRef, &out.SSOSecretRef - *out = new(v1.SecretReference) + *out = new(corev1.SecretReference) **out = **in } } @@ -213,7 +353,7 @@ func (in *DatasourceStatus) DeepCopyInto(out *DatasourceStatus) { in.NextSyncTime.DeepCopyInto(&out.NextSyncTime) if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]metav1.Condition, len(*in)) + *out = make([]v1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -363,12 +503,12 @@ func (in *DecisionSpec) DeepCopyInto(out *DecisionSpec) { } if in.MachineRef != nil { in, out := &in.MachineRef, &out.MachineRef - *out = new(v1.ObjectReference) + *out = new(corev1.ObjectReference) **out = **in } if in.PodRef != nil { in, out := &in.PodRef, &out.PodRef - *out = new(v1.ObjectReference) + *out = new(corev1.ObjectReference) **out = **in } } @@ -393,10 +533,10 @@ func (in *DecisionStatus) DeepCopyInto(out *DecisionStatus) { } if in.History != nil { in, out := &in.History, &out.History - *out = new([]v1.ObjectReference) + *out = new([]corev1.ObjectReference) if **in != nil { in, out := *in, *out - *out = make([]v1.ObjectReference, len(*in)) + *out = make([]corev1.ObjectReference, len(*in)) copy(*out, *in) } } @@ -407,7 +547,7 @@ func (in *DecisionStatus) DeepCopyInto(out *DecisionStatus) { } if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]metav1.Condition, len(*in)) + *out = make([]v1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -503,7 +643,7 @@ func (in *DeschedulingStatus) DeepCopyInto(out *DeschedulingStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]metav1.Condition, len(*in)) + *out = make([]v1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -701,7 +841,7 @@ func (in *HistoryStatus) DeepCopyInto(out *HistoryStatus) { } if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]metav1.Condition, len(*in)) + *out = make([]v1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -765,12 +905,12 @@ func (in *KPIDependenciesSpec) DeepCopyInto(out *KPIDependenciesSpec) { *out = *in if in.Datasources != nil { in, out := &in.Datasources, &out.Datasources - *out = make([]v1.ObjectReference, len(*in)) + *out = make([]corev1.ObjectReference, len(*in)) copy(*out, *in) } if in.Knowledges != nil { in, out := &in.Knowledges, &out.Knowledges - *out = make([]v1.ObjectReference, len(*in)) + *out = make([]corev1.ObjectReference, len(*in)) copy(*out, *in) } } @@ -839,7 +979,7 @@ func (in *KPIStatus) DeepCopyInto(out *KPIStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]metav1.Condition, len(*in)) + *out = make([]v1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -888,12 +1028,12 @@ func (in *KnowledgeDependenciesSpec) DeepCopyInto(out *KnowledgeDependenciesSpec *out = *in if in.Datasources != nil { in, out := &in.Datasources, &out.Datasources - *out = make([]v1.ObjectReference, len(*in)) + *out = make([]corev1.ObjectReference, len(*in)) copy(*out, *in) } if in.Knowledges != nil { in, out := &in.Knowledges, &out.Knowledges - *out = make([]v1.ObjectReference, len(*in)) + *out = make([]corev1.ObjectReference, len(*in)) copy(*out, *in) } } @@ -982,7 +1122,7 @@ func (in *KnowledgeStatus) DeepCopyInto(out *KnowledgeStatus) { in.Raw.DeepCopyInto(&out.Raw) if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]metav1.Condition, len(*in)) + *out = make([]v1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -1248,7 +1388,7 @@ func (in *PipelineStatus) DeepCopyInto(out *PipelineStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]metav1.Condition, len(*in)) + *out = make([]v1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -1403,7 +1543,7 @@ func (in *ReservationStatus) DeepCopyInto(out *ReservationStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]metav1.Condition, len(*in)) + *out = make([]v1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } diff --git a/cmd/manager/main.go b/cmd/manager/main.go index 4c390f5a8..b74b21d1b 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -322,14 +322,16 @@ func main() { hvGVK := schema.GroupVersionKind{Group: "kvm.cloud.sap", Version: "v1", Kind: "Hypervisor"} reservationGVK := schema.GroupVersionKind{Group: "cortex.cloud", Version: "v1alpha1", Kind: "Reservation"} historyGVK := schema.GroupVersionKind{Group: "cortex.cloud", Version: "v1alpha1", Kind: "History"} + committedResourceGVK := schema.GroupVersionKind{Group: "cortex.cloud", Version: "v1alpha1", Kind: "CommittedResource"} multiclusterClient := &multicluster.Client{ HomeCluster: homeCluster, HomeRestConfig: restConfig, HomeScheme: scheme, ResourceRouters: map[schema.GroupVersionKind]multicluster.ResourceRouter{ - hvGVK: multicluster.HypervisorResourceRouter{}, - reservationGVK: multicluster.ReservationsResourceRouter{}, - historyGVK: multicluster.HistoryResourceRouter{}, + hvGVK: multicluster.HypervisorResourceRouter{}, + reservationGVK: multicluster.ReservationsResourceRouter{}, + historyGVK: multicluster.HistoryResourceRouter{}, + committedResourceGVK: multicluster.CommittedResourceRouter{}, }, } multiclusterClientConfig := conf.GetConfigOrDie[multicluster.ClientConfig]() diff --git a/helm/bundles/cortex-nova/values.yaml b/helm/bundles/cortex-nova/values.yaml index 2c896a4cc..c40849739 100644 --- a/helm/bundles/cortex-nova/values.yaml +++ b/helm/bundles/cortex-nova/values.yaml @@ -93,6 +93,8 @@ cortex: &cortex - cortex.cloud/v1alpha1/KPIList - cortex.cloud/v1alpha1/Reservation - cortex.cloud/v1alpha1/ReservationList + - cortex.cloud/v1alpha1/CommittedResource + - cortex.cloud/v1alpha1/CommittedResourceList - kvm.cloud.sap/v1/Hypervisor - kvm.cloud.sap/v1/HypervisorList - v1/Secret diff --git a/helm/library/cortex/templates/rbac/role.yaml b/helm/library/cortex/templates/rbac/role.yaml index 440de5e31..ea75c6897 100644 --- a/helm/library/cortex/templates/rbac/role.yaml +++ b/helm/library/cortex/templates/rbac/role.yaml @@ -13,6 +13,7 @@ rules: - knowledges - datasources - reservations + - committedresources - decisions - deschedulings - pipelines @@ -32,6 +33,7 @@ rules: - knowledges/finalizers - datasources/finalizers - reservations/finalizers + - committedresources/finalizers - decisions/finalizers - deschedulings/finalizers - pipelines/finalizers @@ -45,6 +47,7 @@ rules: - knowledges/status - datasources/status - reservations/status + - committedresources/status - decisions/status - deschedulings/status - pipelines/status diff --git a/internal/scheduling/reservations/commitments/state.go b/internal/scheduling/reservations/commitments/state.go index 1208a4344..698aea428 100644 --- a/internal/scheduling/reservations/commitments/state.go +++ b/internal/scheduling/reservations/commitments/state.go @@ -207,6 +207,38 @@ func FromChangeCommitmentTargetState( }, nil } +// FromCommittedResource reads CommitmentState from a CommittedResource CRD. +// Only memory commitments are supported; cores support is added in a follow-up. +func FromCommittedResource(cr v1alpha1.CommittedResource) (*CommitmentState, error) { + if cr.Spec.ResourceType != v1alpha1.CommittedResourceTypeMemory { + return nil, fmt.Errorf("unsupported resource type %q: only memory commitments are supported", cr.Spec.ResourceType) + } + + if !commitmentUUIDPattern.MatchString(cr.Spec.CommitmentUUID) { + return nil, errors.New("unexpected commitment format") + } + + state := &CommitmentState{ + CommitmentUUID: cr.Spec.CommitmentUUID, + ProjectID: cr.Spec.ProjectID, + DomainID: cr.Spec.DomainID, + FlavorGroupName: cr.Spec.FlavorGroupName, + TotalMemoryBytes: cr.Spec.Amount.Value(), + AvailabilityZone: cr.Spec.AvailabilityZone, + } + + if cr.Spec.StartTime != nil { + t := cr.Spec.StartTime.Time + state.StartTime = &t + } + if cr.Spec.EndTime != nil && !cr.Spec.EndTime.IsZero() { + t := cr.Spec.EndTime.Time + state.EndTime = &t + } + + return state, nil +} + // FromReservations reconstructs CommitmentState from existing Reservation CRDs. func FromReservations(reservations []v1alpha1.Reservation) (*CommitmentState, error) { if len(reservations) == 0 { diff --git a/pkg/multicluster/routers.go b/pkg/multicluster/routers.go index 8c41e822a..16d74dc6f 100644 --- a/pkg/multicluster/routers.go +++ b/pkg/multicluster/routers.go @@ -16,9 +16,10 @@ import ( // for the multicluster client that cortex supports by default. This is used to // route resources to the correct cluster in a multicluster setup. var DefaultResourceRouters = map[schema.GroupVersionKind]ResourceRouter{ - {Group: "kvm.cloud.sap", Version: "v1", Kind: "Hypervisor"}: HypervisorResourceRouter{}, - {Group: "cortex.cloud", Version: "v1alpha1", Kind: "Reservation"}: ReservationsResourceRouter{}, - {Group: "cortex.cloud", Version: "v1alpha1", Kind: "History"}: HistoryResourceRouter{}, + {Group: "kvm.cloud.sap", Version: "v1", Kind: "Hypervisor"}: HypervisorResourceRouter{}, + {Group: "cortex.cloud", Version: "v1alpha1", Kind: "Reservation"}: ReservationsResourceRouter{}, + {Group: "cortex.cloud", Version: "v1alpha1", Kind: "History"}: HistoryResourceRouter{}, + {Group: "cortex.cloud", Version: "v1alpha1", Kind: "CommittedResource"}: CommittedResourceRouter{}, } // ResourceRouter determines which remote cluster a resource should be written to @@ -83,6 +84,33 @@ func (r ReservationsResourceRouter) Match(obj any, labels map[string]string) (bo return reservationAvailabilityZone == availabilityZone, nil } +// CommittedResourceRouter routes committed resources to clusters based on availability zone. +type CommittedResourceRouter struct{} + +func (c CommittedResourceRouter) Match(obj any, labels map[string]string) (bool, error) { + var cr v1alpha1.CommittedResource + + switch v := obj.(type) { + case *v1alpha1.CommittedResource: + if v == nil { + return false, errors.New("object is nil") + } + cr = *v + case v1alpha1.CommittedResource: + cr = v + default: + return false, errors.New("object is not a CommittedResource") + } + availabilityZone, ok := labels["availabilityZone"] + if !ok { + return false, errors.New("cluster does not have availabilityZone label") + } + if cr.Spec.AvailabilityZone == "" { + return false, errors.New("committed resource does not have availability zone in spec") + } + return cr.Spec.AvailabilityZone == availabilityZone, nil +} + // HistoryResourceRouter routes histories to clusters based on availability zone. type HistoryResourceRouter struct{}