Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Machine classification #119

Merged
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
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