karmada/pkg/metricsadapter/provider/custommetrics.go

358 lines
14 KiB
Go
Executable File

package provider
import (
"context"
"fmt"
"reflect"
"strings"
"sync"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/discovery"
"k8s.io/klog/v2"
"k8s.io/metrics/pkg/apis/custom_metrics"
custommetricsv1beta2 "k8s.io/metrics/pkg/apis/custom_metrics/v1beta2"
custommetricsclient "k8s.io/metrics/pkg/client/custom_metrics"
custommetricsschema "k8s.io/metrics/pkg/client/custom_metrics/scheme"
"sigs.k8s.io/custom-metrics-apiserver/pkg/provider"
clusterlister "github.com/karmada-io/karmada/pkg/generated/listers/cluster/v1alpha1"
"github.com/karmada-io/karmada/pkg/metricsadapter/multiclient"
)
var (
versionConverter = custommetricsclient.NewMetricConverter()
)
// CustomMetricsProvider is a custom metrics provider
type CustomMetricsProvider struct {
// multiClusterDiscovery returns a discovery client for member cluster apiserver
multiClusterDiscovery multiclient.MultiClusterDiscoveryInterface
clusterLister clusterlister.ClusterLister
}
// MakeCustomMetricsProvider creates a new custom metrics provider
func MakeCustomMetricsProvider(clusterLister clusterlister.ClusterLister, multiClusterDiscovery multiclient.MultiClusterDiscoveryInterface) *CustomMetricsProvider {
return &CustomMetricsProvider{
clusterLister: clusterLister,
multiClusterDiscovery: multiClusterDiscovery,
}
}
// 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) {
clusters, err := c.clusterLister.List(labels.Everything())
if err != nil {
klog.Errorf("Failed to list clusters: %v", err)
return nil, err
}
metricValueList := &custom_metrics.MetricValueList{}
metricsChanel := make(chan *custom_metrics.MetricValueList)
wg := sync.WaitGroup{}
for _, cluster := range clusters {
wg.Add(1)
go func(clusterName string) {
defer wg.Done()
metrics, err := c.getMetricByName(ctx, clusterName, name, info, metricSelector)
if err != nil {
klog.Warningf("query %s's %s metric from cluster %s failed, err: %+v", info.GroupResource.String(), info.Metric, clusterName, err)
return
}
metricsChanel <- metrics
}(cluster.Name)
}
go func() {
wg.Wait()
close(metricsChanel)
}()
for {
metrics, ok := <-metricsChanel
if !ok {
break
}
metricValueList.Items = append(metricValueList.Items, metrics.Items...)
}
var metrics *custom_metrics.MetricValue
// TODO(chaunceyjiang) The MetricValue items need to be sorted.
for i := range metricValueList.Items {
if metrics == nil {
metrics = &metricValueList.Items[i]
continue
}
// metrics is unique in one cluster, but it may exist in multiple clusters.
// for this situation, we need to add the value of all clusters.
metrics.Value.Add(metricValueList.Items[i].Value)
}
if metrics == nil {
return nil, provider.NewMetricNotFoundError(info.GroupResource, info.Metric)
}
return metrics, nil
}
// 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) {
clusters, err := c.clusterLister.List(labels.Everything())
if err != nil {
klog.Errorf("Failed to list clusters: %v", err)
return nil, err
}
metricValueList := &custom_metrics.MetricValueList{}
wg := sync.WaitGroup{}
metricsChanel := make(chan *custom_metrics.MetricValueList)
for _, cluster := range clusters {
wg.Add(1)
go func(clusterName string) {
defer wg.Done()
metrics, err := c.getMetricBySelector(ctx, clusterName, namespace, selector, info, metricSelector)
if err != nil {
klog.Warningf("query %s's %s metric from cluster %s failed", info.GroupResource.String(), info.Metric, clusterName)
return
}
metricsChanel <- metrics
}(cluster.Name)
}
go func() {
wg.Wait()
close(metricsChanel)
}()
sameMetrics := make(map[string]custom_metrics.MetricValue)
for {
metrics, ok := <-metricsChanel
if !ok {
break
}
// TODO(chaunceyjiang) The MetricValue items need to be sorted.
for _, metric := range metrics.Items {
// metrics is unique in one cluster, but it may exist in multiple clusters.
// for this situation, we need to add the value of all clusters.
if metricValue, same := sameMetrics[metric.DescribedObject.Name]; same {
metric.Value.Add(metricValue.Value)
}
sameMetrics[metric.DescribedObject.Name] = metric
}
}
for _, metric := range sameMetrics {
metricValueList.Items = append(metricValueList.Items, metric)
}
if len(metricValueList.Items) == 0 {
return nil, provider.NewMetricNotFoundError(info.GroupResource, info.Metric)
}
return metricValueList, nil
}
func (c *CustomMetricsProvider) getPreferredVersion(discoveryClient *discovery.DiscoveryClient) (schema.GroupVersion, error) {
apiGroups, err := discoveryClient.ServerGroups()
if err != nil {
return schema.GroupVersion{}, err
}
if gv, support := supportedMetricsAPIVersionAvailable(apiGroups); support {
return gv, nil
}
return schema.GroupVersion{}, fmt.Errorf("custom.metrics.k8s.io not found")
}
func (c *CustomMetricsProvider) getMetricByName(ctx context.Context, clusterName string, name types.NamespacedName, info provider.CustomMetricInfo, metricSelector labels.Selector) (*custom_metrics.MetricValueList, error) {
// handle namespace separately
if info.GroupResource.Resource == "namespaces" && info.GroupResource.Group == "" {
return c.getForNamespace(ctx, clusterName, name.Name, info.Metric, metricSelector)
}
discoveryClient := c.multiClusterDiscovery.Get(clusterName)
if discoveryClient == nil {
err := fmt.Errorf("failed to get MultiClusterDiscovery for cluster(%s)", clusterName)
klog.Error(err)
return nil, err
}
version, err := c.getPreferredVersion(discoveryClient)
if err != nil {
klog.Errorf("failed to get custom.metrics.k8s.io preferred version for cluster(%s),Error: %v", clusterName, err)
return nil, err
}
params, err := versionConverter.ConvertListOptionsToVersion(&custom_metrics.MetricListOptions{
MetricLabelSelector: metricSelector.String(),
}, version)
if err != nil {
klog.Errorf("failed to convert ListOptions to Version for cluster(%s),Error: %v", clusterName, err)
return nil, err
}
req := discoveryClient.RESTClient().Get().Prefix("/apis/" + version.String()).Resource(info.GroupResource.String())
if info.Namespaced {
req = req.Namespace(name.Namespace)
}
result := req.Name(name.Name).SubResource(info.Metric).SpecificallyVersionedParams(params, custommetricsschema.ParameterCodec, version).Do(ctx)
metricObj, err := versionConverter.ConvertResultToVersion(result, custommetricsv1beta2.SchemeGroupVersion)
if err != nil {
return nil, err
}
return metricsConvertCustomMetricsV1beta2ToInternalCustomMetrics(metricObj)
}
func (c *CustomMetricsProvider) getMetricBySelector(ctx context.Context, clusterName, namespace string, selector labels.Selector, info provider.CustomMetricInfo, metricSelector labels.Selector) (*custom_metrics.MetricValueList, error) {
// handle namespace separately
if info.GroupResource.Resource == "namespaces" && info.GroupResource.Group == "" {
return c.getForNamespace(ctx, clusterName, custommetricsv1beta2.AllObjects, info.Metric, metricSelector)
}
discoveryClient := c.multiClusterDiscovery.Get(clusterName)
if discoveryClient == nil {
err := fmt.Errorf("failed to get MultiClusterDiscovery for cluster(%s)", clusterName)
klog.Error(err)
return nil, err
}
version, err := c.getPreferredVersion(discoveryClient)
if err != nil {
klog.Errorf("failed to get custom.metrics.k8s.io preferred version for cluster(%s),Error: %v", clusterName, err)
return nil, err
}
params, err := versionConverter.ConvertListOptionsToVersion(&custom_metrics.MetricListOptions{
MetricLabelSelector: metricSelector.String(),
LabelSelector: selector.String(),
}, version)
if err != nil {
klog.Errorf("failed to convert ListOptions to Version for cluster(%s),Error: %v", clusterName, err)
return nil, err
}
req := discoveryClient.RESTClient().Get().Prefix("/apis/" + version.String()).Resource(info.GroupResource.String())
if info.Namespaced {
req = req.Namespace(namespace)
}
result := req.Name(custommetricsv1beta2.AllObjects).SubResource(info.Metric).
SpecificallyVersionedParams(params, custommetricsschema.ParameterCodec, version).
Do(ctx)
metricObj, err := versionConverter.ConvertResultToVersion(result, custommetricsv1beta2.SchemeGroupVersion)
if err != nil {
return nil, err
}
return metricsConvertCustomMetricsV1beta2ToInternalCustomMetrics(metricObj)
}
func (c *CustomMetricsProvider) getForNamespace(ctx context.Context, clusterName, namespace string, metricName string, metricSelector labels.Selector) (*custom_metrics.MetricValueList, error) {
discoveryClient := c.multiClusterDiscovery.Get(clusterName)
if discoveryClient == nil {
err := fmt.Errorf("failed to get MultiClusterDiscovery for cluster(%s)", clusterName)
klog.Error(err)
return nil, err
}
version, err := c.getPreferredVersion(discoveryClient)
if err != nil {
klog.Errorf("failed to get custom.metrics.k8s.io preferred version for cluster(%s),Error: %v", clusterName, err)
return nil, err
}
params, err := versionConverter.ConvertListOptionsToVersion(&custom_metrics.MetricListOptions{
MetricLabelSelector: metricSelector.String(),
}, version)
if err != nil {
return nil, err
}
result := discoveryClient.RESTClient().Get().Prefix("/apis/"+version.String()).
Resource("metrics").
Namespace(namespace).
Name(metricName).
SpecificallyVersionedParams(params, custommetricsschema.ParameterCodec, version).
Do(ctx)
metricObj, err := versionConverter.ConvertResultToVersion(result, custommetricsv1beta2.SchemeGroupVersion)
if err != nil {
return nil, err
}
return metricsConvertCustomMetricsV1beta2ToInternalCustomMetrics(metricObj)
}
// ListAllMetrics returns all metrics in all member clusters
func (c *CustomMetricsProvider) ListAllMetrics() []provider.CustomMetricInfo {
clusters, err := c.clusterLister.List(labels.Everything())
if err != nil {
klog.Errorf("Failed to list clusters: %v", err)
return []provider.CustomMetricInfo{}
}
var customMetricInfos []provider.CustomMetricInfo
metricInfoChan := make(chan provider.CustomMetricInfo)
wg := sync.WaitGroup{}
for _, cluster := range clusters {
wg.Add(1)
go func(clusterName string) {
defer wg.Done()
discoveryClient := c.multiClusterDiscovery.Get(clusterName)
if discoveryClient == nil {
err := fmt.Errorf("failed to get MultiClusterDiscovery for cluster(%s)", clusterName)
klog.Error(err)
return
}
apiGroups, err := discoveryClient.ServerGroups()
if err != nil {
klog.Errorf("Failed to query resource in cluster(%s): %+v", clusterName, err)
return
}
supportGroupVersion, support := supportedMetricsAPIVersionAvailable(apiGroups)
if !support {
klog.Warningf("custom.metrics.k8s.io not found in cluster(%s)", clusterName)
return
}
resources, err := discoveryClient.ServerResourcesForGroupVersion(supportGroupVersion.String())
if err != nil {
klog.Warningf("Failed to query %s resource in cluster(%s): %+v", supportGroupVersion.String(), clusterName, err)
return
}
for _, resource := range resources.APIResources {
// The name of APIResource is composed of Metric name and GroupResource string, e.g. "jobs.batch/promhttp_metric_handler_requests", "pods/process_cpu_seconds".
// Refer to: vendor/sigs.k8s.io/custom-metrics-apiserver/pkg/provider/resource_lister.go:L45
groupResourceAndMetricName := strings.SplitN(resource.Name, "/", 2)
if len(groupResourceAndMetricName) != 2 {
klog.Warningf("Failed to query %s resource in cluster(%s): %+v", supportGroupVersion.String(), clusterName, err)
continue
}
metricInfoChan <- provider.CustomMetricInfo{
GroupResource: schema.ParseGroupResource(groupResourceAndMetricName[0]),
Namespaced: resource.Namespaced,
Metric: groupResourceAndMetricName[1],
}
}
}(cluster.Name)
}
go func() {
wg.Wait()
close(metricInfoChan)
}()
for {
metricsInfo, ok := <-metricInfoChan
if !ok {
break
}
customMetricInfos = append(customMetricInfos, metricsInfo)
}
return customMetricInfos
}
func supportedMetricsAPIVersionAvailable(discoveredAPIGroups *metav1.APIGroupList) (schema.GroupVersion, bool) {
supportedVersionSet := sets.New[string]()
for _, discoveredAPIGroup := range discoveredAPIGroups.Groups {
if discoveredAPIGroup.Name != custom_metrics.GroupName {
continue
}
for _, version := range discoveredAPIGroup.Versions {
supportedVersionSet.Insert(version.Version)
}
}
for _, supportedVersion := range custommetricsclient.MetricVersions {
if supportedVersionSet.Has(supportedVersion.Version) {
return supportedVersion, true
}
}
return schema.GroupVersion{}, false
}
func metricsConvertCustomMetricsV1beta2ToInternalCustomMetrics(obj runtime.Object) (*custom_metrics.MetricValueList, error) {
var tmp *custommetricsv1beta2.MetricValueList
var ok bool
if tmp, ok = obj.(*custommetricsv1beta2.MetricValueList); !ok {
return nil, fmt.Errorf("the custom metrics API server didn't return MetricValueList, the type is %v", reflect.TypeOf(obj))
}
res := &custom_metrics.MetricValueList{}
if err := custommetricsv1beta2.Convert_v1beta2_MetricValueList_To_custom_metrics_MetricValueList(tmp, res, nil); err != nil {
return nil, err
}
return res, nil
}