From 22f31a52f57ac9fcd7e5ce993b4bba6df987732c Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Thu, 26 Sep 2024 16:43:11 +0200 Subject: [PATCH] feat: metrics aggregate (#269) * metrics aggregate * add DebounceReconciler * init global metrics * add unit tests * fix unit test * fix after rebase --- .mockery.yaml | 3 + Makefile | 4 + api/v1alpha1/metricsaggregate_types.go | 64 ++ api/v1alpha1/zz_generated.deepcopy.go | 80 +++ ...ployments.plural.sh_metricsaggregates.yaml | 132 ++++ cmd/agent/kubernetes.go | 12 + cmd/agent/main.go | 15 +- ...ployments.plural.sh_metricsaggregates.yaml | 132 ++++ config/samples/metricsAggregate.yaml | 9 + go.mod | 3 +- go.sum | 2 + internal/controller/debounce_controller.go | 79 +++ internal/controller/metricsaggregate.go | 215 +++++++ .../metricsaggregate_controller_test.go | 108 ++++ internal/controller/suite_test.go | 7 +- pkg/manifests/template/helm_test.go | 2 +- pkg/test/mocks/DiscoveryInterface_mock.go | 595 ++++++++++++++++++ tools.go | 3 +- 18 files changed, 1453 insertions(+), 12 deletions(-) create mode 100644 api/v1alpha1/metricsaggregate_types.go create mode 100644 charts/deployment-operator/crds/deployments.plural.sh_metricsaggregates.yaml create mode 100644 config/crd/bases/deployments.plural.sh_metricsaggregates.yaml create mode 100644 config/samples/metricsAggregate.yaml create mode 100644 internal/controller/debounce_controller.go create mode 100644 internal/controller/metricsaggregate.go create mode 100644 internal/controller/metricsaggregate_controller_test.go create mode 100644 pkg/test/mocks/DiscoveryInterface_mock.go diff --git a/.mockery.yaml b/.mockery.yaml index 037fd3c5..659e1d15 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -6,3 +6,6 @@ packages: github.com/pluralsh/deployment-operator/pkg/client: interfaces: Client: + k8s.io/client-go/discovery: + interfaces: + DiscoveryInterface: \ No newline at end of file diff --git a/Makefile b/Makefile index 8f2ba2ea..83725b69 100644 --- a/Makefile +++ b/Makefile @@ -182,6 +182,10 @@ crd-ref-docs: --tool controller-gen: TOOL = controller-gen controller-gen: --tool +.PHONY: discovery +discovery: TOOL = discovery +discovery: --tool + # go-get-tool will 'go get' any package $2 and install it to $1. PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST)))) define go-get-tool diff --git a/api/v1alpha1/metricsaggregate_types.go b/api/v1alpha1/metricsaggregate_types.go new file mode 100644 index 00000000..a878dd08 --- /dev/null +++ b/api/v1alpha1/metricsaggregate_types.go @@ -0,0 +1,64 @@ +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func init() { + SchemeBuilder.Register(&MetricsAggregate{}, &MetricsAggregateList{}) +} + +// MetricsAggregateList contains a list of [MetricsAggregate] +// +kubebuilder:object:root=true +type MetricsAggregateList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []MetricsAggregate `json:"items"` +} + +// MetricsAggregate +// +kubebuilder:object:root=true +// +kubebuilder:resource:scope=Cluster +// +kubebuilder:subresource:status +type MetricsAggregate struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Status of the MetricsAggregate + // +kubebuilder:validation:Optional + // +kubebuilder:printcolumn:name="Nodes",type=integer,JSONPath=".status.nodes",description="Number of Cluster Nodes" + // +kubebuilder:printcolumn:name="MemoryTotalBytes",type=integer,JSONPath=".status.memoryTotalBytes",description="Memory total bytes" + // +kubebuilder:printcolumn:name="MemoryAvailableBytes",type=integer,JSONPath=".status.memoryAvailableBytes",description="Memory available bytes" + // +kubebuilder:printcolumn:name="MemoryUsedPercentage",type=integer,JSONPath=".status.memoryUsedPercentage",description="Memory used percentage" + // +kubebuilder:printcolumn:name="CPUTotalMillicores",type=integer,JSONPath=".status.cpuTotalMillicores",description="CPU total millicores" + // +kubebuilder:printcolumn:name="CPUAvailableMillicores",type=integer,JSONPath=".status.cpuAvailableMillicores",description="CPU available millicores" + // +kubebuilder:printcolumn:name="CPUUsedPercentage",type=integer,JSONPath=".status.cpuUsedPercentage",description="CPU used percentage" + Status MetricsAggregateStatus `json:"status,omitempty"` +} + +type MetricsAggregateStatus struct { + Nodes int `json:"nodes,omitempty"` + // MemoryTotalBytes current memory usage in bytes + MemoryTotalBytes int64 `json:"memoryTotalBytes,omitempty"` + // MemoryAvailableBytes available memory for node + MemoryAvailableBytes int64 `json:"memoryAvailableBytes,omitempty"` + // MemoryUsedPercentage in percentage + MemoryUsedPercentage int64 `json:"memoryUsedPercentage,omitempty"` + // CPUTotalMillicores in m cores + CPUTotalMillicores int64 `json:"cpuTotalMillicores,omitempty"` + // CPUAvailableMillicores in m cores + CPUAvailableMillicores int64 `json:"cpuAvailableMillicores,omitempty"` + // CPUUsedPercentage in percentage + CPUUsedPercentage int64 `json:"cpuUsedPercentage,omitempty"` + + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` +} + +func (in *MetricsAggregate) SetCondition(condition metav1.Condition) { + meta.SetStatusCondition(&in.Status.Conditions, condition) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 0b99b29e..0ab1c940 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -442,6 +442,86 @@ func (in *IngressReplicaSpec) DeepCopy() *IngressReplicaSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MetricsAggregate) DeepCopyInto(out *MetricsAggregate) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MetricsAggregate. +func (in *MetricsAggregate) DeepCopy() *MetricsAggregate { + if in == nil { + return nil + } + out := new(MetricsAggregate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *MetricsAggregate) 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 *MetricsAggregateList) DeepCopyInto(out *MetricsAggregateList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]MetricsAggregate, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MetricsAggregateList. +func (in *MetricsAggregateList) DeepCopy() *MetricsAggregateList { + if in == nil { + return nil + } + out := new(MetricsAggregateList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *MetricsAggregateList) 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 *MetricsAggregateStatus) DeepCopyInto(out *MetricsAggregateStatus) { + *out = *in + 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 MetricsAggregateStatus. +func (in *MetricsAggregateStatus) DeepCopy() *MetricsAggregateStatus { + if in == nil { + return nil + } + out := new(MetricsAggregateStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PipelineGate) DeepCopyInto(out *PipelineGate) { *out = *in diff --git a/charts/deployment-operator/crds/deployments.plural.sh_metricsaggregates.yaml b/charts/deployment-operator/crds/deployments.plural.sh_metricsaggregates.yaml new file mode 100644 index 00000000..2479fc89 --- /dev/null +++ b/charts/deployment-operator/crds/deployments.plural.sh_metricsaggregates.yaml @@ -0,0 +1,132 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.3 + name: metricsaggregates.deployments.plural.sh +spec: + group: deployments.plural.sh + names: + kind: MetricsAggregate + listKind: MetricsAggregateList + plural: metricsaggregates + singular: metricsaggregate + scope: Cluster + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: MetricsAggregate + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + status: + description: Status of the MetricsAggregate + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + cpuAvailableMillicores: + description: CPUAvailableMillicores in m cores + format: int64 + type: integer + cpuTotalMillicores: + description: CPUTotalMillicores in m cores + format: int64 + type: integer + cpuUsedPercentage: + description: CPUUsedPercentage in percentage + format: int64 + type: integer + memoryAvailableBytes: + description: MemoryAvailableBytes available memory for node + format: int64 + type: integer + memoryTotalBytes: + description: MemoryTotalBytes current memory usage in bytes + format: int64 + type: integer + memoryUsedPercentage: + description: MemoryUsedPercentage in percentage + format: int64 + type: integer + nodes: + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/cmd/agent/kubernetes.go b/cmd/agent/kubernetes.go index b653e36b..2917be27 100644 --- a/cmd/agent/kubernetes.go +++ b/cmd/agent/kubernetes.go @@ -1,6 +1,7 @@ package main import ( + "context" "net/http" "os" "strings" @@ -11,6 +12,7 @@ import ( "github.com/prometheus/client_golang/prometheus/promhttp" velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/discovery" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" @@ -85,10 +87,12 @@ func initKubeClientsOrDie(config *rest.Config) (rolloutsClient *roclientset.Clie } func registerKubeReconcilersOrDie( + ctx context.Context, manager ctrl.Manager, consoleManager *consolectrl.Manager, config *rest.Config, extConsoleClient consoleclient.Client, + discoveryClient discovery.DiscoveryInterface, ) { rolloutsClient, dynamicClient, kubeClient := initKubeClientsOrDie(config) @@ -208,4 +212,12 @@ func registerKubeReconcilersOrDie( setupLog.Error(err, "unable to create controller", "controller", "Group") os.Exit(1) } + + if err := (&controller.MetricsAggregateReconciler{ + Client: manager.GetClient(), + Scheme: manager.GetScheme(), + DiscoveryClient: discoveryClient, + }).SetupWithManager(ctx, manager); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "MetricsAggregate") + } } diff --git a/cmd/agent/main.go b/cmd/agent/main.go index d6d5b608..7edbac11 100644 --- a/cmd/agent/main.go +++ b/cmd/agent/main.go @@ -8,9 +8,15 @@ import ( rolloutv1alpha1 "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" templatesv1 "github.com/open-policy-agent/frameworks/constraint/pkg/apis/templates/v1" constraintstatusv1beta1 "github.com/open-policy-agent/gatekeeper/v3/apis/status/v1beta1" + deploymentsv1alpha1 "github.com/pluralsh/deployment-operator/api/v1alpha1" + "github.com/pluralsh/deployment-operator/cmd/agent/args" + "github.com/pluralsh/deployment-operator/pkg/cache" + "github.com/pluralsh/deployment-operator/pkg/client" + consolectrl "github.com/pluralsh/deployment-operator/pkg/controller" "k8s.io/client-go/discovery" "k8s.io/client-go/rest" "k8s.io/klog/v2" + metricsv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" @@ -18,12 +24,6 @@ import ( utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" - - deploymentsv1alpha1 "github.com/pluralsh/deployment-operator/api/v1alpha1" - "github.com/pluralsh/deployment-operator/cmd/agent/args" - "github.com/pluralsh/deployment-operator/pkg/cache" - "github.com/pluralsh/deployment-operator/pkg/client" - consolectrl "github.com/pluralsh/deployment-operator/pkg/controller" ) var ( @@ -39,6 +39,7 @@ func init() { utilruntime.Must(constraintstatusv1beta1.AddToScheme(scheme)) utilruntime.Must(templatesv1.AddToScheme(scheme)) utilruntime.Must(rolloutv1alpha1.AddToScheme(scheme)) + utilruntime.Must(metricsv1beta1.AddToScheme(scheme)) //+kubebuilder:scaffold:scheme } @@ -60,7 +61,7 @@ func main() { cache.InitGateCache(args.ControllerCacheTTL(), extConsoleClient) registerConsoleReconcilersOrDie(consoleManager, config, kubeManager.GetClient(), extConsoleClient) - registerKubeReconcilersOrDie(kubeManager, consoleManager, config, extConsoleClient) + registerKubeReconcilersOrDie(ctx, kubeManager, consoleManager, config, extConsoleClient, discoveryClient) //+kubebuilder:scaffold:builder diff --git a/config/crd/bases/deployments.plural.sh_metricsaggregates.yaml b/config/crd/bases/deployments.plural.sh_metricsaggregates.yaml new file mode 100644 index 00000000..2479fc89 --- /dev/null +++ b/config/crd/bases/deployments.plural.sh_metricsaggregates.yaml @@ -0,0 +1,132 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.3 + name: metricsaggregates.deployments.plural.sh +spec: + group: deployments.plural.sh + names: + kind: MetricsAggregate + listKind: MetricsAggregateList + plural: metricsaggregates + singular: metricsaggregate + scope: Cluster + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: MetricsAggregate + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + status: + description: Status of the MetricsAggregate + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + cpuAvailableMillicores: + description: CPUAvailableMillicores in m cores + format: int64 + type: integer + cpuTotalMillicores: + description: CPUTotalMillicores in m cores + format: int64 + type: integer + cpuUsedPercentage: + description: CPUUsedPercentage in percentage + format: int64 + type: integer + memoryAvailableBytes: + description: MemoryAvailableBytes available memory for node + format: int64 + type: integer + memoryTotalBytes: + description: MemoryTotalBytes current memory usage in bytes + format: int64 + type: integer + memoryUsedPercentage: + description: MemoryUsedPercentage in percentage + format: int64 + type: integer + nodes: + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/samples/metricsAggregate.yaml b/config/samples/metricsAggregate.yaml new file mode 100644 index 00000000..ed670a18 --- /dev/null +++ b/config/samples/metricsAggregate.yaml @@ -0,0 +1,9 @@ +apiVersion: deployments.plural.sh/v1alpha1 +kind: MetricsAggregate +metadata: + labels: + app.kubernetes.io/name: metrics-aggregate + app.kubernetes.io/instance: metricsaggregate-sample + app.kubernetes.io/part-of: deployment-operator + app.kubernetes.io/created-by: deployment-operator + name: global diff --git a/go.mod b/go.mod index a9867de3..3d2dbe4d 100644 --- a/go.mod +++ b/go.mod @@ -42,7 +42,6 @@ require ( github.com/yuin/gopher-lua v1.1.1 go.uber.org/zap v1.27.0 golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 - golang.org/x/net v0.29.0 gopkg.in/yaml.v3 v3.0.1 helm.sh/helm/v3 v3.16.1 k8s.io/api v0.31.1 @@ -52,6 +51,7 @@ require ( k8s.io/client-go v0.31.1 k8s.io/klog/v2 v2.130.1 k8s.io/kubectl v0.31.1 + k8s.io/metrics v0.31.1 k8s.io/utils v0.0.0-20240902221715-702e33fdd3c3 layeh.com/gopher-luar v1.0.11 sigs.k8s.io/cli-utils v0.37.2 @@ -356,6 +356,7 @@ require ( golang.org/x/crypto v0.27.0 // indirect golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f // indirect golang.org/x/mod v0.21.0 // indirect + golang.org/x/net v0.29.0 // indirect golang.org/x/oauth2 v0.22.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.25.0 // indirect diff --git a/go.sum b/go.sum index e493da48..5ddf5363 100644 --- a/go.sum +++ b/go.sum @@ -1149,6 +1149,8 @@ k8s.io/kube-openapi v0.0.0-20240430033511-f0e62f92d13f h1:0LQagt0gDpKqvIkAMPaRGc k8s.io/kube-openapi v0.0.0-20240430033511-f0e62f92d13f/go.mod h1:S9tOR0FxgyusSNR+MboCuiDpVWkAifZvaYI1Q2ubgro= k8s.io/kubectl v0.31.1 h1:ih4JQJHxsEggFqDJEHSOdJ69ZxZftgeZvYo7M/cpp24= k8s.io/kubectl v0.31.1/go.mod h1:aNuQoR43W6MLAtXQ/Bu4GDmoHlbhHKuyD49lmTC8eJM= +k8s.io/metrics v0.31.1 h1:h4I4dakgh/zKflWYAOQhwf0EXaqy8LxAIyE/GBvxqRc= +k8s.io/metrics v0.31.1/go.mod h1:JuH1S9tJiH9q1VCY0yzSCawi7kzNLsDzlWDJN4xR+iA= k8s.io/utils v0.0.0-20240902221715-702e33fdd3c3 h1:b2FmK8YH+QEwq/Sy2uAEhmqL5nPfGYbJOcaqjeYYZoA= k8s.io/utils v0.0.0-20240902221715-702e33fdd3c3/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= layeh.com/gopher-luar v1.0.11 h1:8zJudpKI6HWkoh9eyyNFaTM79PY6CAPcIr6X/KTiliw= diff --git a/internal/controller/debounce_controller.go b/internal/controller/debounce_controller.go new file mode 100644 index 00000000..4ec1d49e --- /dev/null +++ b/internal/controller/debounce_controller.go @@ -0,0 +1,79 @@ +package controller + +import ( + "context" + "time" + + "sigs.k8s.io/controller-runtime/pkg/log" + + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +// DebounceReconciler is a Reconciler that debounces reconcile requests. +type DebounceReconciler struct { + client.Client + // Minimum time to wait before processing requests. + debounceDuration time.Duration + // Last request time. + lastRequest time.Time + // Channel to trigger reconciliations. + reconcileChan chan reconcile.Request + // The actual reconciler that processes the requests. + actualReconciler reconcile.Reconciler +} + +// NewDebounceReconciler creates a new DebounceReconciler. +func NewDebounceReconciler(client client.Client, duration time.Duration, actual reconcile.Reconciler) *DebounceReconciler { + return &DebounceReconciler{ + Client: client, + debounceDuration: duration, + reconcileChan: make(chan reconcile.Request, 1), + actualReconciler: actual, + } +} + +// Reconcile implements the reconcile.Reconciler interface. +func (r *DebounceReconciler) Reconcile(_ context.Context, req reconcile.Request) (reconcile.Result, error) { + select { + case r.reconcileChan <- req: + default: + // Channel is full, drop the request to avoid spamming. + } + return reconcile.Result{}, nil +} + +// Start begins the debouncing mechanism. +func (r *DebounceReconciler) Start(ctx context.Context) { + logger := log.FromContext(ctx) + go func() { + ticker := time.NewTicker(r.debounceDuration) + defer ticker.Stop() + + var latestRequest reconcile.Request + + for { + select { + case <-ctx.Done(): + return + case req := <-r.reconcileChan: + latestRequest = req + r.lastRequest = time.Now() + case <-ticker.C: + // Check if enough time has passed since the last request. + if time.Since(r.lastRequest) >= r.debounceDuration { + // Process the debounced request. + if err := r.processRequest(ctx, latestRequest); err != nil { + logger.Error(err, "Error processing request: %v\n") + } + } + } + } + }() +} + +// processRequest performs the actual reconciliation. +func (r *DebounceReconciler) processRequest(ctx context.Context, req reconcile.Request) error { + _, err := r.actualReconciler.Reconcile(ctx, req) + return err +} diff --git a/internal/controller/metricsaggregate.go b/internal/controller/metricsaggregate.go new file mode 100644 index 00000000..13fb1054 --- /dev/null +++ b/internal/controller/metricsaggregate.go @@ -0,0 +1,215 @@ +package controller + +import ( + "context" + "fmt" + "time" + + "github.com/pluralsh/deployment-operator/api/v1alpha1" + "github.com/pluralsh/deployment-operator/internal/utils" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/discovery" + metricsapi "k8s.io/metrics/pkg/apis/metrics" + "k8s.io/metrics/pkg/apis/metrics/v1beta1" + ctrl "sigs.k8s.io/controller-runtime" + k8sClient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +const ( + globalName = "global" + debounceDuration = time.Second * 30 +) + +var supportedMetricsAPIVersions = []string{ + "v1beta1", +} + +// MetricsAggregateReconciler reconciles a MetricsAggregate resource. +type MetricsAggregateReconciler struct { + k8sClient.Client + Scheme *runtime.Scheme + DiscoveryClient discovery.DiscoveryInterface +} + +// Reconcile IngressReplica ensure that stays in sync with Kubernetes cluster. +func (r *MetricsAggregateReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ reconcile.Result, reterr error) { + logger := log.FromContext(ctx) + + apiGroups, err := r.DiscoveryClient.ServerGroups() + if err != nil { + return reconcile.Result{}, err + } + metricsAPIAvailable := SupportedMetricsAPIVersionAvailable(apiGroups) + if !metricsAPIAvailable { + logger.V(5).Info("metrics api not available") + return requeue(time.Minute*5, jitter), nil + } + + // Read resource from Kubernetes cluster. + metrics := &v1alpha1.MetricsAggregate{} + if err := r.Get(ctx, req.NamespacedName, metrics); err != nil { + if apierrors.IsNotFound(err) { + if err := r.initGlobalMetricsAggregate(ctx); err != nil { + return ctrl.Result{}, err + } + return requeue(time.Second, jitter), nil + } + return ctrl.Result{}, err + } + + logger.Info("reconciling MetricsAggregate", "namespace", metrics.Namespace, "name", metrics.Name) + utils.MarkCondition(metrics.SetCondition, v1alpha1.ReadyConditionType, metav1.ConditionFalse, v1alpha1.ReadyConditionReason, "") + + scope, err := NewDefaultScope(ctx, r.Client, metrics) + if err != nil { + logger.Error(err, "failed to create scope") + utils.MarkCondition(metrics.SetCondition, v1alpha1.ReadyConditionType, metav1.ConditionFalse, v1alpha1.ReadyConditionReason, err.Error()) + return ctrl.Result{}, err + } + + // Always patch object when exiting this function, so we can persist any object changes. + defer func() { + if err := scope.PatchObject(); err != nil && reterr == nil { + reterr = err + } + }() + + if !metrics.DeletionTimestamp.IsZero() { + return ctrl.Result{}, nil + } + + nodeList := &corev1.NodeList{} + if err := r.List(ctx, nodeList); err != nil { + return reconcile.Result{}, err + } + + availableResources := make(map[string]corev1.ResourceList) + for _, n := range nodeList.Items { + availableResources[n.Name] = n.Status.Allocatable + } + + nodeDeploymentNodesMetrics := make([]v1beta1.NodeMetrics, 0) + allNodeMetricsList := &v1beta1.NodeMetricsList{} + if err := r.List(ctx, allNodeMetricsList); err != nil { + return reconcile.Result{}, err + } + + for _, m := range allNodeMetricsList.Items { + if _, ok := availableResources[m.Name]; ok { + nodeDeploymentNodesMetrics = append(nodeDeploymentNodesMetrics, m) + } + } + + nodeMetrics, err := ConvertNodeMetrics(nodeDeploymentNodesMetrics, availableResources) + if err != nil { + return reconcile.Result{}, err + } + + // save metrics + metrics.Status.Nodes = len(nodeList.Items) + for _, nm := range nodeMetrics { + metrics.Status.CPUAvailableMillicores += nm.CPUAvailableMillicores + metrics.Status.CPUTotalMillicores += nm.CPUTotalMillicores + metrics.Status.MemoryAvailableBytes += nm.MemoryAvailableBytes + metrics.Status.MemoryTotalBytes += nm.MemoryTotalBytes + } + + fraction := float64(metrics.Status.CPUTotalMillicores) / float64(metrics.Status.CPUAvailableMillicores) * 100 + metrics.Status.CPUUsedPercentage = int64(fraction) + fraction = float64(metrics.Status.MemoryTotalBytes) / float64(metrics.Status.MemoryAvailableBytes) * 100 + metrics.Status.MemoryUsedPercentage = int64(fraction) + + utils.MarkCondition(metrics.SetCondition, v1alpha1.ReadyConditionType, metav1.ConditionTrue, v1alpha1.ReadyConditionReason, "") + + return requeue(requeueAfter, jitter), reterr +} + +// SetupWithManager sets up the controller with the Manager. +func (r *MetricsAggregateReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager) error { + debounceReconciler := NewDebounceReconciler(mgr.GetClient(), debounceDuration, r) + debounceReconciler.Start(ctx) + + return ctrl.NewControllerManagedBy(mgr). + For(&v1alpha1.MetricsAggregate{}). + Complete(debounceReconciler) +} + +func (r *MetricsAggregateReconciler) initGlobalMetricsAggregate(ctx context.Context) error { + // Init global MetricsAggregate object + if err := r.Get(ctx, k8sClient.ObjectKey{Name: globalName}, &v1alpha1.MetricsAggregate{}); err != nil { + if apierrors.IsNotFound(err) { + if err := r.Create(ctx, &v1alpha1.MetricsAggregate{ + ObjectMeta: metav1.ObjectMeta{ + Name: globalName, + }, + }); err != nil { + return err + } + } else { + return err + } + } + return nil +} + +type ResourceMetricsInfo struct { + Name string + Metrics corev1.ResourceList + Available corev1.ResourceList +} + +func ConvertNodeMetrics(metrics []v1beta1.NodeMetrics, availableResources map[string]corev1.ResourceList) ([]v1alpha1.MetricsAggregateStatus, error) { + nodeMetrics := make([]v1alpha1.MetricsAggregateStatus, 0) + + if metrics == nil { + return nil, fmt.Errorf("metric list can not be nil") + } + + for _, m := range metrics { + nodeMetric := v1alpha1.MetricsAggregateStatus{} + + resourceMetricsInfo := ResourceMetricsInfo{ + Name: m.Name, + Metrics: m.Usage.DeepCopy(), + Available: availableResources[m.Name], + } + + if available, found := resourceMetricsInfo.Available[corev1.ResourceCPU]; found { + quantityCPU := resourceMetricsInfo.Metrics[corev1.ResourceCPU] + // cpu in mili cores + nodeMetric.CPUTotalMillicores = quantityCPU.MilliValue() + nodeMetric.CPUAvailableMillicores = available.MilliValue() + } + + if available, found := resourceMetricsInfo.Available[corev1.ResourceMemory]; found { + quantityM := resourceMetricsInfo.Metrics[corev1.ResourceMemory] + // memory in bytes + nodeMetric.MemoryTotalBytes = quantityM.Value() / (1024 * 1024) + nodeMetric.MemoryAvailableBytes = available.Value() / (1024 * 1024) + } + nodeMetrics = append(nodeMetrics, nodeMetric) + } + + return nodeMetrics, nil +} + +func SupportedMetricsAPIVersionAvailable(discoveredAPIGroups *metav1.APIGroupList) bool { + for _, discoveredAPIGroup := range discoveredAPIGroups.Groups { + if discoveredAPIGroup.Name != metricsapi.GroupName { + continue + } + for _, version := range discoveredAPIGroup.Versions { + for _, supportedVersion := range supportedMetricsAPIVersions { + if version.Version == supportedVersion { + return true + } + } + } + } + return false +} diff --git a/internal/controller/metricsaggregate_controller_test.go b/internal/controller/metricsaggregate_controller_test.go new file mode 100644 index 00000000..66cf4819 --- /dev/null +++ b/internal/controller/metricsaggregate_controller_test.go @@ -0,0 +1,108 @@ +package controller + +import ( + "context" + + "github.com/pluralsh/deployment-operator/api/v1alpha1" + "github.com/pluralsh/deployment-operator/pkg/test/mocks" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/metrics/pkg/apis/metrics/v1beta1" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +var _ = Describe("MetricsAggregate Controller", Ordered, func() { + Context("When reconciling a resource", func() { + const ( + nodeMetricsName = "node-metrics" + nodeName = "node" + metricsAggregateName = "global" + namespace = "default" + ) + + ctx := context.Background() + + apiGroups := &metav1.APIGroupList{ + Groups: []metav1.APIGroup{ + { + Name: "metrics.k8s.io", + Versions: []metav1.GroupVersionForDiscovery{ + {GroupVersion: "v1", Version: "v1beta1"}, + }, + }, + }, + } + + nodeMetrics := types.NamespacedName{Name: nodeMetricsName, Namespace: namespace} + node := types.NamespacedName{Name: nodeName, Namespace: namespace} + metricsAggregate := types.NamespacedName{Name: metricsAggregateName, Namespace: namespace} + + nm := &v1beta1.NodeMetrics{} + n := &corev1.Node{} + BeforeAll(func() { + By("Creating node metrics") + err := kClient.Get(ctx, nodeMetrics, nm) + if err != nil && errors.IsNotFound(err) { + Expect(kClient.Create(ctx, &v1beta1.NodeMetrics{ + ObjectMeta: metav1.ObjectMeta{ + Name: nodeMetricsName, + Namespace: namespace, + }, + Timestamp: metav1.Time{}, + Window: metav1.Duration{}, + Usage: map[corev1.ResourceName]resource.Quantity{ + "cpu": resource.MustParse("100m"), + "memory": resource.MustParse("100Mi"), + }, + })).To(Succeed()) + } + + By("Creating Node") + err = kClient.Get(ctx, node, n) + if err != nil && errors.IsNotFound(err) { + resource := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: nodeName, + Namespace: namespace, + }, + Spec: corev1.NodeSpec{}, + Status: corev1.NodeStatus{ + Capacity: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("100m"), + }, + }, + } + Expect(kClient.Create(ctx, resource)).To(Succeed()) + } + }) + + AfterAll(func() { + By("Cleanup node") + n := &corev1.Node{} + Expect(kClient.Get(ctx, node, n)).NotTo(HaveOccurred()) + Expect(kClient.Delete(ctx, n)).To(Succeed()) + }) + + It("should create global metrics aggregate", func() { + + discoveryClient := mocks.NewDiscoveryInterfaceMock(mocks.TestingT) + discoveryClient.On("ServerGroups").Return(apiGroups, nil) + + r := MetricsAggregateReconciler{ + Client: kClient, + Scheme: kClient.Scheme(), + DiscoveryClient: discoveryClient, + } + _, err := r.Reconcile(ctx, reconcile.Request{NamespacedName: metricsAggregate}) + Expect(err).NotTo(HaveOccurred()) + metrics := &v1alpha1.MetricsAggregate{} + Expect(kClient.Get(ctx, metricsAggregate, metrics)).NotTo(HaveOccurred()) + }) + }) +}) diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index 749422b6..0f0949d7 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -24,13 +24,13 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + deploymentsv1alpha1 "github.com/pluralsh/deployment-operator/api/v1alpha1" "k8s.io/client-go/kubernetes/scheme" + "k8s.io/metrics/pkg/apis/metrics/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" - - deploymentsv1alpha1 "github.com/pluralsh/deployment-operator/api/v1alpha1" ) // These tests use Ginkgo (BDD-style Go testing framework). Refer to @@ -58,12 +58,15 @@ var _ = BeforeSuite(func() { Expect(err).NotTo(HaveOccurred()) Expect(cfg).NotTo(BeNil()) + err = v1beta1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) err = deploymentsv1alpha1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) kClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) Expect(err).NotTo(HaveOccurred()) Expect(kClient).NotTo(BeNil()) + }) var _ = AfterSuite(func() { diff --git a/pkg/manifests/template/helm_test.go b/pkg/manifests/template/helm_test.go index 699ee1fc..836fd2fd 100644 --- a/pkg/manifests/template/helm_test.go +++ b/pkg/manifests/template/helm_test.go @@ -64,7 +64,7 @@ var _ = Describe("Helm template", func() { It("should successfully render the helm template", func() { resp, err := NewHelm(dir).Render(svc, utilFactory) Expect(err).NotTo(HaveOccurred()) - Expect(len(resp)).To(Equal(13)) + Expect(len(resp)).To(Equal(14)) }) }) diff --git a/pkg/test/mocks/DiscoveryInterface_mock.go b/pkg/test/mocks/DiscoveryInterface_mock.go new file mode 100644 index 00000000..c220cbad --- /dev/null +++ b/pkg/test/mocks/DiscoveryInterface_mock.go @@ -0,0 +1,595 @@ +// Code generated by mockery v2.45.1. DO NOT EDIT. + +package mocks + +import ( + mock "github.com/stretchr/testify/mock" + discovery "k8s.io/client-go/discovery" + + openapi "k8s.io/client-go/openapi" + + openapi_v2 "github.com/google/gnostic-models/openapiv2" + + rest "k8s.io/client-go/rest" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + version "k8s.io/apimachinery/pkg/version" +) + +// DiscoveryInterfaceMock is an autogenerated mock type for the DiscoveryInterface type +type DiscoveryInterfaceMock struct { + mock.Mock +} + +type DiscoveryInterfaceMock_Expecter struct { + mock *mock.Mock +} + +func (_m *DiscoveryInterfaceMock) EXPECT() *DiscoveryInterfaceMock_Expecter { + return &DiscoveryInterfaceMock_Expecter{mock: &_m.Mock} +} + +// OpenAPISchema provides a mock function with given fields: +func (_m *DiscoveryInterfaceMock) OpenAPISchema() (*openapi_v2.Document, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for OpenAPISchema") + } + + var r0 *openapi_v2.Document + var r1 error + if rf, ok := ret.Get(0).(func() (*openapi_v2.Document, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() *openapi_v2.Document); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*openapi_v2.Document) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DiscoveryInterfaceMock_OpenAPISchema_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OpenAPISchema' +type DiscoveryInterfaceMock_OpenAPISchema_Call struct { + *mock.Call +} + +// OpenAPISchema is a helper method to define mock.On call +func (_e *DiscoveryInterfaceMock_Expecter) OpenAPISchema() *DiscoveryInterfaceMock_OpenAPISchema_Call { + return &DiscoveryInterfaceMock_OpenAPISchema_Call{Call: _e.mock.On("OpenAPISchema")} +} + +func (_c *DiscoveryInterfaceMock_OpenAPISchema_Call) Run(run func()) *DiscoveryInterfaceMock_OpenAPISchema_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *DiscoveryInterfaceMock_OpenAPISchema_Call) Return(_a0 *openapi_v2.Document, _a1 error) *DiscoveryInterfaceMock_OpenAPISchema_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DiscoveryInterfaceMock_OpenAPISchema_Call) RunAndReturn(run func() (*openapi_v2.Document, error)) *DiscoveryInterfaceMock_OpenAPISchema_Call { + _c.Call.Return(run) + return _c +} + +// OpenAPIV3 provides a mock function with given fields: +func (_m *DiscoveryInterfaceMock) OpenAPIV3() openapi.Client { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for OpenAPIV3") + } + + var r0 openapi.Client + if rf, ok := ret.Get(0).(func() openapi.Client); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(openapi.Client) + } + } + + return r0 +} + +// DiscoveryInterfaceMock_OpenAPIV3_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OpenAPIV3' +type DiscoveryInterfaceMock_OpenAPIV3_Call struct { + *mock.Call +} + +// OpenAPIV3 is a helper method to define mock.On call +func (_e *DiscoveryInterfaceMock_Expecter) OpenAPIV3() *DiscoveryInterfaceMock_OpenAPIV3_Call { + return &DiscoveryInterfaceMock_OpenAPIV3_Call{Call: _e.mock.On("OpenAPIV3")} +} + +func (_c *DiscoveryInterfaceMock_OpenAPIV3_Call) Run(run func()) *DiscoveryInterfaceMock_OpenAPIV3_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *DiscoveryInterfaceMock_OpenAPIV3_Call) Return(_a0 openapi.Client) *DiscoveryInterfaceMock_OpenAPIV3_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *DiscoveryInterfaceMock_OpenAPIV3_Call) RunAndReturn(run func() openapi.Client) *DiscoveryInterfaceMock_OpenAPIV3_Call { + _c.Call.Return(run) + return _c +} + +// RESTClient provides a mock function with given fields: +func (_m *DiscoveryInterfaceMock) RESTClient() rest.Interface { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for RESTClient") + } + + var r0 rest.Interface + if rf, ok := ret.Get(0).(func() rest.Interface); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(rest.Interface) + } + } + + return r0 +} + +// DiscoveryInterfaceMock_RESTClient_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RESTClient' +type DiscoveryInterfaceMock_RESTClient_Call struct { + *mock.Call +} + +// RESTClient is a helper method to define mock.On call +func (_e *DiscoveryInterfaceMock_Expecter) RESTClient() *DiscoveryInterfaceMock_RESTClient_Call { + return &DiscoveryInterfaceMock_RESTClient_Call{Call: _e.mock.On("RESTClient")} +} + +func (_c *DiscoveryInterfaceMock_RESTClient_Call) Run(run func()) *DiscoveryInterfaceMock_RESTClient_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *DiscoveryInterfaceMock_RESTClient_Call) Return(_a0 rest.Interface) *DiscoveryInterfaceMock_RESTClient_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *DiscoveryInterfaceMock_RESTClient_Call) RunAndReturn(run func() rest.Interface) *DiscoveryInterfaceMock_RESTClient_Call { + _c.Call.Return(run) + return _c +} + +// ServerGroups provides a mock function with given fields: +func (_m *DiscoveryInterfaceMock) ServerGroups() (*v1.APIGroupList, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ServerGroups") + } + + var r0 *v1.APIGroupList + var r1 error + if rf, ok := ret.Get(0).(func() (*v1.APIGroupList, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() *v1.APIGroupList); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.APIGroupList) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DiscoveryInterfaceMock_ServerGroups_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ServerGroups' +type DiscoveryInterfaceMock_ServerGroups_Call struct { + *mock.Call +} + +// ServerGroups is a helper method to define mock.On call +func (_e *DiscoveryInterfaceMock_Expecter) ServerGroups() *DiscoveryInterfaceMock_ServerGroups_Call { + return &DiscoveryInterfaceMock_ServerGroups_Call{Call: _e.mock.On("ServerGroups")} +} + +func (_c *DiscoveryInterfaceMock_ServerGroups_Call) Run(run func()) *DiscoveryInterfaceMock_ServerGroups_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *DiscoveryInterfaceMock_ServerGroups_Call) Return(_a0 *v1.APIGroupList, _a1 error) *DiscoveryInterfaceMock_ServerGroups_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DiscoveryInterfaceMock_ServerGroups_Call) RunAndReturn(run func() (*v1.APIGroupList, error)) *DiscoveryInterfaceMock_ServerGroups_Call { + _c.Call.Return(run) + return _c +} + +// ServerGroupsAndResources provides a mock function with given fields: +func (_m *DiscoveryInterfaceMock) ServerGroupsAndResources() ([]*v1.APIGroup, []*v1.APIResourceList, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ServerGroupsAndResources") + } + + var r0 []*v1.APIGroup + var r1 []*v1.APIResourceList + var r2 error + if rf, ok := ret.Get(0).(func() ([]*v1.APIGroup, []*v1.APIResourceList, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() []*v1.APIGroup); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*v1.APIGroup) + } + } + + if rf, ok := ret.Get(1).(func() []*v1.APIResourceList); ok { + r1 = rf() + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).([]*v1.APIResourceList) + } + } + + if rf, ok := ret.Get(2).(func() error); ok { + r2 = rf() + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// DiscoveryInterfaceMock_ServerGroupsAndResources_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ServerGroupsAndResources' +type DiscoveryInterfaceMock_ServerGroupsAndResources_Call struct { + *mock.Call +} + +// ServerGroupsAndResources is a helper method to define mock.On call +func (_e *DiscoveryInterfaceMock_Expecter) ServerGroupsAndResources() *DiscoveryInterfaceMock_ServerGroupsAndResources_Call { + return &DiscoveryInterfaceMock_ServerGroupsAndResources_Call{Call: _e.mock.On("ServerGroupsAndResources")} +} + +func (_c *DiscoveryInterfaceMock_ServerGroupsAndResources_Call) Run(run func()) *DiscoveryInterfaceMock_ServerGroupsAndResources_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *DiscoveryInterfaceMock_ServerGroupsAndResources_Call) Return(_a0 []*v1.APIGroup, _a1 []*v1.APIResourceList, _a2 error) *DiscoveryInterfaceMock_ServerGroupsAndResources_Call { + _c.Call.Return(_a0, _a1, _a2) + return _c +} + +func (_c *DiscoveryInterfaceMock_ServerGroupsAndResources_Call) RunAndReturn(run func() ([]*v1.APIGroup, []*v1.APIResourceList, error)) *DiscoveryInterfaceMock_ServerGroupsAndResources_Call { + _c.Call.Return(run) + return _c +} + +// ServerPreferredNamespacedResources provides a mock function with given fields: +func (_m *DiscoveryInterfaceMock) ServerPreferredNamespacedResources() ([]*v1.APIResourceList, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ServerPreferredNamespacedResources") + } + + var r0 []*v1.APIResourceList + var r1 error + if rf, ok := ret.Get(0).(func() ([]*v1.APIResourceList, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() []*v1.APIResourceList); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*v1.APIResourceList) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DiscoveryInterfaceMock_ServerPreferredNamespacedResources_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ServerPreferredNamespacedResources' +type DiscoveryInterfaceMock_ServerPreferredNamespacedResources_Call struct { + *mock.Call +} + +// ServerPreferredNamespacedResources is a helper method to define mock.On call +func (_e *DiscoveryInterfaceMock_Expecter) ServerPreferredNamespacedResources() *DiscoveryInterfaceMock_ServerPreferredNamespacedResources_Call { + return &DiscoveryInterfaceMock_ServerPreferredNamespacedResources_Call{Call: _e.mock.On("ServerPreferredNamespacedResources")} +} + +func (_c *DiscoveryInterfaceMock_ServerPreferredNamespacedResources_Call) Run(run func()) *DiscoveryInterfaceMock_ServerPreferredNamespacedResources_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *DiscoveryInterfaceMock_ServerPreferredNamespacedResources_Call) Return(_a0 []*v1.APIResourceList, _a1 error) *DiscoveryInterfaceMock_ServerPreferredNamespacedResources_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DiscoveryInterfaceMock_ServerPreferredNamespacedResources_Call) RunAndReturn(run func() ([]*v1.APIResourceList, error)) *DiscoveryInterfaceMock_ServerPreferredNamespacedResources_Call { + _c.Call.Return(run) + return _c +} + +// ServerPreferredResources provides a mock function with given fields: +func (_m *DiscoveryInterfaceMock) ServerPreferredResources() ([]*v1.APIResourceList, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ServerPreferredResources") + } + + var r0 []*v1.APIResourceList + var r1 error + if rf, ok := ret.Get(0).(func() ([]*v1.APIResourceList, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() []*v1.APIResourceList); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*v1.APIResourceList) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DiscoveryInterfaceMock_ServerPreferredResources_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ServerPreferredResources' +type DiscoveryInterfaceMock_ServerPreferredResources_Call struct { + *mock.Call +} + +// ServerPreferredResources is a helper method to define mock.On call +func (_e *DiscoveryInterfaceMock_Expecter) ServerPreferredResources() *DiscoveryInterfaceMock_ServerPreferredResources_Call { + return &DiscoveryInterfaceMock_ServerPreferredResources_Call{Call: _e.mock.On("ServerPreferredResources")} +} + +func (_c *DiscoveryInterfaceMock_ServerPreferredResources_Call) Run(run func()) *DiscoveryInterfaceMock_ServerPreferredResources_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *DiscoveryInterfaceMock_ServerPreferredResources_Call) Return(_a0 []*v1.APIResourceList, _a1 error) *DiscoveryInterfaceMock_ServerPreferredResources_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DiscoveryInterfaceMock_ServerPreferredResources_Call) RunAndReturn(run func() ([]*v1.APIResourceList, error)) *DiscoveryInterfaceMock_ServerPreferredResources_Call { + _c.Call.Return(run) + return _c +} + +// ServerResourcesForGroupVersion provides a mock function with given fields: groupVersion +func (_m *DiscoveryInterfaceMock) ServerResourcesForGroupVersion(groupVersion string) (*v1.APIResourceList, error) { + ret := _m.Called(groupVersion) + + if len(ret) == 0 { + panic("no return value specified for ServerResourcesForGroupVersion") + } + + var r0 *v1.APIResourceList + var r1 error + if rf, ok := ret.Get(0).(func(string) (*v1.APIResourceList, error)); ok { + return rf(groupVersion) + } + if rf, ok := ret.Get(0).(func(string) *v1.APIResourceList); ok { + r0 = rf(groupVersion) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.APIResourceList) + } + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(groupVersion) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DiscoveryInterfaceMock_ServerResourcesForGroupVersion_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ServerResourcesForGroupVersion' +type DiscoveryInterfaceMock_ServerResourcesForGroupVersion_Call struct { + *mock.Call +} + +// ServerResourcesForGroupVersion is a helper method to define mock.On call +// - groupVersion string +func (_e *DiscoveryInterfaceMock_Expecter) ServerResourcesForGroupVersion(groupVersion interface{}) *DiscoveryInterfaceMock_ServerResourcesForGroupVersion_Call { + return &DiscoveryInterfaceMock_ServerResourcesForGroupVersion_Call{Call: _e.mock.On("ServerResourcesForGroupVersion", groupVersion)} +} + +func (_c *DiscoveryInterfaceMock_ServerResourcesForGroupVersion_Call) Run(run func(groupVersion string)) *DiscoveryInterfaceMock_ServerResourcesForGroupVersion_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *DiscoveryInterfaceMock_ServerResourcesForGroupVersion_Call) Return(_a0 *v1.APIResourceList, _a1 error) *DiscoveryInterfaceMock_ServerResourcesForGroupVersion_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DiscoveryInterfaceMock_ServerResourcesForGroupVersion_Call) RunAndReturn(run func(string) (*v1.APIResourceList, error)) *DiscoveryInterfaceMock_ServerResourcesForGroupVersion_Call { + _c.Call.Return(run) + return _c +} + +// ServerVersion provides a mock function with given fields: +func (_m *DiscoveryInterfaceMock) ServerVersion() (*version.Info, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ServerVersion") + } + + var r0 *version.Info + var r1 error + if rf, ok := ret.Get(0).(func() (*version.Info, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() *version.Info); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*version.Info) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DiscoveryInterfaceMock_ServerVersion_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ServerVersion' +type DiscoveryInterfaceMock_ServerVersion_Call struct { + *mock.Call +} + +// ServerVersion is a helper method to define mock.On call +func (_e *DiscoveryInterfaceMock_Expecter) ServerVersion() *DiscoveryInterfaceMock_ServerVersion_Call { + return &DiscoveryInterfaceMock_ServerVersion_Call{Call: _e.mock.On("ServerVersion")} +} + +func (_c *DiscoveryInterfaceMock_ServerVersion_Call) Run(run func()) *DiscoveryInterfaceMock_ServerVersion_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *DiscoveryInterfaceMock_ServerVersion_Call) Return(_a0 *version.Info, _a1 error) *DiscoveryInterfaceMock_ServerVersion_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DiscoveryInterfaceMock_ServerVersion_Call) RunAndReturn(run func() (*version.Info, error)) *DiscoveryInterfaceMock_ServerVersion_Call { + _c.Call.Return(run) + return _c +} + +// WithLegacy provides a mock function with given fields: +func (_m *DiscoveryInterfaceMock) WithLegacy() discovery.DiscoveryInterface { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for WithLegacy") + } + + var r0 discovery.DiscoveryInterface + if rf, ok := ret.Get(0).(func() discovery.DiscoveryInterface); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(discovery.DiscoveryInterface) + } + } + + return r0 +} + +// DiscoveryInterfaceMock_WithLegacy_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WithLegacy' +type DiscoveryInterfaceMock_WithLegacy_Call struct { + *mock.Call +} + +// WithLegacy is a helper method to define mock.On call +func (_e *DiscoveryInterfaceMock_Expecter) WithLegacy() *DiscoveryInterfaceMock_WithLegacy_Call { + return &DiscoveryInterfaceMock_WithLegacy_Call{Call: _e.mock.On("WithLegacy")} +} + +func (_c *DiscoveryInterfaceMock_WithLegacy_Call) Run(run func()) *DiscoveryInterfaceMock_WithLegacy_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *DiscoveryInterfaceMock_WithLegacy_Call) Return(_a0 discovery.DiscoveryInterface) *DiscoveryInterfaceMock_WithLegacy_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *DiscoveryInterfaceMock_WithLegacy_Call) RunAndReturn(run func() discovery.DiscoveryInterface) *DiscoveryInterfaceMock_WithLegacy_Call { + _c.Call.Return(run) + return _c +} + +// NewDiscoveryInterfaceMock creates a new instance of DiscoveryInterfaceMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewDiscoveryInterfaceMock(t interface { + mock.TestingT + Cleanup(func()) +}) *DiscoveryInterfaceMock { + mock := &DiscoveryInterfaceMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/tools.go b/tools.go index 3b1045a6..9ec0adec 100644 --- a/tools.go +++ b/tools.go @@ -4,8 +4,9 @@ package tools import ( _ "github.com/elastic/crd-ref-docs" + _ "github.com/golangci/golangci-lint/cmd/golangci-lint" _ "github.com/vektra/mockery/v2" + _ "k8s.io/client-go/discovery" _ "sigs.k8s.io/controller-runtime/tools/setup-envtest" - _ "github.com/golangci/golangci-lint/cmd/golangci-lint" _ "sigs.k8s.io/controller-tools/cmd/controller-gen" )