From 0a359403918094655745725999e2d92fcde477d9 Mon Sep 17 00:00:00 2001 From: ffforest Date: Wed, 29 Nov 2023 12:07:00 +0800 Subject: [PATCH] fix: add missing TODOs --- pkg/registry/cluster/storage.go | 66 +++++++++++++--- pkg/registry/search/relationship/children.go | 8 +- pkg/registry/search/relationship/common.go | 9 +-- pkg/registry/search/relationship/parents.go | 8 +- .../search/relationship/relationship.go | 75 ++++++++++++++----- pkg/registry/search/uniresource/topology.go | 4 +- pkg/util/filters/search_filter.go | 18 ++--- pkg/util/proxy/cluster_context.go | 15 +++- 8 files changed, 146 insertions(+), 57 deletions(-) diff --git a/pkg/registry/cluster/storage.go b/pkg/registry/cluster/storage.go index f0aa1d60..592456f3 100644 --- a/pkg/registry/cluster/storage.go +++ b/pkg/registry/cluster/storage.go @@ -23,6 +23,7 @@ import ( "github.com/KusionStack/karbour/pkg/apis/cluster" "github.com/KusionStack/karbour/pkg/registry/search/relationship" + proxyutil "github.com/KusionStack/karbour/pkg/util/proxy" "github.com/dominikbraun/graph/draw" "github.com/pkg/errors" @@ -33,7 +34,10 @@ import ( "k8s.io/apiserver/pkg/registry/generic" genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" "k8s.io/apiserver/pkg/registry/rest" + "k8s.io/client-go/discovery" "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" + restclient "k8s.io/client-go/rest" "k8s.io/klog/v2" "sigs.k8s.io/structured-merge-diff/v4/fieldpath" ) @@ -110,13 +114,35 @@ func (r *StatusREST) Get(ctx context.Context, name string, options *metav1.GetOp func (r *StatusREST) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { rt := &cluster.Cluster{} - client, err := r.BuildDynamicClient(ctx) + dynamicClient, err := r.BuildDynamicClient(ctx) if err != nil { return rt, err } - graph, rg, _ := relationship.BuildRelationshipGraph(ctx, client, true) + discoveryClient, err := r.BuildDiscoveryClient(ctx) + if err != nil { + return rt, err + } + + graph, rg, _ := relationship.BuildRelationshipGraph(ctx, dynamicClient) + namespace, ok := proxyutil.NamespaceFrom(ctx) + if !ok { + // Count resources in all namespaces + klog.Infof("Retrieving topology for the entire cluster") + rg, err = rg.CountRelationshipGraph(ctx, dynamicClient, discoveryClient, "") + if err != nil { + return rt, err + } + } else { + // Only count resources that belong to a specific namespace + klog.Infof("Retrieving topology for namespace: %s", namespace) + rg, err = rg.CountRelationshipGraph(ctx, dynamicClient, discoveryClient, namespace) + if err != nil { + return rt, err + } + } // Draw graph + // TODO: This is drawn on the server side, not needed eventually file, _ := os.Create("./relationship.gv") _ = draw.DOT(graph, file) @@ -151,6 +177,31 @@ func (r *StatusREST) ConvertToTable(ctx context.Context, object runtime.Object, // BuildDynamicClient returns a dynamic client based on the cluster name in the request func (r *StatusREST) BuildDynamicClient(ctx context.Context) (*dynamic.DynamicClient, error) { + config, err := r.GetConfigFromContext(ctx) + if err != nil { + return nil, err + } + // Create the dynamic client + dynamicClient, err := dynamic.NewForConfig(config) + if err != nil { + return nil, err + } + + return dynamicClient, nil +} + +func (r *StatusREST) BuildDiscoveryClient(ctx context.Context) (*discovery.DiscoveryInterface, error) { + config, err := r.GetConfigFromContext(ctx) + if err != nil { + return nil, err + } + // Create the discovery client + clientset := kubernetes.NewForConfigOrDie(config) + discoveryClient := clientset.Discovery() + return &discoveryClient, nil +} + +func (r *StatusREST) GetConfigFromContext(ctx context.Context) (*restclient.Config, error) { // Extract the cluster name from context info, ok := request.RequestInfoFrom(ctx) if !ok { @@ -164,17 +215,10 @@ func (r *StatusREST) BuildDynamicClient(ctx context.Context) (*dynamic.DynamicCl return nil, err } clusterFromContext := obj.(*cluster.Cluster) - klog.Infof("Cluster found: %s", clusterFromContext.Name) + klog.Infof("Cluster found for discovery client: %s", clusterFromContext.Name) config, err := NewConfigFromCluster(clusterFromContext) if err != nil { return nil, errors.Wrapf(err, "failed to create cluster client config %s", clusterFromContext.Name) } - - // Create the dynamic client - client, err := dynamic.NewForConfig(config) - if err != nil { - return nil, err - } - - return client, nil + return config, nil } diff --git a/pkg/registry/search/relationship/children.go b/pkg/registry/search/relationship/children.go index fb008745..61187f0b 100644 --- a/pkg/registry/search/relationship/children.go +++ b/pkg/registry/search/relationship/children.go @@ -19,15 +19,15 @@ package relationship import ( "context" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/klog/v2" - topologyutil "github.com/KusionStack/karbour/pkg/util/topology" "github.com/dominikbraun/graph" + + "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" + "k8s.io/klog/v2" ) // GetChildResourcesList returns an *unstructured.UnstructuredList representing all resources that matches the child GVK in the current namespace diff --git a/pkg/registry/search/relationship/common.go b/pkg/registry/search/relationship/common.go index 0b7b1d08..65dace62 100644 --- a/pkg/registry/search/relationship/common.go +++ b/pkg/registry/search/relationship/common.go @@ -19,14 +19,13 @@ package relationship import ( "context" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/klog/v2" - + topologyutil "github.com/KusionStack/karbour/pkg/util/topology" "github.com/dominikbraun/graph" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" - - topologyutil "github.com/KusionStack/karbour/pkg/util/topology" + "k8s.io/klog/v2" ) func GetByJSONPath(relatedResList *unstructured.UnstructuredList, relationshipType string, ctx context.Context, client *dynamic.DynamicClient, obj unstructured.Unstructured, relation *Relationship, relatedGVK schema.GroupVersionKind, objResourceNode ResourceGraphNode, relationshipGraph graph.Graph[string, RelationshipGraphNode], resourceGraph graph.Graph[string, ResourceGraphNode]) (graph.Graph[string, ResourceGraphNode], error) { diff --git a/pkg/registry/search/relationship/parents.go b/pkg/registry/search/relationship/parents.go index a50c1a30..c6aef801 100644 --- a/pkg/registry/search/relationship/parents.go +++ b/pkg/registry/search/relationship/parents.go @@ -19,15 +19,15 @@ package relationship import ( "context" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/klog/v2" - topologyutil "github.com/KusionStack/karbour/pkg/util/topology" "github.com/dominikbraun/graph" + + "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" + "k8s.io/klog/v2" ) // GetParentResourcesList returns an *unstructured.UnstructuredList representing all resources that matches the parent GVK in the current namespace diff --git a/pkg/registry/search/relationship/relationship.go b/pkg/registry/search/relationship/relationship.go index b1182b68..f3ca69db 100644 --- a/pkg/registry/search/relationship/relationship.go +++ b/pkg/registry/search/relationship/relationship.go @@ -22,15 +22,17 @@ import ( "os" "reflect" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/dynamic" - "k8s.io/klog/v2" - topologyutil "github.com/KusionStack/karbour/pkg/util/topology" "github.com/dominikbraun/graph" "github.com/dominikbraun/graph/draw" + yaml "gopkg.in/yaml.v3" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/discovery" + "k8s.io/client-go/dynamic" + "k8s.io/klog/v2" ) func (rgn ResourceGraphNode) GetHash() string { @@ -93,7 +95,7 @@ func FindNodeOnGraph(g graph.Graph[string, RelationshipGraphNode], group, versio } // BuildBuiltinRelationshipGraph returns the relationship graph built from the YAML describing resource relationships -func BuildBuiltinRelationshipGraph(ctx context.Context, client *dynamic.DynamicClient, countResouces bool) (graph.Graph[string, RelationshipGraphNode], *RelationshipGraph, error) { +func BuildBuiltinRelationshipGraph(ctx context.Context, client *dynamic.DynamicClient) (graph.Graph[string, RelationshipGraphNode], *RelationshipGraph, error) { r := RelationshipGraph{} yamlFile, err := os.ReadFile("relationship.yaml") if err != nil { @@ -142,19 +144,7 @@ func BuildBuiltinRelationshipGraph(ctx context.Context, client *dynamic.DynamicC // Add Vertices for _, node := range r.RelationshipNodes { klog.Infof("Adding Vertex: %s\n", node.GetHash()) - if countResouces { - resGVR, err := topologyutil.GetGVRFromGVK(schema.GroupVersion{Group: node.Group, Version: node.Version}.String(), node.Kind) - if err != nil { - return nil, nil, err - } - resList, _ := client.Resource(resGVR).List(ctx, metav1.ListOptions{}) - resCount := len(resList.Items) - klog.Infof("Counted resources for Vertex %s: %d\n", node.GetHash(), resCount) - node.ResourceCount = resCount - _ = g.AddVertex(*node, graph.VertexWeight(resCount)) - } else { - _ = g.AddVertex(*node) - } + _ = g.AddVertex(*node) } // Add Edges, requires all vertices to be present for _, node := range r.RelationshipNodes { @@ -176,6 +166,7 @@ func BuildBuiltinRelationshipGraph(ctx context.Context, client *dynamic.DynamicC klog.Infof("Built-in graph completed.") // Draw graph + // TODO: This is drawn on the server side, not needed eventually file, _ := os.Create("./relationship.gv") _ = draw.DOT(g, file) @@ -183,8 +174,8 @@ func BuildBuiltinRelationshipGraph(ctx context.Context, client *dynamic.DynamicC } // BuildRelationshipGraph builds the complete relationship graph including the built-in one and customer-specified one -func BuildRelationshipGraph(ctx context.Context, client *dynamic.DynamicClient, countResouces bool) (graph.Graph[string, RelationshipGraphNode], *RelationshipGraph, error) { - res, rg, _ := BuildBuiltinRelationshipGraph(ctx, client, countResouces) +func BuildRelationshipGraph(ctx context.Context, client *dynamic.DynamicClient) (graph.Graph[string, RelationshipGraphNode], *RelationshipGraph, error) { + res, rg, _ := BuildBuiltinRelationshipGraph(ctx, client) // TODO: Also include customized relationship graph return res, rg, nil } @@ -225,3 +216,47 @@ func InsertIfNotExist(relationList []*Relationship, relation Relationship, relat func RelationshipEquals(r, relation *Relationship) bool { return r.Group == relation.Group && r.Version == relation.Version && r.Kind == relation.Kind && r.Type == relation.Type && reflect.DeepEqual(r.JSONPath, relation.JSONPath) } + +// CountRelationshipGraph returns the same RelationshipGraph with the count for each resource +func (rg *RelationshipGraph) CountRelationshipGraph(ctx context.Context, dynamicClient *dynamic.DynamicClient, discoveryClient *discovery.DiscoveryInterface, countNamespace string) (*RelationshipGraph, error) { + for _, node := range rg.RelationshipNodes { + var resList *unstructured.UnstructuredList + resGVR, err := topologyutil.GetGVRFromGVK(schema.GroupVersion{Group: node.Group, Version: node.Version}.String(), node.Kind) + if err != nil { + return rg, err + } + if countNamespace == "" { + resList, err = dynamicClient.Resource(resGVR).List(ctx, metav1.ListOptions{}) + } else if countNamespace != "" && GVRNamespaced(resGVR, *discoveryClient) { + resList, err = dynamicClient.Resource(resGVR).Namespace(countNamespace).List(ctx, metav1.ListOptions{}) + } else { + continue + } + if err != nil { + return rg, err + } + resCount := len(resList.Items) + klog.Infof("Counted resources for Vertex %s: %d\n", node.GetHash(), resCount) + node.ResourceCount = resCount + } + return rg, nil +} + +// GVRNamespaced returns true if a given GVR is namespaced based on the result of discovery client +func GVRNamespaced(gvr schema.GroupVersionResource, discoveryClient discovery.DiscoveryInterface) bool { + apiResourceList, err := discoveryClient.ServerResourcesForGroupVersion(gvr.GroupVersion().String()) + if err != nil { + return false + } + // Iterate over the APIResources to find the one that matches the Resource and determine if it is namespaced + for _, apiResource := range apiResourceList.APIResources { + if apiResource.Name == gvr.Resource { + if apiResource.Namespaced { + return true + } else { + return false + } + } + } + return false +} diff --git a/pkg/registry/search/uniresource/topology.go b/pkg/registry/search/uniresource/topology.go index ca079c03..212144f3 100644 --- a/pkg/registry/search/uniresource/topology.go +++ b/pkg/registry/search/uniresource/topology.go @@ -81,7 +81,7 @@ func (r *TopologyREST) List(ctx context.Context, options *internalversion.ListOp if err != nil { return nil, err } - rg, _, err := relationship.BuildRelationshipGraph(ctx, client, false) + rg, _, err := relationship.BuildRelationshipGraph(ctx, client) if err != nil { return nil, err } @@ -103,7 +103,7 @@ func (r *TopologyREST) List(ctx context.Context, options *internalversion.ListOp } } // Draw graph - // TODO: This is drawn on the server side, probably not needed eventually + // TODO: This is drawn on the server side, not needed eventually file, _ := os.Create("./resource.gv") _ = draw.DOT(g, file) diff --git a/pkg/util/filters/search_filter.go b/pkg/util/filters/search_filter.go index 2a3c7af8..bf2638be 100644 --- a/pkg/util/filters/search_filter.go +++ b/pkg/util/filters/search_filter.go @@ -21,7 +21,7 @@ import ( "github.com/KusionStack/karbour/pkg/search/storage" ) -type ctxTyp string +type CtxTyp string type Resource struct { Name string @@ -42,7 +42,7 @@ const ( ) func SearchQueryFrom(ctx context.Context) (string, bool) { - query, ok := ctx.Value(ctxTyp(searchQueryKey)).(string) + query, ok := ctx.Value(CtxTyp(searchQueryKey)).(string) if !ok { return "", false } @@ -50,7 +50,7 @@ func SearchQueryFrom(ctx context.Context) (string, bool) { } func PatternTypeFrom(ctx context.Context) (string, bool) { - patternType, ok := ctx.Value(ctxTyp(patternTypeKey)).(string) + patternType, ok := ctx.Value(CtxTyp(patternTypeKey)).(string) if !ok { return "", false } @@ -59,23 +59,23 @@ func PatternTypeFrom(ctx context.Context) (string, bool) { func ResourceDetailFrom(ctx context.Context) (Resource, bool) { res := Resource{} - resourceName, ok := ctx.Value(ctxTyp(resourceNameQueryKey)).(string) + resourceName, ok := ctx.Value(CtxTyp(resourceNameQueryKey)).(string) if !ok { return res, false } - namespace, ok := ctx.Value(ctxTyp(resourceNamespaceQueryKey)).(string) + namespace, ok := ctx.Value(CtxTyp(resourceNamespaceQueryKey)).(string) if !ok { return res, false } - cluster, ok := ctx.Value(ctxTyp(resourceClusterQueryKey)).(string) + cluster, ok := ctx.Value(CtxTyp(resourceClusterQueryKey)).(string) if !ok { return res, false } - apiVersion, ok := ctx.Value(ctxTyp(resourceAPIVersionQueryKey)).(string) + apiVersion, ok := ctx.Value(CtxTyp(resourceAPIVersionQueryKey)).(string) if !ok { return res, false } - kind, ok := ctx.Value(ctxTyp(resourceKindQueryKey)).(string) + kind, ok := ctx.Value(CtxTyp(resourceKindQueryKey)).(string) if !ok { return res, false } @@ -114,5 +114,5 @@ func FromQueryToContext(req *http.Request, key string, defaultVal string) *http. query.Del(key) val = queryVal[0] } - return req.WithContext(context.WithValue(req.Context(), ctxTyp(key), val)) + return req.WithContext(context.WithValue(req.Context(), CtxTyp(key), val)) } diff --git a/pkg/util/proxy/cluster_context.go b/pkg/util/proxy/cluster_context.go index 7146abb0..ed090b21 100644 --- a/pkg/util/proxy/cluster_context.go +++ b/pkg/util/proxy/cluster_context.go @@ -19,14 +19,17 @@ import ( "fmt" "net/http" "path" + + filtersutil "github.com/KusionStack/karbour/pkg/util/filters" ) type clusterKey int const ( // clusterKey is the context key for the request namespace. - clusterContextKey clusterKey = iota - ClusterProxyURL = "/apis/cluster.karbour.com/v1beta1/clusters/%s/proxy/" + clusterContextKey clusterKey = iota + ClusterProxyURL = "/apis/cluster.karbour.com/v1beta1/clusters/%s/proxy/" + clusterNamespaceKey = "namespace" ) // WithCluster returns a context that describes the nested cluster context @@ -43,6 +46,14 @@ func ClusterFrom(ctx context.Context) (string, bool) { return cluster, true } +func NamespaceFrom(ctx context.Context) (string, bool) { + namespace, ok := ctx.Value(filtersutil.CtxTyp(clusterNamespaceKey)).(string) + if !ok { + return "", false + } + return namespace, true +} + func WithProxyByCluster(handler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { cluster, ok := ClusterFrom(req.Context())