Skip to content

Commit

Permalink
Machine classification (#119)
Browse files Browse the repository at this point in the history
* classification types

Signed-off-by: Artem Bortnikov <[email protected]>

* aggregate controller

Signed-off-by: Artem Bortnikov <[email protected]>

* size controller

Signed-off-by: Artem Bortnikov <[email protected]>

* size controller

Signed-off-by: Artem Bortnikov <[email protected]>

* flags to enable controllers

Signed-off-by: Artem Bortnikov <[email protected]>

* update size labels on machine

Signed-off-by: Artem Bortnikov <[email protected]>

* chore updates for types and controllers

Signed-off-by: Artem Bortnikov <[email protected]>

* fix lint

Signed-off-by: Artem Bortnikov <[email protected]>

---------

Signed-off-by: Artem Bortnikov <[email protected]>
  • Loading branch information
aobort authored Jun 13, 2024
1 parent ae2dfcd commit c84ddd7
Show file tree
Hide file tree
Showing 32 changed files with 3,186 additions and 13 deletions.
16 changes: 16 additions & 0 deletions PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,20 @@ resources:
kind: Inventory
path: github.com/ironcore-dev/metal/api/v1alpha1
version: v1alpha1
- api:
crdVersion: v1
controller: true
domain: ironcore.dev
group: metal
kind: Size
path: github.com/ironcore-dev/metal/api/v1alpha1
version: v1alpha1
- api:
crdVersion: v1
controller: true
domain: ironcore.dev
group: metal
kind: Aggregate
path: github.com/ironcore-dev/metal/api/v1alpha1
version: v1alpha1
version: "3"
131 changes: 131 additions & 0 deletions api/v1alpha1/aggregate_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package v1alpha1

import (
"github.com/pkg/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

type AggregateType string

const (
AverageAggregateType AggregateType = "avg"
SumAggregateType AggregateType = "sum"
MinAggregateType AggregateType = "min"
MaxAggregateType AggregateType = "max"
CountAggregateType AggregateType = "count"
)

type AggregateItem struct {
// SourcePath is a path in Inventory spec aggregate will be applied to
// +kubebuilder:validation:Required
SourcePath JSONPath `json:"sourcePath"`
// TargetPath is a path in Inventory status `computed` field
// +kubebuilder:validation:Required
TargetPath JSONPath `json:"targetPath"`
// Aggregate defines whether collection values should be aggregated
// for constraint checks, in case if path defines selector for collection
// +kubebuilder:validation:Optional
// +kubebuilder:validation:Enum=avg;sum;min;max;count
Aggregate AggregateType `json:"aggregate,omitempty"`
}

// AggregateSpec defines the desired state of Aggregate.
type AggregateSpec struct {
// Aggregates is a list of aggregates required to be computed
// +kubebuilder:validation:Required
// +kubebuilder:validation:MinItems=1
Aggregates []AggregateItem `json:"aggregates"`
}

// AggregateStatus defines the observed state of Aggregate.
type AggregateStatus struct{}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +genclient

// Aggregate is the Schema for the aggregates API.
type Aggregate struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec AggregateSpec `json:"spec,omitempty"`
Status AggregateStatus `json:"status,omitempty"`
}

func init() {
SchemeBuilder.Register(&Aggregate{}, &AggregateList{})
}

// +kubebuilder:object:root=true
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

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

func (in *Aggregate) Compute(inventory *Inventory) (interface{}, error) {
resultMap := make(map[string]interface{})

for _, ai := range in.Spec.Aggregates {
jp, err := ai.SourcePath.ToK8sJSONPath()
if err != nil {
return nil, err
}

jp.AllowMissingKeys(true)
data, err := jp.FindResults(inventory)
if err != nil {
return nil, err
}

var aggregatedValue interface{} = nil
tokenizedPath := ai.TargetPath.Tokenize()

dataLen := len(data)
if dataLen == 0 {
if err := setValueToPath(resultMap, tokenizedPath, aggregatedValue); err != nil {
return nil, err
}
continue
}
if dataLen > 1 {
return nil, errors.New("expected only one value collection to be returned for aggregation")
}

values := data[0]
valuesLen := len(values)

if valuesLen == 0 {
if err := setValueToPath(resultMap, tokenizedPath, aggregatedValue); err != nil {
return nil, err
}
continue
}

if ai.Aggregate == "" {
interfacedValues := make([]interface{}, valuesLen)
for i, value := range values {
interfacedValues[i] = value.Interface()
}
aggregatedValue = interfacedValues
} else {
aggregatedValue, err = makeAggregate(ai.Aggregate, values)
if err != nil {
// If we are failing to calculate one aggregate, we can skip it
// and continue to the next one
continue
}
}

if err := setValueToPath(resultMap, tokenizedPath, aggregatedValue); err != nil {
return nil, err
}
}

return resultMap, nil
}
50 changes: 50 additions & 0 deletions api/v1alpha1/aggregation_results.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package v1alpha1

import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/json"
)

// +kubebuilder:validation:Type=object
type AggregationResults struct {
Object map[string]interface{} `json:"-"`
}

func (in *AggregationResults) MarshalJSON() ([]byte, error) {
if in.Object == nil {
return json.Marshal(map[string]interface{}{})
}
return json.Marshal(in.Object)
}

func (in *AggregationResults) UnmarshalJSON(b []byte) error {
stringVal := string(b)
if stringVal == null {
in.Object = nil
return nil
}
if err := json.Unmarshal(b, &in.Object); err != nil {
return err
}

return nil
}

func (in *AggregationResults) DeepCopyInto(out *AggregationResults) {
if in == nil {
out = nil
} else if in.Object == nil {
out.Object = nil
} else {
out.Object = runtime.DeepCopyJSON(in.Object)
}
}

func (in *AggregationResults) DeepCopy() *AggregationResults {
if in == nil {
return nil
}
out := new(AggregationResults)
in.DeepCopyInto(out)
return out
}
6 changes: 4 additions & 2 deletions api/v1alpha1/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ import (
"net/netip"
)

const null = "null"

type Prefix struct {
netip.Prefix `json:"-"`
}

//goland:noinspection GoMixedReceiverTypes
func (p *Prefix) UnmarshalJSON(b []byte) error {
if len(b) == 4 && string(b) == "null" {
if len(b) == 4 && string(b) == null {
p.Prefix = netip.Prefix{}
return nil
}
Expand All @@ -38,7 +40,7 @@ func (p *Prefix) UnmarshalJSON(b []byte) error {
//goland:noinspection GoMixedReceiverTypes
func (p *Prefix) MarshalJSON() ([]byte, error) {
if p.IsZero() {
return []byte("null"), nil
return []byte(null), nil
}

return json.Marshal(p.String())
Expand Down
132 changes: 132 additions & 0 deletions api/v1alpha1/constraint_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package v1alpha1

import (
"reflect"

"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/api/resource"
)

// ConstraintSpec contains conditions of constraint that should be applied on resource.
type ConstraintSpec struct {
// Path is a path to the struct field constraint will be applied to
// +kubebuilder:validation:Optional
Path JSONPath `json:"path,omitempty"`
// Aggregate defines whether collection values should be aggregated
// for constraint checks, in case if path defines selector for collection
// +kubebuilder:validation:Optional
// +kubebuilder:validation:Enum=avg;sum;min;max;count
Aggregate AggregateType `json:"agg,omitempty"`
// Equal contains an exact expected value
// +kubebuilder:validation:Optional
Equal *ConstraintValSpec `json:"eq,omitempty"`
// NotEqual contains an exact not expected value
// +kubebuilder:validation:Optional
NotEqual *ConstraintValSpec `json:"neq,omitempty"`
// LessThan contains an highest expected value, exclusive
// +kubebuilder:validation:Optional
LessThan *resource.Quantity `json:"lt,omitempty"`
// LessThan contains an highest expected value, inclusive
// +kubebuilder:validation:Optional
LessThanOrEqual *resource.Quantity `json:"lte,omitempty"`
// LessThan contains an lowest expected value, exclusive
// +kubebuilder:validation:Optional
GreaterThan *resource.Quantity `json:"gt,omitempty"`
// GreaterThanOrEqual contains an lowest expected value, inclusive
// +kubebuilder:validation:Optional
GreaterThanOrEqual *resource.Quantity `json:"gte,omitempty"`
}

func (in *ConstraintSpec) MatchSingleValue(value *reflect.Value) (bool, error) {
// We do not have special constraints for nil and/or empty values
// so returning false for now if the value is nil
if value.Kind() == reflect.Ptr && value.IsNil() {
return false, nil
}

if in.Equal != nil {
r, err := in.Equal.Compare(value)
if err != nil {
return false, err
}
return r == 0, nil
}
if in.NotEqual != nil {
r, err := in.NotEqual.Compare(value)
if err != nil {
return false, err
}
return r != 0, nil
}

matches := true
q, err := valueToQuantity(value)
if err != nil {
return false, err
}
if in.GreaterThanOrEqual != nil {
r := q.Cmp(*in.GreaterThanOrEqual)
matches = r >= 0
}
if in.GreaterThan != nil {
r := q.Cmp(*in.GreaterThan)
matches = matches && (r > 0)
}
if in.LessThanOrEqual != nil {
r := q.Cmp(*in.LessThanOrEqual)
matches = matches && (r <= 0)
}
if in.LessThan != nil {
r := q.Cmp(*in.LessThan)
matches = matches && (r < 0)
}

return matches, nil
}

func (in *ConstraintSpec) MatchMultipleValues(aggregateType AggregateType, values []reflect.Value) (bool, error) {
if aggregateType == "" {
for _, value := range values {
matches, err := in.MatchSingleValue(&value)
if err != nil {
return false, err
}
if !matches {
return false, nil
}
}
return true, nil
}

agg, err := makeAggregate(aggregateType, values)
if err != nil {
return false, errors.Wrapf(err, "unable to compute aggregate %s", aggregateType)
}

if in.Equal != nil {
return agg.Cmp(*in.Equal.Numeric) == 0, nil
}
if in.NotEqual != nil {
return agg.Cmp(*in.NotEqual.Numeric) != 0, nil
}

matches := true
if in.GreaterThanOrEqual != nil {
r := agg.Cmp(*in.GreaterThanOrEqual)
matches = r >= 0
}
if in.GreaterThan != nil {
r := agg.Cmp(*in.GreaterThan)
matches = matches && (r > 0)
}
if in.LessThanOrEqual != nil {
r := agg.Cmp(*in.LessThanOrEqual)
matches = matches && (r <= 0)
}
if in.LessThan != nil {
r := agg.Cmp(*in.LessThan)
matches = matches && (r < 0)
}

return matches, nil
}
Loading

0 comments on commit c84ddd7

Please sign in to comment.