feat: add metrics-adapter component to support centralized hpa

Signed-off-by: jwcesign <jiangwei115@huawei.com>
This commit is contained in:
jwcesign 2023-05-25 09:39:02 +08:00
parent a18702b1c8
commit ae6c34578c
20 changed files with 1983 additions and 97 deletions

View File

@ -17,7 +17,8 @@ TARGETS := karmada-aggregated-apiserver \
karmada-scheduler-estimator \
karmada-interpreter-webhook-example \
karmada-search \
karmada-operator
karmada-operator \
karmada-metrics-adapter
CTL_TARGETS := karmadactl kubectl-karmada
@ -122,6 +123,7 @@ endif
docker push ${REGISTRY}/karmada-aggregated-apiserver:${VERSION}
docker push ${REGISTRY}/karmada-search:${VERSION}
docker push ${REGISTRY}/karmada-operator:${VERSION}
docker push ${REGISTRY}/karmada-metrics-adapter:${VERSION}
# Build and package binary
#

View File

@ -10,7 +10,7 @@
"version": "unversioned"
},
"paths": {
"/apis/": {
"/apis": {
"get": {
"description": "get available API versions",
"consumes": [
@ -40,7 +40,7 @@
}
}
},
"/apis/cluster.karmada.io/": {
"/apis/cluster.karmada.io": {
"get": {
"description": "get information of a group",
"consumes": [
@ -70,7 +70,7 @@
}
}
},
"/apis/cluster.karmada.io/v1alpha1/": {
"/apis/cluster.karmada.io/v1alpha1": {
"get": {
"description": "get available resources",
"consumes": [
@ -1564,7 +1564,7 @@
}
]
},
"/apis/config.karmada.io/": {
"/apis/config.karmada.io": {
"get": {
"description": "get information of a group",
"consumes": [
@ -1594,7 +1594,7 @@
}
}
},
"/apis/config.karmada.io/v1alpha1/": {
"/apis/config.karmada.io/v1alpha1": {
"get": {
"description": "get available resources",
"consumes": [
@ -2624,7 +2624,7 @@
}
]
},
"/apis/networking.karmada.io/": {
"/apis/networking.karmada.io": {
"get": {
"description": "get information of a group",
"consumes": [
@ -2654,7 +2654,7 @@
}
}
},
"/apis/networking.karmada.io/v1alpha1/": {
"/apis/networking.karmada.io/v1alpha1": {
"get": {
"description": "get available resources",
"consumes": [
@ -3940,7 +3940,7 @@
}
]
},
"/apis/policy.karmada.io/": {
"/apis/policy.karmada.io": {
"get": {
"description": "get information of a group",
"consumes": [
@ -3970,7 +3970,7 @@
}
}
},
"/apis/policy.karmada.io/v1alpha1/": {
"/apis/policy.karmada.io/v1alpha1": {
"get": {
"description": "get available resources",
"consumes": [
@ -9768,7 +9768,7 @@
}
]
},
"/apis/search.karmada.io/": {
"/apis/search.karmada.io": {
"get": {
"description": "get information of a group",
"consumes": [
@ -9798,7 +9798,7 @@
}
}
},
"/apis/search.karmada.io/v1alpha1/": {
"/apis/search.karmada.io/v1alpha1": {
"get": {
"description": "get available resources",
"consumes": [
@ -10828,7 +10828,7 @@
}
]
},
"/apis/work.karmada.io/": {
"/apis/work.karmada.io": {
"get": {
"description": "get information of a group",
"consumes": [
@ -10858,7 +10858,7 @@
}
}
},
"/apis/work.karmada.io/v1alpha1/": {
"/apis/work.karmada.io/v1alpha1": {
"get": {
"description": "get available resources",
"consumes": [
@ -12144,7 +12144,7 @@
}
]
},
"/apis/work.karmada.io/v1alpha2/": {
"/apis/work.karmada.io/v1alpha2": {
"get": {
"description": "get available resources",
"consumes": [
@ -17081,8 +17081,13 @@
"type": "string"
},
"pathType": {
"description": "PathType determines the interpretation of the Path matching. PathType can be one of the following values: * Exact: Matches the URL path exactly. * Prefix: Matches based on a URL path prefix split by '/'. Matching is\n done on a path element by element basis. A path element refers is the\n list of labels in the path split by the '/' separator. A request is a\n match for path p if every p is an element-wise prefix of p of the\n request path. Note that if the last element of the path is a substring\n of the last element in request path, it is not a match (e.g. /foo/bar\n matches /foo/bar/baz, but does not match /foo/barbaz).\n* ImplementationSpecific: Interpretation of the Path matching is up to\n the IngressClass. Implementations can treat this as a separate PathType\n or treat it identically to Prefix or Exact path types.\nImplementations are required to support all path types.",
"type": "string"
"description": "PathType determines the interpretation of the Path matching. PathType can be one of the following values: * Exact: Matches the URL path exactly. * Prefix: Matches based on a URL path prefix split by '/'. Matching is\n done on a path element by element basis. A path element refers is the\n list of labels in the path split by the '/' separator. A request is a\n match for path p if every p is an element-wise prefix of p of the\n request path. Note that if the last element of the path is a substring\n of the last element in request path, it is not a match (e.g. /foo/bar\n matches /foo/bar/baz, but does not match /foo/barbaz).\n* ImplementationSpecific: Interpretation of the Path matching is up to\n the IngressClass. Implementations can treat this as a separate PathType\n or treat it identically to Prefix or Exact path types.\nImplementations are required to support all path types.\n\nPossible enum values:\n - `\"Exact\"` matches the URL path exactly and with case sensitivity.\n - `\"ImplementationSpecific\"` matching is up to the IngressClass. Implementations can treat this as a separate PathType or treat it identically to Prefix or Exact path types.\n - `\"Prefix\"` matches based on a URL path prefix split by '/'. Matching is case sensitive and done on a path element by element basis. A path element refers to the list of labels in the path split by the '/' separator. A request is a match for path p if every p is an element-wise prefix of p of the request path. Note that if the last element of the path is a substring of the last element in request path, it is not a match (e.g. /foo/bar matches /foo/bar/baz, but does not match /foo/barbaz). If multiple matching paths exist in an Ingress spec, the longest matching path is given priority. Examples: - /foo/bar does not match requests to /foo/barbaz - /foo/bar matches request to /foo/bar and /foo/bar/baz - /foo and /foo/ both match requests to /foo and /foo/. If both paths are present in an Ingress spec, the longest matching path (/foo/) is given priority.",
"type": "string",
"enum": [
"Exact",
"ImplementationSpecific",
"Prefix"
]
}
}
},

View File

@ -0,0 +1,25 @@
apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
name: v1beta1.metrics.k8s.io
labels:
app: karmada-metrics-adapter
apiserver: "true"
spec:
insecureSkipTLSVerify: true
group: metrics.k8s.io
groupPriorityMinimum: 2000
service:
name: karmada-metrics-adapter
namespace: karmada-system
version: v1beta1
versionPriority: 10
---
apiVersion: v1
kind: Service
metadata:
name: karmada-metrics-adapter
namespace: karmada-system
spec:
type: ExternalName
externalName: karmada-metrics-adapter.karmada-system.svc.cluster.local

View File

@ -0,0 +1,86 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: karmada-metrics-adapter
namespace: karmada-system
labels:
app: karmada-metrics-adapter
apiserver: "true"
spec:
selector:
matchLabels:
app: karmada-metrics-adapter
apiserver: "true"
replicas: 1
template:
metadata:
labels:
app: karmada-metrics-adapter
apiserver: "true"
spec:
automountServiceAccountToken: false
containers:
- name: karmada-metrics-adapter
image: docker.io/karmada/karmada-metrics-adapter:latest
imagePullPolicy: IfNotPresent
volumeMounts:
- name: karmada-certs
mountPath: /etc/karmada/pki
readOnly: true
- name: kubeconfig
subPath: kubeconfig
mountPath: /etc/kubeconfig
command:
- /bin/karmada-metrics-adapter
- --kubeconfig=/etc/kubeconfig
- --authentication-kubeconfig=/etc/kubeconfig
- --authorization-kubeconfig=/etc/kubeconfig
- --client-ca-file=/etc/karmada/pki/ca.crt
- --audit-log-path=-
- --audit-log-maxage=0
- --audit-log-maxbackup=0
readinessProbe:
httpGet:
path: /readyz
port: 443
scheme: HTTPS
initialDelaySeconds: 1
failureThreshold: 3
periodSeconds: 3
timeoutSeconds: 15
livenessProbe:
httpGet:
path: /healthz
port: 443
scheme: HTTPS
initialDelaySeconds: 10
failureThreshold: 3
periodSeconds: 10
timeoutSeconds: 15
resources:
requests:
cpu: 100m
volumes:
- name: karmada-certs
secret:
secretName: karmada-cert-secret
- name: kubeconfig
secret:
secretName: kubeconfig
---
apiVersion: v1
kind: Service
metadata:
name: karmada-metrics-adapter
namespace: karmada-system
labels:
app: karmada-metrics-adapter
apiserver: "true"
spec:
ports:
- port: 443
protocol: TCP
targetPort: 443
selector:
app: karmada-metrics-adapter

View File

@ -0,0 +1,62 @@
package app
import (
"context"
"fmt"
"github.com/spf13/cobra"
cliflag "k8s.io/component-base/cli/flag"
"k8s.io/component-base/term"
"github.com/karmada-io/karmada/cmd/metrics-adapter/app/options"
"github.com/karmada-io/karmada/pkg/sharedcli"
"github.com/karmada-io/karmada/pkg/sharedcli/klogflag"
"github.com/karmada-io/karmada/pkg/version/sharedcommand"
)
// NewMetricsAdapterCommand creates a *cobra.Command object with default parameters
func NewMetricsAdapterCommand(ctx context.Context) *cobra.Command {
opts := options.NewOptions()
cmd := &cobra.Command{
Use: "karmada-metrics-adapter",
Long: `The karmada-metrics-adapter is a adapter to aggregate the metrics from member clusters.`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := opts.Complete(); err != nil {
return err
}
if err := opts.Validate(); err != nil {
return err
}
if err := opts.Run(ctx); err != nil {
return err
}
return nil
},
Args: func(cmd *cobra.Command, args []string) error {
for _, arg := range args {
if len(arg) > 0 {
return fmt.Errorf("%q does not take any arguments, got %q", cmd.CommandPath(), args)
}
}
return nil
},
}
fss := cliflag.NamedFlagSets{}
genericFlagSet := fss.FlagSet("generic")
opts.AddFlags(genericFlagSet)
// Set klog flags
logsFlagSet := fss.FlagSet("logs")
klogflag.Add(logsFlagSet)
cmd.AddCommand(sharedcommand.NewCmdVersion("karmada-metrics-adapter"))
cmd.Flags().AddFlagSet(genericFlagSet)
cmd.Flags().AddFlagSet(logsFlagSet)
cols, _, _ := term.TerminalSize(cmd.OutOrStdout())
sharedcli.SetUsageAndHelpFunc(cmd, fss, cols)
return cmd
}

View File

@ -0,0 +1,99 @@
package options
import (
"context"
"github.com/spf13/pflag"
openapinamer "k8s.io/apiserver/pkg/endpoints/openapi"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/klog/v2"
"sigs.k8s.io/custom-metrics-apiserver/pkg/cmd/options"
"sigs.k8s.io/metrics-server/pkg/api"
karmadaclientset "github.com/karmada-io/karmada/pkg/generated/clientset/versioned"
informerfactory "github.com/karmada-io/karmada/pkg/generated/informers/externalversions"
generatedopenapi "github.com/karmada-io/karmada/pkg/generated/openapi"
"github.com/karmada-io/karmada/pkg/metricsadapter"
"github.com/karmada-io/karmada/pkg/version"
)
// Options contains everything necessary to create and run metrics-adapter.
type Options struct {
CustomMetricsAdapterServerOptions *options.CustomMetricsAdapterServerOptions
KubeConfig string
}
// NewOptions builds a default metrics-adapter options.
func NewOptions() *Options {
o := &Options{
CustomMetricsAdapterServerOptions: options.NewCustomMetricsAdapterServerOptions(),
}
return o
}
// Complete fills in fields required to have valid data.
func (o *Options) Complete() error {
return nil
}
// AddFlags adds flags to the specified FlagSet.
func (o *Options) AddFlags(fs *pflag.FlagSet) {
o.CustomMetricsAdapterServerOptions.AddFlags(fs)
fs.StringVar(&o.KubeConfig, "kubeconfig", o.KubeConfig, "Path to karmada control plane kubeconfig file.")
}
// Config returns config for the metrics-adapter server given Options
func (o *Options) Config() (*metricsadapter.MetricsServer, error) {
restConfig, err := clientcmd.BuildConfigFromFlags("", o.KubeConfig)
if err != nil {
klog.Errorf("Unable to build restConfig: %v", err)
return nil, err
}
karmadaClient := karmadaclientset.NewForConfigOrDie(restConfig)
factory := informerfactory.NewSharedInformerFactory(karmadaClient, 0)
metricsController := metricsadapter.NewMetricsController(restConfig, factory)
metricsAdapter := metricsadapter.NewMetricsAdapter(metricsController, o.CustomMetricsAdapterServerOptions)
metricsAdapter.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig(generatedopenapi.GetOpenAPIDefinitions, openapinamer.NewDefinitionNamer(api.Scheme))
metricsAdapter.OpenAPIConfig.Info.Title = "karmada-metrics-adapter"
metricsAdapter.OpenAPIConfig.Info.Version = "1.0.0"
server, err := metricsAdapter.Server()
if err != nil {
klog.Errorf("Unable to construct metrics adapter: %v", err)
return nil, err
}
err = server.GenericAPIServer.AddPostStartHook("start-karmada-informers", func(context genericapiserver.PostStartHookContext) error {
factory.Start(context.StopCh)
return nil
})
if err != nil {
klog.Errorf("Unable to add post hook: %v", err)
return nil, err
}
if err := api.Install(metricsAdapter, metricsAdapter.PodLister, metricsAdapter.NodeLister, server.GenericAPIServer, nil); err != nil {
klog.Errorf("unable to install resource metrics adapter: %v", err)
return nil, err
}
return metricsadapter.NewMetricsServer(metricsController, metricsAdapter), nil
}
// Run runs the metrics-adapter with options. This should never exit.
func (o *Options) Run(ctx context.Context) error {
klog.Infof("karmada-metrics-adapter version: %s", version.Get())
metricsServer, err := o.Config()
if err != nil {
return err
}
return metricsServer.StartServer(ctx.Done())
}

View File

@ -0,0 +1,14 @@
package options
import (
utilerrors "k8s.io/apimachinery/pkg/util/errors"
)
// Validate checks Options and return a slice of found errs.
func (o *Options) Validate() error {
var errs []error
errs = append(errs, o.CustomMetricsAdapterServerOptions.Validate()...)
return utilerrors.NewAggregate(errs)
}

18
cmd/metrics-adapter/main.go Executable file
View File

@ -0,0 +1,18 @@
package main
import (
"os"
"k8s.io/component-base/cli"
_ "k8s.io/component-base/logs/json/register" // for JSON log format registration
controllerruntime "sigs.k8s.io/controller-runtime"
"github.com/karmada-io/karmada/cmd/metrics-adapter/app"
)
func main() {
ctx := controllerruntime.SetupSignalHandler()
cmd := app.NewMetricsAdapterCommand(ctx)
code := cli.Run(cmd)
os.Exit(code)
}

19
go.mod
View File

@ -17,7 +17,7 @@ require (
github.com/prometheus/client_golang v1.14.0
github.com/spf13/cobra v1.6.1
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.1
github.com/stretchr/testify v1.8.2
github.com/vektra/mockery/v2 v2.10.0
github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64
go.uber.org/atomic v1.9.0
@ -38,16 +38,19 @@ require (
k8s.io/code-generator v0.26.2
k8s.io/component-base v0.26.2
k8s.io/component-helpers v0.26.2
k8s.io/klog/v2 v2.80.1
k8s.io/klog/v2 v2.90.1
k8s.io/kube-aggregator v0.26.2
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280
k8s.io/kube-openapi v0.0.0-20230303024457-afdc3dddf62d
k8s.io/kubectl v0.26.2
k8s.io/utils v0.0.0-20221128185143-99ec85e7a448
k8s.io/metrics v0.26.2
k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5
layeh.com/gopher-json v0.0.0-20201124131017-552bb3c4c3bf
sigs.k8s.io/cluster-api v1.4.0
sigs.k8s.io/controller-runtime v0.14.5
sigs.k8s.io/custom-metrics-apiserver v1.25.1-0.20230308103314-bd3192a29bc8
sigs.k8s.io/kind v0.17.0
sigs.k8s.io/mcs-api v0.1.0
sigs.k8s.io/metrics-server v0.6.1-0.20230509102056-1a23b5bd2e12
sigs.k8s.io/structured-merge-diff/v4 v4.2.3
sigs.k8s.io/yaml v1.3.0
)
@ -69,7 +72,7 @@ require (
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emicklei/go-restful/v3 v3.9.0 // indirect
github.com/emicklei/go-restful/v3 v3.10.1 // indirect
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
github.com/fatih/camelcase v1.0.0 // indirect
@ -80,8 +83,8 @@ require (
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-logr/zapr v1.2.3 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.1 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
github.com/gobuffalo/flect v1.0.2 // indirect
@ -175,7 +178,7 @@ require (
k8s.io/gengo v0.0.0-20220902162205-c0856e24416d // indirect
k8s.io/kms v0.26.2 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.35 // indirect
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/kustomize/api v0.12.1 // indirect
sigs.k8s.io/kustomize/kyaml v0.13.9 // indirect
)

39
go.sum
View File

@ -199,8 +199,8 @@ github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7fo
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE=
github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/emicklei/go-restful/v3 v3.10.1 h1:rc42Y5YTp7Am7CS630D7JmhRjq4UlEUuEKfrDac4bSQ=
github.com/emicklei/go-restful/v3 v3.10.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@ -283,15 +283,15 @@ github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwds
github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=
github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=
github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA=
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
github.com/go-openapi/jsonreference v0.20.1 h1:FBLnyygC4/IZZr893oiomc9XaghoveYTrLC1F86HID8=
github.com/go-openapi/jsonreference v0.20.1/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
@ -533,6 +533,7 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@ -779,8 +780,9 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
@ -1400,6 +1402,7 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
@ -1487,22 +1490,24 @@ k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
k8s.io/klog/v2 v2.80.1 h1:atnLQ121W371wYYFawwYx1aEY2eUfs4l3J72wtgAwV4=
k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw=
k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
k8s.io/kms v0.26.2 h1:GM1gg3tFK3OUU/QQFi93yGjG3lJT8s8l3Wkn2+VxBLM=
k8s.io/kms v0.26.2/go.mod h1:69qGnf1NsFOQP07fBYqNLZklqEHSJF024JqYCaeVxHg=
k8s.io/kube-aggregator v0.26.2 h1:WtcLGisa5aCKBbBI1/Xe7gdjPlVb5Xhvs4a8Rdk8EXs=
k8s.io/kube-aggregator v0.26.2/go.mod h1:swDTw0k/XghVLR+PCWnP6Y36wR2+DsqL2HUVq8eu0RI=
k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E=
k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E=
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 h1:+70TFaan3hfJzs+7VK2o+OGxg8HsuBr/5f6tVAjDu6E=
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280/go.mod h1:+Axhij7bCpeqhklhUTe3xmOn6bWxolyZEeyaFpjGtl4=
k8s.io/kube-openapi v0.0.0-20230303024457-afdc3dddf62d h1:VcFq5n7wCJB2FQMCIHfC+f+jNcGgNMar1uKd6rVlifU=
k8s.io/kube-openapi v0.0.0-20230303024457-afdc3dddf62d/go.mod h1:y5VtZWM9sHHc2ZodIH/6SHzXj+TPU5USoA8lcIeKEKY=
k8s.io/kubectl v0.26.2 h1:SMPB4j48eVFxsYluBq3VLyqXtE6b72YnszkbTAtFye4=
k8s.io/kubectl v0.26.2/go.mod h1:KYWOXSwp2BrDn3kPeoU/uKzKtdqvhK1dgZGd0+no4cM=
k8s.io/metrics v0.26.2 h1:2gUvUWWnHPdE2tyA5DvyHC8HGryr+izhY9i5dzLP06s=
k8s.io/metrics v0.26.2/go.mod h1:PX1wm9REV9hSGuw9GcXTFNDgab1KRXck3mNeiLYbRho=
k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
k8s.io/utils v0.0.0-20200603063816-c1c6865ac451/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
k8s.io/utils v0.0.0-20221128185143-99ec85e7a448 h1:KTgPnR10d5zhztWptI952TNtt/4u5h3IzDXkdIMuo2Y=
k8s.io/utils v0.0.0-20221128185143-99ec85e7a448/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5 h1:kmDqav+P+/5e1i9tFfHq1qcF3sOrDp+YEkVDAHu7Jwk=
k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
layeh.com/gopher-json v0.0.0-20201124131017-552bb3c4c3bf h1:rRz0YsF7VXj9fXRF6yQgFI7DzST+hsI3TeFSGupntu0=
layeh.com/gopher-json v0.0.0-20201124131017-552bb3c4c3bf/go.mod h1:ivKkcY8Zxw5ba0jldhZCYYQfGdb2K6u9tbYK1AwMIBc=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
@ -1517,8 +1522,10 @@ sigs.k8s.io/controller-runtime v0.6.1/go.mod h1:XRYBPdbf5XJu9kpS84VJiZ7h/u1hF3gE
sigs.k8s.io/controller-runtime v0.14.5 h1:6xaWFqzT5KuAQ9ufgUaj1G/+C4Y1GRkhrxl+BJ9i+5s=
sigs.k8s.io/controller-runtime v0.14.5/go.mod h1:WqIdsAY6JBsjfc/CqO0CORmNtoCtE4S6qbPc9s68h+0=
sigs.k8s.io/controller-tools v0.3.0/go.mod h1:enhtKGfxZD1GFEoMgP8Fdbu+uKQ/cq1/WGJhdVChfvI=
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k=
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
sigs.k8s.io/custom-metrics-apiserver v1.25.1-0.20230308103314-bd3192a29bc8 h1:0dZXAPEWoIAJ3KtBHKAViBSwE1yMRH0PI/UdYb4bIhE=
sigs.k8s.io/custom-metrics-apiserver v1.25.1-0.20230308103314-bd3192a29bc8/go.mod h1:9nUXR/EgdYZto1aQ6yhwOksPR7J979jSyOqic1IgaOo=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
sigs.k8s.io/kind v0.8.1/go.mod h1:oNKTxUVPYkV9lWzY6CVMNluVq8cBsyq+UgPJdvA3uu4=
sigs.k8s.io/kind v0.17.0 h1:CScmGz/wX66puA06Gj8OZb76Wmk7JIjgWf5JDvY7msM=
sigs.k8s.io/kind v0.17.0/go.mod h1:Qqp8AiwOlMZmJWs37Hgs31xcbiYXjtXlRBSftcnZXQk=
@ -1528,6 +1535,8 @@ sigs.k8s.io/kustomize/kyaml v0.13.9 h1:Qz53EAaFFANyNgyOEJbT/yoIHygK40/ZcvU3rgry2
sigs.k8s.io/kustomize/kyaml v0.13.9/go.mod h1:QsRbD0/KcU+wdk0/L0fIp2KLnohkVzs6fQ85/nOXac4=
sigs.k8s.io/mcs-api v0.1.0 h1:edDbg0oRGfXw8TmZjKYep06LcJLv/qcYLidejnUp0PM=
sigs.k8s.io/mcs-api v0.1.0/go.mod h1:gGiAryeFNB4GBsq2LBmVqSgKoobLxt+p7ii/WG5QYYw=
sigs.k8s.io/metrics-server v0.6.1-0.20230509102056-1a23b5bd2e12 h1:M2W5nfr7ATwuEaFjH6qH0GeK2M1LoIw78kRvVxhEX+E=
sigs.k8s.io/metrics-server v0.6.1-0.20230509102056-1a23b5bd2e12/go.mod h1:PuZhxxSf39LvZ27uzIaiUoGwaz/RoZzmXdQbi252D5Y=
sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE=

View File

@ -197,6 +197,7 @@ openapi-gen \
--input-dirs "k8s.io/apimachinery/pkg/apis/meta/v1,k8s.io/apimachinery/pkg/runtime,k8s.io/apimachinery/pkg/version" \
--input-dirs "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1,k8s.io/api/admissionregistration/v1,k8s.io/api/networking/v1" \
--input-dirs "github.com/karmada-io/karmada/pkg/apis/search/v1alpha1" \
--input-dirs "k8s.io/metrics/pkg/apis/custom_metrics,k8s.io/metrics/pkg/apis/custom_metrics/v1beta1,k8s.io/metrics/pkg/apis/custom_metrics/v1beta2,k8s.io/metrics/pkg/apis/external_metrics,k8s.io/metrics/pkg/apis/external_metrics/v1beta1,k8s.io/metrics/pkg/apis/metrics,k8s.io/metrics/pkg/apis/metrics/v1beta1" \
--output-package "github.com/karmada-io/karmada/pkg/generated/openapi" \
-O zz_generated.openapi

View File

@ -19,6 +19,7 @@ INTERPRETER_WEBHOOK_EXAMPLE_LABEL="karmada-interpreter-webhook-example"
KARMADA_SEARCH_LABEL="karmada-search"
KARMADA_OPENSEARCH_LABEL="karmada-opensearch"
KARMADA_OPENSEARCH_DASHBOARDS_LABEL="karmada-opensearch-dashboards"
KARMADA_METRICS_ADAPTER_LABEL="karmada-metrics-adapter"
KARMADA_GO_PACKAGE="github.com/karmada-io/karmada"
@ -37,6 +38,7 @@ KARMADA_TARGET_SOURCE=(
karmada-interpreter-webhook-example=examples/customresourceinterpreter/webhook
karmada-search=cmd/karmada-search
karmada-operator=operator/cmd/operator
karmada-metrics-adapter=cmd/metrics-adapter
)
#https://textkool.com/en/ascii-art-generator?hl=default&vl=default&font=DOS%20Rebel&text=KARMADA

928
pkg/generated/openapi/zz_generated.openapi.go Normal file → Executable file

File diff suppressed because it is too large Load Diff

29
pkg/metricsadapter/adapter.go Executable file
View File

@ -0,0 +1,29 @@
package metricsadapter
import (
basecmd "sigs.k8s.io/custom-metrics-apiserver/pkg/cmd"
"sigs.k8s.io/custom-metrics-apiserver/pkg/cmd/options"
"github.com/karmada-io/karmada/pkg/metricsadapter/provider"
)
// MetricsAdapter is a metrics adapter to provider native metrics, custom metrics and external metrics
type MetricsAdapter struct {
basecmd.AdapterBase
*provider.ResourceMetricsProvider
}
// NewMetricsAdapter creates a new metrics adapter
func NewMetricsAdapter(controller *MetricsController, customMetricsAdapterServerOptions *options.CustomMetricsAdapterServerOptions) *MetricsAdapter {
adapter := &MetricsAdapter{}
adapter.CustomMetricsAdapterServerOptions = customMetricsAdapterServerOptions
adapter.ResourceMetricsProvider = provider.NewResourceMetricsProvider(controller.ClusterLister, controller.InformerManager)
customProvider := provider.MakeCustomMetricsProvider()
externalProvider := provider.MakeExternalMetricsProvider()
adapter.WithCustomMetrics(customProvider)
adapter.WithExternalMetrics(externalProvider)
return adapter
}

21
pkg/metricsadapter/apiserver.go Executable file
View File

@ -0,0 +1,21 @@
package metricsadapter
// MetricsServer is a metrics server
type MetricsServer struct {
metricsController *MetricsController
metricsAdapter *MetricsAdapter
}
// NewMetricsServer creates a new metrics server
func NewMetricsServer(controller *MetricsController, metricsAdapter *MetricsAdapter) *MetricsServer {
return &MetricsServer{
metricsController: controller,
metricsAdapter: metricsAdapter,
}
}
// StartServer starts the metrics server
func (m *MetricsServer) StartServer(stopCh <-chan struct{}) error {
go m.metricsController.startController(stopCh)
return m.metricsAdapter.Run(stopCh)
}

158
pkg/metricsadapter/controller.go Executable file
View File

@ -0,0 +1,158 @@
package metricsadapter
import (
"time"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
"k8s.io/klog/v2"
clusterV1alpha1 "github.com/karmada-io/karmada/pkg/apis/cluster/v1alpha1"
informerfactory "github.com/karmada-io/karmada/pkg/generated/informers/externalversions"
clusterlister "github.com/karmada-io/karmada/pkg/generated/listers/cluster/v1alpha1"
"github.com/karmada-io/karmada/pkg/metricsadapter/provider"
"github.com/karmada-io/karmada/pkg/util"
"github.com/karmada-io/karmada/pkg/util/fedinformer/genericmanager"
"github.com/karmada-io/karmada/pkg/util/gclient"
)
// MetricsController is a controller for metrics, control the lifecycle of multi-clusters informer
type MetricsController struct {
InformerFactory informerfactory.SharedInformerFactory
ClusterLister clusterlister.ClusterLister
InformerManager genericmanager.MultiClusterInformerManager
queue workqueue.RateLimitingInterface
restConfig *rest.Config
}
// NewMetricsController creates a new metrics controller
func NewMetricsController(restConfig *rest.Config, factory informerfactory.SharedInformerFactory) *MetricsController {
clusterLister := factory.Cluster().V1alpha1().Clusters().Lister()
controller := &MetricsController{
InformerFactory: factory,
ClusterLister: clusterLister,
InformerManager: genericmanager.GetInstance(),
restConfig: restConfig,
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "metrics-adapter"),
}
controller.addEventHandler()
return controller
}
// addEventHandler adds event handler for cluster
func (m *MetricsController) addEventHandler() {
clusterInformer := m.InformerFactory.Cluster().V1alpha1().Clusters().Informer()
_, err := clusterInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: m.addCluster,
// Update event and delete event will be handled by the same handler
UpdateFunc: m.updateCluster,
})
if err != nil {
klog.Errorf("Failed to add cluster event handler for cluster: %v", err)
}
}
// addCluster adds cluster to queue
func (m *MetricsController) addCluster(obj interface{}) {
cluster := obj.(*clusterV1alpha1.Cluster)
m.queue.Add(cluster.GetName())
}
// updateCluster updates cluster in queue
func (m *MetricsController) updateCluster(oldObj, curObj interface{}) {
curCluster := curObj.(*clusterV1alpha1.Cluster)
oldCluster := oldObj.(*clusterV1alpha1.Cluster)
if curCluster.ResourceVersion == oldCluster.ResourceVersion {
// no change, do nothing.
return
}
if oldCluster.DeletionTimestamp.IsZero() != curCluster.DeletionTimestamp.IsZero() {
// cluster is being deleted.
m.queue.Add(curCluster.GetName())
}
if util.IsClusterSpecPartCriticalUpdated(curCluster.Spec, oldCluster.Spec) ||
util.IsClusterReady(&curCluster.Status) != util.IsClusterReady(&oldCluster.Status) {
// Cluster.Spec or Cluster health state is changed, rebuild informer.
m.InformerManager.Stop(curCluster.GetName())
m.queue.Add(curCluster.GetName())
}
}
// startController starts controller
func (m *MetricsController) startController(stopCh <-chan struct{}) {
m.InformerFactory.WaitForCacheSync(stopCh)
go wait.Until(m.worker, time.Second, stopCh)
go func() {
<-stopCh
genericmanager.StopInstance()
klog.Infof("Shutting down karmada-metrics-adapter")
}()
}
// worker is a worker for handle the data in queue
func (m *MetricsController) worker() {
for m.handleClusters() {
}
}
// handleClusters handles clusters changes
func (m *MetricsController) handleClusters() bool {
key, shutdown := m.queue.Get()
if shutdown {
klog.Errorf("Fail to pop item from queue")
return false
}
defer m.queue.Done(key)
clusterName := key.(string)
cls, err := m.ClusterLister.Get(clusterName)
if err != nil {
if apierrors.IsNotFound(err) {
klog.Infof("try to stop cluster informer %s", clusterName)
m.InformerManager.Stop(clusterName)
return true
}
return false
}
if !cls.DeletionTimestamp.IsZero() {
klog.Infof("try to stop cluster informer %s", clusterName)
m.InformerManager.Stop(clusterName)
return true
}
if !util.IsClusterReady(&cls.Status) {
klog.Warningf("cluster %s is notReady try to stop this cluster informer", clusterName)
m.InformerManager.Stop(clusterName)
return false
}
if !m.InformerManager.IsManagerExist(clusterName) {
klog.Info("Try to build informer manager for cluster ", clusterName)
controlPlaneClient := gclient.NewForConfigOrDie(m.restConfig)
clusterDynamicClient, err := util.NewClusterDynamicClientSet(clusterName, controlPlaneClient)
if err != nil {
return false
}
_ = m.InformerManager.ForCluster(clusterName, clusterDynamicClient.DynamicClientSet, 0)
}
sci := m.InformerManager.GetSingleClusterManager(clusterName)
// Just trigger the informer to work
_ = sci.Lister(provider.PodsGVR)
_ = sci.Lister(provider.NodesGVR)
sci.Start()
_ = sci.WaitForCacheSync()
return true
}

View File

@ -0,0 +1,35 @@
package provider
import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/types"
"k8s.io/metrics/pkg/apis/custom_metrics"
"sigs.k8s.io/custom-metrics-apiserver/pkg/provider"
)
// CustomMetricsProvider is a custom metrics provider
type CustomMetricsProvider struct {
}
// MakeCustomMetricsProvider creates a new custom metrics provider
func MakeCustomMetricsProvider() *CustomMetricsProvider {
return &CustomMetricsProvider{}
}
// GetMetricByName will query metrics by name from member clusters and return the result
func (c *CustomMetricsProvider) GetMetricByName(ctx context.Context, name types.NamespacedName, info provider.CustomMetricInfo, metricSelector labels.Selector) (*custom_metrics.MetricValue, error) {
return nil, fmt.Errorf("karmada-metrics-adapter still not implement it")
}
// GetMetricBySelector will query metrics by selector from member clusters and return the result
func (c *CustomMetricsProvider) GetMetricBySelector(ctx context.Context, namespace string, selector labels.Selector, info provider.CustomMetricInfo, metricSelector labels.Selector) (*custom_metrics.MetricValueList, error) {
return nil, fmt.Errorf("karmada-metrics-adapter still not implement it")
}
// ListAllMetrics returns all metrics in all member clusters
func (c *CustomMetricsProvider) ListAllMetrics() []provider.CustomMetricInfo {
return []provider.CustomMetricInfo{}
}

View File

@ -0,0 +1,29 @@
package provider
import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/metrics/pkg/apis/external_metrics"
"sigs.k8s.io/custom-metrics-apiserver/pkg/provider"
)
// ExternalMetricsProvider is a custom metrics provider
type ExternalMetricsProvider struct {
}
// MakeExternalMetricsProvider creates a new custom metrics provider
func MakeExternalMetricsProvider() *ExternalMetricsProvider {
return &ExternalMetricsProvider{}
}
// GetExternalMetric will query metrics by selector from member clusters and return the result
func (c *ExternalMetricsProvider) GetExternalMetric(ctx context.Context, namespace string, metricSelector labels.Selector, info provider.ExternalMetricInfo) (*external_metrics.ExternalMetricValueList, error) {
return nil, fmt.Errorf("karmada-metrics-adapter still not implement it")
}
// ListAllExternalMetrics returns all metrics in all member clusters
func (c *ExternalMetricsProvider) ListAllExternalMetrics() []provider.ExternalMetricInfo {
return []provider.ExternalMetricInfo{}
}

View File

@ -0,0 +1,462 @@
package provider
import (
"context"
"sync"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/tools/cache"
"k8s.io/klog/v2"
"k8s.io/metrics/pkg/apis/metrics"
metricsv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
clusterlister "github.com/karmada-io/karmada/pkg/generated/listers/cluster/v1alpha1"
"github.com/karmada-io/karmada/pkg/util/fedinformer/genericmanager"
"github.com/karmada-io/karmada/pkg/util/helper"
)
const (
// labelSelectorAnnotationInternal is the annotation used internal in karmada-metrics-adapter,
// to record the selector specified by the user
labelSelectorAnnotationInternal = "internal.karmada.io/selector"
)
var (
// podMetricsGVR is the gvr of pod metrics(v1beta1 version)
podMetricsGVR = metricsv1beta1.SchemeGroupVersion.WithResource("pods")
// nodeMetricsGVR is the gvr of node metrics(v1beta1 version)
nodeMetricsGVR = metricsv1beta1.SchemeGroupVersion.WithResource("nodes")
// PodsGVR is the gvr of pods
PodsGVR = corev1.SchemeGroupVersion.WithResource("pods")
// NodesGVR is the gvr of nodes
NodesGVR = corev1.SchemeGroupVersion.WithResource("nodes")
)
type queryResourceFromClustersFunc func(sci genericmanager.SingleClusterInformerManager, clusterName string) error
type queryMetricsFromClustersFunc func(sci genericmanager.SingleClusterInformerManager, clusterName string) (interface{}, error)
// ResourceMetricsProvider is a resource metrics provider, to provide cpu/memory metrics
type ResourceMetricsProvider struct {
PodLister *PodLister
NodeLister *NodeLister
clusterLister clusterlister.ClusterLister
informerManager genericmanager.MultiClusterInformerManager
}
// NewResourceMetricsProvider creates a new resource metrics provider
func NewResourceMetricsProvider(clusterLister clusterlister.ClusterLister, informerManager genericmanager.MultiClusterInformerManager) *ResourceMetricsProvider {
return &ResourceMetricsProvider{
clusterLister: clusterLister,
informerManager: informerManager,
PodLister: NewPodLister(),
NodeLister: NewNodeLister(),
}
}
// getMetricsParallel is a parallel func of to query metrics from member clusters
func (r *ResourceMetricsProvider) getMetricsParallel(resourceFunc queryResourceFromClustersFunc,
metricsFunc queryMetricsFromClustersFunc) ([]interface{}, error) {
clusters, err := r.clusterLister.List(labels.Everything())
if err != nil {
klog.Errorf("Failed to list clusters: %v", err)
return nil, err
}
// step 1. Find out the target clusters with lister cache
var targetClusters []string
for _, cluster := range clusters {
sci := r.informerManager.GetSingleClusterManager(cluster.Name)
if sci == nil {
klog.Errorf("Failed to get cluster(%s) manager", cluster.Name)
continue
}
err := resourceFunc(sci, cluster.Name)
if err != nil {
if !errors.IsNotFound(err) {
klog.Errorf("Failed to query resource in cluster(%s): %v", cluster.Name, err)
}
continue
}
targetClusters = append(targetClusters, cluster.Name)
}
var metrics []interface{}
if len(targetClusters) == 0 {
return metrics, nil
}
// step 2. Query metrics from the filtered target clusters
metricsChanel := make(chan interface{})
var wg sync.WaitGroup
for _, clusterName := range targetClusters {
wg.Add(1)
go func(cluster string) {
defer wg.Done()
sci := r.informerManager.GetSingleClusterManager(cluster)
if sci == nil {
klog.Errorf("Failed to get cluster(%s) manager", cluster)
return
}
metrics, err := metricsFunc(sci, cluster)
if err != nil {
if !errors.IsNotFound(err) {
klog.Errorf("Failed to query metrics in cluster(%s): %v", cluster, err)
}
return
}
// If there are multiple metrics with same name, it's ok because it's an array instead of a map.
// The HPA controller will calculate the average utilization with the array.
metricsChanel <- metrics
}(clusterName)
}
go func() {
wg.Wait()
close(metricsChanel)
}()
for {
data, ok := <-metricsChanel
if !ok {
break
}
metrics = append(metrics, data)
}
return metrics, nil
}
// queryPodMetricsByName queries metrics by pod name from target clusters
func (r *ResourceMetricsProvider) queryPodMetricsByName(name, namespace string) ([]metrics.PodMetrics, error) {
resourceQueryFunc := func(sci genericmanager.SingleClusterInformerManager, _ string) error {
_, err := sci.Lister(PodsGVR).ByNamespace(namespace).Get(name)
return err
}
metricsQueryFunc := func(sci genericmanager.SingleClusterInformerManager, _ string) (interface{}, error) {
metrics, err := sci.GetClient().Resource(podMetricsGVR).
Namespace(namespace).Get(context.Background(), name, metav1.GetOptions{})
return metrics, err
}
metricsQuery, err := r.getMetricsParallel(resourceQueryFunc, metricsQueryFunc)
if err != nil {
return nil, err
}
var podMetrics []metrics.PodMetrics
for index := range metricsQuery {
internalMetrics, err := metricsConvertV1beta1PodToInternalPod(*metricsQuery[index].(*unstructured.Unstructured))
if err != nil {
continue
}
podMetrics = append(podMetrics, internalMetrics...)
}
return podMetrics, nil
}
// queryPodMetricsBySelector queries metrics by pod selector from target clusters
func (r *ResourceMetricsProvider) queryPodMetricsBySelector(selector, namespace string) ([]metrics.PodMetrics, error) {
labelSelector, err := labels.Parse(selector)
if err != nil {
klog.Errorf("Failed to parse label selector: %v", err)
return nil, err
}
resourceQueryFunc := func(sci genericmanager.SingleClusterInformerManager, clusterName string) error {
pods, err := sci.Lister(PodsGVR).ByNamespace(namespace).List(labelSelector)
if err != nil {
klog.Errorf("Failed to list pods in cluster(%s): %v", clusterName, err)
return err
}
if len(pods) == 0 {
return errors.NewNotFound(PodsGVR.GroupResource(), "")
}
return nil
}
metricsQueryFunc := func(sci genericmanager.SingleClusterInformerManager, _ string) (interface{}, error) {
metrics, err := sci.GetClient().Resource(podMetricsGVR).
Namespace(namespace).List(context.Background(), metav1.ListOptions{
LabelSelector: selector,
})
return metrics, err
}
metricsQuery, err := r.getMetricsParallel(resourceQueryFunc, metricsQueryFunc)
if err != nil {
return nil, err
}
var podMetrics []metrics.PodMetrics
for index := range metricsQuery {
metricsData := metricsQuery[index].(*unstructured.UnstructuredList)
internalMetrics, err := metricsConvertV1beta1PodToInternalPod(metricsData.Items...)
if err != nil {
continue
}
podMetrics = append(podMetrics, internalMetrics...)
}
return podMetrics, nil
}
// queryNodeMetricsByName queries metrics by node name from target clusters
func (r *ResourceMetricsProvider) queryNodeMetricsByName(name string) ([]metrics.NodeMetrics, error) {
resourceQueryFunc := func(sci genericmanager.SingleClusterInformerManager, _ string) error {
_, err := sci.Lister(NodesGVR).Get(name)
return err
}
metricsQueryFunc := func(sci genericmanager.SingleClusterInformerManager, _ string) (interface{}, error) {
metrics, err := sci.GetClient().Resource(nodeMetricsGVR).Get(context.Background(), name, metav1.GetOptions{})
return metrics, err
}
metricsQuery, err := r.getMetricsParallel(resourceQueryFunc, metricsQueryFunc)
if err != nil {
return nil, err
}
var nodeMetrics []metrics.NodeMetrics
for index := range metricsQuery {
internalMetrics, err := metricsConvertV1beta1NodeToInternalNode(*metricsQuery[index].(*unstructured.Unstructured))
if err != nil {
continue
}
nodeMetrics = append(nodeMetrics, internalMetrics...)
}
return nodeMetrics, nil
}
// queryNodeMetricsBySelector queries metrics by node selector from target clusters
func (r *ResourceMetricsProvider) queryNodeMetricsBySelector(selector string) ([]metrics.NodeMetrics, error) {
labelSelector, err := labels.Parse(selector)
if err != nil {
klog.Errorf("Failed to parse label selector: %v", err)
return nil, err
}
resourceQueryFunc := func(sci genericmanager.SingleClusterInformerManager, clusterName string) error {
nodes, err := sci.Lister(NodesGVR).List(labelSelector)
if err != nil {
klog.Errorf("Failed to list pods in cluster(%s): %v", clusterName, err)
return err
}
if len(nodes) == 0 {
return errors.NewNotFound(PodsGVR.GroupResource(), "")
}
return nil
}
metricsQueryFunc := func(sci genericmanager.SingleClusterInformerManager, _ string) (interface{}, error) {
metrics, err := sci.GetClient().Resource(nodeMetricsGVR).List(context.Background(), metav1.ListOptions{
LabelSelector: selector,
})
return metrics, err
}
metricsQuery, err := r.getMetricsParallel(resourceQueryFunc, metricsQueryFunc)
if err != nil {
return nil, err
}
var nodeMetrics []metrics.NodeMetrics
for index := range metricsQuery {
metricsData := metricsQuery[index].(*unstructured.UnstructuredList)
internalMetrics, err := metricsConvertV1beta1NodeToInternalNode(metricsData.Items...)
if err != nil {
continue
}
nodeMetrics = append(nodeMetrics, internalMetrics...)
}
return nodeMetrics, nil
}
// GetPodMetrics queries metrics by the internal constructed pod
func (r *ResourceMetricsProvider) GetPodMetrics(pods ...*metav1.PartialObjectMetadata) ([]metrics.PodMetrics, error) {
var podMetrics []metrics.PodMetrics
// In the previous step, we construct pods list with only one element.
if len(pods) != 1 {
return podMetrics, nil
}
// In the previous step, if query with label selector, the name will be set to empty
if pods[0].Name == "" {
namespace := pods[0].Namespace
selectorStr := pods[0].Annotations[labelSelectorAnnotationInternal]
return r.queryPodMetricsBySelector(selectorStr, namespace)
}
return r.queryPodMetricsByName(pods[0].Name, pods[0].Namespace)
}
// GetNodeMetrics queries metrics by the internal constructed node
func (r *ResourceMetricsProvider) GetNodeMetrics(nodes ...*corev1.Node) ([]metrics.NodeMetrics, error) {
var nodeMetrics []metrics.NodeMetrics
// In the previous step, we construct node list with only one element, this should never happen
if len(nodes) != 1 {
// never reach here
return nodeMetrics, nil
}
// In the previous step, if query with label selector, the name will be set to empty
if nodes[0].Name == "" {
selectorStr := nodes[0].Annotations[labelSelectorAnnotationInternal]
return r.queryNodeMetricsBySelector(selectorStr)
}
return r.queryNodeMetricsByName(nodes[0].Name)
}
// PodLister is an internal lister for pods
type PodLister struct {
namespaceSpecified string
}
// NewPodLister creates an internal new PodLister
func NewPodLister() *PodLister {
return &PodLister{}
}
// List returns the internal constructed pod with label selector info
func (p *PodLister) List(selector labels.Selector) (ret []runtime.Object, err error) {
klog.V(4).Infof("List query pods with selector: %v", selector.String())
podData := &v1.PartialObjectMetadata{
TypeMeta: v1.TypeMeta{},
ObjectMeta: v1.ObjectMeta{
Namespace: p.namespaceSpecified,
Annotations: map[string]string{
labelSelectorAnnotationInternal: selector.String(),
},
},
}
return []runtime.Object{podData}, nil
}
// Get returns the internal constructed pod with name info
func (p *PodLister) Get(name string) (runtime.Object, error) {
klog.V(4).Infof("Query pod in namespace(%s) with name:%s", p.namespaceSpecified, name)
podData := &v1.PartialObjectMetadata{
TypeMeta: v1.TypeMeta{},
ObjectMeta: v1.ObjectMeta{
Name: name,
Namespace: p.namespaceSpecified,
},
}
return podData, nil
}
// ByNamespace returns the pod lister with namespace info
func (p *PodLister) ByNamespace(namespace string) cache.GenericNamespaceLister {
klog.V(4).Infof("Query Pods in namespace: %s", namespace)
listerCopy := &PodLister{}
listerCopy.namespaceSpecified = namespace
return listerCopy
}
// NodeLister is an internal lister for nodes
type NodeLister struct {
}
// NewNodeLister creates an internal new NodeLister
func NewNodeLister() *NodeLister {
return &NodeLister{}
}
// List returns the internal constructed node with label selector info
func (n *NodeLister) List(selector labels.Selector) (ret []*corev1.Node, err error) {
klog.V(4).Infof("Query node metrics with selector: %s", selector.String())
node := &corev1.Node{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
labelSelectorAnnotationInternal: selector.String(),
},
},
}
return []*corev1.Node{node}, nil
}
// Get returns the internal constructed node with name info
func (n *NodeLister) Get(name string) (*corev1.Node, error) {
klog.V(4).Infof("Query node metrics with name:%s", name)
node := &corev1.Node{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
}
return node, nil
}
// metricsConvertV1beta1PodToInternalPod converts metricsv1beta1.PodMetrics to metrics.PodMetrics
func metricsConvertV1beta1PodToInternalPod(objs ...unstructured.Unstructured) ([]metrics.PodMetrics, error) {
var podMetricsV1beta1 []metricsv1beta1.PodMetrics
for index := range objs {
single := metricsv1beta1.PodMetrics{}
if err := helper.ConvertToTypedObject(&objs[index], &single); err != nil {
klog.Errorf("Failed to convert to typed object: %v", err)
return nil, err
}
podMetricsV1beta1 = append(podMetricsV1beta1, single)
}
var podMetricsInternal []metrics.PodMetrics
for index := range podMetricsV1beta1 {
single := metrics.PodMetrics{}
if err := metricsv1beta1.Convert_v1beta1_PodMetrics_To_metrics_PodMetrics(&podMetricsV1beta1[index], &single, nil); err != nil {
klog.Errorf("Failed to convert to typed object: %v", err)
return nil, err
}
podMetricsInternal = append(podMetricsInternal, single)
}
return podMetricsInternal, nil
}
// metricsConvertV1beta1NodeToInternalNode converts metricsv1beta1.NodeMetrics to metrics.NodeMetrics
func metricsConvertV1beta1NodeToInternalNode(objs ...unstructured.Unstructured) ([]metrics.NodeMetrics, error) {
var nodeMetricsV1beta1 []metricsv1beta1.NodeMetrics
for index := range objs {
single := metricsv1beta1.NodeMetrics{}
if err := helper.ConvertToTypedObject(&objs[index], &single); err != nil {
klog.Errorf("Failed to convert to typed object: %v", err)
return nil, err
}
nodeMetricsV1beta1 = append(nodeMetricsV1beta1, single)
}
var nodeMetricsInternal []metrics.NodeMetrics
for index := range nodeMetricsV1beta1 {
single := metrics.NodeMetrics{}
if err := metricsv1beta1.Convert_v1beta1_NodeMetrics_To_metrics_NodeMetrics(&nodeMetricsV1beta1[index], &single, nil); err != nil {
klog.Errorf("Failed to convert to typed object: %v", err)
return nil, err
}
nodeMetricsInternal = append(nodeMetricsInternal, single)
}
return nodeMetricsInternal, nil
}

View File

@ -6,6 +6,7 @@ import (
"reflect"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/equality"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -190,3 +191,14 @@ func IsClusterIdentifyUnique(controlPlaneClient karmadaclientset.Interface, id s
}
return true, "", nil
}
// IsClusterSpecPartCriticalUpdated checks whether the critical part of ClusterSpec has been updated
func IsClusterSpecPartCriticalUpdated(newSpec, oldSpec clusterv1alpha1.ClusterSpec) bool {
if oldSpec.APIEndpoint == newSpec.APIEndpoint &&
oldSpec.InsecureSkipTLSVerification == newSpec.InsecureSkipTLSVerification &&
oldSpec.ProxyURL == newSpec.ProxyURL &&
equality.Semantic.DeepEqual(oldSpec.ProxyHeader, newSpec.ProxyHeader) {
return false
}
return true
}