Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add sorting support for topology query #447

Merged
merged 1 commit into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions query/topology.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,18 @@ import (
"github.com/flanksource/duty/context"
"github.com/flanksource/duty/models"
"github.com/jackc/pgx/v5"
"github.com/samber/lo"
)

const DefaultDepth = 5

type TopologyQuerySortBy string

const (
TopologyQuerySortByName TopologyQuerySortBy = "name"
TopologyQuerySortByField TopologyQuerySortBy = "field:"
)

type TopologyOptions struct {
ID string
Owner string
Expand All @@ -26,6 +34,9 @@ type TopologyOptions struct {
Types []string
Status []string

SortBy TopologyQuerySortBy
SortOrder 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
Expand Down Expand Up @@ -253,18 +264,62 @@ func Topology(ctx context.Context, params TopologyOptions) (*TopologyResponse, e

// 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)
}

if params.SortBy != "" {
sortComponents(root[0].Components, params.SortBy, params.SortOrder != "desc")
}
} else {
for i := range root {
removeComponentFields(root[i].Components)
}

if params.SortBy != "" {
sortComponents(root, params.SortBy, params.SortOrder != "desc")
}
}
return &response, nil
}

func sortComponents(c models.Components, sortBy TopologyQuerySortBy, asc bool) {
switch {
case sortBy == TopologyQuerySortByName:
sort.Slice(c, func(i, j int) bool {
if !asc {
i, j = j, i
}
return c[i].Name < c[j].Name
})

case strings.HasPrefix(string(sortBy), string(TopologyQuerySortByField)):
field := strings.TrimPrefix(string(sortBy), string(TopologyQuerySortByField))
isTextProperty := lo.Reduce(c, func(val bool, comp *models.Component, _ int) bool {
return val && comp.Properties.Find(field).Text != ""
}, true)

sort.Slice(c, func(i, j int) bool {
if !asc {
i, j = j, i
}
propI := c[i].Properties.Find(field)
propJ := c[j].Properties.Find(field)
if propI == nil || propJ == nil {
return false
}

if isTextProperty {
return propI.Text < propJ.Text
} else {
return propI.Value < propJ.Value
}
})
}
}

// applyDepthFilter limits the tree size to the given depth and also
// dereferences pointer cycles by creating new copies of components
// to prevent cyclic errors during json.Marshal
Expand Down
3 changes: 3 additions & 0 deletions tests/fixtures/dummy/components.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ var LogisticsAPIPod = models.Component{
ParentId: &PodsComponent.ID,
CreatedAt: DummyCreatedAt,
Path: fmt.Sprintf("%s.%s", ClusterComponent.ID.String(), PodsComponent.ID.String()),
Properties: []*models.Property{{Name: "memory", Unit: "bytes", Value: 100}},
}

var LogisticsUIPod = models.Component{
Expand All @@ -154,6 +155,7 @@ var LogisticsUIPod = models.Component{
ParentId: &PodsComponent.ID,
CreatedAt: DummyCreatedAt,
Path: fmt.Sprintf("%s.%s", ClusterComponent.ID.String(), PodsComponent.ID.String()),
Properties: []*models.Property{{Name: "memory", Unit: "bytes", Value: 200}},
}

var LogisticsWorkerPod = models.Component{
Expand All @@ -167,6 +169,7 @@ var LogisticsWorkerPod = models.Component{
ParentId: &PodsComponent.ID,
CreatedAt: DummyCreatedAt,
Path: fmt.Sprintf("%s.%s", ClusterComponent.ID.String(), PodsComponent.ID.String()),
Properties: []*models.Property{{Name: "memory", Unit: "bytes", Value: 300}},
}

var PaymentsAPI = models.Component{
Expand Down
14 changes: 14 additions & 0 deletions tests/fixtures/expectations/topology_child_tree.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@
"tooltip": "Logistic API Pod",
"icon": "icon-kubernetes-pod",
"type": "KubernetesPod",
"properties": [
{
"name": "memory",
"value": 100,
"unit": "bytes"
}
],
"path": "018681fe-8156-4b91-d178-caf8b3c2818c.018681ff-559f-7183-19d1-7d898b4e1413",
"summary": {
"healthy": 1
Expand All @@ -49,6 +56,13 @@
"tooltip": "Logistic UI Pod",
"icon": "icon-kubernetes-pod",
"type": "KubernetesPod",
"properties": [
{
"name": "memory",
"value": 200,
"unit": "bytes"
}
],
"path": "018681fe-8156-4b91-d178-caf8b3c2818c.018681ff-559f-7183-19d1-7d898b4e1413",
"summary": {
"healthy": 1
Expand Down
117 changes: 117 additions & 0 deletions tests/fixtures/expectations/topology_tree_with_desc_sort.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
{
"components": [
{
"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",
"tooltip": "Kubernetes Pods",
"icon": "icon-kubernetes-pod",
"type": "KubernetesPods",
"path": "018681fe-8156-4b91-d178-caf8b3c2818c",
"summary": {
"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-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",
"name": "logistics-worker-79cb67d8f5-lr66n",
"status": "healthy",
"tooltip": "Logistic Worker Pod",
"icon": "icon-kubernetes-pod",
"type": "KubernetesPod",
"properties": [
{
"name": "memory",
"value": 300,
"unit": "bytes"
}
],
"path": "018681fe-8156-4b91-d178-caf8b3c2818c.018681ff-559f-7183-19d1-7d898b4e1413",
"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"
]
},
{
"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",
"status": "healthy",
"tooltip": "Logistic UI Pod",
"icon": "icon-kubernetes-pod",
"type": "KubernetesPod",
"properties": [
{
"name": "memory",
"value": 200,
"unit": "bytes"
}
],
"path": "018681fe-8156-4b91-d178-caf8b3c2818c.018681ff-559f-7183-19d1-7d898b4e1413",
"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"
]
},
{
"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",
"tooltip": "Logistic API Pod",
"icon": "icon-kubernetes-pod",
"type": "KubernetesPod",
"properties": [
{
"name": "memory",
"value": 100,
"unit": "bytes"
}
],
"path": "018681fe-8156-4b91-d178-caf8b3c2818c.018681ff-559f-7183-19d1-7d898b4e1413",
"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"
]
}
]
}
],
"healthStatuses": [
"healthy"
],
"teams": [],
"tags": null,
"types": [
"KubernetesPod",
"KubernetesPods"
]
}
117 changes: 117 additions & 0 deletions tests/fixtures/expectations/topology_tree_with_sort.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
{
"components": [
{
"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",
"tooltip": "Kubernetes Pods",
"icon": "icon-kubernetes-pod",
"type": "KubernetesPods",
"path": "018681fe-8156-4b91-d178-caf8b3c2818c",
"summary": {
"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",
"name": "logistics-api-574dc95b5d-mp64w",
"status": "healthy",
"tooltip": "Logistic API Pod",
"icon": "icon-kubernetes-pod",
"type": "KubernetesPod",
"properties": [
{
"name": "memory",
"value": 100,
"unit": "bytes"
}
],
"path": "018681fe-8156-4b91-d178-caf8b3c2818c.018681ff-559f-7183-19d1-7d898b4e1413",
"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"
]
},
{
"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",
"status": "healthy",
"tooltip": "Logistic UI Pod",
"icon": "icon-kubernetes-pod",
"type": "KubernetesPod",
"properties": [
{
"name": "memory",
"value": 200,
"unit": "bytes"
}
],
"path": "018681fe-8156-4b91-d178-caf8b3c2818c.018681ff-559f-7183-19d1-7d898b4e1413",
"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"
]
},
{
"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",
"name": "logistics-worker-79cb67d8f5-lr66n",
"status": "healthy",
"tooltip": "Logistic Worker Pod",
"icon": "icon-kubernetes-pod",
"type": "KubernetesPod",
"properties": [
{
"name": "memory",
"value": 300,
"unit": "bytes"
}
],
"path": "018681fe-8156-4b91-d178-caf8b3c2818c.018681ff-559f-7183-19d1-7d898b4e1413",
"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"
]
}
]
}
],
"healthStatuses": [
"healthy"
],
"teams": [],
"tags": null,
"types": [
"KubernetesPod",
"KubernetesPods"
]
}
6 changes: 6 additions & 0 deletions tests/topology_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,10 @@ var _ = ginkgo.Describe("Topology behavior", func() {
ginkgo.It("Should test tree with agent ID filter", func() {
testTopologyJSON(query.TopologyOptions{AgentID: dummy.GCPAgent.ID.String()}, "fixtures/expectations/topology_tree_with_agent_id.json")
})

ginkgo.It("Should test tree with sort options", func() {
testTopologyJSON(query.TopologyOptions{ID: dummy.PodsComponent.ID.String(), SortBy: "field:memory"}, "fixtures/expectations/topology_tree_with_sort.json")

testTopologyJSON(query.TopologyOptions{ID: dummy.PodsComponent.ID.String(), SortBy: "field:memory", SortOrder: "desc"}, "fixtures/expectations/topology_tree_with_desc_sort.json")
})
})
Loading