From f78f24fa51bb79f1512f1b1971dc7b75e0dca55b Mon Sep 17 00:00:00 2001 From: Evsyukov Denis Date: Thu, 27 Mar 2025 18:30:55 +0300 Subject: [PATCH 01/16] refactor: simplify jq filter initialization and remove unused libpath Signed-off-by: Evsyukov Denis --- cmd/shell-operator/main.go | 2 +- go.mod | 3 +- go.sum | 6 ++- pkg/app/app.go | 1 - pkg/app/jq.go | 13 ------ pkg/filter/jq/apply_jq.go | 46 ++++++++++++++++++ pkg/filter/jq/apply_jq_exec.go | 29 ------------ pkg/filter/jq/apply_libjq_go.go | 49 -------------------- pkg/filter/jq/jq_exec.go | 43 ----------------- pkg/kube/object_patch/operation.go | 3 +- pkg/kube_events_manager/filter_test.go | 2 +- pkg/kube_events_manager/resource_informer.go | 5 +- pkg/shell-operator/bootstrap.go | 2 +- 13 files changed, 58 insertions(+), 146 deletions(-) delete mode 100644 pkg/app/jq.go create mode 100644 pkg/filter/jq/apply_jq.go delete mode 100644 pkg/filter/jq/apply_jq_exec.go delete mode 100644 pkg/filter/jq/apply_libjq_go.go delete mode 100644 pkg/filter/jq/jq_exec.go diff --git a/cmd/shell-operator/main.go b/cmd/shell-operator/main.go index 2c0e7ae5..300dda19 100644 --- a/cmd/shell-operator/main.go +++ b/cmd/shell-operator/main.go @@ -33,7 +33,7 @@ func main() { // print version kpApp.Command("version", "Show version.").Action(func(_ *kingpin.ParseContext) error { fmt.Printf("%s %s\n", app.AppName, app.Version) - fl := jq.NewFilter(app.JqLibraryPath) + fl := jq.NewFilter() fmt.Println(fl.FilterInfo()) return nil }) diff --git a/go.mod b/go.mod index 74187ef0..01def175 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.23.1 require ( github.com/deckhouse/deckhouse/pkg/log v0.0.0-20241205040953-7b376bae249c github.com/flant/kube-client v1.2.2 - github.com/flant/libjq-go v1.6.3-0.20201126171326-c46a40ff22ee // branch: master github.com/go-chi/chi/v5 v5.2.1 github.com/go-openapi/spec v0.19.8 github.com/go-openapi/strfmt v0.19.5 @@ -36,6 +35,7 @@ replace github.com/go-openapi/validate => github.com/flant/go-openapi-validate v require ( github.com/deckhouse/module-sdk v0.2.0 github.com/gojuno/minimock/v3 v3.4.5 + github.com/itchyny/gojq v0.12.17 ) require ( @@ -71,6 +71,7 @@ require ( github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/imdario/mergo v0.3.16 // indirect + github.com/itchyny/timefmt-go v0.1.6 // indirect github.com/jonboulle/clockwork v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect diff --git a/go.sum b/go.sum index 4a496ab5..0f8e09ca 100644 --- a/go.sum +++ b/go.sum @@ -41,8 +41,6 @@ github.com/flant/go-openapi-validate v0.19.12-flant.0 h1:xk6kvc9fHKMgUdB6J7kbpbL github.com/flant/go-openapi-validate v0.19.12-flant.0/go.mod h1:Rzou8hA/CBw8donlS6WNEUQupNvUZ0waH08tGe6kAQ4= github.com/flant/kube-client v1.2.2 h1:27LBs+PKJEFnkQXjPU9eIps7a7iyI13AKcSYj897DCU= github.com/flant/kube-client v1.2.2/go.mod h1:eMa3aJ6V1PRWSQ/RCROkObDpY4S74uM84SJS4G/LINg= -github.com/flant/libjq-go v1.6.3-0.20201126171326-c46a40ff22ee h1:evii83J+/6QGNvyf6tjQ/p27DPY9iftxIBb37ALJRTg= -github.com/flant/libjq-go v1.6.3-0.20201126171326-c46a40ff22ee/go.mod h1:f+REaGl/+pZR97rbTcwHEka/MAipoQQ2Mc0iQUj4ak0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= @@ -176,6 +174,10 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/itchyny/gojq v0.12.17 h1:8av8eGduDb5+rvEdaOO+zQUjA04MS0m3Ps8HiD+fceg= +github.com/itchyny/gojq v0.12.17/go.mod h1:WBrEMkgAfAGO1LUcGOckBl5O726KPp+OlkKug0I/FEY= +github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/myJ5Q= +github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= diff --git a/pkg/app/app.go b/pkg/app/app.go index f0edf74d..4d5561d3 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -134,7 +134,6 @@ func DefineStartCommandFlags(kpApp *kingpin.Application, cmd *kingpin.CmdClause) DefineKubeClientFlags(cmd) DefineValidatingWebhookFlags(cmd) DefineConversionWebhookFlags(cmd) - DefineJqFlags(cmd) DefineLoggingFlags(cmd) DefineDebugFlags(kpApp, cmd) } diff --git a/pkg/app/jq.go b/pkg/app/jq.go deleted file mode 100644 index 09323702..00000000 --- a/pkg/app/jq.go +++ /dev/null @@ -1,13 +0,0 @@ -package app - -import "gopkg.in/alecthomas/kingpin.v2" - -var JqLibraryPath = "" - -// DefineJqFlags set flag for jq library -func DefineJqFlags(cmd *kingpin.CmdClause) { - cmd.Flag("jq-library-path", "Prepend directory to the search list for jq modules (-L flag). Can be set with $JQ_LIBRARY_PATH."). - Envar("JQ_LIBRARY_PATH"). - Default(JqLibraryPath). - StringVar(&JqLibraryPath) -} diff --git a/pkg/filter/jq/apply_jq.go b/pkg/filter/jq/apply_jq.go new file mode 100644 index 00000000..a1679569 --- /dev/null +++ b/pkg/filter/jq/apply_jq.go @@ -0,0 +1,46 @@ +package jq + +import ( + "fmt" + + "github.com/itchyny/gojq" + + "github.com/flant/shell-operator/pkg/filter" +) + +var _ filter.Filter = (*Filter)(nil) + +func NewFilter() *Filter { + return &Filter{} +} + +type Filter struct{} + +// ApplyFilter runs jq expression provided in jqFilter with jsonData as input. +func (f *Filter) ApplyFilter(jqFilter string, jsonData []byte) (string, error) { + query, err := gojq.Parse(jqFilter) + if err != nil { + return "", err + } + iter := query.Run(jsonData) + var result string + for { + v, ok := iter.Next() + if !ok { + break + } + if v == nil { + continue + } + if _, ok := v.(error); ok { + continue + } + result += fmt.Sprintf("%v", v) + } + + return result, nil +} + +func (f *Filter) FilterInfo() string { + return "jqFilter implementation: use jq binary from $PATH" +} diff --git a/pkg/filter/jq/apply_jq_exec.go b/pkg/filter/jq/apply_jq_exec.go deleted file mode 100644 index 6543cf31..00000000 --- a/pkg/filter/jq/apply_jq_exec.go +++ /dev/null @@ -1,29 +0,0 @@ -//go:build !cgo || (cgo && !use_libjq) -// +build !cgo cgo,!use_libjq - -package jq - -import "github.com/flant/shell-operator/pkg/filter" - -var _ filter.Filter = (*Filter)(nil) - -func NewFilter(libpath string) *Filter { - return &Filter{ - Libpath: libpath, - } -} - -type Filter struct { - Libpath string -} - -// ApplyJqFilter runs jq expression provided in jqFilter with jsonData as input. -// -// It uses jq as a subprocess. -func (f *Filter) ApplyFilter(jqFilter string, jsonData []byte) (string, error) { - return jqExec(jqFilter, jsonData, f.Libpath) -} - -func (f *Filter) FilterInfo() string { - return "jqFilter implementation: use jq binary from $PATH" -} diff --git a/pkg/filter/jq/apply_libjq_go.go b/pkg/filter/jq/apply_libjq_go.go deleted file mode 100644 index e4b3a7d6..00000000 --- a/pkg/filter/jq/apply_libjq_go.go +++ /dev/null @@ -1,49 +0,0 @@ -//go:build cgo && use_libjq -// +build cgo,use_libjq - -package jq - -import ( - "fmt" - "os" - - libjq "github.com/flant/libjq-go" - "github.com/flant/shell-operator/pkg/filter" -) - -var _ filter.Filter = (*Filter)(nil) - -func NewFilter(libpath string) *Filter { - return &Filter{ - Libpath: libpath, - } -} - -type Filter struct { - Libpath string -} - -// Note: add build tag 'use_libjg' to build with libjq-go. - -// ApplyJqFilter runs jq expression provided in jqFilter with jsonData as input. -// -// It uses libjq-go or executes jq as a binary if $JQ_EXEC is set to "yes". -func (f *Filter) ApplyFilter(jqFilter string, jsonData []byte) (string, error) { - // Use jq exec filtering if environment variable is present. - if os.Getenv("JQ_EXEC") == "yes" { - return jqExec(jqFilter, jsonData, f.Libpath) - } - - result, err := libjq.Jq().WithLibPath(f.Libpath).Program(jqFilter).Cached().Run(string(jsonData)) - if err != nil { - return "", fmt.Errorf("libjq filter '%s': '%s'", jqFilter, err) - } - return result, nil -} - -func (f *Filter) FilterInfo() string { - if os.Getenv("JQ_EXEC") == "yes" { - return "jqFilter implementation: use jq binary from $PATH (JQ_EXEC=yes is set)" - } - return "jqFilter implementation: use embedded libjq-go" -} diff --git a/pkg/filter/jq/jq_exec.go b/pkg/filter/jq/jq_exec.go deleted file mode 100644 index 591cab8f..00000000 --- a/pkg/filter/jq/jq_exec.go +++ /dev/null @@ -1,43 +0,0 @@ -package jq - -import ( - "bytes" - "fmt" - "os/exec" - "strings" - - "github.com/flant/shell-operator/pkg/executor" -) - -// jqExec is a subprocess implementation of the jq filtering. -func jqExec(jqFilter string, jsonData []byte, libPath string) (string, error) { - var cmd *exec.Cmd - if libPath == "" { - cmd = exec.Command("jq", jqFilter) - } else { - cmd = exec.Command("jq", "-L", libPath, jqFilter) - } - - var stdinBuf bytes.Buffer - _, err := stdinBuf.WriteString(string(jsonData)) - if err != nil { - panic(err) - } - - var stdoutBuf bytes.Buffer - var stderrBuf bytes.Buffer - - cmd.Stdin = &stdinBuf - cmd.Stdout = &stdoutBuf - cmd.Stderr = &stderrBuf - - err = executor.Run(cmd) - stdout := strings.TrimSpace(stdoutBuf.String()) - stderr := strings.TrimSpace(stderrBuf.String()) - - if err != nil { - return "", fmt.Errorf("exec jq: \nerr: '%s'\nstderr: '%s'", err, stderr) - } - - return stdout, nil -} diff --git a/pkg/kube/object_patch/operation.go b/pkg/kube/object_patch/operation.go index 6b5a95e4..6f8ab26c 100644 --- a/pkg/kube/object_patch/operation.go +++ b/pkg/kube/object_patch/operation.go @@ -11,7 +11,6 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/types" - "github.com/flant/shell-operator/pkg/app" "github.com/flant/shell-operator/pkg/filter/jq" ) @@ -289,7 +288,7 @@ func newPatchOperation(patchType types.PatchType, patch any, apiVersion, kind, n func NewPatchWithJQOperation(jqQuery string, apiVersion string, kind string, namespace string, name string, opts ...sdkpkg.PatchCollectorOption) sdkpkg.PatchCollectorOperation { return newFilterOperation(func(u *unstructured.Unstructured) (*unstructured.Unstructured, error) { - filter := jq.NewFilter(app.JqLibraryPath) + filter := jq.NewFilter() return applyJQPatch(jqQuery, filter, u) }, apiVersion, kind, namespace, name, opts...) } diff --git a/pkg/kube_events_manager/filter_test.go b/pkg/kube_events_manager/filter_test.go index 70b16d00..5c1961ab 100644 --- a/pkg/kube_events_manager/filter_test.go +++ b/pkg/kube_events_manager/filter_test.go @@ -13,7 +13,7 @@ import ( func TestApplyFilter(t *testing.T) { t.Run("filter func with error", func(t *testing.T) { uns := &unstructured.Unstructured{Object: map[string]interface{}{"foo": "bar"}} - filter := jq.NewFilter("") + filter := jq.NewFilter() _, err := applyFilter("", filter, filterFuncWithError, uns) assert.EqualError(t, err, "filterFn (github.com/flant/shell-operator/pkg/kube_events_manager.filterFuncWithError) contains an error: invalid character 'a' looking for beginning of value") }) diff --git a/pkg/kube_events_manager/resource_informer.go b/pkg/kube_events_manager/resource_informer.go index 443877fd..3d15e570 100644 --- a/pkg/kube_events_manager/resource_informer.go +++ b/pkg/kube_events_manager/resource_informer.go @@ -16,7 +16,6 @@ import ( "k8s.io/client-go/tools/cache" klient "github.com/flant/kube-client/client" - "github.com/flant/shell-operator/pkg/app" "github.com/flant/shell-operator/pkg/filter/jq" kemtypes "github.com/flant/shell-operator/pkg/kube_events_manager/types" "github.com/flant/shell-operator/pkg/metric" @@ -227,7 +226,7 @@ func (ei *resourceInformer) loadExistedObjects() error { defer measure.Duration(func(d time.Duration) { ei.metricStorage.HistogramObserve("{PREFIX}kube_jq_filter_duration_seconds", d.Seconds(), ei.Monitor.Metadata.MetricLabels, nil) })() - filter := jq.NewFilter(app.JqLibraryPath) + filter := jq.NewFilter() objFilterRes, err = applyFilter(ei.Monitor.JqFilter, filter, ei.Monitor.FilterFunc, &obj) }() @@ -307,7 +306,7 @@ func (ei *resourceInformer) handleWatchEvent(object interface{}, eventType kemty defer measure.Duration(func(d time.Duration) { ei.metricStorage.HistogramObserve("{PREFIX}kube_jq_filter_duration_seconds", d.Seconds(), ei.Monitor.Metadata.MetricLabels, nil) })() - filter := jq.NewFilter(app.JqLibraryPath) + filter := jq.NewFilter() objFilterRes, err = applyFilter(ei.Monitor.JqFilter, filter, ei.Monitor.FilterFunc, obj) }() if err != nil { diff --git a/pkg/shell-operator/bootstrap.go b/pkg/shell-operator/bootstrap.go index 88288391..3c81428c 100644 --- a/pkg/shell-operator/bootstrap.go +++ b/pkg/shell-operator/bootstrap.go @@ -28,7 +28,7 @@ func Init(logger *log.Logger) (*ShellOperator, error) { // Log version and jq filtering implementation. logger.Info(app.AppStartMessage) - fl := jq.NewFilter(app.JqLibraryPath) + fl := jq.NewFilter() logger.Debug(fl.FilterInfo()) hooksDir, err := utils.RequireExistingDirectory(app.HooksDir) From f205383112a1bdf45489e6041092c450e86c8a48 Mon Sep 17 00:00:00 2001 From: Evsyukov Denis Date: Thu, 27 Mar 2025 18:37:23 +0300 Subject: [PATCH 02/16] Update pkg/filter/jq/apply_jq.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pkg/filter/jq/apply_jq.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/filter/jq/apply_jq.go b/pkg/filter/jq/apply_jq.go index a1679569..e72c88c3 100644 --- a/pkg/filter/jq/apply_jq.go +++ b/pkg/filter/jq/apply_jq.go @@ -42,5 +42,5 @@ func (f *Filter) ApplyFilter(jqFilter string, jsonData []byte) (string, error) { } func (f *Filter) FilterInfo() string { - return "jqFilter implementation: use jq binary from $PATH" + return "jqFilter implementation: using itchyny/gojq" } From e9ae9978675851a8f10941a2df6dfb281e2a214c Mon Sep 17 00:00:00 2001 From: Evsyukov Denis Date: Fri, 28 Mar 2025 11:17:44 +0300 Subject: [PATCH 03/16] refactor: improve JSON handling in jq filter application Signed-off-by: Evsyukov Denis --- pkg/filter/jq/apply_jq.go | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/pkg/filter/jq/apply_jq.go b/pkg/filter/jq/apply_jq.go index e72c88c3..891c5b77 100644 --- a/pkg/filter/jq/apply_jq.go +++ b/pkg/filter/jq/apply_jq.go @@ -1,9 +1,8 @@ package jq import ( - "fmt" - "github.com/itchyny/gojq" + "k8s.io/apimachinery/pkg/util/json" "github.com/flant/shell-operator/pkg/filter" ) @@ -22,7 +21,13 @@ func (f *Filter) ApplyFilter(jqFilter string, jsonData []byte) (string, error) { if err != nil { return "", err } - iter := query.Run(jsonData) + + var data map[string]any + if err := json.Unmarshal(jsonData, &data); err != nil { + return "", err + } + + iter := query.Run(data) var result string for { v, ok := iter.Next() @@ -32,10 +37,11 @@ func (f *Filter) ApplyFilter(jqFilter string, jsonData []byte) (string, error) { if v == nil { continue } - if _, ok := v.(error); ok { - continue + bytes, err := json.Marshal(v.(map[string]any)) + if err != nil { + return "", err } - result += fmt.Sprintf("%v", v) + result += string(bytes) } return result, nil From b26838c35ccaddc5782a7ba5f1d8ca056f1e5c4f Mon Sep 17 00:00:00 2001 From: Evsyukov Denis Date: Fri, 28 Mar 2025 11:45:40 +0300 Subject: [PATCH 04/16] refactor: remove prebuilt libjq dependencies from build and test configurations Signed-off-by: Evsyukov Denis --- .github/workflows/build.yaml | 9 +-------- .github/workflows/tests-labeled.yaml | 7 ------- .github/workflows/tests.yaml | 7 ------- Dockerfile | 13 ++----------- 4 files changed, 3 insertions(+), 33 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index bf8085ef..2566ffe5 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -30,18 +30,11 @@ jobs: go mod download echo -n "Go modules unpacked size is: " && du -sh $HOME/go/pkg/mod - - name: Download prebuilt libjq static libraries - run: | - curl -sSfL https://github.com/flant/libjq-go/releases/download/jq-b6be13d5-0/libjq-glibc-amd64.tgz | tar zxf - - - name: Build binary run: | - export CGO_ENABLED=1 - export CGO_CFLAGS="-I$GITHUB_WORKSPACE/libjq/include" - export CGO_LDFLAGS="-L$GITHUB_WORKSPACE/libjq/lib" export GOOS=linux - go build -tags use_libjq ./cmd/shell-operator + go build ./cmd/shell-operator # MacOS build works fine because jq package already has static libraries. # Windows build requires jq compilation, this should be done in libjq-go. diff --git a/.github/workflows/tests-labeled.yaml b/.github/workflows/tests-labeled.yaml index 72789546..a7979210 100644 --- a/.github/workflows/tests-labeled.yaml +++ b/.github/workflows/tests-labeled.yaml @@ -87,10 +87,6 @@ jobs: go mod download echo -n "Go modules unpacked size is: " && du -sh $HOME/go/pkg/mod - - name: Download prebuilt libjq static libraries - run: | - curl -sSfL https://github.com/flant/libjq-go/releases/download/jq-b6be13d5-0/libjq-glibc-amd64.tgz | tar zxf - - - name: Install ginkgo run: | go install github.com/onsi/ginkgo/ginkgo @@ -109,9 +105,6 @@ jobs: env: CLUSTER_NAME: ${{ matrix.cluster_name }} run: | - export CGO_ENABLED=1 - export CGO_CFLAGS="-I$GITHUB_WORKSPACE/libjq/include" - export CGO_LDFLAGS="-L$GITHUB_WORKSPACE/libjq/lib" export GOOS=linux ginkgo \ diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index ec5f21db..98904d3d 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -33,15 +33,8 @@ jobs: go mod download echo -n "Go modules unpacked size is: " && du -sh $HOME/go/pkg/mod - - name: Download prebuilt libjq static libraries - run: | - curl -sSfL https://github.com/flant/libjq-go/releases/download/jq-b6be13d5-0/libjq-glibc-amd64.tgz | tar zxf - - - name: Run unit tests run: | - export CGO_ENABLED=1 - export CGO_CFLAGS="-I$GITHUB_WORKSPACE/libjq/include" - export CGO_LDFLAGS="-L$GITHUB_WORKSPACE/libjq/lib" export GOOS=linux go test \ diff --git a/Dockerfile b/Dockerfile index b2973e94..72dd58fc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,3 @@ -# Prebuilt libjq. -FROM --platform=${TARGETPLATFORM:-linux/amd64} flant/jq:b6be13d5-musl as libjq - # Go builder. FROM --platform=${TARGETPLATFORM:-linux/amd64} golang:1.23-alpine3.21 AS builder @@ -12,15 +9,10 @@ ADD go.mod go.sum /app/ WORKDIR /app RUN go mod download -COPY --from=libjq /libjq /libjq ADD . /app -RUN CGO_ENABLED=1 \ - CGO_CFLAGS="-I/libjq/include" \ - CGO_LDFLAGS="-L/libjq/lib" \ - GOOS=linux \ - go build -ldflags="-linkmode external -extldflags '-static' -s -w -X 'github.com/flant/shell-operator/pkg/app.Version=$appVersion'" \ - -tags use_libjq \ +RUN GOOS=linux \ + go build -ldflags="-s -w -X 'github.com/flant/shell-operator/pkg/app.Version=$appVersion'" \ -o shell-operator \ ./cmd/shell-operator @@ -35,7 +27,6 @@ RUN apk --no-cache add ca-certificates bash sed tini && \ mkdir /hooks ADD frameworks/shell /frameworks/shell ADD shell_lib.sh / -COPY --from=libjq /bin/jq /usr/bin COPY --from=builder /app/shell-operator / WORKDIR / ENV SHELL_OPERATOR_HOOKS_DIR /hooks From a82a10a3f0275f9e5fbee31f4cfd29141434182d Mon Sep 17 00:00:00 2001 From: Evsyukov Denis Date: Fri, 28 Mar 2025 11:47:55 +0300 Subject: [PATCH 05/16] refactor: enhance error handling in jq filter application Signed-off-by: Evsyukov Denis --- pkg/filter/jq/apply_jq.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pkg/filter/jq/apply_jq.go b/pkg/filter/jq/apply_jq.go index 891c5b77..7596a553 100644 --- a/pkg/filter/jq/apply_jq.go +++ b/pkg/filter/jq/apply_jq.go @@ -1,6 +1,8 @@ package jq import ( + "errors" + "github.com/itchyny/gojq" "k8s.io/apimachinery/pkg/util/json" @@ -34,8 +36,12 @@ func (f *Filter) ApplyFilter(jqFilter string, jsonData []byte) (string, error) { if !ok { break } - if v == nil { - continue + if err, ok := v.(error); ok { + var errGoJq *gojq.HaltError + if errors.As(err, &errGoJq) && errGoJq.Value() == nil { + break + } + return "", err } bytes, err := json.Marshal(v.(map[string]any)) if err != nil { From a079043f4d59153de6047084ed8f91d18ada72a6 Mon Sep 17 00:00:00 2001 From: Evsyukov Denis Date: Fri, 28 Mar 2025 11:52:33 +0300 Subject: [PATCH 06/16] refactor: rename apply_jq.go to apply.go for consistency Signed-off-by: Evsyukov Denis --- pkg/filter/jq/{apply_jq.go => apply.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename pkg/filter/jq/{apply_jq.go => apply.go} (100%) diff --git a/pkg/filter/jq/apply_jq.go b/pkg/filter/jq/apply.go similarity index 100% rename from pkg/filter/jq/apply_jq.go rename to pkg/filter/jq/apply.go From 577774a7209ce63ae4fcd500d42bb77def558b40 Mon Sep 17 00:00:00 2001 From: Evsyukov Denis Date: Fri, 28 Mar 2025 12:14:10 +0300 Subject: [PATCH 07/16] refactor: improve type handling in JSON marshaling within apply.go Signed-off-by: Evsyukov Denis --- pkg/filter/jq/apply.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pkg/filter/jq/apply.go b/pkg/filter/jq/apply.go index 7596a553..9cfc180d 100644 --- a/pkg/filter/jq/apply.go +++ b/pkg/filter/jq/apply.go @@ -43,11 +43,13 @@ func (f *Filter) ApplyFilter(jqFilter string, jsonData []byte) (string, error) { } return "", err } - bytes, err := json.Marshal(v.(map[string]any)) - if err != nil { - return "", err + if resultMap, ok := v.(map[string]any); ok { + bytes, err := json.Marshal(resultMap) + if err != nil { + return "", err + } + result += string(bytes) } - result += string(bytes) } return result, nil From 7ceb176673e8d35eb37cbfbe8c30de79ecda00f5 Mon Sep 17 00:00:00 2001 From: Evsyukov Denis Date: Fri, 28 Mar 2025 12:23:21 +0300 Subject: [PATCH 08/16] refactor: streamline environment variable declarations in Dockerfile Signed-off-by: Evsyukov Denis --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 72dd58fc..c059f42b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,7 +29,7 @@ ADD frameworks/shell /frameworks/shell ADD shell_lib.sh / COPY --from=builder /app/shell-operator / WORKDIR / -ENV SHELL_OPERATOR_HOOKS_DIR /hooks -ENV LOG_TYPE json +ENV SHELL_OPERATOR_HOOKS_DIR=/hooks +ENV LOG_TYPE=json ENTRYPOINT ["/sbin/tini", "--", "/shell-operator"] CMD ["start"] From 32665cdc008a882ada512d1223427081523d76a1 Mon Sep 17 00:00:00 2001 From: Evsyukov Denis Date: Sat, 29 Mar 2025 08:53:27 +0300 Subject: [PATCH 09/16] test: add unit tests for ApplyFilter function in jq package --- pkg/filter/jq/apply_test.go | 72 +++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 pkg/filter/jq/apply_test.go diff --git a/pkg/filter/jq/apply_test.go b/pkg/filter/jq/apply_test.go new file mode 100644 index 00000000..8f7f8cf2 --- /dev/null +++ b/pkg/filter/jq/apply_test.go @@ -0,0 +1,72 @@ +package jq + +import ( + "testing" + + . "github.com/onsi/gomega" +) + +func Test_ApplyFilter_SingleDocumentModification(t *testing.T) { + g := NewWithT(t) + filter := NewFilter() + + jsonData := []byte(`{"name": "John", "age": 30}`) + jqFilter := `. + {"status": "active"}` + + result, err := filter.ApplyFilter(jqFilter, jsonData) + + g.Expect(err).Should(BeNil()) + g.Expect(result).Should(MatchJSON(`{"name": "John", "age": 30, "status": "active"}`)) +} + +func Test_ApplyFilter_ExtractValuesFromDocument(t *testing.T) { + g := NewWithT(t) + filter := NewFilter() + + jsonData := []byte(`{"user": {"name": "John", "details": {"location": "New York", "occupation": "Developer"}}}`) + jqFilter := `.user.details` + + result, err := filter.ApplyFilter(jqFilter, jsonData) + + g.Expect(err).Should(BeNil()) + g.Expect(result).Should(MatchJSON(`{"location": "New York", "occupation": "Developer"}`)) +} + +func Test_ApplyFilter_MultipleJsonDocumentsInArray(t *testing.T) { + g := NewWithT(t) + filter := NewFilter() + + jsonData := []byte(`{"users": [{"name": "John", "status": "inactive"}, {"name": "Jane", "status": "inactive"}]}`) + jqFilter := `.users[] | . + {"status": "active"}` + + result, err := filter.ApplyFilter(jqFilter, jsonData) + + g.Expect(err).Should(BeNil()) + g.Expect(result).Should(Equal(`{"name":"John","status":"active"}{"name":"Jane","status":"active"}`)) +} + +func Test_ApplyFilter_InvalidFilter(t *testing.T) { + g := NewWithT(t) + filter := NewFilter() + + jsonData := []byte(`{"name": "John"}`) + invalidFilter := `. | invalid_function` + + result, err := filter.ApplyFilter(invalidFilter, jsonData) + + g.Expect(err).ShouldNot(BeNil()) + g.Expect(result).Should(BeEmpty()) +} + +func Test_ApplyFilter_InvalidJson(t *testing.T) { + g := NewWithT(t) + filter := NewFilter() + + invalidJson := []byte(`{"name": "John" invalid_json`) + jqFilter := `.name` + + result, err := filter.ApplyFilter(jqFilter, invalidJson) + + g.Expect(err).ShouldNot(BeNil()) + g.Expect(result).Should(BeEmpty()) +} From 4398c455f3f549b8a5bf2352d91cb9b2a2b43ed1 Mon Sep 17 00:00:00 2001 From: Evsyukov Denis Date: Sat, 29 Mar 2025 16:52:26 +0300 Subject: [PATCH 10/16] refactor: update ApplyFilter to use map[string]any and improve data handling Signed-off-by: Evsyukov Denis --- pkg/filter/filter.go | 2 +- pkg/filter/jq/apply.go | 24 +++++++----------------- pkg/kube/object_patch/helpers.go | 13 +++---------- pkg/kube_events_manager/filter.go | 22 +++++++++++++--------- 4 files changed, 24 insertions(+), 37 deletions(-) diff --git a/pkg/filter/filter.go b/pkg/filter/filter.go index c24d29d6..3dcfe68d 100644 --- a/pkg/filter/filter.go +++ b/pkg/filter/filter.go @@ -1,6 +1,6 @@ package filter type Filter interface { - ApplyFilter(filterStr string, data []byte) (string, error) + ApplyFilter(filterStr string, data map[string]any) (map[string]any, error) FilterInfo() string } diff --git a/pkg/filter/jq/apply.go b/pkg/filter/jq/apply.go index 9cfc180d..76490464 100644 --- a/pkg/filter/jq/apply.go +++ b/pkg/filter/jq/apply.go @@ -2,11 +2,10 @@ package jq import ( "errors" - - "github.com/itchyny/gojq" - "k8s.io/apimachinery/pkg/util/json" + "maps" "github.com/flant/shell-operator/pkg/filter" + "github.com/itchyny/gojq" ) var _ filter.Filter = (*Filter)(nil) @@ -18,19 +17,14 @@ func NewFilter() *Filter { type Filter struct{} // ApplyFilter runs jq expression provided in jqFilter with jsonData as input. -func (f *Filter) ApplyFilter(jqFilter string, jsonData []byte) (string, error) { +func (f *Filter) ApplyFilter(jqFilter string, data map[string]any) (map[string]any, error) { query, err := gojq.Parse(jqFilter) if err != nil { - return "", err - } - - var data map[string]any - if err := json.Unmarshal(jsonData, &data); err != nil { - return "", err + return nil, err } iter := query.Run(data) - var result string + var result map[string]any for { v, ok := iter.Next() if !ok { @@ -41,14 +35,10 @@ func (f *Filter) ApplyFilter(jqFilter string, jsonData []byte) (string, error) { if errors.As(err, &errGoJq) && errGoJq.Value() == nil { break } - return "", err + return nil, err } if resultMap, ok := v.(map[string]any); ok { - bytes, err := json.Marshal(resultMap) - if err != nil { - return "", err - } - result += string(bytes) + maps.Copy(result, resultMap) } } diff --git a/pkg/kube/object_patch/helpers.go b/pkg/kube/object_patch/helpers.go index cf82a357..45b057e0 100644 --- a/pkg/kube/object_patch/helpers.go +++ b/pkg/kube/object_patch/helpers.go @@ -65,21 +65,14 @@ func unmarshalFromYaml(yamlSpecs []byte) ([]OperationSpec, error) { } func applyJQPatch(jqFilter string, fl filter.Filter, obj *unstructured.Unstructured) (*unstructured.Unstructured, error) { - objBytes, err := obj.MarshalJSON() - if err != nil { - return nil, err - } - - filterResult, err := fl.ApplyFilter(jqFilter, objBytes) + filterResult, err := fl.ApplyFilter(jqFilter, obj.UnstructuredContent()) if err != nil { return nil, fmt.Errorf("failed to apply jqFilter:\n%sto Object:\n%s\n"+ "error: %s", jqFilter, obj, err) } - retObj := &unstructured.Unstructured{} - _, _, err = unstructured.UnstructuredJSONScheme.Decode([]byte(filterResult), nil, retObj) - if err != nil { - return nil, fmt.Errorf("failed to convert filterResult:\n%s\nto Unstructured Object\nerror: %s", filterResult, err) + retObj := &unstructured.Unstructured{ + Object: filterResult, } return retObj, nil diff --git a/pkg/kube_events_manager/filter.go b/pkg/kube_events_manager/filter.go index b740bfc2..89fd90fb 100644 --- a/pkg/kube_events_manager/filter.go +++ b/pkg/kube_events_manager/filter.go @@ -46,23 +46,27 @@ func applyFilter(jqFilter string, fl filter.Filter, filterFn func(obj *unstructu } // Render obj to JSON text to apply jq filter. - data, err := json.Marshal(obj) - if err != nil { - return nil, err - } - if jqFilter == "" { + data, err := json.Marshal(obj) + if err != nil { + return nil, err + } res.Metadata.Checksum = utils_checksum.CalculateChecksum(string(data)) } else { var err error - var filtered string - filtered, err = fl.ApplyFilter(jqFilter, data) + var filtered map[string]any + filtered, err = fl.ApplyFilter(jqFilter, obj.UnstructuredContent()) if err != nil { return nil, fmt.Errorf("jqFilter: %v", err) } - res.FilterResult = filtered - res.Metadata.Checksum = utils_checksum.CalculateChecksum(filtered) + bytes, err := json.Marshal(filtered) + if err != nil { + return nil, fmt.Errorf("jqFilter: %v", err) + } + filteredStr := string(bytes) + res.FilterResult = filteredStr + res.Metadata.Checksum = utils_checksum.CalculateChecksum(filteredStr) } return res, nil From 4dfb4ed3e5bc748574ff2081cd3f8a7ed226d788 Mon Sep 17 00:00:00 2001 From: Evsyukov Denis Date: Sat, 29 Mar 2025 16:57:41 +0300 Subject: [PATCH 11/16] refactor: update ApplyFilter tests to use map[string]any for improved type consistency Signed-off-by: Evsyukov Denis --- pkg/filter/jq/apply.go | 2 +- pkg/filter/jq/apply_test.go | 34 ++++++++++++++++++---------------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/pkg/filter/jq/apply.go b/pkg/filter/jq/apply.go index 76490464..18c622cf 100644 --- a/pkg/filter/jq/apply.go +++ b/pkg/filter/jq/apply.go @@ -24,7 +24,7 @@ func (f *Filter) ApplyFilter(jqFilter string, data map[string]any) (map[string]a } iter := query.Run(data) - var result map[string]any + var result = make(map[string]any) for { v, ok := iter.Next() if !ok { diff --git a/pkg/filter/jq/apply_test.go b/pkg/filter/jq/apply_test.go index 8f7f8cf2..3e06f2f0 100644 --- a/pkg/filter/jq/apply_test.go +++ b/pkg/filter/jq/apply_test.go @@ -10,63 +10,65 @@ func Test_ApplyFilter_SingleDocumentModification(t *testing.T) { g := NewWithT(t) filter := NewFilter() - jsonData := []byte(`{"name": "John", "age": 30}`) jqFilter := `. + {"status": "active"}` - result, err := filter.ApplyFilter(jqFilter, jsonData) + result, err := filter.ApplyFilter(jqFilter, map[string]any{"name": "John", "age": 30}) g.Expect(err).Should(BeNil()) - g.Expect(result).Should(MatchJSON(`{"name": "John", "age": 30, "status": "active"}`)) + g.Expect(result).Should(Equal(map[string]any{"name": "John", "age": 30, "status": "active"})) } func Test_ApplyFilter_ExtractValuesFromDocument(t *testing.T) { g := NewWithT(t) filter := NewFilter() - jsonData := []byte(`{"user": {"name": "John", "details": {"location": "New York", "occupation": "Developer"}}}`) jqFilter := `.user.details` - result, err := filter.ApplyFilter(jqFilter, jsonData) + result, err := filter.ApplyFilter(jqFilter, map[string]any{"user": map[string]any{"name": "John", "details": map[string]any{"location": "New York", "occupation": "Developer"}}}) g.Expect(err).Should(BeNil()) - g.Expect(result).Should(MatchJSON(`{"location": "New York", "occupation": "Developer"}`)) + g.Expect(result).Should(Equal(map[string]any{"location": "New York", "occupation": "Developer"})) } func Test_ApplyFilter_MultipleJsonDocumentsInArray(t *testing.T) { g := NewWithT(t) filter := NewFilter() - jsonData := []byte(`{"users": [{"name": "John", "status": "inactive"}, {"name": "Jane", "status": "inactive"}]}`) jqFilter := `.users[] | . + {"status": "active"}` - result, err := filter.ApplyFilter(jqFilter, jsonData) + result, err := filter.ApplyFilter(jqFilter, map[string]any{"users": []any{map[string]any{"name": "John", "status": "inactive"}, map[string]any{"name": "Jane", "status": "inactive"}}}) g.Expect(err).Should(BeNil()) - g.Expect(result).Should(Equal(`{"name":"John","status":"active"}{"name":"Jane","status":"active"}`)) + + expected1 := map[string]any{"name": "John", "status": "active"} + expected2 := map[string]any{"name": "Jane", "status": "active"} + + g.Expect(result).Should(SatisfyAny( + Equal(expected1), + Equal(expected2), + )) } func Test_ApplyFilter_InvalidFilter(t *testing.T) { g := NewWithT(t) filter := NewFilter() - jsonData := []byte(`{"name": "John"}`) invalidFilter := `. | invalid_function` - result, err := filter.ApplyFilter(invalidFilter, jsonData) + result, err := filter.ApplyFilter(invalidFilter, map[string]any{"name": "John"}) g.Expect(err).ShouldNot(BeNil()) - g.Expect(result).Should(BeEmpty()) + g.Expect(result).Should(BeNil()) } func Test_ApplyFilter_InvalidJson(t *testing.T) { g := NewWithT(t) filter := NewFilter() - invalidJson := []byte(`{"name": "John" invalid_json`) jqFilter := `.name` - result, err := filter.ApplyFilter(jqFilter, invalidJson) + result, err := filter.ApplyFilter(jqFilter, map[string]any{"name": "John"}) - g.Expect(err).ShouldNot(BeNil()) - g.Expect(result).Should(BeEmpty()) + g.Expect(err).Should(BeNil()) + g.Expect(result).ShouldNot(BeNil()) } From f41b921fa34f2ebbfd4790c8911bfa7d49004e63 Mon Sep 17 00:00:00 2001 From: Evsyukov Denis Date: Sat, 29 Mar 2025 17:05:59 +0300 Subject: [PATCH 12/16] refactor: optimize filter result assignment and checksum calculation in filter.go Signed-off-by: Evsyukov Denis --- pkg/kube_events_manager/filter.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pkg/kube_events_manager/filter.go b/pkg/kube_events_manager/filter.go index 89fd90fb..6037b3fd 100644 --- a/pkg/kube_events_manager/filter.go +++ b/pkg/kube_events_manager/filter.go @@ -64,9 +64,8 @@ func applyFilter(jqFilter string, fl filter.Filter, filterFn func(obj *unstructu if err != nil { return nil, fmt.Errorf("jqFilter: %v", err) } - filteredStr := string(bytes) - res.FilterResult = filteredStr - res.Metadata.Checksum = utils_checksum.CalculateChecksum(filteredStr) + res.FilterResult = filtered + res.Metadata.Checksum = utils_checksum.CalculateChecksum(string(bytes)) } return res, nil From 89697f78b69dc104cea7b3746787dbd1817dfb9f Mon Sep 17 00:00:00 2001 From: Evsyukov Denis Date: Sat, 29 Mar 2025 17:10:29 +0300 Subject: [PATCH 13/16] refactor: simplify result map initialization in apply.go Signed-off-by: Evsyukov Denis --- pkg/filter/jq/apply.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/filter/jq/apply.go b/pkg/filter/jq/apply.go index 18c622cf..f9ace82b 100644 --- a/pkg/filter/jq/apply.go +++ b/pkg/filter/jq/apply.go @@ -4,8 +4,9 @@ import ( "errors" "maps" - "github.com/flant/shell-operator/pkg/filter" "github.com/itchyny/gojq" + + "github.com/flant/shell-operator/pkg/filter" ) var _ filter.Filter = (*Filter)(nil) @@ -24,7 +25,7 @@ func (f *Filter) ApplyFilter(jqFilter string, data map[string]any) (map[string]a } iter := query.Run(data) - var result = make(map[string]any) + result := make(map[string]any) for { v, ok := iter.Next() if !ok { From 319cb26ada8d793a241c8ee3bdb1aa1992cee55f Mon Sep 17 00:00:00 2001 From: Evsyukov Denis Date: Wed, 2 Apr 2025 16:49:39 +0300 Subject: [PATCH 14/16] refactor: create a copy of input data to prevent mutations in apply.go Signed-off-by: Evsyukov Denis --- pkg/filter/jq/apply.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/filter/jq/apply.go b/pkg/filter/jq/apply.go index f9ace82b..4c5c66db 100644 --- a/pkg/filter/jq/apply.go +++ b/pkg/filter/jq/apply.go @@ -24,7 +24,10 @@ func (f *Filter) ApplyFilter(jqFilter string, data map[string]any) (map[string]a return nil, err } - iter := query.Run(data) + // gojs will normalize numbers in the input data, we should create new map for prevent changes in input data + workData := make(map[string]any) + maps.Copy(workData, data) + iter := query.Run(workData) result := make(map[string]any) for { v, ok := iter.Next() From 14c932c63da424428762caa9f0d26ee42b6aeaca Mon Sep 17 00:00:00 2001 From: Evsyukov Denis Date: Wed, 2 Apr 2025 19:14:15 +0300 Subject: [PATCH 15/16] refactor: implement deepCopy function to prevent input data mutations in apply.go Signed-off-by: Evsyukov Denis --- pkg/filter/jq/apply.go | 11 +++++++++-- pkg/filter/jq/apply_test.go | 23 +++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/pkg/filter/jq/apply.go b/pkg/filter/jq/apply.go index 4c5c66db..45b87f1d 100644 --- a/pkg/filter/jq/apply.go +++ b/pkg/filter/jq/apply.go @@ -1,6 +1,7 @@ package jq import ( + "encoding/json" "errors" "maps" @@ -25,8 +26,7 @@ func (f *Filter) ApplyFilter(jqFilter string, data map[string]any) (map[string]a } // gojs will normalize numbers in the input data, we should create new map for prevent changes in input data - workData := make(map[string]any) - maps.Copy(workData, data) + workData := deepCopy(data) iter := query.Run(workData) result := make(map[string]any) for { @@ -52,3 +52,10 @@ func (f *Filter) ApplyFilter(jqFilter string, data map[string]any) (map[string]a func (f *Filter) FilterInfo() string { return "jqFilter implementation: using itchyny/gojq" } + +func deepCopy(input map[string]any) map[string]any { + data, _ := json.Marshal(input) + var output map[string]any + _ = json.Unmarshal(data, &output) + return output +} diff --git a/pkg/filter/jq/apply_test.go b/pkg/filter/jq/apply_test.go index 3e06f2f0..3ca6aca0 100644 --- a/pkg/filter/jq/apply_test.go +++ b/pkg/filter/jq/apply_test.go @@ -72,3 +72,26 @@ func Test_ApplyFilter_InvalidJson(t *testing.T) { g.Expect(err).Should(BeNil()) g.Expect(result).ShouldNot(BeNil()) } + +func Test_deepCopy(t *testing.T) { + g := NewWithT(t) + + original := map[string]any{ + "name": "John", + "age": 30.0, + "address": map[string]any{ + "city": "New York", + "state": "NY", + }, + } + + cp := deepCopy(original) + + g.Expect(cp).Should(Equal(original)) + + cp["name"] = "Jane" + cp["address"].(map[string]any)["city"] = "Los Angeles" + + g.Expect(original["name"]).Should(Equal("John")) + g.Expect(original["address"].(map[string]any)["city"]).Should(Equal("New York")) +} From eb601662be81af2c8f2af873288092ead937b9cb Mon Sep 17 00:00:00 2001 From: Evsyukov Denis Date: Thu, 10 Apr 2025 14:20:45 +0300 Subject: [PATCH 16/16] refactor: update expected result type in ApplyFilter test for consistency Signed-off-by: Evsyukov Denis --- pkg/filter/jq/apply_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/filter/jq/apply_test.go b/pkg/filter/jq/apply_test.go index 3ca6aca0..dbcfbecc 100644 --- a/pkg/filter/jq/apply_test.go +++ b/pkg/filter/jq/apply_test.go @@ -15,7 +15,7 @@ func Test_ApplyFilter_SingleDocumentModification(t *testing.T) { result, err := filter.ApplyFilter(jqFilter, map[string]any{"name": "John", "age": 30}) g.Expect(err).Should(BeNil()) - g.Expect(result).Should(Equal(map[string]any{"name": "John", "age": 30, "status": "active"})) + g.Expect(result).Should(Equal(map[string]any{"name": "John", "age": 30.0, "status": "active"})) } func Test_ApplyFilter_ExtractValuesFromDocument(t *testing.T) {