diff --git a/envvar.go b/envvar.go index 38c1982c..d9616ebb 100644 --- a/envvar.go +++ b/envvar.go @@ -48,7 +48,7 @@ func GetSecretFromCache(c kubernetes.Interface, namespace, name, key string) (st } secret, err := c.CoreV1().Secrets(namespace).Get(context.Background(), name, metav1.GetOptions{}) if secret == nil { - return "", fmt.Errorf("Could not get contents of secret %v from namespace %v: %v", name, namespace, err) + return "", fmt.Errorf("could not get contents of secret %v from namespace %v: %w", name, namespace, err) } value, ok := secret.Data[key] @@ -58,7 +58,7 @@ func GetSecretFromCache(c kubernetes.Interface, namespace, name, key string) (st for k := range secret.Data { names = append(names, k) } - return "", fmt.Errorf("Could not find key %v in secret %v (%s)", key, name, strings.Join(names, ", ")) + return "", fmt.Errorf("could not find key %v in secret %v (%s)", key, name, strings.Join(names, ", ")) } envCache.Set(id, string(value), 5*time.Minute) return string(value), nil @@ -71,7 +71,7 @@ func GetConfigMapFromCache(c kubernetes.Interface, namespace, name, key string) } configMap, err := c.CoreV1().ConfigMaps(namespace).Get(context.Background(), name, metav1.GetOptions{}) if configMap == nil { - return "", fmt.Errorf("Could not get contents of configmap %v from namespace %v: %v", name, namespace, err) + return "", fmt.Errorf("could not get contents of configmap %v from namespace %v: %w", name, namespace, err) } value, ok := configMap.Data[key] @@ -80,7 +80,7 @@ func GetConfigMapFromCache(c kubernetes.Interface, namespace, name, key string) for k := range configMap.Data { names = append(names, k) } - return "", fmt.Errorf("Could not find key %v in configmap %v (%s)", key, name, strings.Join(names, ", ")) + return "", fmt.Errorf("could not find key %v in configmap %v (%s)", key, name, strings.Join(names, ", ")) } envCache.Set(id, string(value), 5*time.Minute) return string(value), nil diff --git a/fixtures/dummy/components.go b/fixtures/dummy/components.go index 2d63a6a5..0541d2e1 100644 --- a/fixtures/dummy/components.go +++ b/fixtures/dummy/components.go @@ -1,6 +1,8 @@ package dummy import ( + "fmt" + "github.com/flanksource/duty/models" "github.com/flanksource/duty/types" "github.com/google/uuid" @@ -72,76 +74,99 @@ var ClusterComponent = models.Component{ Type: "KubernetesCluster", Status: types.ComponentStatusHealthy, CreatedAt: DummyCreatedAt, + Tooltip: "Kubernetes Cluster", + Icon: "icon-cluster", } var NodesComponent = models.Component{ ID: uuid.MustParse("018681fe-b27e-7627-72c2-ad18e93f72f4"), Name: "Nodes", + Icon: "icon-kubernetes-node", + Tooltip: "Kubernetes Nodes", ExternalId: "dummy/nodes", Type: "KubernetesNodes", Status: types.ComponentStatusHealthy, ParentId: &ClusterComponent.ID, CreatedAt: DummyCreatedAt, + Path: ClusterComponent.ID.String(), } var NodeA = models.Component{ ID: uuid.MustParse("018681fe-f5aa-37e9-83f7-47b5b0232d5e"), Name: "node-a", + Icon: "icon-kubernetes-node", + Tooltip: "Node A", ExternalId: "dummy/node-a", Type: "KubernetesNode", Status: types.ComponentStatusHealthy, ParentId: &NodesComponent.ID, CreatedAt: DummyCreatedAt, + Path: fmt.Sprintf("%s.%s", ClusterComponent.ID.String(), NodesComponent.ID.String()), } var NodeB = models.Component{ ID: uuid.MustParse("018681ff-227e-4d71-b38e-0693cc862213"), Name: "node-b", + Icon: "icon-kubernetes-node", + Tooltip: "Node B", ExternalId: "dummy/node-b", Type: "KubernetesNode", Status: types.ComponentStatusHealthy, ParentId: &NodesComponent.ID, CreatedAt: DummyCreatedAt, + Path: fmt.Sprintf("%s.%s", ClusterComponent.ID.String(), NodesComponent.ID.String()), } var PodsComponent = models.Component{ ID: uuid.MustParse("018681ff-559f-7183-19d1-7d898b4e1413"), Name: "Pods", + Icon: "icon-kubernetes-pod", + Tooltip: "Kubernetes Pods", ExternalId: "dummy/pods", Type: "KubernetesPods", Status: types.ComponentStatusHealthy, ParentId: &ClusterComponent.ID, CreatedAt: DummyCreatedAt, + Path: ClusterComponent.ID.String(), } var LogisticsAPIPod = models.Component{ ID: uuid.MustParse("018681ff-80ed-d10d-21ef-c74f152b085b"), Name: "logistics-api-574dc95b5d-mp64w", + Icon: "icon-kubernetes-pod", + Tooltip: "Logistic API Pod", ExternalId: "dummy/logistics-api-574dc95b5d-mp64w", Type: "KubernetesPod", Status: types.ComponentStatusHealthy, ParentId: &PodsComponent.ID, CreatedAt: DummyCreatedAt, + Path: fmt.Sprintf("%s.%s", ClusterComponent.ID.String(), PodsComponent.ID.String()), } var LogisticsUIPod = models.Component{ ID: uuid.MustParse("018681ff-b6c1-a14d-2fd4-8c7dac94cddd"), Name: "logistics-ui-676b85b87c-tjjcp", + Icon: "icon-kubernetes-pod", + Tooltip: "Logistic UI Pod", Type: "KubernetesPod", ExternalId: "dummy/logistics-ui-676b85b87c-tjjcp", Status: types.ComponentStatusHealthy, ParentId: &PodsComponent.ID, CreatedAt: DummyCreatedAt, + Path: fmt.Sprintf("%s.%s", ClusterComponent.ID.String(), PodsComponent.ID.String()), } var LogisticsWorkerPod = models.Component{ ID: uuid.MustParse("018681ff-e578-a926-e366-d2dc0646eafa"), Name: "logistics-worker-79cb67d8f5-lr66n", + Icon: "icon-kubernetes-pod", + Tooltip: "Logistic Worker Pod", ExternalId: "dummy/logistics-worker-79cb67d8f5-lr66n", Type: "KubernetesPod", Status: types.ComponentStatusHealthy, ParentId: &PodsComponent.ID, CreatedAt: DummyCreatedAt, + Path: fmt.Sprintf("%s.%s", ClusterComponent.ID.String(), PodsComponent.ID.String()), } var PaymentsAPI = models.Component{ diff --git a/fixtures/expectations/topology_child_tree.json b/fixtures/expectations/topology_child_tree.json index 6994cf59..8fb7854d 100644 --- a/fixtures/expectations/topology_child_tree.json +++ b/fixtures/expectations/topology_child_tree.json @@ -1,19 +1,8 @@ { "components": [ { - "id": "018681fe-f5aa-37e9-83f7-47b5b0232d5e", "agent_id": "00000000-0000-0000-0000-000000000000", - "external_id": "dummy/node-a", - "parent_id": "018681fe-b27e-7627-72c2-ad18e93f72f4", - "name": "node-a", - "status": "healthy", - "type": "KubernetesNode", - "summary": { - "healthy": 2 - }, - "is_leaf": false, - "created_at": "2023-01-01T05:29:00+05:30", - "updated_at": "2023-01-01T05:29:00+05:30", + "children": ["018681ff-80ed-d10d-21ef-c74f152b085b", "018681ff-b6c1-a14d-2fd4-8c7dac94cddd"], "components": [ { "id": "018681ff-80ed-d10d-21ef-c74f152b085b", @@ -29,9 +18,10 @@ "is_leaf": false, "created_at": "2023-01-01T05:29:00+05:30", "updated_at": "2023-01-01T05:29:00+05:30", - "parents": [ - "018681fe-f5aa-37e9-83f7-47b5b0232d5e" - ] + "icon": "icon-kubernetes-pod", + "tooltip": "Logistic API Pod", + "parents": ["018681fe-f5aa-37e9-83f7-47b5b0232d5e"], + "path": "018681fe-8156-4b91-d178-caf8b3c2818c.018681ff-559f-7183-19d1-7d898b4e1413" }, { "id": "018681ff-b6c1-a14d-2fd4-8c7dac94cddd", @@ -47,15 +37,49 @@ "is_leaf": false, "created_at": "2023-01-01T05:29:00+05:30", "updated_at": "2023-01-01T05:29:00+05:30", - "parents": [ - "018681fe-f5aa-37e9-83f7-47b5b0232d5e" - ] + "icon": "icon-kubernetes-pod", + "tooltip": "Logistic UI Pod", + "parents": ["018681fe-f5aa-37e9-83f7-47b5b0232d5e"], + "path": "018681fe-8156-4b91-d178-caf8b3c2818c.018681ff-559f-7183-19d1-7d898b4e1413" + }, + { + "agent_id": "00000000-0000-0000-0000-000000000000", + "id": "018681ff-80ed-d10d-21ef-c74f152b085b", + "is_leaf": false, + "name": "logistics-api-574dc95b5d-mp64w", + "parent_id": "018681ff-559f-7183-19d1-7d898b4e1413", + "parents": ["018681fe-f5aa-37e9-83f7-47b5b0232d5e"], + "status": "healthy", + "summary": { + "healthy": 1 + }, + "type": "KubernetesPod" + }, + { + "agent_id": "00000000-0000-0000-0000-000000000000", + "id": "018681ff-b6c1-a14d-2fd4-8c7dac94cddd", + "is_leaf": false, + "name": "logistics-ui-676b85b87c-tjjcp", + "parent_id": "018681ff-559f-7183-19d1-7d898b4e1413", + "parents": ["018681fe-f5aa-37e9-83f7-47b5b0232d5e"], + "status": "healthy", + "summary": { + "healthy": 1 + }, + "type": "KubernetesPod" } ], - "children": [ - "018681ff-80ed-d10d-21ef-c74f152b085b", - "018681ff-b6c1-a14d-2fd4-8c7dac94cddd" - ] + "external_id": "dummy/node-a", + "icon": "icon-kubernetes-node", + "id": "018681fe-f5aa-37e9-83f7-47b5b0232d5e", + "is_leaf": false, + "name": "node-a", + "parent_id": "018681fe-b27e-7627-72c2-ad18e93f72f4", + "path": "018681fe-8156-4b91-d178-caf8b3c2818c.018681fe-b27e-7627-72c2-ad18e93f72f4", + "status": "healthy", + "summary": { "healthy": 4 }, + "tooltip": "Node A", + "type": "KubernetesNode" } ], "healthStatuses": [ diff --git a/fixtures/expectations/topology_cluster_component_tree.json b/fixtures/expectations/topology_cluster_component_tree.json new file mode 100644 index 00000000..b9fc73ec --- /dev/null +++ b/fixtures/expectations/topology_cluster_component_tree.json @@ -0,0 +1,173 @@ +{ + "components": [ + { + "agent_id": "00000000-0000-0000-0000-000000000000", + "components": [ + { + "agent_id": "00000000-0000-0000-0000-000000000000", + "components": [ + { + "agent_id": "00000000-0000-0000-0000-000000000000", + "components": [ + { + "agent_id": "00000000-0000-0000-0000-000000000000", + "id": "018681ff-e578-a926-e366-d2dc0646eafa", + "is_leaf": false, + "name": "logistics-worker-79cb67d8f5-lr66n", + "status": "healthy", + "summary": { + "healthy": 1 + }, + "type": "KubernetesPod" + } + ], + "id": "018681ff-227e-4d71-b38e-0693cc862213", + "is_leaf": false, + "name": "node-b", + "status": "healthy", + "summary": { + "healthy": 1, + "insights": { + "security": { + "critical": 1 + } + } + }, + "type": "KubernetesNode" + }, + { + "agent_id": "00000000-0000-0000-0000-000000000000", + "components": [ + { + "agent_id": "00000000-0000-0000-0000-000000000000", + "id": "018681ff-80ed-d10d-21ef-c74f152b085b", + "is_leaf": false, + "name": "logistics-api-574dc95b5d-mp64w", + "status": "healthy", + "summary": { + "healthy": 1 + }, + "type": "KubernetesPod" + }, + { + "agent_id": "00000000-0000-0000-0000-000000000000", + "id": "018681ff-b6c1-a14d-2fd4-8c7dac94cddd", + "is_leaf": false, + "name": "logistics-ui-676b85b87c-tjjcp", + "status": "healthy", + "summary": { + "healthy": 1 + }, + "type": "KubernetesPod" + } + ], + "id": "018681fe-f5aa-37e9-83f7-47b5b0232d5e", + "is_leaf": false, + "name": "node-a", + "status": "healthy", + "summary": { + "healthy": 2 + }, + "type": "KubernetesNode" + } + ], + "external_id": "dummy/nodes", + "icon": "icon-kubernetes-node", + "id": "018681fe-b27e-7627-72c2-ad18e93f72f4", + "is_leaf": false, + "name": "Nodes", + "parent_id": "018681fe-8156-4b91-d178-caf8b3c2818c", + "path": "018681fe-8156-4b91-d178-caf8b3c2818c", + "status": "healthy", + "summary": { + "healthy": 2, + "insights": { + "security": { + "critical": 1 + } + } + }, + "tooltip": "Kubernetes Nodes", + "type": "KubernetesNodes" + }, + { + "agent_id": "00000000-0000-0000-0000-000000000000", + "components": [ + { + "agent_id": "00000000-0000-0000-0000-000000000000", + "id": "018681ff-80ed-d10d-21ef-c74f152b085b", + "is_leaf": false, + "name": "logistics-api-574dc95b5d-mp64w", + "status": "healthy", + "summary": { + "healthy": 1 + }, + "type": "KubernetesPod" + }, + { + "agent_id": "00000000-0000-0000-0000-000000000000", + "id": "018681ff-b6c1-a14d-2fd4-8c7dac94cddd", + "is_leaf": false, + "name": "logistics-ui-676b85b87c-tjjcp", + "status": "healthy", + "summary": { + "healthy": 1 + }, + "type": "KubernetesPod" + }, + { + "agent_id": "00000000-0000-0000-0000-000000000000", + "id": "018681ff-e578-a926-e366-d2dc0646eafa", + "is_leaf": false, + "name": "logistics-worker-79cb67d8f5-lr66n", + "status": "healthy", + "summary": { + "healthy": 1 + }, + "type": "KubernetesPod" + } + ], + "external_id": "dummy/pods", + "icon": "icon-kubernetes-pod", + "id": "018681ff-559f-7183-19d1-7d898b4e1413", + "is_leaf": false, + "name": "Pods", + "parent_id": "018681fe-8156-4b91-d178-caf8b3c2818c", + "path": "018681fe-8156-4b91-d178-caf8b3c2818c", + "status": "healthy", + "summary": { + "healthy": 3 + }, + "tooltip": "Kubernetes Pods", + "type": "KubernetesPods" + } + ], + "external_id": "dummy/cluster", + "icon": "icon-cluster", + "id": "018681fe-8156-4b91-d178-caf8b3c2818c", + "is_leaf": false, + "name": "cluster", + "status": "healthy", + "summary": { + "healthy": 2, + "insights": { + "security": { + "critical": 1 + } + } + }, + "tooltip": "Kubernetes Cluster", + "type": "KubernetesCluster" + } + ], + "healthStatuses": ["healthy"], + "tags": null, + "teams": [], + "types": [ + "KubernetesCluster", + "KubernetesNode", + "KubernetesNodes", + "KubernetesPod", + "KubernetesPods" + ] +} diff --git a/fixtures/expectations/topology_depth_1_root_tree.json b/fixtures/expectations/topology_depth_1_root_tree.json index fefd817c..b7c43a1c 100644 --- a/fixtures/expectations/topology_depth_1_root_tree.json +++ b/fixtures/expectations/topology_depth_1_root_tree.json @@ -13,7 +13,6 @@ "owner": "logistics-team", "summary": { "healthy": 1, - "warning": 1, "incidents": { "availability": { "Blocker": 1 @@ -23,7 +22,8 @@ "security": { "critical": 1 } - } + }, + "warning": 1 }, "is_leaf": false, "created_at": "2023-01-01T05:29:00+05:30", @@ -44,6 +44,8 @@ } } }, + "icon": "icon-cluster", + "tooltip": "Kubernetes Cluster", "is_leaf": false, "created_at": "2023-01-01T05:29:00+05:30", "updated_at": "2023-01-01T05:29:00+05:30" diff --git a/fixtures/expectations/topology_depth_2_root_tree.json b/fixtures/expectations/topology_depth_2_root_tree.json index bd0879ef..01c0c42f 100644 --- a/fixtures/expectations/topology_depth_2_root_tree.json +++ b/fixtures/expectations/topology_depth_2_root_tree.json @@ -1,50 +1,17 @@ { "components": [ { - "id": "018681fc-e54f-bd4f-42be-068a9a69eeb5", "agent_id": "00000000-0000-0000-0000-000000000000", - "external_id": "dummy/logistics", - "name": "logistics", - "labels": { - "telemetry": "enabled" - }, - "status": "warning", - "type": "Entity", - "owner": "logistics-team", - "summary": { - "healthy": 1, - "warning": 1, - "incidents": { - "availability": { - "Blocker": 1 - } - }, - "insights": { - "security": { - "critical": 1 - } - } - }, - "is_leaf": false, - "created_at": "2023-01-01T05:29:00+05:30", - "updated_at": "2023-01-01T05:29:00+05:30", "components": [ { - "id": "018681fd-5770-336f-227c-259435d7fc6b", "agent_id": "00000000-0000-0000-0000-000000000000", - "external_id": "dummy/logistics-api", - "parent_id": "018681fc-e54f-bd4f-42be-068a9a69eeb5", + "id": "018681fd-5770-336f-227c-259435d7fc6b", + "is_leaf": false, + "labels": { "telemetry": "enabled" }, "name": "logistics-api", - "labels": { - "telemetry": "enabled" - }, "status": "warning", - "type": "Application", - "owner": "logistics-team", - "path": "018681fc-e54f-bd4f-42be-068a9a69eeb5", "summary": { "healthy": 1, - "unhealthy": 1, "incidents": { "availability": { "Blocker": 1 @@ -54,33 +21,47 @@ "security": { "critical": 1 } - } + }, + "unhealthy": 1 }, - "is_leaf": false, - "created_at": "2023-01-01T05:29:00+05:30", - "updated_at": "2023-01-01T05:29:00+05:30", - "checks": { - "healthy": 2 - } + "type": "Application" }, { - "id": "018681fd-c1ff-16ee-dff0-8c8796e4263e", "agent_id": "00000000-0000-0000-0000-000000000000", - "external_id": "dummy/logistics-ui", - "parent_id": "018681fc-e54f-bd4f-42be-068a9a69eeb5", + "id": "018681fd-c1ff-16ee-dff0-8c8796e4263e", + "is_leaf": false, "name": "logistics-ui", "status": "healthy", - "type": "Application", - "owner": "logistics-team", - "path": "018681fc-e54f-bd4f-42be-068a9a69eeb5", "summary": { "healthy": 1 }, - "is_leaf": false, - "created_at": "2023-01-01T05:29:00+05:30", - "updated_at": "2023-01-01T05:29:00+05:30" + "type": "Application" } - ] + ], + "external_id": "dummy/logistics", + "id": "018681fc-e54f-bd4f-42be-068a9a69eeb5", + "is_leaf": false, + "labels": { + "telemetry": "enabled" + }, + "name": "logistics", + "owner": "logistics-team", + "status": "warning", + "summary": { + "healthy": 1, + "incidents": { + "availability": { + "Blocker": 1 + } + }, + "insights": { + "security": { + "critical": 1 + } + }, + "warning": 1 + }, + "type": "Entity" }, { "id": "018681fe-8156-4b91-d178-caf8b3c2818c", @@ -97,15 +78,13 @@ } } }, + "icon": "icon-cluster", + "tooltip": "Kubernetes Cluster", "is_leaf": false, - "created_at": "2023-01-01T05:29:00+05:30", - "updated_at": "2023-01-01T05:29:00+05:30", "components": [ { "id": "018681fe-b27e-7627-72c2-ad18e93f72f4", "agent_id": "00000000-0000-0000-0000-000000000000", - "external_id": "dummy/nodes", - "parent_id": "018681fe-8156-4b91-d178-caf8b3c2818c", "name": "Nodes", "status": "healthy", "type": "KubernetesNodes", @@ -117,51 +96,36 @@ } } }, - "is_leaf": false, - "created_at": "2023-01-01T05:29:00+05:30", - "updated_at": "2023-01-01T05:29:00+05:30" + "is_leaf": false }, { "id": "018681ff-559f-7183-19d1-7d898b4e1413", "agent_id": "00000000-0000-0000-0000-000000000000", - "external_id": "dummy/pods", - "parent_id": "018681fe-8156-4b91-d178-caf8b3c2818c", "name": "Pods", "status": "healthy", "type": "KubernetesPods", "summary": { "healthy": 3 }, - "is_leaf": false, - "created_at": "2023-01-01T05:29:00+05:30", - "updated_at": "2023-01-01T05:29:00+05:30" + "is_leaf": false } ] }, { - "id": "4643e4de-6215-4c71-9600-9cf69b2cbbee", "agent_id": "ebd4cbf7-267e-48f9-a050-eca12e535ce1", "external_id": "dummy/payments-api", + "id": "4643e4de-6215-4c71-9600-9cf69b2cbbee", + "is_leaf": false, "name": "payments-api", "status": "healthy", - "type": "Application", - "summary": { - "healthy": 1 - }, - "is_leaf": false, - "created_at": "2023-01-01T05:29:00+05:30", - "updated_at": "2023-01-01T05:29:00+05:30" + "summary": { "healthy": 1 }, + "type": "Application" } ], - "healthStatuses": [ - "healthy", - "unhealthy" - ], + "healthStatuses": ["healthy", "unhealthy"], "teams": [], "tags": { - "telemetry": [ - "enabled" - ] + "telemetry": ["enabled"] }, "types": [ "Application", diff --git a/fixtures/expectations/topology_root_tree.json b/fixtures/expectations/topology_root_tree.json index a5ef8794..907d8f69 100644 --- a/fixtures/expectations/topology_root_tree.json +++ b/fixtures/expectations/topology_root_tree.json @@ -1,96 +1,30 @@ { "components": [ { - "id": "018681fc-e54f-bd4f-42be-068a9a69eeb5", "agent_id": "00000000-0000-0000-0000-000000000000", - "external_id": "dummy/logistics", - "name": "logistics", - "labels": { - "telemetry": "enabled" - }, - "status": "warning", - "type": "Entity", - "owner": "logistics-team", - "summary": { - "healthy": 1, - "warning": 1, - "incidents": { - "availability": { - "Blocker": 1 - } - }, - "insights": { - "security": { - "critical": 1 - } - } - }, - "is_leaf": false, - "created_at": "2023-01-01T05:29:00+05:30", - "updated_at": "2023-01-01T05:29:00+05:30", "components": [ { - "id": "018681fd-5770-336f-227c-259435d7fc6b", "agent_id": "00000000-0000-0000-0000-000000000000", - "external_id": "dummy/logistics-api", - "parent_id": "018681fc-e54f-bd4f-42be-068a9a69eeb5", - "name": "logistics-api", - "labels": { - "telemetry": "enabled" - }, - "status": "warning", - "type": "Application", - "owner": "logistics-team", - "path": "018681fc-e54f-bd4f-42be-068a9a69eeb5", - "summary": { - "healthy": 1, - "unhealthy": 1, - "incidents": { - "availability": { - "Blocker": 1 - } - }, - "insights": { - "security": { - "critical": 1 - } - } - }, - "is_leaf": false, - "created_at": "2023-01-01T05:29:00+05:30", - "updated_at": "2023-01-01T05:29:00+05:30", - "checks": { - "healthy": 2 - }, "components": [ { - "id": "018681fe-010a-6647-74ad-58b3a136dfe4", "agent_id": "00000000-0000-0000-0000-000000000000", - "external_id": "dummy/logistics-worker", - "parent_id": "018681fd-5770-336f-227c-259435d7fc6b", + "id": "018681fe-010a-6647-74ad-58b3a136dfe4", + "is_leaf": false, "name": "logistics-worker", "status": "healthy", - "type": "Application", - "path": "018681fc-e54f-bd4f-42be-068a9a69eeb5.018681fd-5770-336f-227c-259435d7fc6b", "summary": { "healthy": 1 }, - "is_leaf": false, - "created_at": "2023-01-01T05:29:00+05:30", - "updated_at": "2023-01-01T05:29:00+05:30" + "type": "Application" }, { - "id": "018681fe-4529-c50f-26fd-530fa9c57319", "agent_id": "00000000-0000-0000-0000-000000000000", - "external_id": "dummy/logistics-db", - "parent_id": "018681fd-5770-336f-227c-259435d7fc6b", + "id": "018681fe-4529-c50f-26fd-530fa9c57319", + "is_leaf": false, "name": "logistics-db", "status": "unhealthy", "status_reason": "database not accepting connections", - "type": "Database", - "path": "018681fc-e54f-bd4f-42be-068a9a69eeb5.018681fd-5770-336f-227c-259435d7fc6b", "summary": { - "unhealthy": 1, "incidents": { "availability": { "Blocker": 1 @@ -100,140 +34,95 @@ "security": { "critical": 1 } - } - }, - "is_leaf": false, - "created_at": "2023-01-01T05:29:00+05:30", - "updated_at": "2023-01-01T05:29:00+05:30", - "checks": { + }, "unhealthy": 1 - } + }, + "type": "Database" } - ] + ], + "id": "018681fd-5770-336f-227c-259435d7fc6b", + "is_leaf": false, + "labels": { + "telemetry": "enabled" + }, + "name": "logistics-api", + "status": "warning", + "summary": { + "healthy": 1, + "incidents": { + "availability": { + "Blocker": 1 + } + }, + "insights": { + "security": { + "critical": 1 + } + }, + "unhealthy": 1 + }, + "type": "Application" }, { - "id": "018681fd-c1ff-16ee-dff0-8c8796e4263e", "agent_id": "00000000-0000-0000-0000-000000000000", - "external_id": "dummy/logistics-ui", - "parent_id": "018681fc-e54f-bd4f-42be-068a9a69eeb5", + "id": "018681fd-c1ff-16ee-dff0-8c8796e4263e", + "is_leaf": false, "name": "logistics-ui", "status": "healthy", - "type": "Application", - "owner": "logistics-team", - "path": "018681fc-e54f-bd4f-42be-068a9a69eeb5", - "summary": { - "healthy": 1 - }, - "is_leaf": false, - "created_at": "2023-01-01T05:29:00+05:30", - "updated_at": "2023-01-01T05:29:00+05:30" + "summary": {"healthy": 1}, + "type": "Application" } - ] - }, - { - "id": "018681fe-8156-4b91-d178-caf8b3c2818c", - "agent_id": "00000000-0000-0000-0000-000000000000", - "external_id": "dummy/cluster", - "name": "cluster", - "status": "healthy", - "type": "KubernetesCluster", + ], + "external_id": "dummy/logistics", + "id": "018681fc-e54f-bd4f-42be-068a9a69eeb5", + "is_leaf": false, + "labels": { + "telemetry": "enabled" + }, + "name": "logistics", + "owner": "logistics-team", + "status": "warning", "summary": { - "healthy": 2, + "healthy": 1, + "incidents": { + "availability": { + "Blocker": 1 + } + }, "insights": { "security": { "critical": 1 } - } + }, + "warning": 1 }, - "is_leaf": false, - "created_at": "2023-01-01T05:29:00+05:30", - "updated_at": "2023-01-01T05:29:00+05:30", + "type": "Entity" + }, + { + "agent_id": "00000000-0000-0000-0000-000000000000", "components": [ { - "id": "018681fe-b27e-7627-72c2-ad18e93f72f4", "agent_id": "00000000-0000-0000-0000-000000000000", - "external_id": "dummy/nodes", - "parent_id": "018681fe-8156-4b91-d178-caf8b3c2818c", - "name": "Nodes", - "status": "healthy", - "type": "KubernetesNodes", - "summary": { - "healthy": 2, - "insights": { - "security": { - "critical": 1 - } - } - }, - "is_leaf": false, - "created_at": "2023-01-01T05:29:00+05:30", - "updated_at": "2023-01-01T05:29:00+05:30", "components": [ { - "id": "018681fe-f5aa-37e9-83f7-47b5b0232d5e", "agent_id": "00000000-0000-0000-0000-000000000000", - "external_id": "dummy/node-a", - "parent_id": "018681fe-b27e-7627-72c2-ad18e93f72f4", - "name": "node-a", - "status": "healthy", - "type": "KubernetesNode", - "summary": { - "healthy": 2 - }, - "is_leaf": false, - "created_at": "2023-01-01T05:29:00+05:30", - "updated_at": "2023-01-01T05:29:00+05:30", "components": [ { - "id": "018681ff-80ed-d10d-21ef-c74f152b085b", "agent_id": "00000000-0000-0000-0000-000000000000", - "external_id": "dummy/logistics-api-574dc95b5d-mp64w", - "parent_id": "018681ff-559f-7183-19d1-7d898b4e1413", - "name": "logistics-api-574dc95b5d-mp64w", - "status": "healthy", - "type": "KubernetesPod", - "summary": { - "healthy": 1 - }, + "id": "018681ff-e578-a926-e366-d2dc0646eafa", "is_leaf": false, - "created_at": "2023-01-01T05:29:00+05:30", - "updated_at": "2023-01-01T05:29:00+05:30", - "parents": [ - "018681fe-f5aa-37e9-83f7-47b5b0232d5e" - ] - }, - { - "id": "018681ff-b6c1-a14d-2fd4-8c7dac94cddd", - "agent_id": "00000000-0000-0000-0000-000000000000", - "external_id": "dummy/logistics-ui-676b85b87c-tjjcp", - "parent_id": "018681ff-559f-7183-19d1-7d898b4e1413", - "name": "logistics-ui-676b85b87c-tjjcp", + "name": "logistics-worker-79cb67d8f5-lr66n", "status": "healthy", - "type": "KubernetesPod", "summary": { "healthy": 1 }, - "is_leaf": false, - "created_at": "2023-01-01T05:29:00+05:30", - "updated_at": "2023-01-01T05:29:00+05:30", - "parents": [ - "018681fe-f5aa-37e9-83f7-47b5b0232d5e" - ] + "type": "KubernetesPod" } ], - "children": [ - "018681ff-80ed-d10d-21ef-c74f152b085b", - "018681ff-b6c1-a14d-2fd4-8c7dac94cddd" - ] - }, - { "id": "018681ff-227e-4d71-b38e-0693cc862213", - "agent_id": "00000000-0000-0000-0000-000000000000", - "external_id": "dummy/node-b", - "parent_id": "018681fe-b27e-7627-72c2-ad18e93f72f4", + "is_leaf": false, "name": "node-b", "status": "healthy", - "type": "KubernetesNode", "summary": { "healthy": 1, "insights": { @@ -242,40 +131,61 @@ } } }, - "is_leaf": false, - "created_at": "2023-01-01T05:29:00+05:30", - "updated_at": "2023-01-01T05:29:00+05:30", + "type": "KubernetesNode" + }, + { + "agent_id": "00000000-0000-0000-0000-000000000000", "components": [ { - "id": "018681ff-e578-a926-e366-d2dc0646eafa", + "id": "018681ff-80ed-d10d-21ef-c74f152b085b", "agent_id": "00000000-0000-0000-0000-000000000000", - "external_id": "dummy/logistics-worker-79cb67d8f5-lr66n", - "parent_id": "018681ff-559f-7183-19d1-7d898b4e1413", - "name": "logistics-worker-79cb67d8f5-lr66n", + "name": "logistics-api-574dc95b5d-mp64w", "status": "healthy", "type": "KubernetesPod", "summary": { "healthy": 1 }, + "is_leaf": false + }, + { + "agent_id": "00000000-0000-0000-0000-000000000000", + "id": "018681ff-b6c1-a14d-2fd4-8c7dac94cddd", "is_leaf": false, - "created_at": "2023-01-01T05:29:00+05:30", - "updated_at": "2023-01-01T05:29:00+05:30", - "parents": [ - "018681ff-227e-4d71-b38e-0693cc862213" - ] + "name": "logistics-ui-676b85b87c-tjjcp", + "status": "healthy", + "summary": { + "healthy": 1 + }, + "type": "KubernetesPod" } ], - "children": [ - "018681ff-e578-a926-e366-d2dc0646eafa" - ] + "id": "018681fe-f5aa-37e9-83f7-47b5b0232d5e", + "is_leaf": false, + "name": "node-a", + "status": "healthy", + "summary": { + "healthy": 2 + }, + "type": "KubernetesNode" } - ] + ], + "id": "018681fe-b27e-7627-72c2-ad18e93f72f4", + "is_leaf": false, + "name": "Nodes", + "status": "healthy", + "summary": { + "healthy": 2, + "insights": { + "security": { + "critical": 1 + } + } + }, + "type": "KubernetesNodes" }, { "id": "018681ff-559f-7183-19d1-7d898b4e1413", "agent_id": "00000000-0000-0000-0000-000000000000", - "external_id": "dummy/pods", - "parent_id": "018681fe-8156-4b91-d178-caf8b3c2818c", "name": "Pods", "status": "healthy", "type": "KubernetesPods", @@ -283,80 +193,65 @@ "healthy": 3 }, "is_leaf": false, - "created_at": "2023-01-01T05:29:00+05:30", - "updated_at": "2023-01-01T05:29:00+05:30", "components": [ { - "id": "018681ff-80ed-d10d-21ef-c74f152b085b", "agent_id": "00000000-0000-0000-0000-000000000000", - "external_id": "dummy/logistics-api-574dc95b5d-mp64w", - "parent_id": "018681ff-559f-7183-19d1-7d898b4e1413", + "id": "018681ff-80ed-d10d-21ef-c74f152b085b", + "is_leaf": false, "name": "logistics-api-574dc95b5d-mp64w", "status": "healthy", - "type": "KubernetesPod", - "summary": { - "healthy": 1 - }, - "is_leaf": false, - "created_at": "2023-01-01T05:29:00+05:30", - "updated_at": "2023-01-01T05:29:00+05:30", - "parents": [ - "018681fe-f5aa-37e9-83f7-47b5b0232d5e" - ] + "summary": { "healthy": 1 }, + "type": "KubernetesPod" }, { - "id": "018681ff-b6c1-a14d-2fd4-8c7dac94cddd", "agent_id": "00000000-0000-0000-0000-000000000000", - "external_id": "dummy/logistics-ui-676b85b87c-tjjcp", - "parent_id": "018681ff-559f-7183-19d1-7d898b4e1413", + "id": "018681ff-b6c1-a14d-2fd4-8c7dac94cddd", + "is_leaf": false, "name": "logistics-ui-676b85b87c-tjjcp", "status": "healthy", - "type": "KubernetesPod", - "summary": { - "healthy": 1 - }, - "is_leaf": false, - "created_at": "2023-01-01T05:29:00+05:30", - "updated_at": "2023-01-01T05:29:00+05:30", - "parents": [ - "018681fe-f5aa-37e9-83f7-47b5b0232d5e" - ] + "summary": { "healthy": 1 }, + "type": "KubernetesPod" }, { - "id": "018681ff-e578-a926-e366-d2dc0646eafa", "agent_id": "00000000-0000-0000-0000-000000000000", - "external_id": "dummy/logistics-worker-79cb67d8f5-lr66n", - "parent_id": "018681ff-559f-7183-19d1-7d898b4e1413", + "id": "018681ff-e578-a926-e366-d2dc0646eafa", + "is_leaf": false, "name": "logistics-worker-79cb67d8f5-lr66n", "status": "healthy", - "type": "KubernetesPod", - "summary": { - "healthy": 1 - }, - "is_leaf": false, - "created_at": "2023-01-01T05:29:00+05:30", - "updated_at": "2023-01-01T05:29:00+05:30", - "parents": [ - "018681ff-227e-4d71-b38e-0693cc862213" - ] + "summary": { "healthy": 1 }, + "type": "KubernetesPod" } ] } - ] + ], + "external_id": "dummy/cluster", + "icon": "icon-cluster", + "id": "018681fe-8156-4b91-d178-caf8b3c2818c", + "is_leaf": false, + "name": "cluster", + "status": "healthy", + "summary": { + "healthy": 2, + "insights": { + "security": { + "critical": 1 + } + } + }, + "tooltip": "Kubernetes Cluster", + "type": "KubernetesCluster" }, { - "id": "4643e4de-6215-4c71-9600-9cf69b2cbbee", "agent_id": "ebd4cbf7-267e-48f9-a050-eca12e535ce1", "external_id": "dummy/payments-api", + "id": "4643e4de-6215-4c71-9600-9cf69b2cbbee", + "is_leaf": false, "name": "payments-api", "status": "healthy", - "type": "Application", "summary": { "healthy": 1 }, - "is_leaf": false, - "created_at": "2023-01-01T05:29:00+05:30", - "updated_at": "2023-01-01T05:29:00+05:30" + "type": "Application" } ], "healthStatuses": [ diff --git a/fixtures/expectations/topology_tree_with_agent_id.json b/fixtures/expectations/topology_tree_with_agent_id.json index 57ed2566..53f9904a 100644 --- a/fixtures/expectations/topology_tree_with_agent_id.json +++ b/fixtures/expectations/topology_tree_with_agent_id.json @@ -10,9 +10,7 @@ "summary": { "healthy": 1 }, - "is_leaf": false, - "created_at": "2023-01-01T05:29:00+05:30", - "updated_at": "2023-01-01T05:29:00+05:30" + "is_leaf": false } ], "healthStatuses": [ diff --git a/fixtures/expectations/topology_tree_with_label_filter.json b/fixtures/expectations/topology_tree_with_label_filter.json index 5e9a59cb..c3d661f0 100644 --- a/fixtures/expectations/topology_tree_with_label_filter.json +++ b/fixtures/expectations/topology_tree_with_label_filter.json @@ -2,26 +2,13 @@ "components": [ { "id": "018681fc-e54f-bd4f-42be-068a9a69eeb5", - "agent_id": "00000000-0000-0000-0000-000000000000", - "external_id": "dummy/logistics", - "name": "logistics", - "labels": { - "telemetry": "enabled" - }, - "status": "healthy", - "type": "Entity", - "owner": "logistics-team", - "summary": { - "healthy": 1 - }, - "is_leaf": false, - "created_at": "2023-01-01T05:29:00+05:30", - "updated_at": "2023-01-01T05:29:00+05:30", "components": [ { "id": "018681fd-5770-336f-227c-259435d7fc6b", + "checks": { + "healthy": 2 + }, "agent_id": "00000000-0000-0000-0000-000000000000", - "external_id": "dummy/logistics-api", "parent_id": "018681fc-e54f-bd4f-42be-068a9a69eeb5", "name": "logistics-api", "labels": { @@ -29,32 +16,31 @@ }, "status": "healthy", "type": "Application", - "owner": "logistics-team", - "path": "018681fc-e54f-bd4f-42be-068a9a69eeb5", "summary": { "healthy": 1 }, - "is_leaf": false, - "created_at": "2023-01-01T05:29:00+05:30", - "updated_at": "2023-01-01T05:29:00+05:30", - "checks": { - "healthy": 2 - } + "is_leaf": false } - ] + ], + "agent_id": "00000000-0000-0000-0000-000000000000", + "external_id": "dummy/logistics", + "name": "logistics", + "labels": { + "telemetry": "enabled" + }, + "status": "healthy", + "type": "Entity", + "owner": "logistics-team", + "summary": { + "healthy": 1 + }, + "is_leaf": false } ], - "healthStatuses": [ - "healthy" - ], + "healthStatuses": ["healthy"], "teams": [], "tags": { - "telemetry": [ - "enabled" - ] + "telemetry": ["enabled"] }, - "types": [ - "Application", - "Entity" - ] + "types": ["Application", "Entity"] } diff --git a/fixtures/expectations/topology_tree_with_owner_filter.json b/fixtures/expectations/topology_tree_with_owner_filter.json index 37ecfa4c..d503e9ad 100644 --- a/fixtures/expectations/topology_tree_with_owner_filter.json +++ b/fixtures/expectations/topology_tree_with_owner_filter.json @@ -21,7 +21,9 @@ { "id": "018681fd-5770-336f-227c-259435d7fc6b", "agent_id": "00000000-0000-0000-0000-000000000000", - "external_id": "dummy/logistics-api", + "checks": { + "healthy": 2 + }, "parent_id": "018681fc-e54f-bd4f-42be-068a9a69eeb5", "name": "logistics-api", "labels": { @@ -29,34 +31,22 @@ }, "status": "healthy", "type": "Application", - "owner": "logistics-team", - "path": "018681fc-e54f-bd4f-42be-068a9a69eeb5", "summary": { "healthy": 1 }, - "is_leaf": false, - "created_at": "2023-01-01T05:29:00+05:30", - "updated_at": "2023-01-01T05:29:00+05:30", - "checks": { - "healthy": 2 - } + "is_leaf": false }, { "id": "018681fd-c1ff-16ee-dff0-8c8796e4263e", "agent_id": "00000000-0000-0000-0000-000000000000", - "external_id": "dummy/logistics-ui", "parent_id": "018681fc-e54f-bd4f-42be-068a9a69eeb5", "name": "logistics-ui", "status": "healthy", "type": "Application", - "owner": "logistics-team", - "path": "018681fc-e54f-bd4f-42be-068a9a69eeb5", "summary": { "healthy": 1 }, - "is_leaf": false, - "created_at": "2023-01-01T05:29:00+05:30", - "updated_at": "2023-01-01T05:29:00+05:30" + "is_leaf": false } ] } diff --git a/fixtures/expectations/topology_tree_with_status_filter.json b/fixtures/expectations/topology_tree_with_status_filter.json index 088c3045..c8ace2e2 100644 --- a/fixtures/expectations/topology_tree_with_status_filter.json +++ b/fixtures/expectations/topology_tree_with_status_filter.json @@ -1,96 +1,33 @@ { "components": [ { - "id": "018681fc-e54f-bd4f-42be-068a9a69eeb5", "agent_id": "00000000-0000-0000-0000-000000000000", - "external_id": "dummy/logistics", - "name": "logistics", - "labels": { - "telemetry": "enabled" - }, - "status": "warning", - "type": "Entity", - "owner": "logistics-team", - "summary": { - "healthy": 1, - "warning": 1, - "incidents": { - "availability": { - "Blocker": 1 - } - }, - "insights": { - "security": { - "critical": 1 - } - } - }, - "is_leaf": false, - "created_at": "2023-01-01T05:29:00+05:30", - "updated_at": "2023-01-01T05:29:00+05:30", "components": [ { - "id": "018681fd-5770-336f-227c-259435d7fc6b", "agent_id": "00000000-0000-0000-0000-000000000000", - "external_id": "dummy/logistics-api", - "parent_id": "018681fc-e54f-bd4f-42be-068a9a69eeb5", - "name": "logistics-api", - "labels": { - "telemetry": "enabled" - }, - "status": "warning", - "type": "Application", - "owner": "logistics-team", - "path": "018681fc-e54f-bd4f-42be-068a9a69eeb5", - "summary": { - "healthy": 1, - "unhealthy": 1, - "incidents": { - "availability": { - "Blocker": 1 - } - }, - "insights": { - "security": { - "critical": 1 - } - } - }, - "is_leaf": false, - "created_at": "2023-01-01T05:29:00+05:30", - "updated_at": "2023-01-01T05:29:00+05:30", "checks": { "healthy": 2 }, "components": [ { - "id": "018681fe-010a-6647-74ad-58b3a136dfe4", "agent_id": "00000000-0000-0000-0000-000000000000", - "external_id": "dummy/logistics-worker", - "parent_id": "018681fd-5770-336f-227c-259435d7fc6b", + "id": "018681fe-010a-6647-74ad-58b3a136dfe4", + "is_leaf": false, "name": "logistics-worker", "status": "healthy", - "type": "Application", - "path": "018681fc-e54f-bd4f-42be-068a9a69eeb5.018681fd-5770-336f-227c-259435d7fc6b", "summary": { "healthy": 1 }, - "is_leaf": false, - "created_at": "2023-01-01T05:29:00+05:30", - "updated_at": "2023-01-01T05:29:00+05:30" + "type": "Application" }, { - "id": "018681fe-4529-c50f-26fd-530fa9c57319", "agent_id": "00000000-0000-0000-0000-000000000000", - "external_id": "dummy/logistics-db", - "parent_id": "018681fd-5770-336f-227c-259435d7fc6b", + "id": "018681fe-4529-c50f-26fd-530fa9c57319", + "is_leaf": false, "name": "logistics-db", "status": "unhealthy", "status_reason": "database not accepting connections", - "type": "Database", - "path": "018681fc-e54f-bd4f-42be-068a9a69eeb5.018681fd-5770-336f-227c-259435d7fc6b", "summary": { - "unhealthy": 1, "incidents": { "availability": { "Blocker": 1 @@ -100,29 +37,67 @@ "security": { "critical": 1 } - } - }, - "is_leaf": false, - "created_at": "2023-01-01T05:29:00+05:30", - "updated_at": "2023-01-01T05:29:00+05:30", - "checks": { + }, "unhealthy": 1 - } + }, + "type": "Database" } - ] + ], + "id": "018681fd-5770-336f-227c-259435d7fc6b", + "is_leaf": false, + "labels": { + "telemetry": "enabled" + }, + "name": "logistics-api", + "parent_id": "018681fc-e54f-bd4f-42be-068a9a69eeb5", + "status": "warning", + "summary": { + "healthy": 1, + "incidents": { + "availability": { + "Blocker": 1 + } + }, + "insights": { + "security": { + "critical": 1 + } + }, + "unhealthy": 1 + }, + "type": "Application" } - ] + ], + "external_id": "dummy/logistics", + "id": "018681fc-e54f-bd4f-42be-068a9a69eeb5", + "is_leaf": false, + "labels": { + "telemetry": "enabled" + }, + "name": "logistics", + "owner": "logistics-team", + "status": "warning", + "summary": { + "healthy": 1, + "incidents": { + "availability": { + "Blocker": 1 + } + }, + "insights": { + "security": { + "critical": 1 + } + }, + "warning": 1 + }, + "type": "Entity" } ], - "healthStatuses": [ - "healthy", - "unhealthy" - ], + "healthStatuses": ["healthy", "unhealthy"], "teams": [], "tags": { - "telemetry": [ - "enabled" - ] + "telemetry": ["enabled"] }, "types": [ "Application", diff --git a/suite_test.go b/suite_test.go index f8fe83c1..5c035bda 100644 --- a/suite_test.go +++ b/suite_test.go @@ -126,16 +126,16 @@ func parseJQ(v []byte, expr string) ([]byte, error) { return jsonVal, nil } -func matchJSON(a []byte, b []byte, jqExpr *string) { - var valueA, valueB = a, b +func matchJSON(actual []byte, expected []byte, jqExpr *string) { + var valueA, valueB = actual, expected var err error if jqExpr != nil { - valueA, err = parseJQ(a, *jqExpr) + valueA, err = parseJQ(actual, *jqExpr) if err != nil { Expect(err).ToNot(HaveOccurred()) } - valueB, err = parseJQ(b, *jqExpr) + valueB, err = parseJQ(expected, *jqExpr) if err != nil { Expect(err).ToNot(HaveOccurred()) } diff --git a/topology.go b/topology.go index c5a648c1..79e1575b 100644 --- a/topology.go +++ b/topology.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "sort" "strings" "github.com/flanksource/commons/collections" @@ -25,17 +26,43 @@ type TopologyOptions struct { // TODO: Filter status and types in DB Query Types []string Status []string + + // when set to true, only the children (except the direct children) are returned. + // when set to false, the direct children & the parent itself is fetched. + nonDirectChildrenOnly bool } func (opt TopologyOptions) String() string { return fmt.Sprintf("%#v", opt) } +// selectClause returns the columns that should be selected from the topology view. +func (opt TopologyOptions) selectClause() string { + if !opt.nonDirectChildrenOnly { + return "*" + } + + // parents & (incidents, analysis, checks) columns need to fetched to create the topology tree even though they may not be essential to the UI. + return "name, namespace, id, is_leaf, status, status_reason, summary, topology_type, labels, team_names, type, parent_id, parents, incidents, analysis, checks" +} + func (opt TopologyOptions) componentWhereClause() string { s := "WHERE components.deleted_at IS NULL" + if opt.ID != "" { - s += " AND (components.id = @id OR components.path LIKE @path)" + if !opt.nonDirectChildrenOnly { + s += " AND (components.id = @id OR components.parent_id = @id)" + } else { + s += " AND (components.path LIKE @path AND components.id != @id AND components.parent_id != @id)" + } + } else { + if !opt.nonDirectChildrenOnly { + s += " AND components.parent_id IS NULL" + } else { + s += " AND components.parent_id IS NOT NULL" + } } + if opt.Owner != "" { s += " AND (components.owner = @owner)" } @@ -57,7 +84,17 @@ func (opt TopologyOptions) componentRelationWhereClause() string { s += ` AND (parent.labels @> @labels)` } if opt.ID != "" { - s += ` AND (component_relationships.relationship_id = @id OR parent.path LIKE @path)` + if !opt.nonDirectChildrenOnly { + s += " AND (component_relationships.relationship_id = @id OR parent.parent_id = @id)" + } else { + s += " AND (component_relationships.relationship_id = @id OR (parent.path LIKE @path AND parent.parent_id != @id))" + } + } else { + if !opt.nonDirectChildrenOnly { + s += " AND component_relationships.component_id = NULL" + } else { + s += " AND component_relationships.component_id IS NOT NULL" + } } return s } @@ -73,7 +110,7 @@ func generateQuery(opts TopologyOptions) (string, map[string]any) { subQuery := fmt.Sprintf(selectSubQuery, opts.componentWhereClause(), opts.componentRelationWhereClause()) query := fmt.Sprintf(` WITH topology_result AS ( - SELECT * FROM topology + SELECT %s FROM topology WHERE id IN (%s) ) SELECT @@ -92,7 +129,7 @@ func generateQuery(opts TopologyOptions) (string, map[string]any) { ) FROM topology_result - `, subQuery) + `, opts.selectClause(), subQuery) args := make(map[string]any) if opts.ID != "" { @@ -123,31 +160,71 @@ type TopologyResponse struct { Types []string `json:"types"` } -func QueryTopology(ctx context.Context, dbpool *pgxpool.Pool, params TopologyOptions) (*TopologyResponse, error) { - query, args := generateQuery(params) +func fetchAllComponents(ctx context.Context, dbpool *pgxpool.Pool, params TopologyOptions) (TopologyResponse, error) { + // Fetch the children (with all the details) + // & the rest of the decendents (minimal details) in two separate queries - if _, ok := ctx.Deadline(); !ok { - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, DefaultQueryTimeout) - defer cancel() - } + var response TopologyResponse + query, args := generateQuery(params) rows, err := dbpool.Query(ctx, query, pgx.NamedArgs(args)) if err != nil { - return nil, err + return response, fmt.Errorf("failed to query component & its direct children: %w", err) } defer rows.Close() - var response TopologyResponse for rows.Next() { if rows.RawValues()[0] == nil { continue } if err := json.Unmarshal(rows.RawValues()[0], &response); err != nil { - return nil, fmt.Errorf("failed to unmarshal TopologyResponse:%w for %s", err, rows.RawValues()[0]) + return response, fmt.Errorf("failed to unmarshal TopologyResponse:%w for %s", err, rows.RawValues()[0]) + } + } + + params.nonDirectChildrenOnly = true + query, args = generateQuery(params) + rows, err = dbpool.Query(ctx, query, pgx.NamedArgs(args)) + if err != nil { + return response, fmt.Errorf("failed to query rest of the children: %w", err) + } + defer rows.Close() + + var nonDirectChildren TopologyResponse + for rows.Next() { + if rows.RawValues()[0] == nil { + continue } + + if err := json.Unmarshal(rows.RawValues()[0], &nonDirectChildren); err != nil { + return response, fmt.Errorf("failed to unmarshal TopologyResponse:%w for %s", err, rows.RawValues()[0]) + } + } + + if len(nonDirectChildren.Components) > 0 { + response.Components = append(response.Components, nonDirectChildren.Components...) + response.HealthStatuses = append(response.HealthStatuses, nonDirectChildren.HealthStatuses...) + response.Teams = append(response.Teams, nonDirectChildren.Teams...) + response.Types = append(response.Types, nonDirectChildren.Types...) + if response.Tags != nil || nonDirectChildren.Tags != nil { + response.Tags = collections.MergeMap(response.Tags, nonDirectChildren.Tags) + } + } + + return response, nil +} + +func QueryTopology(ctx context.Context, dbpool *pgxpool.Pool, params TopologyOptions) (*TopologyResponse, error) { + if _, ok := ctx.Deadline(); !ok { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, DefaultQueryTimeout) + defer cancel() } + response, err := fetchAllComponents(ctx, dbpool, params) + if err != nil { + return nil, err + } response.Components = applyTypeFilter(response.Components, params.Types...) if !params.Flatten { @@ -164,6 +241,18 @@ func QueryTopology(ctx context.Context, dbpool *pgxpool.Pool, params TopologyOpt response = updateMetadata(response) + // Remove fields from children that aren't required by the UI + root := response.Components + if len(root) == 1 { + for j := range root[0].Components { + removeComponentFields(root[0].Components[j].Components) + } + } else { + for i := range root { + removeComponentFields(root[i].Components) + } + } + return &response, nil } @@ -242,6 +331,7 @@ func createComponentTree(params TopologyOptions, components models.Components) [ root = append(root, c) } } + return root } @@ -283,6 +373,22 @@ func updateMetadata(resp TopologyResponse) TopologyResponse { // Clean teams resp.Teams = collections.DeleteEmptyStrings(resp.Teams) + resp.Teams = collections.Dedup(resp.Teams) + resp.HealthStatuses = collections.Dedup(resp.HealthStatuses) + resp.Types = collections.Dedup(resp.Types) + + sort.Slice(resp.Teams, func(i, j int) bool { + return resp.Teams[i] < resp.Teams[j] + }) + + sort.Slice(resp.HealthStatuses, func(i, j int) bool { + return resp.HealthStatuses[i] < resp.HealthStatuses[j] + }) + + sort.Slice(resp.Types, func(i, j int) bool { + return resp.Types[i] < resp.Types[j] + }) + return resp } @@ -321,3 +427,18 @@ func GetComponent(ctx context.Context, db *gorm.DB, id string) (*models.Componen return &component, nil } + +// removeComponentFields recursively removes some of the fields from components +// and their children and so on. +func removeComponentFields(components models.Components) { + for i := range components { + c := components[i] + + c.ParentId = nil + c.Parents = nil + c.Checks = nil + c.Incidents = nil + c.Analysis = nil + removeComponentFields(c.Components) + } +} diff --git a/topology_test.go b/topology_test.go index 0095aa04..c9b50a94 100644 --- a/topology_test.go +++ b/topology_test.go @@ -11,6 +11,7 @@ import ( "github.com/flanksource/duty/types" ginkgo "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/onsi/gomega/format" ) // For debugging @@ -40,15 +41,20 @@ func testTopologyJSON(opts TopologyOptions, path string) { expected := readTestFile(path) jqExpr := `del(.. | .created_at?, .updated_at?)` - matchJSON([]byte(expected), treeJSON, &jqExpr) + matchJSON(treeJSON, []byte(expected), &jqExpr) } var _ = ginkgo.Describe("Topology behavior", func() { + format.MaxLength = 0 // Do not truncate diffs ginkgo.It("Should create root tree", func() { testTopologyJSON(TopologyOptions{}, "fixtures/expectations/topology_root_tree.json") }) + ginkgo.It("Should fetch minimal details of other children", func() { + testTopologyJSON(TopologyOptions{ID: dummy.ClusterComponent.ID.String(), Depth: 5}, "fixtures/expectations/topology_cluster_component_tree.json") + }) + ginkgo.It("Should create child tree", func() { testTopologyJSON(TopologyOptions{ID: dummy.NodeA.ID.String()}, "fixtures/expectations/topology_child_tree.json") })