Skip to content

Commit

Permalink
ehpa recmmendation
Browse files Browse the repository at this point in the history
  • Loading branch information
qmhu committed Mar 10, 2022
1 parent a909e4e commit 5531fc4
Show file tree
Hide file tree
Showing 23 changed files with 805 additions and 117 deletions.
9 changes: 8 additions & 1 deletion deploy/craned/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,16 @@ data:
configs:
- targets: []
properties:
cpu-request-percentile: "0.98"
resource.cpu-request-percentile: "0.98"
ehpa.deployment-min-replicas: "1"
ehpa.statefulset-min-replicas: "1"
ehpa.workload-min-replicas: "1"
ehpa.pod-min-ready-seconds: "30"
ehpa.pod-available-ratio: "0.5"
ehpa.default-min-replicas: "2"
ehpa.max-replicas-factor: "3"
ehpa.min-cpu-usage-threshold: "10"
ehpa.fluctuation-threshold: "3"
ehpa.min-cpu-target-utilization: "30"
ehpa.max-cpu-target-utilization: "75"
ehpa.reference-hpa: "true"
14 changes: 13 additions & 1 deletion examples/config_set.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,16 @@ kind: ConfigSet
configs:
- targets: []
properties:
cpu-request-percentile: "0.98"
resource.cpu-request-percentile: "0.98"
ehpa.deployment-min-replicas: "1"
ehpa.statefulset-min-replicas: "1"
ehpa.workload-min-replicas: "1"
ehpa.pod-min-ready-seconds: "30"
ehpa.pod-available-ratio: "0.5"
ehpa.default-min-replicas: "2"
ehpa.max-replicas-factor: "3"
ehpa.min-cpu-usage-threshold: "10"
ehpa.fluctuation-threshold: "3"
ehpa.min-cpu-target-utilization: "30"
ehpa.max-cpu-target-utilization: "75"
ehpa.reference-hpa: "true"
33 changes: 17 additions & 16 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.17

require (
github.com/go-echarts/go-echarts/v2 v2.2.4
github.com/gocrane/api v0.2.1-0.20220308075341-c4f28c1981e6
github.com/gocrane/api v0.2.1-0.20220309033244-699efd59d009
github.com/google/cadvisor v0.39.2
github.com/mjibson/go-dsp v0.0.0-20180508042940-11479a337f12
github.com/prometheus/client_golang v1.11.0
Expand All @@ -31,6 +31,22 @@ require (
)

require (
github.com/StackExchange/wmi v1.2.1 // indirect
github.com/gin-contrib/cors v1.3.1
github.com/gin-contrib/pprof v1.3.0
github.com/gin-gonic/gin v1.7.7
github.com/golang/mock v1.5.0
github.com/grafana-tools/sdk v0.0.0-20211220201350-966b3088eec9
github.com/montanaflynn/stats v0.6.6
github.com/tklauser/go-sysconf v0.3.9 // indirect
github.com/zsais/go-gin-prometheus v0.1.0
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
gopkg.in/gcfg.v1 v1.2.0
k8s.io/kube-openapi v0.0.0-20210817084001-7fbd8d59e5b8 // indirect
)

require (
github.com/Microsoft/go-winio v0.5.1 // indirect
github.com/NYTimes/gziphandler v1.1.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
Expand Down Expand Up @@ -139,23 +155,8 @@ require (
sigs.k8s.io/yaml v1.2.0 // indirect
)

require (
github.com/StackExchange/wmi v1.2.1 // indirect
github.com/gin-contrib/cors v1.3.1
github.com/gin-contrib/pprof v1.3.0
github.com/gin-gonic/gin v1.7.7
github.com/golang/mock v1.5.0
github.com/grafana-tools/sdk v0.0.0-20211220201350-966b3088eec9
github.com/tklauser/go-sysconf v0.3.9 // indirect
github.com/zsais/go-gin-prometheus v0.1.0
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
gopkg.in/gcfg.v1 v1.2.0
k8s.io/kube-openapi v0.0.0-20210817084001-7fbd8d59e5b8 // indirect
)

require (
cloud.google.com/go v0.84.0 // indirect
github.com/Microsoft/go-winio v0.5.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
Expand Down
10 changes: 6 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -306,10 +306,10 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.1.0-rc.5 h1:QOAag7FoBaBYYHRqzqkhhd8fq5RTubvI4v3Ft/gDVVQ=
github.com/gobwas/ws v1.1.0-rc.5/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0=
github.com/gocrane/api v0.2.0 h1:6e8urUDsN8dx/KvLk01m8uMsox+2Y09N9rm6H9AquIM=
github.com/gocrane/api v0.2.0/go.mod h1:GxI+t9AW8+NsHkz2JkPBIJN//9eLUjTZl1ScYAbXMbk=
github.com/gocrane/api v0.2.1-0.20220308075341-c4f28c1981e6 h1:++2IaGyBIoUff3082SG7BNjBR4Nn/X4qjH6+bnzJIv4=
github.com/gocrane/api v0.2.1-0.20220308075341-c4f28c1981e6/go.mod h1:GxI+t9AW8+NsHkz2JkPBIJN//9eLUjTZl1ScYAbXMbk=
github.com/gocrane/api v0.2.1-0.20220307082411-6171d03c2dc5 h1:Q8ZYSeMCoz8VLor5nFZ8BIVuKVm+KgnfSMOr+XTLyOU=
github.com/gocrane/api v0.2.1-0.20220307082411-6171d03c2dc5/go.mod h1:GxI+t9AW8+NsHkz2JkPBIJN//9eLUjTZl1ScYAbXMbk=
github.com/gocrane/api v0.2.1-0.20220309033244-699efd59d009 h1:xd175jH+TT03ea52N4187vD5uoZmHQUAvMWTUPOIj2Y=
github.com/gocrane/api v0.2.1-0.20220309033244-699efd59d009/go.mod h1:GxI+t9AW8+NsHkz2JkPBIJN//9eLUjTZl1ScYAbXMbk=
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
Expand Down Expand Up @@ -556,6 +556,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mohae/deepcopy v0.0.0-20170603005431-491d3605edfb/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4=
github.com/montanaflynn/stats v0.6.6 h1:Duep6KMIDpY4Yo11iFsvyqJDyfzLF9+sndUKT+v64GQ=
github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mrunalp/fileutils v0.5.0 h1:NKzVxiH7eSk+OQ4M+ZYW1K6h27RUV3MI6NUTsHhU6Z4=
Expand Down
2 changes: 0 additions & 2 deletions pkg/controller/analytics/analytics_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -358,8 +358,6 @@ func (c *Controller) GetIdentities(ctx context.Context, analytics *analysisv1alp

func (c *Controller) UpdateStatus(ctx context.Context, analytics *analysisv1alph1.Analytics, newStatus *analysisv1alph1.AnalyticsStatus) {
if !equality.Semantic.DeepEqual(&analytics.Status, newStatus) {
klog.V(4).Infof("Analytics status should be updated, currentStatus %v newStatus %v", &analytics.Status, newStatus)

analytics.Status = *newStatus
err := c.Update(ctx, analytics)
if err != nil {
Expand Down
2 changes: 0 additions & 2 deletions pkg/controller/ehpa/effective_hpa_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,6 @@ func (c *EffectiveHPAController) Reconcile(ctx context.Context, req ctrl.Request

func (c *EffectiveHPAController) UpdateStatus(ctx context.Context, ehpa *autoscalingapi.EffectiveHorizontalPodAutoscaler, newStatus *autoscalingapi.EffectiveHorizontalPodAutoscalerStatus) {
if !equality.Semantic.DeepEqual(&ehpa.Status, newStatus) {
klog.V(4).Infof("EffectiveHorizontalPodAutoscaler status should be updated, currentStatus %v newStatus %v", &ehpa.Status, newStatus)

ehpa.Status = *newStatus
err := c.Status().Update(ctx, ehpa)
if err != nil {
Expand Down
4 changes: 1 addition & 3 deletions pkg/controller/ehpa/hpa.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func (c *EffectiveHPAController) CreateHPA(ctx context.Context, ehpa *autoscalin
hpa, err := c.NewHPAObject(ctx, ehpa, substitute)
if err != nil {
c.Recorder.Event(ehpa, v1.EventTypeNormal, "FailedCreateHPAObject", err.Error())
klog.Errorf("Failed to create object, HorizontalPodAutoscaler %s error %v", hpa, err)
klog.Errorf("Failed to create object, HorizontalPodAutoscaler %s error %v", klog.KObj(hpa), err)
return nil, err
}

Expand Down Expand Up @@ -160,8 +160,6 @@ func (c *EffectiveHPAController) UpdateHPAIfNeed(ctx context.Context, ehpa *auto
}

if needUpdate {
klog.V(4).Infof("HorizontalPodAutoscaler is unsynced according to EffectiveHorizontalPodAutoscaler, should be updated, currentHPA %v expectHPA %v", hpaExist, hpa)

err := c.Update(ctx, hpaExist)
if err != nil {
c.Recorder.Event(ehpa, v1.EventTypeNormal, "FailedUpdateHPA", err.Error())
Expand Down
4 changes: 1 addition & 3 deletions pkg/controller/ehpa/predict.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,11 @@ func (c *EffectiveHPAController) UpdatePredictionIfNeed(ctx context.Context, ehp
prediction, err := c.NewPredictionObject(ehpa)
if err != nil {
c.Recorder.Event(ehpa, v1.EventTypeNormal, "FailedCreatePredictionObject", err.Error())
klog.Errorf("Failed to create object, TimeSeriesPrediction %s error %v", prediction, err)
klog.Errorf("Failed to create object, TimeSeriesPrediction %s error %v", klog.KObj(prediction), err)
return nil, err
}

if !equality.Semantic.DeepEqual(&predictionExist.Spec, &prediction.Spec) {
klog.V(4).Infof("TimeSeriesPrediction is unsynced according to EffectiveHorizontalPodAutoscaler, should be updated, currentTimeSeriesPrediction %v expectTimeSeriesPrediction %v", predictionExist.Spec, prediction.Spec)

predictionExist.Spec = prediction.Spec
err := c.Update(ctx, predictionExist)
if err != nil {
Expand Down
2 changes: 0 additions & 2 deletions pkg/controller/ehpa/substitute.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,6 @@ func (c *EffectiveHPAController) NewSubstituteObject(ehpa *autoscalingapi.Effect

func (c *EffectiveHPAController) UpdateSubstituteIfNeed(ctx context.Context, ehpa *autoscalingapi.EffectiveHorizontalPodAutoscaler, substituteExist *autoscalingapi.Substitute, scale *autoscalingapiv1.Scale) (*autoscalingapi.Substitute, error) {
if !equality.Semantic.DeepEqual(&substituteExist.Spec.SubstituteTargetRef, &ehpa.Spec.ScaleTargetRef) {
klog.V(4).Infof("Substitute is unsynced according to EffectiveHorizontalPodAutoscaler, should be updated, currentTarget %v expectTarget %v", substituteExist.Spec.SubstituteTargetRef, ehpa.Spec.ScaleTargetRef)

substituteExist.Spec.SubstituteTargetRef = ehpa.Spec.ScaleTargetRef
err := c.Update(ctx, substituteExist)
if err != nil {
Expand Down
2 changes: 0 additions & 2 deletions pkg/controller/ehpa/substitute_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,6 @@ func (c *SubstituteController) Reconcile(ctx context.Context, req ctrl.Request)
}

if !equality.Semantic.DeepEqual(&substitute.Status, &newStatus) {
klog.V(4).Infof("Substitute status should be updated", "current %v new %v", substitute.Status, newStatus)

substitute.Status = newStatus
err := c.Status().Update(ctx, substitute)
if err != nil {
Expand Down
30 changes: 16 additions & 14 deletions pkg/controller/recommendation/recommendation_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"fmt"
"time"

"gopkg.in/yaml.v2"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/meta"
Expand Down Expand Up @@ -56,6 +55,11 @@ func (c *Controller) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
return ctrl.Result{}, nil
}

// defaulting for TargetRef.Namespace
if recommendation.Spec.TargetRef.Namespace == "" {
recommendation.Spec.TargetRef.Namespace = recommendation.Namespace
}

c.DoRecommend(ctx, recommendation)

if recommendation.Spec.CompletionStrategy.CompletionStrategyType == analysisv1alph1.CompletionStrategyPeriodical {
Expand Down Expand Up @@ -98,7 +102,7 @@ func (c *Controller) DoRecommend(ctx context.Context, recommendation *analysisv1

recommender, err := recommend.NewRecommender(c.Client, c.RestMapper, c.ScaleClient, recommendation, c.Predictors, c.Provider, c.ConfigSet)
if err != nil {
c.Recorder.Event(recommendation, v1.EventTypeNormal, "FailedCreateRecommender", err.Error())
c.Recorder.Event(recommendation, v1.EventTypeWarning, "FailedCreateRecommender", err.Error())
msg := fmt.Sprintf("Failed to create recommender, Recommendation %s error %v", klog.KObj(recommendation), err)
klog.Errorf(msg)
setReadyCondition(newStatus, metav1.ConditionFalse, "FailedCreateRecommender", msg)
Expand All @@ -108,22 +112,22 @@ func (c *Controller) DoRecommend(ctx context.Context, recommendation *analysisv1

proposed, err := recommender.Offer()
if err != nil {
c.Recorder.Event(recommendation, v1.EventTypeNormal, "FailedOfferRecommendation", err.Error())
c.Recorder.Event(recommendation, v1.EventTypeWarning, "FailedOfferRecommendation", err.Error())
msg := fmt.Sprintf("Failed to offer recommend, Recommendation %s: %v", klog.KObj(recommendation), err)
klog.Errorf(msg)
setReadyCondition(newStatus, metav1.ConditionFalse, "FailedOfferRecommend", msg)
c.UpdateStatus(ctx, recommendation, newStatus)
return
}

if proposed != nil {
if proposed.ResourceRequest != nil {
val, _ := yaml.Marshal(proposed.ResourceRequest)
newStatus.RecommendedValue = string(val)
} else if proposed.EffectiveHPA != nil {
val, _ := yaml.Marshal(proposed.EffectiveHPA)
newStatus.RecommendedValue = string(val)
}
err = c.UpdateRecommendation(ctx, recommendation, proposed, newStatus)
if err != nil {
c.Recorder.Event(recommendation, v1.EventTypeWarning, "FailedUpdateRecommendationValue", err.Error())
msg := fmt.Sprintf("Failed to update recommendation value, Recommendation %s: %v", klog.KObj(recommendation), err)
klog.Errorf(msg)
setReadyCondition(newStatus, metav1.ConditionFalse, "FailedUpdateRecommendationValue", msg)
c.UpdateStatus(ctx, recommendation, newStatus)
return
}

setReadyCondition(newStatus, metav1.ConditionTrue, "RecommendationReady", "Recommendation is ready")
Expand All @@ -132,15 +136,13 @@ func (c *Controller) DoRecommend(ctx context.Context, recommendation *analysisv1

func (c *Controller) UpdateStatus(ctx context.Context, recommendation *analysisv1alph1.Recommendation, newStatus *analysisv1alph1.RecommendationStatus) {
if !equality.Semantic.DeepEqual(&recommendation.Status, newStatus) {
klog.V(4).Infof("Recommendation status should be updated, currentStatus %v newStatus %v", &recommendation.Status, newStatus)

recommendation.Status = *newStatus
timeNow := metav1.Now()
recommendation.Status.LastUpdateTime = &timeNow

err := c.Update(ctx, recommendation)
if err != nil {
c.Recorder.Event(recommendation, v1.EventTypeNormal, "FailedUpdateStatus", err.Error())
c.Recorder.Event(recommendation, v1.EventTypeWarning, "FailedUpdateStatus", err.Error())
klog.Errorf("Failed to update status, Recommendation %s error %v", klog.KObj(recommendation), err)
return
}
Expand Down
120 changes: 120 additions & 0 deletions pkg/controller/recommendation/updater.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package recommendation

import (
"context"
"fmt"

autoscalingv2 "k8s.io/api/autoscaling/v2beta2"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/equality"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml"

analysisapi "github.com/gocrane/api/analysis/v1alpha1"
autoscalingapi "github.com/gocrane/api/autoscaling/v1alpha1"

"github.com/gocrane/crane/pkg/known"
"github.com/gocrane/crane/pkg/recommend/types"
"github.com/gocrane/crane/pkg/utils"
)

func (c *Controller) UpdateRecommendation(ctx context.Context, recommendation *analysisapi.Recommendation, proposed *types.ProposedRecommendation, status *analysisapi.RecommendationStatus) error {
var value string
if proposed.ResourceRequest != nil {
valueBytes, err := yaml.Marshal(proposed.ResourceRequest)
if err != nil {
return err
}
value = string(valueBytes)
} else if proposed.EffectiveHPA != nil {
valueBytes, err := yaml.Marshal(proposed.EffectiveHPA)
if err != nil {
return err
}
value = string(valueBytes)
}

status.RecommendedValue = value
if recommendation.Spec.AdoptionType == analysisapi.AdoptionTypeStatus {
return nil
}

unstructed := &unstructured.Unstructured{}
unstructed.SetAPIVersion(recommendation.Spec.TargetRef.APIVersion)
unstructed.SetKind(recommendation.Spec.TargetRef.Kind)
err := c.Client.Get(ctx, client.ObjectKey{Name: recommendation.Spec.TargetRef.Name, Namespace: recommendation.Spec.TargetRef.Namespace}, unstructed)
if err != nil {
return fmt.Errorf("Get target object failed: %v. ", err)
}

if recommendation.Spec.AdoptionType == analysisapi.AdoptionTypeStatusAndAnnotation || recommendation.Spec.AdoptionType == analysisapi.AdoptionTypeAuto {
annotation := unstructed.GetAnnotations()
if annotation == nil {
annotation = map[string]string{}
}

annotation[known.RecommendationValueAnnotation] = value
unstructed.SetAnnotations(annotation)
err = c.Client.Update(ctx, unstructed)
if err != nil {
return fmt.Errorf("Update target annotation failed: %v. ", err)
}
}

// Only support Auto Type for EHPA recommendation
if recommendation.Spec.AdoptionType == analysisapi.AdoptionTypeAuto && proposed.EffectiveHPA != nil {
ehpa, err := utils.GetEHPAFromScaleTarget(ctx, c.Client, recommendation.Namespace, recommendation.Spec.TargetRef)
if err != nil {
return fmt.Errorf("Get EHPA from target failed: %v. ", err)
}
if ehpa == nil {
ehpa = &autoscalingapi.EffectiveHorizontalPodAutoscaler{
ObjectMeta: metav1.ObjectMeta{
Namespace: recommendation.Spec.TargetRef.Namespace,
Name: recommendation.Spec.TargetRef.Name,
},
Spec: autoscalingapi.EffectiveHorizontalPodAutoscalerSpec{
MinReplicas: proposed.EffectiveHPA.MinReplicas,
MaxReplicas: *proposed.EffectiveHPA.MaxReplicas,
Metrics: proposed.EffectiveHPA.Metrics,
ScaleStrategy: autoscalingapi.ScaleStrategyPreview,
Prediction: proposed.EffectiveHPA.Prediction,
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
Kind: recommendation.Spec.TargetRef.Kind,
APIVersion: recommendation.Spec.TargetRef.APIVersion,
Name: recommendation.Spec.TargetRef.Name,
},
},
}

err = c.Client.Create(ctx, ehpa)
if err == nil {
c.Recorder.Event(ehpa, v1.EventTypeNormal, "UpdateValue", "Created EffectiveHorizontalPodAutoscaler.")
klog.Infof("Create EffectiveHorizontalPodAutoscaler successfully, recommendation %s", klog.KObj(recommendation))
}
return err
} else {
// we don't override ScaleStrategy, because we always use preview to be the default version,
// if user change it, we don't want to override it.
// The reason for Prediction is the same.
ehpaUpdate := ehpa.DeepCopy()
ehpaUpdate.Spec.MinReplicas = proposed.EffectiveHPA.MinReplicas
ehpaUpdate.Spec.MaxReplicas = *proposed.EffectiveHPA.MaxReplicas
ehpaUpdate.Spec.Metrics = proposed.EffectiveHPA.Metrics

if !equality.Semantic.DeepEqual(&ehpaUpdate.Spec, &ehpa.Spec) {
err = c.Client.Update(ctx, ehpaUpdate)
if err == nil {
c.Recorder.Event(ehpa, v1.EventTypeNormal, "UpdateValue", "Updated EffectiveHorizontalPodAutoscaler.")
klog.Infof("Update EffectiveHorizontalPodAutoscaler successfully, recommendation %s", klog.KObj(recommendation))
}
return err
}
}
}

return nil
}
4 changes: 4 additions & 0 deletions pkg/known/annotation.go
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
package known

const (
RecommendationValueAnnotation = "analysis.crane.io/recommendation-value"
)
Loading

0 comments on commit 5531fc4

Please sign in to comment.