From a60dbbada725ed5c50211d55176b999dc94accb4 Mon Sep 17 00:00:00 2001 From: Luca Sepe Date: Fri, 22 Aug 2025 11:43:08 +0200 Subject: [PATCH] feat: align widgetdata crd validation with kube apiserver strict mode --- go.mod | 2 +- internal/resolvers/crds/schema/extract.go | 28 --- internal/resolvers/crds/schema/validate.go | 59 +++++ .../resolvers/crds/schema/validate_test.go | 12 +- .../missing-additional-props/table.crd.yaml | 204 ++++++++++++++++ testdata/missing-additional-props/table.json | 220 ++++++++++++++++++ .../table.schema.json | 166 +++++++++++++ testdata/missing-additional-props/table.yaml | 159 +++++++++++++ 8 files changed, 816 insertions(+), 34 deletions(-) create mode 100644 testdata/missing-additional-props/table.crd.yaml create mode 100644 testdata/missing-additional-props/table.json create mode 100644 testdata/missing-additional-props/table.schema.json create mode 100644 testdata/missing-additional-props/table.yaml diff --git a/go.mod b/go.mod index 5c87428..db48e4a 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/krateoplatformops/snowplow go 1.24.2 require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/google/go-cmp v0.7.0 github.com/itchyny/gojq v0.12.17 github.com/krateoplatformops/plumbing v0.6.2 @@ -27,7 +28,6 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/fatih/color v1.18.0 // indirect diff --git a/internal/resolvers/crds/schema/extract.go b/internal/resolvers/crds/schema/extract.go index 1dd02bd..e2c75c2 100644 --- a/internal/resolvers/crds/schema/extract.go +++ b/internal/resolvers/crds/schema/extract.go @@ -5,9 +5,7 @@ import ( "github.com/krateoplatformops/plumbing/maps" apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" - apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "sigs.k8s.io/yaml" ) func extractOpenAPISchemaFromCRD(crd map[string]any, version string) (*apiextensions.CustomResourceValidation, error) { @@ -65,29 +63,3 @@ func dumpSchemaRecursively(s *apiextensions.JSONSchemaProps, prefix string) { } } */ - -func buildValidationFromSchemaData(data map[string]interface{}) (*apiextensions.CustomResourceValidation, error) { - // 1. From map to v1.JSONSchemaProps via YAML - yml, err := yaml.Marshal(data) - if err != nil { - return nil, fmt.Errorf("marshal to YAML: %w", err) - } - var schemaV1 apiextv1.JSONSchemaProps - if err := yaml.Unmarshal(yml, &schemaV1); err != nil { - return nil, fmt.Errorf("unmarshal to v1 JSONSchemaProps: %w", err) - } - - // 2. From v1.JSONSchemaProps to internal JSONSchemaProps - var schemaInternal apiextensions.JSONSchemaProps - if err := apiextv1.Convert_v1_JSONSchemaProps_To_apiextensions_JSONSchemaProps( - &schemaV1, &schemaInternal, nil); err != nil { - return nil, fmt.Errorf("convert v1→internal: %w", err) - } - - // 3. Dump for debug (to be removed ASAP) - //dumpSchemaRecursively(&schemaInternal, "") - - return &apiextensions.CustomResourceValidation{ - OpenAPIV3Schema: &schemaInternal, - }, nil -} diff --git a/internal/resolvers/crds/schema/validate.go b/internal/resolvers/crds/schema/validate.go index d61e9e9..58aa649 100644 --- a/internal/resolvers/crds/schema/validate.go +++ b/internal/resolvers/crds/schema/validate.go @@ -2,11 +2,70 @@ package schema import ( "errors" + "fmt" apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" + apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apiextensions-apiserver/pkg/apiserver/validation" + "sigs.k8s.io/yaml" ) +func buildValidationFromSchemaData(data map[string]any) (*apiextensions.CustomResourceValidation, error) { + // 1. Set additionalProperties=false + enforceStrictObjects(data) + + // 2. From map to YAML + yml, err := yaml.Marshal(data) + if err != nil { + return nil, fmt.Errorf("marshal to YAML: %w", err) + } + + var schemaV1 apiextv1.JSONSchemaProps + if err := yaml.Unmarshal(yml, &schemaV1); err != nil { + return nil, fmt.Errorf("unmarshal to v1 JSONSchemaProps: %w", err) + } + + // 3. From v1.JSONSchemaProps to internal JSONSchemaProps + var schemaInternal apiextensions.JSONSchemaProps + if err := apiextv1.Convert_v1_JSONSchemaProps_To_apiextensions_JSONSchemaProps( + &schemaV1, &schemaInternal, nil); err != nil { + return nil, fmt.Errorf("convert v1→internal: %w", err) + } + + return &apiextensions.CustomResourceValidation{ + OpenAPIV3Schema: &schemaInternal, + }, nil +} + +// enforceStrictObjects set additionalProperties=false on all nodes (type: object) +func enforceStrictObjects(node map[string]any) { + if nodeType, ok := node["type"].(string); ok && nodeType == "object" { + if _, exists := node["additionalProperties"]; !exists { + node["additionalProperties"] = false + } + } + + // Recursion for properties + if props, ok := node["properties"].(map[string]any); ok { + for _, v := range props { + if child, ok := v.(map[string]any); ok { + enforceStrictObjects(child) + } + } + } + + // Recursion for items (array) + if items, ok := node["items"].(map[string]any); ok { + enforceStrictObjects(items) + } else if itemsSlice, ok := node["items"].([]any); ok { + for _, it := range itemsSlice { + if child, ok := it.(map[string]any); ok { + enforceStrictObjects(child) + } + } + } +} + func validateCustomResource(crv *apiextensions.CustomResourceValidation, doc map[string]any) error { validator, _, err := validation.NewSchemaValidator(crv.OpenAPIV3Schema) if err != nil { diff --git a/internal/resolvers/crds/schema/validate_test.go b/internal/resolvers/crds/schema/validate_test.go index 9e10915..d58ed68 100644 --- a/internal/resolvers/crds/schema/validate_test.go +++ b/internal/resolvers/crds/schema/validate_test.go @@ -5,6 +5,7 @@ import ( "os" "testing" + "github.com/davecgh/go-spew/spew" "github.com/stretchr/testify/assert" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" "sigs.k8s.io/yaml" @@ -42,8 +43,8 @@ func TestValidate(t *testing.T) { }) } -func TestValidateIssue(t *testing.T) { - data, err := os.ReadFile("../../../../testdata/issues/validation/sample-schema.yaml") +func TestValidateMissingAdditionalPropertiesOnObjectIssue(t *testing.T) { + data, err := os.ReadFile("../../../../testdata/missing-additional-props/table.crd.yaml") assert.NoError(t, err) var crd map[string]any @@ -53,15 +54,15 @@ func TestValidateIssue(t *testing.T) { schema, err := extractOpenAPISchemaFromCRD(crd, "v1beta1") assert.NoError(t, err) - doc, err := os.ReadFile("../../../../testdata/issues/validation/sample-cr.json") + doc, err := os.ReadFile("../../../../testdata/missing-additional-props/table.json") assert.NoError(t, err) var jsonObj map[string]any err = json.Unmarshal(doc, &jsonObj) assert.NoError(t, err) - tmp, ok := jsonObj["status"].(map[string]any) - assert.True(t, ok, "status should be map[string]any") + tmp, ok := jsonObj["spec"].(map[string]any) + assert.True(t, ok, "spec should be map[string]any") tmp, ok = tmp["widgetData"].(map[string]any) assert.True(t, ok, "widgetData should be map[string]any") @@ -69,4 +70,5 @@ func TestValidateIssue(t *testing.T) { err = validateCustomResource(schema, tmp) assert.Error(t, err) + spew.Dump(err) } diff --git a/testdata/missing-additional-props/table.crd.yaml b/testdata/missing-additional-props/table.crd.yaml new file mode 100644 index 0000000..b62a62c --- /dev/null +++ b/testdata/missing-additional-props/table.crd.yaml @@ -0,0 +1,204 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + name: tables.widgets.templates.krateo.io +spec: + group: widgets.templates.krateo.io + names: + categories: + - widgets + - krateo + kind: Table + listKind: TableList + plural: tables + singular: table + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: AGE + type: date + - jsonPath: .status.conditions[?(@.type=='Ready')].status + name: READY + type: string + name: v1beta1 + schema: + openAPIV3Schema: + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + spec: + properties: + apiRef: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + resourcesRefs: + properties: + _slice_: + properties: + continue: + type: boolean + offset: + type: integer + page: + type: integer + perPage: + type: integer + required: + - page + - perPage + type: object + items: + items: + properties: + apiVersion: + type: string + id: + type: string + name: + type: string + namespace: + type: string + payload: + type: object + resource: + type: string + verb: + enum: + - POST + - PUT + - PATCH + - DELETE + - GET + type: string + required: + - id + type: object + type: array + required: + - items + type: object + resourcesRefsTemplate: + items: + properties: + iterator: + type: string + template: + properties: + apiVersion: + type: string + id: + type: string + name: + type: string + namespace: + type: string + payload: + type: object + x-kubernetes-preserve-unknown-fields: true + resource: + type: string + verb: + enum: + - POST + - PUT + - PATCH + - DELETE + - GET + type: string + type: object + type: object + type: array + widgetData: + properties: + columns: + description: configuration of the table's columns + items: + properties: + color: + description: the color of the value (or the icon) to be + represented + enum: + - blue + - darkBlue + - orange + - gray + - red + - green + type: string + kind: + description: type of data to be represented + enum: + - value + - icon + type: string + title: + description: column header label + type: string + valueKey: + description: key used to extract the value from row data + type: string + required: + - title + - valueKey + type: object + type: array + data: + description: array of objects representing the table's row data + items: + type: object + #x-kubernetes-preserve-unknown-fields: true + type: array + pageSize: + description: number of rows displayed per page + type: integer + prefix: + description: it's the filters prefix to get right values + type: string + required: + - columns + - data + type: object + widgetDataTemplate: + items: + properties: + expression: + type: string + forPath: + type: string + type: object + type: array + required: + - widgetData + type: object + status: + type: object + x-kubernetes-preserve-unknown-fields: true + type: object + served: true + storage: true + subresources: + status: {} diff --git a/testdata/missing-additional-props/table.json b/testdata/missing-additional-props/table.json new file mode 100644 index 0000000..c0bd80d --- /dev/null +++ b/testdata/missing-additional-props/table.json @@ -0,0 +1,220 @@ +{ + "kind": "Table", + "apiVersion": "widgets.templates.krateo.io/v1beta1", + "metadata": { + "name": "table-fin-ops-test", + "namespace": "krateo-system" + }, + "spec": { + "widgetData": { + "columns": [ + { + "valueKey": "releaseName", + "title": "Name" + }, + { + "title": "Namespace", + "valueKey": "namespace" + }, + { + "title": "App Version", + "valueKey": "appVersion" + }, + { + "title": "Chart Version", + "valueKey": "chartVersion" + }, + { + "title": "Revision", + "valueKey": "revision" + }, + { + "title": "Status", + "valueKey": "status" + }, + { + "title": "Last Update", + "valueKey": "updated" + } + ], + "data": [ + { + "releaseName": "test" + }, + { + "appVersion": "9.0.1", + "chartVersion": "0.1.0", + "namespace": "krateo-system", + "releaseName": "opa-kube-mgmt", + "revision": 15, + "status": "deployed", + "updated": "2025-08-18T15:22:07Z" + }, + { + "appVersion": "0.1.0", + "chartVersion": "0.1.0", + "namespace": "krateo-system", + "releaseName": "finops-moving-window-policy", + "revision": 15, + "status": "deployed", + "updated": "2025-08-18T15:22:04Z" + }, + { + "appVersion": "0.4.1", + "chartVersion": "0.4.1", + "namespace": "krateo-system", + "releaseName": "finops-operator-scraper", + "revision": 15, + "status": "deployed", + "updated": "2025-08-18T15:22:03Z" + }, + { + "appVersion": "0.1.2", + "chartVersion": "0.1.3", + "namespace": "krateo-system", + "releaseName": "finops-composition-definition-parser", + "revision": 15, + "status": "deployed", + "updated": "2025-08-18T15:22:01Z" + }, + { + "appVersion": "0.4.3", + "chartVersion": "0.4.3", + "namespace": "krateo-system", + "releaseName": "finops-database-handler", + "revision": 15, + "status": "deployed", + "updated": "2025-08-18T15:22:00Z" + }, + { + "appVersion": "0.4.2", + "chartVersion": "0.4.2", + "namespace": "krateo-system", + "releaseName": "finops-operator-focus", + "revision": 15, + "status": "deployed", + "updated": "2025-08-18T15:21:58Z" + }, + { + "appVersion": "0.4.2", + "chartVersion": "0.4.2", + "namespace": "krateo-system", + "releaseName": "finops-operator-exporter", + "revision": 15, + "status": "deployed", + "updated": "2025-08-18T15:21:57Z" + }, + { + "appVersion": "5.9.6", + "chartVersion": "0.1.4", + "namespace": "krateo-system", + "releaseName": "cratedb", + "revision": 15, + "status": "deployed", + "updated": "2025-08-18T15:21:55Z" + }, + { + "appVersion": "0.5.2", + "chartVersion": "0.5.3", + "namespace": "krateo-system", + "releaseName": "oasgen-provider", + "revision": 15, + "status": "deployed", + "updated": "2025-08-18T15:21:53Z" + }, + { + "appVersion": "0.24.9", + "chartVersion": "0.32.0", + "namespace": "krateo-system", + "releaseName": "core-provider", + "revision": 15, + "status": "deployed", + "updated": "2025-08-18T15:21:51Z" + }, + { + "appVersion": "0.1.10", + "chartVersion": "0.1.10", + "namespace": "krateo-system", + "releaseName": "composable-portal-starter", + "revision": 1, + "status": "deployed", + "updated": "2025-08-18T15:21:48Z" + }, + { + "appVersion": "0.3.0", + "chartVersion": "0.3.0", + "namespace": "krateo-system", + "releaseName": "resource-tree-handler", + "revision": 146, + "status": "deployed", + "updated": "2025-08-18T15:21:47Z" + }, + { + "appVersion": "0.0.17", + "chartVersion": "0.0.17", + "namespace": "krateo-system", + "releaseName": "frontend", + "revision": 151, + "status": "deployed", + "updated": "2025-08-18T15:21:46Z" + }, + { + "appVersion": "0.5.3", + "chartVersion": "0.5.3", + "namespace": "krateo-system", + "releaseName": "eventsse", + "revision": 149, + "status": "deployed", + "updated": "2025-08-18T15:21:44Z" + }, + { + "appVersion": "3.5.19", + "chartVersion": "11.1.3", + "namespace": "krateo-system", + "releaseName": "eventsse-etcd", + "revision": 151, + "status": "deployed", + "updated": "2025-08-18T15:21:38Z" + }, + { + "appVersion": "0.5.5", + "chartVersion": "0.5.8", + "namespace": "krateo-system", + "releaseName": "eventrouter", + "revision": 151, + "status": "deployed", + "updated": "2025-08-18T15:21:37Z" + }, + { + "appVersion": "0.11.7", + "chartVersion": "0.11.7", + "namespace": "krateo-system", + "releaseName": "snowplow", + "revision": 156, + "status": "deployed", + "updated": "2025-08-18T15:21:36Z" + }, + { + "appVersion": "0.6.0", + "chartVersion": "0.6.0", + "namespace": "krateo-system", + "releaseName": "smithery", + "revision": 159, + "status": "deployed", + "updated": "2025-08-18T15:21:34Z" + }, + { + "appVersion": "0.20.1", + "chartVersion": "0.20.1", + "namespace": "krateo-system", + "releaseName": "authn", + "revision": 160, + "status": "deployed", + "updated": "2025-08-18T15:21:32Z" + } + ], + "pageSize": 10, + "prefix": "filter-table-fin-ops-test" + } + } +} \ No newline at end of file diff --git a/testdata/missing-additional-props/table.schema.json b/testdata/missing-additional-props/table.schema.json new file mode 100644 index 0000000..fbf30ba --- /dev/null +++ b/testdata/missing-additional-props/table.schema.json @@ -0,0 +1,166 @@ +{ + "type": "object", + "additionalProperties": false, + "properties": { + "version": { + "type": "string", + "default": "v1beta1" + }, + "kind": { + "default": "Table", + "description": "Table displays structured data with customizable columns and pagination", + "type": "string" + }, + "spec": { + "type": "object", + "properties": { + "widgetData": { + "type": "object", + "required": ["data", "columns"], + "properties": { + "prefix": { + "description": "it's the filters prefix to get right values", + "type": "string" + }, + "pageSize": { + "description": "number of rows displayed per page", + "type": "integer" + }, + "data": { + "description": "array of objects representing the table's row data", + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "columns": { + "description": "configuration of the table's columns", + "type": "array", + "items": { + "type": "object", + "properties": { + "color": { + "description": "the color of the value (or the icon) to be represented", + "type": "string", + "enum": [ + "blue", + "darkBlue", + "orange", + "gray", + "red", + "green" + ] + }, + "kind": { + "description": "type of data to be represented", + "type": "string", + "enum": ["value", "icon"] + }, + "title": { + "description": "column header label", + "type": "string" + }, + "valueKey": { + "description": "key used to extract the value from row data", + "type": "string" + } + }, + "required": ["valueKey", "title"], + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "resourcesRefs": { + "type": "object", + "properties": { + "_slice_": { + "type": "object", + "properties": { + "offset": { + "type": "integer" + }, + "page": { + "type": "integer" + }, + "perPage": { + "type": "integer" + }, + "continue": { + "type": "boolean" + } + }, + "required": ["page", "perPage"] + }, + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "resource": { + "type": "string" + }, + "apiVersion": { + "type": "string" + }, + "verb": { + "type": "string", + "enum": ["POST", "PUT", "PATCH", "DELETE", "GET"] + }, + "payload": { + "type": "object" + } + }, + "required": ["id"] + } + } + }, + "required": ["items"] + }, + "apiRef": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + } + }, + "additionalProperties": false, + "required": ["name", "namespace"] + }, + "widgetDataTemplate": { + "type": "array", + "items": { + "type": "object", + "properties": { + "forPath": { + "type": "string" + }, + "expression": { + "type": "string" + } + }, + "additionalProperties": false + }, + "additionalProperties": false + } + }, + "required": ["widgetData"], + "additionalProperties": false + } + }, + "required": ["kind", "spec", "version"] +} \ No newline at end of file diff --git a/testdata/missing-additional-props/table.yaml b/testdata/missing-additional-props/table.yaml new file mode 100644 index 0000000..aafe58c --- /dev/null +++ b/testdata/missing-additional-props/table.yaml @@ -0,0 +1,159 @@ +kind: Table +apiVersion: widgets.templates.krateo.io/v1beta1 +metadata: + name: table-fin-ops-test + namespace: krateo-system +spec: + widgetData: + columns: + - valueKey: releaseName + title: Name + - title: Namespace + valueKey: namespace + - title: App Version + valueKey: appVersion + - title: Chart Version + valueKey: chartVersion + - title: Revision + valueKey: revision + - title: Status + valueKey: status + - title: Last Update + valueKey: updated + data: + - releaseName: test + - appVersion: "9.0.1" + chartVersion: "0.1.0" + namespace: krateo-system + releaseName: opa-kube-mgmt + revision: 15 + status: deployed + updated: "2025-08-18T15:22:07Z" + - appVersion: "0.1.0" + chartVersion: "0.1.0" + namespace: krateo-system + releaseName: finops-moving-window-policy + revision: 15 + status: deployed + updated: "2025-08-18T15:22:04Z" + - appVersion: "0.4.1" + chartVersion: "0.4.1" + namespace: krateo-system + releaseName: finops-operator-scraper + revision: 15 + status: deployed + updated: "2025-08-18T15:22:03Z" + - appVersion: "0.1.2" + chartVersion: "0.1.3" + namespace: krateo-system + releaseName: finops-composition-definition-parser + revision: 15 + status: deployed + updated: "2025-08-18T15:22:01Z" + - appVersion: "0.4.3" + chartVersion: "0.4.3" + namespace: krateo-system + releaseName: finops-database-handler + revision: 15 + status: deployed + updated: "2025-08-18T15:22:00Z" + - appVersion: "0.4.2" + chartVersion: "0.4.2" + namespace: krateo-system + releaseName: finops-operator-focus + revision: 15 + status: deployed + updated: "2025-08-18T15:21:58Z" + - appVersion: "0.4.2" + chartVersion: "0.4.2" + namespace: krateo-system + releaseName: finops-operator-exporter + revision: 15 + status: deployed + updated: "2025-08-18T15:21:57Z" + - appVersion: "5.9.6" + chartVersion: "0.1.4" + namespace: krateo-system + releaseName: cratedb + revision: 15 + status: deployed + updated: "2025-08-18T15:21:55Z" + - appVersion: "0.5.2" + chartVersion: "0.5.3" + namespace: krateo-system + releaseName: oasgen-provider + revision: 15 + status: deployed + updated: "2025-08-18T15:21:53Z" + - appVersion: "0.24.9" + chartVersion: "0.32.0" + namespace: krateo-system + releaseName: core-provider + revision: 15 + status: deployed + updated: "2025-08-18T15:21:51Z" + - appVersion: "0.1.10" + chartVersion: "0.1.10" + namespace: krateo-system + releaseName: composable-portal-starter + revision: 1 + status: deployed + updated: "2025-08-18T15:21:48Z" + - appVersion: "0.3.0" + chartVersion: "0.3.0" + namespace: krateo-system + releaseName: resource-tree-handler + revision: 146 + status: deployed + updated: "2025-08-18T15:21:47Z" + - appVersion: "0.0.17" + chartVersion: "0.0.17" + namespace: krateo-system + releaseName: frontend + revision: 151 + status: deployed + updated: "2025-08-18T15:21:46Z" + - appVersion: "0.5.3" + chartVersion: "0.5.3" + namespace: krateo-system + releaseName: eventsse + revision: 149 + status: deployed + updated: "2025-08-18T15:21:44Z" + - appVersion: "3.5.19" + chartVersion: "11.1.3" + namespace: krateo-system + releaseName: eventsse-etcd + revision: 151 + status: deployed + updated: "2025-08-18T15:21:38Z" + - appVersion: "0.5.5" + chartVersion: "0.5.8" + namespace: krateo-system + releaseName: eventrouter + revision: 151 + status: deployed + updated: "2025-08-18T15:21:37Z" + - appVersion: "0.11.7" + chartVersion: "0.11.7" + namespace: krateo-system + releaseName: snowplow + revision: 156 + status: deployed + updated: "2025-08-18T15:21:36Z" + - appVersion: "0.6.0" + chartVersion: "0.6.0" + namespace: krateo-system + releaseName: smithery + revision: 159 + status: deployed + updated: "2025-08-18T15:21:34Z" + - appVersion: "0.20.1" + chartVersion: "0.20.1" + namespace: krateo-system + releaseName: authn + revision: 160 + status: deployed + updated: "2025-08-18T15:21:32Z" + pageSize: 10 + prefix: filter-table-fin-ops-test