diff --git a/apis/nodecore/v1alpha1/flavour_types.go b/apis/nodecore/v1alpha1/flavour_types.go index 513f5ed..83ac7d4 100644 --- a/apis/nodecore/v1alpha1/flavour_types.go +++ b/apis/nodecore/v1alpha1/flavour_types.go @@ -137,6 +137,9 @@ type FlavourSpec struct { // This field is used to specify the optional fields that can be retrieved from the Flavour. // In the future it will be expanded to include more optional fields defined in the REAR Protocol or custom ones. OptionalFields OptionalFields `json:"optionalFields"` + + // This field is used to represent the available quantity of transactions of the Flavour. + QuantityAvailable int `json:"quantityAvailable,omitempty"` } // FlavourStatus defines the observed state of Flavour. diff --git a/deployments/node/crds/advertisement.fluidos.eu_discoveries.yaml b/deployments/node/crds/advertisement.fluidos.eu_discoveries.yaml index 60ef6cb..e46f43d 100644 --- a/deployments/node/crds/advertisement.fluidos.eu_discoveries.yaml +++ b/deployments/node/crds/advertisement.fluidos.eu_discoveries.yaml @@ -510,6 +510,11 @@ spec: ProviderID is the ID of the FLUIDOS Node ID that provides this Flavour. It can correspond to ID of the owner FLUIDOS Node or to the ID of a FLUIDOS SuperNode that represents the entry point to a FLUIDOS Domain type: string + quantityAvailable: + description: This field is used to represent + the available quantity of transactions of + the Flavour. + type: integer type: description: Type is the type of the Flavour. Currently, only K8S is supported. diff --git a/deployments/node/crds/advertisement.fluidos.eu_peeringcandidates.yaml b/deployments/node/crds/advertisement.fluidos.eu_peeringcandidates.yaml index 474f89d..14343eb 100644 --- a/deployments/node/crds/advertisement.fluidos.eu_peeringcandidates.yaml +++ b/deployments/node/crds/advertisement.fluidos.eu_peeringcandidates.yaml @@ -275,6 +275,10 @@ spec: ProviderID is the ID of the FLUIDOS Node ID that provides this Flavour. It can correspond to ID of the owner FLUIDOS Node or to the ID of a FLUIDOS SuperNode that represents the entry point to a FLUIDOS Domain type: string + quantityAvailable: + description: This field is used to represent the available + quantity of transactions of the Flavour. + type: integer type: description: Type is the type of the Flavour. Currently, only K8S is supported. diff --git a/deployments/node/crds/nodecore.fluidos.eu_flavours.yaml b/deployments/node/crds/nodecore.fluidos.eu_flavours.yaml index 8991d08..dceef18 100644 --- a/deployments/node/crds/nodecore.fluidos.eu_flavours.yaml +++ b/deployments/node/crds/nodecore.fluidos.eu_flavours.yaml @@ -260,6 +260,10 @@ spec: ProviderID is the ID of the FLUIDOS Node ID that provides this Flavour. It can correspond to ID of the owner FLUIDOS Node or to the ID of a FLUIDOS SuperNode that represents the entry point to a FLUIDOS Domain type: string + quantityAvailable: + description: This field is used to represent the available quantity + of transactions of the Flavour. + type: integer type: description: Type is the type of the Flavour. Currently, only K8S is supported. diff --git a/deployments/node/crds/reservation.fluidos.eu_contracts.yaml b/deployments/node/crds/reservation.fluidos.eu_contracts.yaml index e512e7e..747ad84 100644 --- a/deployments/node/crds/reservation.fluidos.eu_contracts.yaml +++ b/deployments/node/crds/reservation.fluidos.eu_contracts.yaml @@ -334,6 +334,10 @@ spec: ProviderID is the ID of the FLUIDOS Node ID that provides this Flavour. It can correspond to ID of the owner FLUIDOS Node or to the ID of a FLUIDOS SuperNode that represents the entry point to a FLUIDOS Domain type: string + quantityAvailable: + description: This field is used to represent the available + quantity of transactions of the Flavour. + type: integer type: description: Type is the type of the Flavour. Currently, only K8S is supported. diff --git a/pkg/local-resource-manager/controller_manager.go b/pkg/local-resource-manager/controller_manager.go index 956790f..721cd3f 100644 --- a/pkg/local-resource-manager/controller_manager.go +++ b/pkg/local-resource-manager/controller_manager.go @@ -18,10 +18,12 @@ import ( "context" "fmt" "log" + "reflect" "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/client" + nodecorev1alpha1 "github.com/fluidos-project/node/apis/nodecore/v1alpha1" "github.com/fluidos-project/node/pkg/utils/getters" "github.com/fluidos-project/node/pkg/utils/resourceforge" ) @@ -38,6 +40,108 @@ import ( // resources calculation. // Start starts the controller. + +func canMergeFlavours(flavour1, flavour2 *nodecorev1alpha1.Flavour) bool { + if flavour1.Spec.ProviderID != flavour2.Spec.ProviderID { + return false + } + + if flavour1.TypeMeta != flavour2.TypeMeta { + return false + } + + if flavour1.Namespace != flavour2.Namespace { + return false + } + + if flavour1.Spec.OptionalFields.Availability != flavour2.Spec.OptionalFields.Availability { + return false + } + + if flavour1.Spec.Owner != flavour2.Spec.Owner { + return false + } + + if flavour1.Spec.Price != flavour2.Spec.Price { + return false + } + + if flavour1.Spec.Type != flavour2.Spec.Type { + return false + } + + if !reflect.DeepEqual(flavour1.Spec.Policy, flavour2.Spec.Policy) { + return false + } + + if flavour1.Spec.Characteristics.Architecture != flavour2.Spec.Characteristics.Architecture { + return false + } + + if flavour1.Spec.Characteristics.Gpu != flavour2.Spec.Characteristics.Gpu { + return false + } + + if flavour1.Spec.Characteristics.Pods != flavour2.Spec.Characteristics.Pods { + return false + } + + if flavour1.Spec.Characteristics.Cpu.Value() != flavour2.Spec.Characteristics.Cpu.Value() { + return false + } + + if flavour1.Spec.Characteristics.Memory.Value()/ + flavour1.Spec.Policy.Partitionable.MemoryStep.Value() != + flavour2.Spec.Characteristics.Memory.Value()/ + flavour2.Spec.Policy.Partitionable.MemoryStep.Value() { + return false + } + + if flavour1.Spec.Characteristics.EphemeralStorage.Value() != flavour2.Spec.Characteristics.EphemeralStorage.Value() { + return false + } + + if flavour1.Spec.Characteristics.PersistentStorage.Value() != flavour2.Spec.Characteristics.PersistentStorage.Value() { + return false + } + + return true +} + +func mergeFlavours(flavours []nodecorev1alpha1.Flavour) []nodecorev1alpha1.Flavour { + mergedFlavours := []nodecorev1alpha1.Flavour{} + + for len(flavours) > 0 { + mergedFlavour := flavours[0] + mergedFlavour.Spec.QuantityAvailable = 1 + + mergedFlavour.Spec.Characteristics.Memory.Set( + (mergedFlavour.Spec.Characteristics.Memory.Value() / + mergedFlavour.Spec.Policy.Partitionable.MemoryStep.Value()) * + mergedFlavour.Spec.Policy.Partitionable.MemoryStep.Value(), + ) + + mergedFlavour.Spec.Characteristics.Cpu.Set( + (mergedFlavour.Spec.Characteristics.Cpu.Value() / + mergedFlavour.Spec.Policy.Partitionable.CpuStep.Value()) * + mergedFlavour.Spec.Policy.Partitionable.CpuStep.Value(), + ) + + flavours = flavours[1:] + + for i := len(flavours) - 1; i >= 0; i-- { + if canMergeFlavours(&mergedFlavour, &flavours[i]) { + mergedFlavour.Spec.QuantityAvailable += flavours[i].Spec.QuantityAvailable + flavours = append(flavours[:i], flavours[i+1:]...) + } + } + + mergedFlavours = append(mergedFlavours, mergedFlavour) + } + + return mergedFlavours +} + func Start(ctx context.Context, cl client.Client) error { klog.Info("Getting FLUIDOS Node identity...") @@ -47,24 +151,44 @@ func Start(ctx context.Context, cl client.Client) error { return fmt.Errorf("error getting FLUIDOS Node identity") } - klog.Info("Getting nodes resources...") - nodes, err := GetNodesResources(ctx, cl) + // Check current flavours + flavours := &nodecorev1alpha1.FlavourList{} + err := cl.List(ctx, flavours) if err != nil { - log.Printf("Error getting nodes resources: %v", err) + log.Printf("Error getting flavours: %v", err) return err } - klog.Infof("Creating Flavours: found %d nodes", len(nodes)) - - // For each node create a Flavour - for i := range nodes { - flavour := resourceforge.ForgeFlavourFromMetrics(&nodes[i], *nodeIdentity) - err := cl.Create(ctx, flavour) + if len(flavours.Items) == 0 { + // Verify with current node status + klog.Info("Getting nodes resources...") + nodes, err := GetNodesResources(ctx, cl) if err != nil { - log.Printf("Error creating Flavour: %v", err) + log.Printf("Error getting nodes resources: %v", err) return err } - klog.Infof("Flavour created: %s", flavour.Name) + + klog.Infof("Creating Flavours: found %d nodes", len(nodes)) + + forgedFlavours := []nodecorev1alpha1.Flavour{} + + // For each node create a Flavour + // This creates a Flavour + for i := range nodes { + flavour := resourceforge.ForgeFlavourFromMetrics(&nodes[i], *nodeIdentity) + forgedFlavours = append(forgedFlavours, *flavour) + } + + // Perform Flavour merge + mergedFlavours := mergeFlavours(forgedFlavours) + + for i := range mergedFlavours { + if err := cl.Create(ctx, &mergedFlavours[i]); err != nil { + log.Printf("Error creating Flavour: %v", err) + return err + } + klog.Infof("Flavour created: %s", mergedFlavours[i].Name) + } } return nil diff --git a/pkg/utils/models/models.go b/pkg/utils/models/models.go index b631ed6..c8e84f3 100644 --- a/pkg/utils/models/models.go +++ b/pkg/utils/models/models.go @@ -22,15 +22,16 @@ import ( // Flavour represents a Flavour object with its characteristics and policies. type Flavour struct { - FlavourID string `json:"flavourID"` - ProviderID string `json:"providerID"` - Type string `json:"type"` - Characteristics Characteristics `json:"characteristics"` - Policy Policy `json:"policy"` - Owner NodeIdentity `json:"owner"` - Price Price `json:"price"` - ExpirationTime time.Time `json:"expirationTime"` - OptionalFields OptionalFields `json:"optionalFields"` + FlavourID string `json:"flavourID"` + ProviderID string `json:"providerID"` + Type string `json:"type"` + Characteristics Characteristics `json:"characteristics"` + Policy Policy `json:"policy"` + Owner NodeIdentity `json:"owner"` + Price Price `json:"price"` + ExpirationTime time.Time `json:"expirationTime"` + QuantityAvailable int `json:"quantityAvailable,omitempty"` + OptionalFields OptionalFields `json:"optionalFields"` } // Characteristics represents the characteristics of a Flavour, such as CPU and RAM. diff --git a/pkg/utils/resourceforge/forge.go b/pkg/utils/resourceforge/forge.go index d7bdb00..9e9cb62 100644 --- a/pkg/utils/resourceforge/forge.go +++ b/pkg/utils/resourceforge/forge.go @@ -147,16 +147,30 @@ func ForgeContract(flavour *nodecorev1alpha1.Flavour, transaction *models.Transa } } +// ForgePartitionable creates the partition characteristics of a flavour. +func ForgePartitionable() (partitionable *nodecorev1alpha1.Partitionable) { + return &nodecorev1alpha1.Partitionable{ + CpuMin: parseutil.ParseQuantityFromString(flags.CPUMin), + MemoryMin: parseutil.ParseQuantityFromString(flags.MemoryMin), + PodsMin: parseutil.ParseQuantityFromString(flags.PodsMin), + CpuStep: parseutil.ParseQuantityFromString(flags.CPUStep), + MemoryStep: parseutil.ParseQuantityFromString(flags.MemoryStep), + PodsStep: parseutil.ParseQuantityFromString(flags.PodsStep), + } +} + // ForgeFlavourFromMetrics creates a new flavour custom resource from the metrics of the node. func ForgeFlavourFromMetrics(node *models.NodeInfo, ni nodecorev1alpha1.NodeIdentity) (flavour *nodecorev1alpha1.Flavour) { + // TODO MinCount and MaxCount error handeling return &nodecorev1alpha1.Flavour{ ObjectMeta: metav1.ObjectMeta{ Name: namings.ForgeFlavourName(node.UID, "", ni.Domain), Namespace: flags.FluidoNamespace, }, Spec: nodecorev1alpha1.FlavourSpec{ - ProviderID: ni.NodeID, - Type: nodecorev1alpha1.K8S, + ProviderID: ni.NodeID, + Type: nodecorev1alpha1.K8S, + QuantityAvailable: 1, Characteristics: nodecorev1alpha1.Characteristics{ Architecture: node.Architecture, Cpu: node.ResourceMetrics.CPUAvailable, @@ -167,14 +181,7 @@ func ForgeFlavourFromMetrics(node *models.NodeInfo, ni nodecorev1alpha1.NodeIden Gpu: parseutil.ParseQuantityFromString("0"), }, Policy: nodecorev1alpha1.Policy{ - Partitionable: &nodecorev1alpha1.Partitionable{ - CpuMin: parseutil.ParseQuantityFromString(flags.CPUMin), - MemoryMin: parseutil.ParseQuantityFromString(flags.MemoryMin), - PodsMin: parseutil.ParseQuantityFromString(flags.PodsMin), - CpuStep: parseutil.ParseQuantityFromString(flags.CPUStep), - MemoryStep: parseutil.ParseQuantityFromString(flags.MemoryStep), - PodsStep: parseutil.ParseQuantityFromString(flags.PodsStep), - }, + Partitionable: ForgePartitionable(), Aggregatable: &nodecorev1alpha1.Aggregatable{ MinCount: int(flags.MinCount), MaxCount: int(flags.MaxCount), @@ -189,6 +196,7 @@ func ForgeFlavourFromMetrics(node *models.NodeInfo, ni nodecorev1alpha1.NodeIden OptionalFields: nodecorev1alpha1.OptionalFields{ Availability: true, // This previously was the node UID that maybe is not the best choice to manage the scheduling + // Does this make any sense in the case we want to aggregate the Flavours? WorkerID: node.Name, }, }, @@ -203,12 +211,13 @@ func ForgeFlavourFromRef(f *nodecorev1alpha1.Flavour, char *nodecorev1alpha1.Cha Namespace: flags.FluidoNamespace, }, Spec: nodecorev1alpha1.FlavourSpec{ - ProviderID: f.Spec.ProviderID, - Type: f.Spec.Type, - Characteristics: *char, - Policy: f.Spec.Policy, - Owner: f.Spec.Owner, - Price: f.Spec.Price, + ProviderID: f.Spec.ProviderID, + Type: f.Spec.Type, + Characteristics: *char, + Policy: f.Spec.Policy, + Owner: f.Spec.Owner, + Price: f.Spec.Price, + QuantityAvailable: f.Spec.QuantityAvailable, OptionalFields: nodecorev1alpha1.OptionalFields{ Availability: true, WorkerID: f.Spec.OptionalFields.WorkerID, @@ -359,8 +368,9 @@ func ForgeFlavourFromObj(flavour *models.Flavour) *nodecorev1alpha1.Flavour { Namespace: flags.FluidoNamespace, }, Spec: nodecorev1alpha1.FlavourSpec{ - ProviderID: flavour.Owner.NodeID, - Type: nodecorev1alpha1.K8S, + ProviderID: flavour.Owner.NodeID, + Type: nodecorev1alpha1.K8S, + QuantityAvailable: flavour.QuantityAvailable, Characteristics: nodecorev1alpha1.Characteristics{ Cpu: flavour.Characteristics.CPU, Memory: flavour.Characteristics.Memory,