diff --git a/README.md b/README.md index 3e966c90..87ed9edd 100644 --- a/README.md +++ b/README.md @@ -175,3 +175,10 @@ This can be used by the `Manager` to validate all API access. ```go mgr, _ := jsm.New(nc, jsm.WithAPIValidation(new(SchemaValidator))) ``` + +## Build tag + +This library provides a `noexprlang` build tag that disables expression matching +for Streams and Consumers queries. The purpose of this build tag is to disable +the use of the `github.com/expr-lang/expr` module that disables go compiler's dead +code elimination because it uses some types and functions of the `reflect` package. diff --git a/consumer_query.go b/consumer_query.go index 03b3abb9..bb46ad20 100644 --- a/consumer_query.go +++ b/consumer_query.go @@ -19,9 +19,7 @@ import ( "strconv" "time" - "github.com/expr-lang/expr" "github.com/nats-io/jsm.go/api" - "gopkg.in/yaml.v3" ) type consumerMatcher func([]*Consumer) ([]*Consumer, error) @@ -59,14 +57,6 @@ func ConsumerQueryApiLevelMin(level int) ConsumerQueryOpt { } } -// ConsumerQueryExpression filters the consumers using the expr expression language -func ConsumerQueryExpression(e string) ConsumerQueryOpt { - return func(q *consumerQuery) error { - q.expression = e - return nil - } -} - // ConsumerQueryLeaderServer finds clustered consumers where a certain node is the leader func ConsumerQueryLeaderServer(server string) ConsumerQueryOpt { return func(q *consumerQuery) error { @@ -355,50 +345,3 @@ func (q *consumerQuery) matchApiLevel(consumers []*Consumer) ([]*Consumer, error return (!q.invert && requiredLevel >= q.apiLevel) || (q.invert && requiredLevel < q.apiLevel) }) } - -func (q *consumerQuery) matchExpression(consumers []*Consumer) ([]*Consumer, error) { - if q.expression == "" { - return consumers, nil - } - - var matched []*Consumer - - for _, consumer := range consumers { - cfg := map[string]any{} - state := map[string]any{} - - cfgBytes, _ := yaml.Marshal(consumer.Configuration()) - yaml.Unmarshal(cfgBytes, &cfg) - nfo, _ := consumer.LatestState() - stateBytes, _ := yaml.Marshal(nfo) - yaml.Unmarshal(stateBytes, &state) - - env := map[string]any{ - "config": cfg, - "state": state, - "info": state, - "Info": nfo, - } - - program, err := expr.Compile(q.expression, expr.Env(env), expr.AsBool()) - if err != nil { - return nil, err - } - - out, err := expr.Run(program, env) - if err != nil { - return nil, err - } - - should, ok := out.(bool) - if !ok { - return nil, fmt.Errorf("expression did not return a boolean") - } - - if should { - matched = append(matched, consumer) - } - } - - return matched, nil -} diff --git a/jsm.go b/jsm.go index 36a7f221..735e8dd4 100644 --- a/jsm.go +++ b/jsm.go @@ -31,6 +31,10 @@ import ( "github.com/nats-io/jsm.go/api" ) +// ErrNoExprLangBuild warns that expression matching is disabled when compiling +// a go binary with the `noexprlang` build tag. +var ErrNoExprLangBuild = fmt.Errorf("binary has been built with `noexprlang` build tag and thus does not support expression matching") + // standard api responses with error embedded type jetStreamResponseError interface { ToError() error diff --git a/match.go b/match.go new file mode 100644 index 00000000..1bbf1915 --- /dev/null +++ b/match.go @@ -0,0 +1,127 @@ +//go:build !noexprlang + +package jsm + +import ( + "fmt" + + "github.com/expr-lang/expr" + "gopkg.in/yaml.v3" +) + +// StreamQueryExpression filters the stream using the expr expression language +// Using this option with a binary built with the `noexprlang` build tag will +// always return [ErrNoExprLangBuild]. +func StreamQueryExpression(e string) StreamQueryOpt { + return func(q *streamQuery) error { + q.expression = e + return nil + } +} + +func (q *streamQuery) matchExpression(streams []*Stream) ([]*Stream, error) { + if q.expression == "" { + return streams, nil + } + + var matched []*Stream + + for _, stream := range streams { + cfg := map[string]any{} + state := map[string]any{} + info := map[string]any{} + + cfgBytes, _ := yaml.Marshal(stream.Configuration()) + yaml.Unmarshal(cfgBytes, &cfg) + nfo, _ := stream.LatestInformation() + nfoBytes, _ := yaml.Marshal(nfo) + yaml.Unmarshal(nfoBytes, &info) + stateBytes, _ := yaml.Marshal(nfo.State) + yaml.Unmarshal(stateBytes, &state) + + env := map[string]any{ + "config": cfg, + "state": state, + "info": info, + "Info": nfo, + } + + program, err := expr.Compile(q.expression, expr.Env(env), expr.AsBool()) + if err != nil { + return nil, err + } + + out, err := expr.Run(program, env) + if err != nil { + return nil, err + } + + should, ok := out.(bool) + if !ok { + return nil, fmt.Errorf("expression did not return a boolean") + } + + if should { + matched = append(matched, stream) + } + } + + return matched, nil +} + +// ConsumerQueryExpression filters the consumers using the expr expression language +// Using this option with a binary built with the `noexprlang` build tag will +// always return [ErrNoExprLangBuild]. +func ConsumerQueryExpression(e string) ConsumerQueryOpt { + return func(q *consumerQuery) error { + q.expression = e + return nil + } +} + +func (q *consumerQuery) matchExpression(consumers []*Consumer) ([]*Consumer, error) { + if q.expression == "" { + return consumers, nil + } + + var matched []*Consumer + + for _, consumer := range consumers { + cfg := map[string]any{} + state := map[string]any{} + + cfgBytes, _ := yaml.Marshal(consumer.Configuration()) + yaml.Unmarshal(cfgBytes, &cfg) + nfo, _ := consumer.LatestState() + stateBytes, _ := yaml.Marshal(nfo) + yaml.Unmarshal(stateBytes, &state) + + env := map[string]any{ + "config": cfg, + "state": state, + "info": state, + "Info": nfo, + } + + program, err := expr.Compile(q.expression, expr.Env(env), expr.AsBool()) + if err != nil { + return nil, err + } + + out, err := expr.Run(program, env) + if err != nil { + return nil, err + } + + should, ok := out.(bool) + if !ok { + return nil, fmt.Errorf("expression did not return a boolean") + } + + if should { + matched = append(matched, consumer) + } + } + + return matched, nil +} diff --git a/match_noexpr.go b/match_noexpr.go new file mode 100644 index 00000000..1bc3044e --- /dev/null +++ b/match_noexpr.go @@ -0,0 +1,37 @@ +//go:build noexprlang + +package jsm + +// StreamQueryExpression filters the stream using the expr expression language +// Using this option with a binary built with the `noexprlang` build tag will +// always return [ErrNoExprLangBuild]. +func StreamQueryExpression(e string) StreamQueryOpt { + return func(q *streamQuery) error { + q.expression = e + return ErrNoExprLangBuild + } +} + +func (q *streamQuery) matchExpression(streams []*Stream) ([]*Stream, error) { + if q.expression == "" { + return streams, nil + } + return nil, ErrNoExprLangBuild +} + +// ConsumerQueryExpression filters the consumers using the expr expression language +// Using this option with a binary built with the `noexprlang` build tag will +// always return [ErrNoExprLangBuild]. +func ConsumerQueryExpression(e string) ConsumerQueryOpt { + return func(q *consumerQuery) error { + q.expression = e + return ErrNoExprLangBuild + } +} + +func (q *consumerQuery) matchExpression(consumers []*Consumer) ([]*Consumer, error) { + if q.expression == "" { + return consumers, nil + } + return nil, ErrNoExprLangBuild +} diff --git a/stream_query.go b/stream_query.go index df2a7962..15ed5125 100644 --- a/stream_query.go +++ b/stream_query.go @@ -14,15 +14,12 @@ package jsm import ( - "fmt" "regexp" "strconv" "strings" "time" - "github.com/expr-lang/expr" "github.com/nats-io/jsm.go/api" - "gopkg.in/yaml.v3" ) type streamMatcher func([]*Stream) ([]*Stream, error) @@ -57,14 +54,6 @@ func StreamQueryApiLevelMin(level int) StreamQueryOpt { } } -// StreamQueryExpression filters the stream using the expr expression language -func StreamQueryExpression(e string) StreamQueryOpt { - return func(q *streamQuery) error { - q.expression = e - return nil - } -} - func StreamQueryIsSourced() StreamQueryOpt { return func(q *streamQuery) error { q.sourced = true @@ -231,56 +220,6 @@ func (q *streamQuery) Filter(streams []*Stream) ([]*Stream, error) { return matched, nil } -func (q *streamQuery) matchExpression(streams []*Stream) ([]*Stream, error) { - if q.expression == "" { - return streams, nil - } - - var matched []*Stream - - for _, stream := range streams { - cfg := map[string]any{} - state := map[string]any{} - info := map[string]any{} - - cfgBytes, _ := yaml.Marshal(stream.Configuration()) - yaml.Unmarshal(cfgBytes, &cfg) - nfo, _ := stream.LatestInformation() - nfoBytes, _ := yaml.Marshal(nfo) - yaml.Unmarshal(nfoBytes, &info) - stateBytes, _ := yaml.Marshal(nfo.State) - yaml.Unmarshal(stateBytes, &state) - - env := map[string]any{ - "config": cfg, - "state": state, - "info": info, - "Info": nfo, - } - - program, err := expr.Compile(q.expression, expr.Env(env), expr.AsBool()) - if err != nil { - return nil, err - } - - out, err := expr.Run(program, env) - if err != nil { - return nil, err - } - - should, ok := out.(bool) - if !ok { - return nil, fmt.Errorf("expression did not return a boolean") - } - - if should { - matched = append(matched, stream) - } - } - - return matched, nil -} - func (q *streamQuery) matchLeaderServer(streams []*Stream) ([]*Stream, error) { if q.leader == "" { return streams, nil