From abddd3e77800cd859e708c28474267c23038ee4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Loussinian?= Date: Wed, 26 Nov 2025 15:29:50 +0100 Subject: [PATCH 1/9] feat(nestedArray): parsing * on array of array --- pkg/customresourcestate/registry_factory.go | 77 ++++++++++++++--- .../registry_factory_test.go | 85 +++++++++++++++++-- 2 files changed, 144 insertions(+), 18 deletions(-) diff --git a/pkg/customresourcestate/registry_factory.go b/pkg/customresourcestate/registry_factory.go index 2a2cb067f..a4dd3627f 100644 --- a/pkg/customresourcestate/registry_factory.go +++ b/pkg/customresourcestate/registry_factory.go @@ -222,7 +222,6 @@ func (c *compiledGauge) Values(v interface{}) (result []eachValue, errs []error) onError := func(err error) { errs = append(errs, fmt.Errorf("%s: %v", c.Path(), err)) } - switch iter := v.(type) { case map[string]interface{}: for key, it := range iter { @@ -279,16 +278,33 @@ func (c *compiledGauge) Values(v interface{}) (result []eachValue, errs []error) } case []interface{}: for i, it := range iter { - value, err := c.value(it) - if err != nil { - onError(fmt.Errorf("[%d]: %w", i, err)) - continue - } - if value == nil { - continue + // test si it est un []interface{} si oui boucle dessus + if nestedIter, ok := it.([]interface{}); ok { + for j, nestedIt := range nestedIter { + value, err := c.value(nestedIt) + + if err != nil { + onError(fmt.Errorf("[%d]: %w", j, err)) + continue + } + if value == nil { + continue + } + addPathLabels(nestedIt, c.LabelFromPath(), value.Labels) + result = append(result, *value) + } + } else { + value, err := c.value(it) + if err != nil { + onError(fmt.Errorf("[%d]: %w", i, err)) + continue + } + if value == nil { + continue + } + addPathLabels(it, c.LabelFromPath(), value.Labels) + result = append(result, *value) } - addPathLabels(it, c.LabelFromPath(), value.Labels) - result = append(result, *value) } default: value, err := c.value(v) @@ -561,7 +577,26 @@ func (p valuePath) Get(obj interface{}) interface{} { if obj == nil { return nil } - obj = op.op(obj) + + if slice, ok := obj.([]interface{}); ok && op.part == "[*]" { + // wildcard: garder tous les éléments + var all []interface{} + for _, el := range slice { + all = append(all, el) + } + obj = all + + } else if slice, ok := obj.([]interface{}); ok && op.part != "[*]" { + var all []interface{} + for _, el := range slice { + all = append(all, op.op(el)) + } + obj = all + } else { + obj = op.op(obj) + } + + //obj = op.op(obj) } return obj } @@ -582,7 +617,23 @@ func (p valuePath) String() string { func compilePath(path []string) (out valuePath, _ error) { for i := range path { part := path[i] + + if part == "[*]" { + // wildcard: retourner tous les éléments d'un array + out = append(out, pathOp{ + part: part, + op: func(m interface{}) interface{} { + if s, ok := m.([]interface{}); ok { + return s + } + return nil + }, + }) + continue + } + if strings.HasPrefix(part, "[") && strings.HasSuffix(part, "]") { + // list lookup: [key=value] eq := strings.SplitN(part[1:len(part)-1], "=", 2) if len(eq) != 2 { @@ -627,7 +678,9 @@ func compilePath(path []string) (out valuePath, _ error) { } else { out = append(out, pathOp{ part: part, + // Function qui sera utilisé dans le Get pour descendre dans l'arborescence (op.op(obj)) op: func(m interface{}) interface{} { + // On cherche du clé valeur dans une map if mp, ok := m.(map[string]interface{}); ok { kv := strings.Split(part, "=") if len(kv) == 2 /* k=v */ { @@ -640,6 +693,8 @@ func compilePath(path []string) (out valuePath, _ error) { } } return mp[part] + // On va checher un index dans une liste + // ex: [0], [1], ... -> on pourrait ici faire le boulot si * } else if s, ok := m.([]interface{}); ok { i, err := strconv.Atoi(part) if err != nil { diff --git a/pkg/customresourcestate/registry_factory_test.go b/pkg/customresourcestate/registry_factory_test.go index 7f5d1a092..38ce8fd89 100644 --- a/pkg/customresourcestate/registry_factory_test.go +++ b/pkg/customresourcestate/registry_factory_test.go @@ -18,7 +18,6 @@ package customresourcestate import ( "encoding/json" - "errors" "reflect" "testing" @@ -88,6 +87,64 @@ func init() { "status": "False", }, }, + "parents": Array{ + Obj{ + "conditions": Array{ + Obj{ + "type": "Ready", + "status": "True", + "message": "All good", + "reason": "AsExpected", + "lastTransitionTime": "2022-06-28T00:00:00Z", + "observedGeneration": 42, + }, + Obj{ + "type": "Synced", + "status": "False", + "message": "Not synced", + "reason": "SyncError", + "lastTransitionTime": "2022-06-28T00:00:00Z", + "observedGeneration": 43, + }, + }, + "controllerName": "foo.bar/baz", + "parentRef": Obj{ + "group": "foo.bar", + "kind": "Baz", + "name": "baz-1", + "namespace": "default", + }, + }, + Obj{ + "conditions": Array{ + Obj{ + "type": "Ready", + "status": "False", + "message": "Not ready", + "reason": "NotReady", + "lastTransitionTime": "2022-06-28T00:00:00Z", + "observedGeneration": 44, + }, + }, + "controllerName": "qux.corge/grault", + "parentRef": Obj{ + "group": "qux.corge", + "kind": "Grault", + "name": "grault-1", + "namespace": "default", + }, + }, + Obj{ + "conditions": Array{}, + "controllerName": "garply.waldo/fred", + "parentRef": Obj{ + "group": "garply.waldo", + "kind": "Fred", + "name": "fred-1", + "namespace": "default", + }, + }, + }, }, "metadata": Obj{ "name": "foo", @@ -164,7 +221,7 @@ func Test_values(t *testing.T) { } tests := []tc{ - {name: "single", each: &compiledGauge{ + /*{name: "single", each: &compiledGauge{ compiledCommon: compiledCommon{ path: mustCompilePath(t, "spec", "replicas"), }, @@ -351,16 +408,30 @@ func Test_values(t *testing.T) { }, wantResult: []eachValue{ newEachValue(t, 0, "type", "Provisioned"), newEachValue(t, 1, "type", "Ready"), - }}, - {name: "= expression matching", each: &compiledInfo{ + }},*/ + {name: "status_parents_conditions", each: &compiledGauge{ compiledCommon: compiledCommon{ + path: mustCompilePath(t, "status", "parents", "[*]", "conditions"), + //path: mustCompilePath(t, "status", "conditions"), labelFromPath: map[string]valuePath{ - "bar": mustCompilePath(t, "metadata", "annotations", "bar=baz"), + "reason": mustCompilePath(t, "reason"), }, }, + ValueFrom: mustCompilePath(t, "status"), }, wantResult: []eachValue{ - newEachValue(t, 1, "bar", "baz"), - }}, + newEachValue(t, 1, "reason", "AsExpected"), + newEachValue(t, 0, "reason", "NotReady"), + newEachValue(t, 0, "reason", "SyncError"), + }}, /* + {name: "= expression matching", each: &compiledInfo{ + compiledCommon: compiledCommon{ + labelFromPath: map[string]valuePath{ + "bar": mustCompilePath(t, "metadata", "annotations", "bar=baz"), + }, + }, + }, wantResult: []eachValue{ + newEachValue(t, 1, "bar", "baz"), + }},*/ } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From e287bd6e80e5e32e8f118322899b26974bd2c3b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Loussinian?= Date: Wed, 26 Nov 2025 15:43:07 +0100 Subject: [PATCH 2/9] feat(nestedArray): reduce complexity on valuePath.Get --- pkg/customresourcestate/registry_factory.go | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/pkg/customresourcestate/registry_factory.go b/pkg/customresourcestate/registry_factory.go index a4dd3627f..2edfda440 100644 --- a/pkg/customresourcestate/registry_factory.go +++ b/pkg/customresourcestate/registry_factory.go @@ -578,25 +578,24 @@ func (p valuePath) Get(obj interface{}) interface{} { return nil } - if slice, ok := obj.([]interface{}); ok && op.part == "[*]" { - // wildcard: garder tous les éléments + if slice, ok := obj.([]interface{}); ok { var all []interface{} for _, el := range slice { + // non-wildcard: apply operation + // wildcard: keep all elements + if op.part != "[*]" { + el = op.op(el) + } + all = append(all, el) } - obj = all - } else if slice, ok := obj.([]interface{}); ok && op.part != "[*]" { - var all []interface{} - for _, el := range slice { - all = append(all, op.op(el)) - } obj = all + } else { obj = op.op(obj) } - //obj = op.op(obj) } return obj } From f08aebf80a498a49bb1bde431377e78551fd32b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Loussinian?= Date: Wed, 26 Nov 2025 15:50:14 +0100 Subject: [PATCH 3/9] feat(nestedArray): integrate wildcard filter in compilePath --- pkg/customresourcestate/registry_factory.go | 33 ++++++++++----------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/pkg/customresourcestate/registry_factory.go b/pkg/customresourcestate/registry_factory.go index 2edfda440..03700f3a4 100644 --- a/pkg/customresourcestate/registry_factory.go +++ b/pkg/customresourcestate/registry_factory.go @@ -617,22 +617,23 @@ func compilePath(path []string) (out valuePath, _ error) { for i := range path { part := path[i] - if part == "[*]" { - // wildcard: retourner tous les éléments d'un array - out = append(out, pathOp{ - part: part, - op: func(m interface{}) interface{} { - if s, ok := m.([]interface{}); ok { - return s - } - return nil - }, - }) - continue - } - if strings.HasPrefix(part, "[") && strings.HasSuffix(part, "]") { + // Wildcard with filter: [*] + if part == "[*]" { + // function to return all elements in a list + out = append(out, pathOp{ + part: part, + op: func(m interface{}) interface{} { + if s, ok := m.([]interface{}); ok { + return s + } + return nil + }, + }) + continue + } + // list lookup: [key=value] eq := strings.SplitN(part[1:len(part)-1], "=", 2) if len(eq) != 2 { @@ -677,9 +678,7 @@ func compilePath(path []string) (out valuePath, _ error) { } else { out = append(out, pathOp{ part: part, - // Function qui sera utilisé dans le Get pour descendre dans l'arborescence (op.op(obj)) op: func(m interface{}) interface{} { - // On cherche du clé valeur dans une map if mp, ok := m.(map[string]interface{}); ok { kv := strings.Split(part, "=") if len(kv) == 2 /* k=v */ { @@ -692,8 +691,6 @@ func compilePath(path []string) (out valuePath, _ error) { } } return mp[part] - // On va checher un index dans une liste - // ex: [0], [1], ... -> on pourrait ici faire le boulot si * } else if s, ok := m.([]interface{}); ok { i, err := strconv.Atoi(part) if err != nil { From 23e6357967e37bde7b58c99d0675c7e5b21ce6b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Loussinian?= Date: Wed, 26 Nov 2025 15:58:20 +0100 Subject: [PATCH 4/9] feat(nestedArray): translate comments --- pkg/customresourcestate/registry_factory.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/customresourcestate/registry_factory.go b/pkg/customresourcestate/registry_factory.go index 03700f3a4..9c711f4f4 100644 --- a/pkg/customresourcestate/registry_factory.go +++ b/pkg/customresourcestate/registry_factory.go @@ -278,7 +278,8 @@ func (c *compiledGauge) Values(v interface{}) (result []eachValue, errs []error) } case []interface{}: for i, it := range iter { - // test si it est un []interface{} si oui boucle dessus + // Check if it is a [][]interface{} if yes loop over it + // Else process normally if nestedIter, ok := it.([]interface{}); ok { for j, nestedIt := range nestedIter { value, err := c.value(nestedIt) From e33f89efe37969149458ea148b963ef8f08119a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Loussinian?= Date: Thu, 27 Nov 2025 10:29:44 +0100 Subject: [PATCH 5/9] feat(nestedArray): refactor to pass tests --- pkg/customresourcestate/registry_factory.go | 54 ++++++++----------- .../registry_factory_test.go | 23 ++++---- 2 files changed, 35 insertions(+), 42 deletions(-) diff --git a/pkg/customresourcestate/registry_factory.go b/pkg/customresourcestate/registry_factory.go index 9c711f4f4..971bbdbba 100644 --- a/pkg/customresourcestate/registry_factory.go +++ b/pkg/customresourcestate/registry_factory.go @@ -578,25 +578,7 @@ func (p valuePath) Get(obj interface{}) interface{} { if obj == nil { return nil } - - if slice, ok := obj.([]interface{}); ok { - var all []interface{} - for _, el := range slice { - // non-wildcard: apply operation - // wildcard: keep all elements - if op.part != "[*]" { - el = op.op(el) - } - - all = append(all, el) - } - - obj = all - - } else { - obj = op.op(obj) - } - + obj = op.op(obj) } return obj } @@ -693,21 +675,31 @@ func compilePath(path []string) (out valuePath, _ error) { } return mp[part] } else if s, ok := m.([]interface{}); ok { + // case part is an integer index i, err := strconv.Atoi(part) - if err != nil { - // This means we are here: [ , , ... ] (eg., [ "foo", "0", ... ], i.e., .foo[0]... - // ^ - // Skip over. - return nil - } - if i < 0 { - // negative index - i += len(s) + if err == nil { + if i < 0 { + // negative index + i += len(s) + } + if i < 0 || i >= len(s) { + return fmt.Errorf("list index out of range: %s", part) + } + return s[i] } - if i < 0 || i >= len(s) { - return fmt.Errorf("list index out of range: %s", part) + + // case we have a list and the part is an index + var result []interface{} + for _, el := range s { + if m, ok := el.(map[string]interface{}); ok { + if v, ok := m[part]; ok { + result = append(result, v) + } + } else { + continue + } } - return s[i] + return result } return nil }, diff --git a/pkg/customresourcestate/registry_factory_test.go b/pkg/customresourcestate/registry_factory_test.go index 38ce8fd89..94492ebe7 100644 --- a/pkg/customresourcestate/registry_factory_test.go +++ b/pkg/customresourcestate/registry_factory_test.go @@ -18,6 +18,7 @@ package customresourcestate import ( "encoding/json" + "errors" "reflect" "testing" @@ -221,7 +222,7 @@ func Test_values(t *testing.T) { } tests := []tc{ - /*{name: "single", each: &compiledGauge{ + {name: "single", each: &compiledGauge{ compiledCommon: compiledCommon{ path: mustCompilePath(t, "spec", "replicas"), }, @@ -408,7 +409,7 @@ func Test_values(t *testing.T) { }, wantResult: []eachValue{ newEachValue(t, 0, "type", "Provisioned"), newEachValue(t, 1, "type", "Ready"), - }},*/ + }}, {name: "status_parents_conditions", each: &compiledGauge{ compiledCommon: compiledCommon{ path: mustCompilePath(t, "status", "parents", "[*]", "conditions"), @@ -422,16 +423,16 @@ func Test_values(t *testing.T) { newEachValue(t, 1, "reason", "AsExpected"), newEachValue(t, 0, "reason", "NotReady"), newEachValue(t, 0, "reason", "SyncError"), - }}, /* - {name: "= expression matching", each: &compiledInfo{ - compiledCommon: compiledCommon{ - labelFromPath: map[string]valuePath{ - "bar": mustCompilePath(t, "metadata", "annotations", "bar=baz"), - }, + }}, + {name: "= expression matching", each: &compiledInfo{ + compiledCommon: compiledCommon{ + labelFromPath: map[string]valuePath{ + "bar": mustCompilePath(t, "metadata", "annotations", "bar=baz"), }, - }, wantResult: []eachValue{ - newEachValue(t, 1, "bar", "baz"), - }},*/ + }, + }, wantResult: []eachValue{ + newEachValue(t, 1, "bar", "baz"), + }}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From 44788f2ca5c8097c40dfa76962b3100a48040b74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Loussinian?= Date: Thu, 27 Nov 2025 10:30:59 +0100 Subject: [PATCH 6/9] feat(nestedArray): add example --- pkg/customresourcestate/example_config.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/customresourcestate/example_config.yaml b/pkg/customresourcestate/example_config.yaml index 39a479b2b..6b411e7ea 100644 --- a/pkg/customresourcestate/example_config.yaml +++ b/pkg/customresourcestate/example_config.yaml @@ -47,6 +47,13 @@ spec: path: [status, other] errorLogV: 5 + - name: "other_count_array_deep" + each: + type: Gauge + gauge: + path: [ status, other, *, detail ] + errorLogV: 5 + - name: "info" each: type: Info From cf5c7980cdb45d722b8ec0412e24762959362bfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Loussinian?= Date: Thu, 27 Nov 2025 10:39:45 +0100 Subject: [PATCH 7/9] feat(nestedArray): clean comments --- pkg/customresourcestate/registry_factory.go | 1 + pkg/customresourcestate/registry_factory_test.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/customresourcestate/registry_factory.go b/pkg/customresourcestate/registry_factory.go index 971bbdbba..a85d06e28 100644 --- a/pkg/customresourcestate/registry_factory.go +++ b/pkg/customresourcestate/registry_factory.go @@ -222,6 +222,7 @@ func (c *compiledGauge) Values(v interface{}) (result []eachValue, errs []error) onError := func(err error) { errs = append(errs, fmt.Errorf("%s: %v", c.Path(), err)) } + switch iter := v.(type) { case map[string]interface{}: for key, it := range iter { diff --git a/pkg/customresourcestate/registry_factory_test.go b/pkg/customresourcestate/registry_factory_test.go index 94492ebe7..d25ddcb66 100644 --- a/pkg/customresourcestate/registry_factory_test.go +++ b/pkg/customresourcestate/registry_factory_test.go @@ -413,7 +413,6 @@ func Test_values(t *testing.T) { {name: "status_parents_conditions", each: &compiledGauge{ compiledCommon: compiledCommon{ path: mustCompilePath(t, "status", "parents", "[*]", "conditions"), - //path: mustCompilePath(t, "status", "conditions"), labelFromPath: map[string]valuePath{ "reason": mustCompilePath(t, "reason"), }, From f3c12f45bd7f7a3166a635f193a34eadd4dd6e4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Loussinian?= Date: Thu, 27 Nov 2025 10:42:21 +0100 Subject: [PATCH 8/9] feat(nestedArray): update example --- pkg/customresourcestate/example_config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/customresourcestate/example_config.yaml b/pkg/customresourcestate/example_config.yaml index 6b411e7ea..819e00551 100644 --- a/pkg/customresourcestate/example_config.yaml +++ b/pkg/customresourcestate/example_config.yaml @@ -51,7 +51,7 @@ spec: each: type: Gauge gauge: - path: [ status, other, *, detail ] + path: [ status, other, '[*]', detail ] errorLogV: 5 - name: "info" From dd517770809ddab5effa47e756d097646e08e94a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Loussinian?= Date: Tue, 2 Dec 2025 10:15:53 +0100 Subject: [PATCH 9/9] feat(nestedArray): remove else condition with a continue --- pkg/customresourcestate/registry_factory.go | 23 +++++++++++---------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/pkg/customresourcestate/registry_factory.go b/pkg/customresourcestate/registry_factory.go index a85d06e28..56f4df804 100644 --- a/pkg/customresourcestate/registry_factory.go +++ b/pkg/customresourcestate/registry_factory.go @@ -295,18 +295,19 @@ func (c *compiledGauge) Values(v interface{}) (result []eachValue, errs []error) addPathLabels(nestedIt, c.LabelFromPath(), value.Labels) result = append(result, *value) } - } else { - value, err := c.value(it) - if err != nil { - onError(fmt.Errorf("[%d]: %w", i, err)) - continue - } - if value == nil { - continue - } - addPathLabels(it, c.LabelFromPath(), value.Labels) - result = append(result, *value) + continue + } + + value, err := c.value(it) + if err != nil { + onError(fmt.Errorf("[%d]: %w", i, err)) + continue + } + if value == nil { + continue } + addPathLabels(it, c.LabelFromPath(), value.Labels) + result = append(result, *value) } default: value, err := c.value(v)