feat(topology): add resource topology visualization#494
feat(topology): add resource topology visualization#494SunsetB612 wants to merge 1 commit intokarmada-io:mainfrom
Conversation
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
There was a problem hiding this comment.
Code Review
This pull request implements a resource topology feature in the Karmada dashboard, providing a visual representation of the propagation chain from control-plane workloads to member clusters. Key additions include a backend API that utilizes custom informer indexers for resource tracing and a frontend graph component built with React Flow. Feedback identifies several areas for refinement, such as adopting a dynamic client to support CRDs, ensuring proper context propagation, and improving the robustness of cluster name extraction. Additionally, the tracing logic needs to prevent duplicate node IDs, and the UI's resource search should be dynamically populated to improve the user experience.
| func getWorkload(ctx context.Context, k8sClient kubeclient.Interface, namespace, name, kind string) (types.UID, map[string]string, error) { | ||
| switch kind { | ||
| case "Deployment": | ||
| obj, err := k8sClient.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{}) | ||
| if err != nil { | ||
| return "", nil, err | ||
| } | ||
| return obj.UID, obj.Annotations, nil | ||
| case "StatefulSet": | ||
| obj, err := k8sClient.AppsV1().StatefulSets(namespace).Get(ctx, name, metav1.GetOptions{}) | ||
| if err != nil { | ||
| return "", nil, err | ||
| } | ||
| return obj.UID, obj.Annotations, nil | ||
| case "DaemonSet": | ||
| obj, err := k8sClient.AppsV1().DaemonSets(namespace).Get(ctx, name, metav1.GetOptions{}) | ||
| if err != nil { | ||
| return "", nil, err | ||
| } | ||
| return obj.UID, obj.Annotations, nil | ||
| case "Job": | ||
| obj, err := k8sClient.BatchV1().Jobs(namespace).Get(ctx, name, metav1.GetOptions{}) | ||
| if err != nil { | ||
| return "", nil, err | ||
| } | ||
| return obj.UID, obj.Annotations, nil | ||
| case "CronJob": | ||
| obj, err := k8sClient.BatchV1().CronJobs(namespace).Get(ctx, name, metav1.GetOptions{}) | ||
| if err != nil { | ||
| return "", nil, err | ||
| } | ||
| return obj.UID, obj.Annotations, nil | ||
| default: | ||
| return "", nil, fmt.Errorf("unsupported workload kind: %s", kind) | ||
| } | ||
| } |
There was a problem hiding this comment.
The getWorkload function is currently limited to a hardcoded set of built-in Kubernetes resources (Deployment, StatefulSet, etc.) because it uses the typed kubernetes.Interface. Karmada is designed to propagate any resource type, including Custom Resource Definitions (CRDs).
Consider using a dynamic.Interface to fetch the workload. This would make the topology feature generic and compatible with all resources managed by Karmada.
| return | ||
| } | ||
|
|
||
| result, err := topology.GetResourceTopology(k8sClient, namespace, name, kind) |
There was a problem hiding this comment.
The request context should be passed to GetResourceTopology to ensure that any cancellation or timeout of the HTTP request is correctly propagated to the underlying resource fetching logic.
| result, err := topology.GetResourceTopology(k8sClient, namespace, name, kind) | |
| result, err := topology.GetResourceTopology(c.Request.Context(), k8sClient, namespace, name, kind) |
| func GetResourceTopology( | ||
| k8sClient kubeclient.Interface, | ||
| namespace, name, kind string) (*TopologyResponse, error) { | ||
| return traceChain(context.TODO(), k8sClient, namespace, name, kind) | ||
| } |
There was a problem hiding this comment.
The GetResourceTopology function should accept a context.Context parameter instead of using context.TODO(). This allows for proper request lifecycle management and cancellation propagation.
| func GetResourceTopology( | |
| k8sClient kubeclient.Interface, | |
| namespace, name, kind string) (*TopologyResponse, error) { | |
| return traceChain(context.TODO(), k8sClient, namespace, name, kind) | |
| } | |
| func GetResourceTopology( | |
| ctx context.Context, | |
| k8sClient kubeclient.Interface, | |
| namespace, name, kind string) (*TopologyResponse, error) { | |
| return traceChain(ctx, k8sClient, namespace, name, kind) | |
| } |
| // clusterNameFromWorkNamespace extracts cluster name from "karmada-es-{cluster}" namespace. | ||
| func clusterNameFromWorkNamespace(ns string) string { | ||
| const prefix = "karmada-es-" | ||
| if len(ns) > len(prefix) { | ||
| return ns[len(prefix):] | ||
| } | ||
| return "" | ||
| } |
There was a problem hiding this comment.
The logic for extracting the cluster name is fragile. It currently assumes that any namespace longer than the prefix starts with that prefix, which can lead to incorrect cluster names if a resource is in a different namespace that happens to be long enough. It's safer to use strings.HasPrefix and strings.TrimPrefix.
| // clusterNameFromWorkNamespace extracts cluster name from "karmada-es-{cluster}" namespace. | |
| func clusterNameFromWorkNamespace(ns string) string { | |
| const prefix = "karmada-es-" | |
| if len(ns) > len(prefix) { | |
| return ns[len(prefix):] | |
| } | |
| return "" | |
| } | |
| // clusterNameFromWorkNamespace extracts cluster name from "karmada-es-{cluster}" namespace. | |
| func clusterNameFromWorkNamespace(ns string) string { | |
| const prefix = "karmada-es-" | |
| if strings.HasPrefix(ns, prefix) { | |
| return strings.TrimPrefix(ns, prefix) | |
| } | |
| return "" | |
| } |
| func getMemberWorkloadStatus(ctx context.Context, clusterName, namespace, name, kind string) NodeStatus { | ||
| memberClient := client.InClusterClientForMemberCluster(clusterName) | ||
| if memberClient == nil { | ||
| return NodeStatusAbnormal | ||
| } | ||
| switch kind { | ||
| case "Deployment": | ||
| deploy, err := memberClient.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{}) | ||
| if err != nil { | ||
| klog.V(4).InfoS("Failed to get member deployment", "cluster", clusterName, "err", err) | ||
| return NodeStatusAbnormal | ||
| } | ||
| if deploy.Spec.Replicas != nil && deploy.Status.ReadyReplicas == *deploy.Spec.Replicas { | ||
| return NodeStatusHealthy | ||
| } | ||
| return NodeStatusProgressing | ||
| default: | ||
| return NodeStatusHealthy | ||
| } | ||
| } |
There was a problem hiding this comment.
The getMemberWorkloadStatus function only implements health checks for Deployment. For all other resource types, it defaults to NodeStatusHealthy, which could be misleading if the actual resource in the member cluster is in a failing state.
Consider implementing status checks for other common types or using a more generic approach to determine resource health.
| k8sClient kubeclient.Interface, | ||
| namespace, name, kind string, | ||
| ) (*TopologyResponse, error) { | ||
| resp := &TopologyResponse{} |
There was a problem hiding this comment.
The traceChain function appends nodes to a slice without checking for duplicate IDs. In complex propagation scenarios, multiple paths might lead to the same resource (e.g., the same member cluster workload). Duplicate IDs in the TopologyResponse will cause issues in the UI as React Flow requires unique node IDs.
Consider using a map to track added nodes by their ID before appending them to resp.Nodes.
| allowClear | ||
| value={name || undefined} | ||
| onChange={(v) => setName(v || '')} | ||
| options={[]} |
f97033c to
1c47416
Compare
fcb96ab to
ad34869
Compare
Signed-off-by: SunsetB612 <10235101575@stu.ecnu.edu.cn>
ad34869 to
bf17c36
Compare
No description provided.