From 19243f7e89544ef73246f190f06de61b74851a42 Mon Sep 17 00:00:00 2001 From: Snow Pettersen Date: Tue, 9 Nov 2021 18:28:06 +0000 Subject: [PATCH 1/5] xdsmatcher: add evaluation framework for xDS matching API Signed-off-by: Snow Pettersen --- build/do_ci.sh | 6 +- xdsmatcher/.gitignore | 1 + xdsmatcher/Makefile | 11 + xdsmatcher/README.md | 9 + xdsmatcher/go.mod | 15 + xdsmatcher/go.sum | 128 +++++ xdsmatcher/internal/proto/proto.go | 46 ++ xdsmatcher/pkg/matcher/matcher.go | 507 +++++++++++++++++++ xdsmatcher/pkg/matcher/matcher_test.go | 277 ++++++++++ xdsmatcher/pkg/matcher/registry/registry.go | 43 ++ xdsmatcher/pkg/matcher/types/types.go | 33 ++ xdsmatcher/test/extensions.go | 126 +++++ xdsmatcher/test/proto/test.pb.go | 529 ++++++++++++++++++++ xdsmatcher/test/proto/test.proto | 27 + 14 files changed, 1757 insertions(+), 1 deletion(-) create mode 100644 xdsmatcher/.gitignore create mode 100644 xdsmatcher/Makefile create mode 100644 xdsmatcher/README.md create mode 100644 xdsmatcher/go.mod create mode 100644 xdsmatcher/go.sum create mode 100644 xdsmatcher/internal/proto/proto.go create mode 100644 xdsmatcher/pkg/matcher/matcher.go create mode 100644 xdsmatcher/pkg/matcher/matcher_test.go create mode 100644 xdsmatcher/pkg/matcher/registry/registry.go create mode 100644 xdsmatcher/pkg/matcher/types/types.go create mode 100644 xdsmatcher/test/extensions.go create mode 100644 xdsmatcher/test/proto/test.pb.go create mode 100644 xdsmatcher/test/proto/test.proto diff --git a/build/do_ci.sh b/build/do_ci.sh index 944d7d6d23..32b138ac61 100755 --- a/build/do_ci.sh +++ b/build/do_ci.sh @@ -17,4 +17,8 @@ make build make bin/example make examples make test -make integration \ No newline at end of file +make integration + +cd ./xdsmatcher +make test +# TODO(snowp): Output coverage in CI diff --git a/xdsmatcher/.gitignore b/xdsmatcher/.gitignore new file mode 100644 index 0000000000..2d830686d4 --- /dev/null +++ b/xdsmatcher/.gitignore @@ -0,0 +1 @@ +coverage.out diff --git a/xdsmatcher/Makefile b/xdsmatcher/Makefile new file mode 100644 index 0000000000..b437e49b35 --- /dev/null +++ b/xdsmatcher/Makefile @@ -0,0 +1,11 @@ +.PHONY: test +test: + go test ./... -race +coverage: + go test ./... -race -covermode=atomic -coverprofile=coverage.out +coverage_html: coverage + go tool cover -html=coverage.out +proto: test/proto/* + protoc --proto_path=test/proto --go_out=test/proto --go_opt=paths=source_relative test.proto + + diff --git a/xdsmatcher/README.md b/xdsmatcher/README.md new file mode 100644 index 0000000000..f26ce263e0 --- /dev/null +++ b/xdsmatcher/README.md @@ -0,0 +1,9 @@ + +# Development +To install the required dev tools on Linux: + +```` +apt-get install -y protobuf-compiler + +go install google.golang.org/protobuf/cmd/protoc-gen-go +``` diff --git a/xdsmatcher/go.mod b/xdsmatcher/go.mod new file mode 100644 index 0000000000..7f21cf0d50 --- /dev/null +++ b/xdsmatcher/go.mod @@ -0,0 +1,15 @@ +module github.com/envoyproxy/go-control-plane/xdsmatcher + +go 1.15 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/envoyproxy/go-control-plane v0.9.9 + github.com/golang/protobuf v1.5.2 // indirect + github.com/kr/pretty v0.1.0 // indirect + github.com/stretchr/testify v1.7.0 + google.golang.org/protobuf v1.26.0 + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect + gopkg.in/yaml.v2 v2.4.0 + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect +) diff --git a/xdsmatcher/go.sum b/xdsmatcher/go.sum new file mode 100644 index 0000000000..408b094a3b --- /dev/null +++ b/xdsmatcher/go.sum @@ -0,0 +1,128 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed h1:OZmjad4L3H8ncOIR8rnb5MREYqG8ixi5+WbeUsquF0c= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9 h1:vQLjymTobffN2R0F8eTqw6q7iozfRO5Z0m+/4Vw+/uA= +github.com/envoyproxy/go-control-plane v0.9.9/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/xdsmatcher/internal/proto/proto.go b/xdsmatcher/internal/proto/proto.go new file mode 100644 index 0000000000..97926482ee --- /dev/null +++ b/xdsmatcher/internal/proto/proto.go @@ -0,0 +1,46 @@ +package internal + +import ( + "encoding/json" + + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + "gopkg.in/yaml.v2" +) + +func ProtoFromYaml(s []byte, m proto.Message) error { + var obj interface{} + if err := yaml.Unmarshal(s, &obj); err != nil { + return err + } + + obj = convert(obj) + // Encode YAML to JSON. + rawJSON, err := json.Marshal(obj) + if err != nil { + return err + } + + // Use protojson to convert the JSON into the desired proto. + err = protojson.Unmarshal(rawJSON, m) + if err != nil { + return err + } + return nil +} + +func convert(i interface{}) interface{} { + switch x := i.(type) { + case map[interface{}]interface{}: + m2 := map[string]interface{}{} + for k, v := range x { + m2[k.(string)] = convert(v) + } + return m2 + case []interface{}: + for i, v := range x { + x[i] = convert(v) + } + } + return i +} diff --git a/xdsmatcher/pkg/matcher/matcher.go b/xdsmatcher/pkg/matcher/matcher.go new file mode 100644 index 0000000000..96e999625d --- /dev/null +++ b/xdsmatcher/pkg/matcher/matcher.go @@ -0,0 +1,507 @@ +package matcher + +import ( + "errors" + "fmt" + "regexp" + "strings" + + pbmatcher "github.com/envoyproxy/go-control-plane/envoy/config/common/matcher/v3" + pbtypematcher "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3" + "github.com/envoyproxy/go-control-plane/xdsmatcher/pkg/matcher/registry" + "github.com/envoyproxy/go-control-plane/xdsmatcher/pkg/matcher/types" +) + +type MatcherTree struct { + onNoMatch *types.OnMatch + matchRoot types.Matcher + + logger *tracingLogger +} + +func (m MatcherTree) Match(data types.MatchingData) (types.Result, error) { + result, err := m.matchRoot.Match(data) + if err != nil { + return types.Result{}, err + } + if result.MatchResult != nil && result.NeedMoreData { + return types.Result{}, errors.New("invalid result, invariant violation") + } + + // We arrived at a result. + if result.MatchResult != nil { + // The result is a recursive matcher, so recurse. + if result.MatchResult.Matcher != nil { + m.logger.log("recursively matching") + m.logger.push() + defer m.logger.pop() + return result.MatchResult.Matcher.Match(data) + } + + m.logger.log("arrived at result %+v, %+v", result, result.MatchResult) + + // Otherwise pass through the result. + return result, nil + } + + // We failed to arrive at a result due to the data not being available, + // report this back up. + if result.NeedMoreData { + m.logger.log("matcher tree requires more data") + return result, nil + } + + m.logger.log("matcher tree failed to match, falling back to OnNoMatch (%v)", m.onNoMatch) + // We have all the data, so we know that we failed to match. Return OnNoMatch. + return types.Result{ + MatchResult: m.onNoMatch, + NeedMoreData: false, + }, nil +} + +func Create(matcher *pbmatcher.Matcher) (*MatcherTree, error) { + return create(matcher, &tracingLogger{}) +} + +func create(matcher *pbmatcher.Matcher, logger *tracingLogger) (*MatcherTree, error) { + onNoMatch, err := createOnMatch(matcher.OnNoMatch, logger) + if err != nil { + return nil, err + } + + switch m := matcher.MatcherType.(type) { + case *pbmatcher.Matcher_MatcherList_: + matcher, err := createMatcherList(m.MatcherList, logger) + if err != nil { + return nil, err + } + return &MatcherTree{ + onNoMatch: onNoMatch, + matchRoot: matcher, + logger: logger, + }, nil + case *pbmatcher.Matcher_MatcherTree_: + matcher, err := createMatchTree(m.MatcherTree, logger) + if err != nil { + return nil, err + } + + return &MatcherTree{ + onNoMatch: onNoMatch, + matchRoot: matcher, + logger: logger, + }, nil + default: + return nil, errors.New("not implemented: matcher tree") + + } +} + +func createOnMatch(onMatch *pbmatcher.Matcher_OnMatch, logger *tracingLogger) (*types.OnMatch, error) { + // TODO move this to call sites, this is part of validation + if onMatch == nil { + return nil, nil + } + switch om := onMatch.OnMatch.(type) { + case *pbmatcher.Matcher_OnMatch_Matcher: + matcher, err := create(om.Matcher, logger) + if err != nil { + return nil, err + } + + return &types.OnMatch{ + Matcher: matcher, + }, nil + case *pbmatcher.Matcher_OnMatch_Action: + action, err := registry.ResolveActionExtension(om.Action) + if err != nil { + return nil, err + } + return &types.OnMatch{Action: action}, nil + default: + return nil, errors.New("unexpected on match type") + } +} + +type matcherResult struct { + match bool + needMoreData bool +} + +type predicateFunc interface { + match(types.MatchingData) (matcherResult, error) +} + +type InputMatcher interface { + match(value *string) (matcherResult, error) +} + +type simpleMatcher interface { + match(value *string) bool +} + +type singlePredicate struct { + input types.DataInput + matcher simpleMatcher + logger *tracingLogger +} + +func (s singlePredicate) match(data types.MatchingData) (matcherResult, error) { + matchFunc := func(input *string) *types.OnMatch { + s.logger.log("attempting to match '%v'", safePrint(input)) + if s.matcher.match(input) { + return &types.OnMatch{} + } + + return nil + } + + result, err := handlePossiblyMissingInput(s.input, data, matchFunc, s.logger) + if err != nil { + return matcherResult{}, err + } + + matchResult := matcherResult{ + match: result.MatchResult != nil, + needMoreData: result.NeedMoreData, + } + + s.logger.log("single predicate evaluation result: %+v", matchResult) + + return matchResult, nil +} + +type conjunctionPredicate struct { + predicates []predicateFunc + logger *tracingLogger +} + +func (a conjunctionPredicate) match(data types.MatchingData) (matcherResult, error) { + a.logger.log("performing and matching") + a.logger.push() + defer a.logger.pop() + for _, p := range a.predicates { + result, err := p.match(data) + if err != nil { + return matcherResult{}, err + } + + if result.needMoreData { + return result, nil + } + + if !result.match { + return result, nil + } + } + + return matcherResult{ + match: true, + needMoreData: false, + }, nil +} + +type disjunctionPredicate struct { + predicates []predicateFunc + logger *tracingLogger +} + +func (a disjunctionPredicate) match(data types.MatchingData) (matcherResult, error) { + a.logger.log("performing or matching") + a.logger.push() + defer a.logger.pop() + + resultCouldChange := false + for _, p := range a.predicates { + result, err := p.match(data) + if err != nil { + return matcherResult{}, err + } + + if result.needMoreData { + resultCouldChange = true + continue + } + + if result.match { + return result, nil + } + } + + if resultCouldChange { + return matcherResult{ + match: false, + needMoreData: true, + }, nil + } + + return matcherResult{ + match: false, + needMoreData: false, + }, nil +} + +type matchEntry struct { + onMatch *types.OnMatch + predicate predicateFunc +} + +type regexMatcher struct { + regex *regexp.Regexp +} + +func (r regexMatcher) match(s *string) bool { + if s == nil { + return false + } + + return r.regex.Match([]byte(*s)) +} + +func createSinglePredicate(predicate *pbmatcher.Matcher_MatcherList_Predicate_SinglePredicate, logger *tracingLogger) (*singlePredicate, error) { + input, err := registry.ResolveInputExtension(predicate.Input) + if err != nil { + return nil, err + } + + switch m := predicate.Matcher.(type) { + case *pbmatcher.Matcher_MatcherList_Predicate_SinglePredicate_ValueMatch: + matcher, err := createValueMatcher(m.ValueMatch) + if err != nil { + return nil, err + } + + return &singlePredicate{ + logger: logger, + input: input, + matcher: matcher, + }, nil + default: + return nil, errors.New("not implemented: custom match") + } +} + +func createValueMatcher(valueMatcher *pbtypematcher.StringMatcher) (simpleMatcher, error) { + switch m := valueMatcher.MatchPattern.(type) { + case *pbtypematcher.StringMatcher_SafeRegex: + r, err := regexp.Compile(m.SafeRegex.Regex) + if err != nil { + return nil, err + } + return regexMatcher{regex: r}, nil + default: + return nil, errors.New("not implemented: value match") + } +} + +type matcherList struct { + matchEntries []matchEntry + logger *tracingLogger +} + +func (m matcherList) Match(data types.MatchingData) (types.Result, error) { + m.logger.log("attempting to match against match list") + m.logger.push() + defer m.logger.pop() + + for _, me := range m.matchEntries { + m.logger.log("predicate type %T", me.predicate) + result, err := me.predicate.match(data) + if err != nil { + return types.Result{}, err + } + + if result.match { + m.logger.log("found match entry matching input: %+v") + return types.Result{ + MatchResult: me.onMatch, + NeedMoreData: false, + }, nil + } + if result.needMoreData { + m.logger.log("found match entry that needs more data, bailing") + return types.Result{ + MatchResult: nil, + NeedMoreData: true, + }, nil + } + } + + m.logger.log("no matching match entry was found") + + return types.Result{ + MatchResult: nil, + NeedMoreData: false, + }, nil +} + +func createPredicate(predicate *pbmatcher.Matcher_MatcherList_Predicate, logger *tracingLogger) (predicateFunc, error) { + switch predicate := predicate.MatchType.(type) { + case *pbmatcher.Matcher_MatcherList_Predicate_AndMatcher: + predicates := []predicateFunc{} + for _, p := range predicate.AndMatcher.Predicate { + pp, err := createPredicate(p, logger) + if err != nil { + return nil, err + } + predicates = append(predicates, pp) + } + + return conjunctionPredicate{ + predicates: predicates, + logger: logger, + }, nil + case *pbmatcher.Matcher_MatcherList_Predicate_OrMatcher: + predicates := []predicateFunc{} + for _, p := range predicate.OrMatcher.Predicate { + pp, err := createPredicate(p, logger) + if err != nil { + return nil, err + } + predicates = append(predicates, pp) + } + + return disjunctionPredicate{ + predicates: predicates, + logger: logger, + }, nil + case *pbmatcher.Matcher_MatcherList_Predicate_SinglePredicate_: + return createSinglePredicate(predicate.SinglePredicate, logger) + default: + return nil, fmt.Errorf("unexpected predicate type %T", predicate) + } +} + +func createMatcherList(list *pbmatcher.Matcher_MatcherList, logger *tracingLogger) (*matcherList, error) { + ml := &matcherList{logger: logger} + for _, m := range list.Matchers { + onMatch, err := createOnMatch(m.OnMatch, logger) + if err != nil { + return nil, err + } + if onMatch == nil { + return nil, errors.New("matcher list requires on match") + } + predicate, err := createPredicate(m.Predicate, logger) + if err != nil { + return nil, err + } + + entry := matchEntry{onMatch: onMatch, predicate: predicate} + + ml.matchEntries = append(ml.matchEntries, entry) + } + + return ml, nil +} + +type exactMatchTree struct { + tree map[string]*types.OnMatch + input types.DataInput + logger *tracingLogger +} + +func (m exactMatchTree) Match(data types.MatchingData) (types.Result, error) { + m.logger.log("attempting to match against exact tree") + m.logger.push() + defer m.logger.pop() + + matchFunc := func(data *string) *types.OnMatch { + if data == nil { + m.logger.log("attempted to exact match nil, no match") + return nil + } + if r, ok := m.tree[*data]; ok { + m.logger.log("input '%s' found in exact match map", *data) + return r + } + + m.logger.log("input '%s' not found in exact match map", *data) + return nil + } + + return handlePossiblyMissingInput(m.input, data, matchFunc, m.logger) +} + +func safePrint(s *string) string { + if s == nil { + return "" + } + + return *s +} + +func handlePossiblyMissingInput(dataInput types.DataInput, data types.MatchingData, matchFunc func(*string) *types.OnMatch, logger *tracingLogger) (types.Result, error) { + input, err := dataInput.Input(data) + if err != nil { + return types.Result{}, err + } + + logger.log("handling input '%v'", safePrint(input.Data)) + + switch input.Availability { + case types.AllDataAvailable: + logger.log("data is available, performing match") + return types.Result{ + MatchResult: matchFunc(input.Data), + NeedMoreData: false, + }, nil + case types.MoreDataMightBeAvailable: + logger.log("more data might be available, performing eager match") + result := matchFunc(input.Data) + return types.Result{ + MatchResult: result, + NeedMoreData: result == nil, + }, nil + case types.NotAvailable: + logger.log("data not available, bailing") + return types.Result{MatchResult: nil, NeedMoreData: true}, nil + default: + return types.Result{}, errors.New("unexpected return value") + } + +} + +func createMatchTree(matcherTree *pbmatcher.Matcher_MatcherTree, logger *tracingLogger) (*exactMatchTree, error) { + i, err := registry.ResolveInputExtension(matcherTree.Input) + if err != nil { + return nil, err + } + + switch m := matcherTree.TreeType.(type) { + case *pbmatcher.Matcher_MatcherTree_ExactMatchMap: + tree := &exactMatchTree{ + logger: logger, + tree: map[string]*types.OnMatch{}, + input: i, + } + for k, v := range m.ExactMatchMap.Map { + onMatch, err := createOnMatch(v, logger) + if err != nil { + return nil, err + } + tree.tree[k] = onMatch + } + return tree, nil + default: + return nil, errors.New("not implemented: tree type") + } +} + +// Internal logger that helps debugging by providing indention during matching to provide some visual guidance to the current nesting level. +// TODO(snowp): Make this optional, not ideal if this ends up being used outside of testing scenarios. +type tracingLogger struct { + indent int +} + +func (t *tracingLogger) push() { + t.indent++ +} + +func (t *tracingLogger) pop() { + t.indent-- +} + +func (t *tracingLogger) log(formatString string, args ...interface{}) { + fmt.Printf(fmt.Sprintf("%s%s\n", strings.Repeat(" ", t.indent), formatString), args...) +} diff --git a/xdsmatcher/pkg/matcher/matcher_test.go b/xdsmatcher/pkg/matcher/matcher_test.go new file mode 100644 index 0000000000..b852da0731 --- /dev/null +++ b/xdsmatcher/pkg/matcher/matcher_test.go @@ -0,0 +1,277 @@ +package matcher + +import ( + "fmt" + "log" + "testing" + + "github.com/stretchr/testify/assert" + + pbmatcher "github.com/envoyproxy/go-control-plane/envoy/config/common/matcher/v3" + iproto "github.com/envoyproxy/go-control-plane/xdsmatcher/internal/proto" + _ "github.com/envoyproxy/go-control-plane/xdsmatcher/test" + pbtest "github.com/envoyproxy/go-control-plane/xdsmatcher/test/proto" +) + +func TestSimple(t *testing.T) { + configuration := func(inputType string) string { + return fmt.Sprintf(` +matcher_tree: + input: + name: foo + typed_config: + "@type": %s + exact_match_map: + map: + "foo": + action: + name: action + typed_config: + "@type": type.googleapis.com/xdsmatcher.test.proto.MatchAction +`, inputType) + } + + testHelper(t, configuration) +} + +func TestConjunction(t *testing.T) { + conjunctionMatcherConfig := func(inputType string) string { + return fmt.Sprintf(` +matcher_list: + matchers: + - predicate: + and_matcher: + predicate: + - single_predicate: + input: + name: bar + typed_config: + "@type": type.googleapis.com/xdsmatcher.test.proto.BarInput + index: 1 + value_match: + safe_regex: + regex: two + google_re2: {} + - single_predicate: + input: + name: foo + typed_config: + "@type": %s + value_match: + safe_regex: + regex: foo + google_re2: {} + on_match: + action: + name: action + typed_config: + "@type": type.googleapis.com/xdsmatcher.test.proto.MatchAction + `, inputType) + } + + testHelper(t, conjunctionMatcherConfig) +} + +func TestDisjunctionMatch(t *testing.T) { + disjunnctionMatcherConfig := func(inputType string) string { + return fmt.Sprintf(` +matcher_list: + matchers: + - predicate: + or_matcher: + predicate: + - single_predicate: + input: + name: bar + typed_config: + "@type": type.googleapis.com/xdsmatcher.test.proto.BarInput + index: 1 + value_match: + safe_regex: + regex: wrong # something that won't match + google_re2: {} + - single_predicate: + input: + name: bar + typed_config: + "@type": %s + value_match: + safe_regex: + regex: ^foo$ + google_re2: {} + on_match: + action: + name: action + typed_config: + "@type": type.googleapis.com/xdsmatcher.test.proto.MatchAction + `, inputType) + } + + testHelper(t, disjunnctionMatcherConfig) +} + +func testHelper(t *testing.T, configurationGenerator func(typeName string) string) { + t.Helper() + tcs := []struct { + name string + inputType string + input interface{} + hasMatch bool + needsMoreData bool + matchingError bool + }{ + { + name: "successful matching FooInput against foo", + inputType: "type.googleapis.com/xdsmatcher.test.proto.FooInput", + input: testData1, + hasMatch: true, + }, + { + name: "failure to match FooInput against 'not foo'", + inputType: "type.googleapis.com/xdsmatcher.test.proto.FooInput", + input: testData2, + }, + { + name: "failure to match against nil, more data available", + inputType: "type.googleapis.com/xdsmatcher.test.proto.MoreDataAvailableInput", + input: testData1, + needsMoreData: true, + }, + { + name: "input returning error", + inputType: "type.googleapis.com/xdsmatcher.test.proto.ErrorInput", + input: testData1, + needsMoreData: true, + matchingError: true, + }, + { + name: "not available input", + inputType: "type.googleapis.com/xdsmatcher.test.proto.NoDataAvailableInput", + input: testData1, + needsMoreData: true, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + configuration := configurationGenerator(tc.inputType) + m, err := createMatcher(t, configuration) + assert.NoError(t, err) + + r, err := m.Match(tc.input) + + if tc.matchingError { + assert.Error(t, err) + return + } + + assert.NoError(t, err) + + if tc.hasMatch { + assert.NotNil(t, r.MatchResult) + assert.NotNil(t, r.MatchResult.Action) + } else { + assert.Nil(t, r.MatchResult) + } + + assert.Equal(t, tc.needsMoreData, r.NeedMoreData) + }) + } +} + +func TestNested(t *testing.T) { + configuration := ` +matcher_tree: + input: + name: foo + typed_config: + "@type": type.googleapis.com/xdsmatcher.test.proto.FooInput + exact_match_map: + map: + "foo": + matcher: + matcher_list: + matchers: + - predicate: + single_predicate: + input: + name: bar + typed_config: + "@type": type.googleapis.com/xdsmatcher.test.proto.BarInput + index: 1 + value_match: + safe_regex: + regex: t.o + google_re2: {} + on_match: + action: + name: action + typed_config: + "@type": type.googleapis.com/xdsmatcher.test.proto.MatchAction +` + + m, err := createMatcher(t, configuration) + assert.NoError(t, err) + r, err := m.Match(testData1) + assert.NoError(t, err) + + log.Printf("%+v", m) + assert.NotNil(t, r.MatchResult) + assert.NotNil(t, r.MatchResult.Action) +} + +func TestCreateInvalidExtensions(t *testing.T) { + config := ` +matcher_tree: + input: + name: foo + typed_config: + "@type": type.googleapis.com/xdsmatcher.test.proto.NotRegistered + exact_match_map: + map: + "foo": + action: + name: action + typed_config: + "@type": type.googleapis.com/xdsmatcher.test.proto.NotRegistered +` + + m, err := createMatcher(t, config) + assert.Error(t, err) + assert.Nil(t, m) + + config = ` +matcher_tree: + input: + name: foo + typed_config: + "@type": type.googleapis.com/xdsmatcher.test.proto.FooInput + exact_match_map: + map: + "foo": + action: + name: action + typed_config: + "@type": type.googleapis.com/xdsmatcher.test.proto.NotRegistered +` + + m, err = createMatcher(t, config) + assert.Error(t, err) + assert.Nil(t, m) +} + +var testData1 = &pbtest.TestData{ + Foo: "foo", + Bar: []string{"one", "two"}, +} + +var testData2 = &pbtest.TestData{ + Foo: "not-foo", +} + +func createMatcher(t *testing.T, yaml string) (*MatcherTree, error) { + matcher := &pbmatcher.Matcher{} + assert.NoError(t, iproto.ProtoFromYaml([]byte(yaml), matcher)) + + return Create(matcher) +} diff --git a/xdsmatcher/pkg/matcher/registry/registry.go b/xdsmatcher/pkg/matcher/registry/registry.go new file mode 100644 index 0000000000..1ea2a7925d --- /dev/null +++ b/xdsmatcher/pkg/matcher/registry/registry.go @@ -0,0 +1,43 @@ +package registry + +import ( + "errors" + + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/anypb" + + pbcore "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + "github.com/envoyproxy/go-control-plane/xdsmatcher/pkg/matcher/types" +) + +type RegistryType map[string]func(proto.Message) (interface{}, error) + +var InputExtensions = make(RegistryType) +var ActionExtensions = make(RegistryType) + +func ResolveInputExtension(tec *pbcore.TypedExtensionConfig) (types.DataInput, error) { + out, err := resolveExtension(tec, InputExtensions) + if err != nil { + return nil, err + } + + return out.(types.DataInput), nil +} + +func ResolveActionExtension(tec *pbcore.TypedExtensionConfig) (types.Action, error) { + return resolveExtension(tec, ActionExtensions) +} + +func resolveExtension(tec *pbcore.TypedExtensionConfig, registry RegistryType) (interface{}, error) { + factory, ok := registry[tec.TypedConfig.TypeUrl] + if !ok { + return nil, errors.New("extension not implemented: extension " + tec.TypedConfig.TypeUrl) + } + + m, err := anypb.UnmarshalNew(tec.TypedConfig, proto.UnmarshalOptions{}) + if err != nil { + return nil, err + } + + return factory(m) +} diff --git a/xdsmatcher/pkg/matcher/types/types.go b/xdsmatcher/pkg/matcher/types/types.go new file mode 100644 index 0000000000..20685d0347 --- /dev/null +++ b/xdsmatcher/pkg/matcher/types/types.go @@ -0,0 +1,33 @@ +package types + +type DataInputAvailability int + +type Matcher interface { + Match(MatchingData) (Result, error) +} + +type OnMatch struct { + Matcher Matcher + Action Action +} +type Action interface{} +type MatchingData interface{} +type Result struct { + MatchResult *OnMatch + NeedMoreData bool +} + +const ( + NotAvailable DataInputAvailability = iota + MoreDataMightBeAvailable DataInputAvailability = iota + AllDataAvailable DataInputAvailability = iota +) + +type DataInputResult struct { + Availability DataInputAvailability + Data *string +} + +type DataInput interface { + Input(MatchingData) (DataInputResult, error) +} diff --git a/xdsmatcher/test/extensions.go b/xdsmatcher/test/extensions.go new file mode 100644 index 0000000000..a3ca4a8f36 --- /dev/null +++ b/xdsmatcher/test/extensions.go @@ -0,0 +1,126 @@ +package test + +import ( + "errors" + + "google.golang.org/protobuf/proto" + + "github.com/envoyproxy/go-control-plane/xdsmatcher/pkg/matcher/registry" + "github.com/envoyproxy/go-control-plane/xdsmatcher/pkg/matcher/types" + pbtest "github.com/envoyproxy/go-control-plane/xdsmatcher/test/proto" +) + +// This file contains a number of input and action extensions registered for the purpose of testing the framework. + +func init() { + registry.InputExtensions["type.googleapis.com/xdsmatcher.test.proto.FooInput"] = fooInputFactory + registry.InputExtensions["type.googleapis.com/xdsmatcher.test.proto.BarInput"] = barInputFactory + registry.InputExtensions["type.googleapis.com/xdsmatcher.test.proto.MoreDataAvailableInput"] = moreDataAvailableInputFactory + registry.InputExtensions["type.googleapis.com/xdsmatcher.test.proto.ErrorInput"] = errorInputFactory + registry.InputExtensions["type.googleapis.com/xdsmatcher.test.proto.NoDataAvailableInput"] = noDataInputFactory + + registry.ActionExtensions["type.googleapis.com/xdsmatcher.test.proto.MatchAction"] = matchActionFactory +} + +type fooInput struct { +} + +func fooInputFactory(m proto.Message) (interface{}, error) { + _, ok := m.(*pbtest.FooInput) + if !ok { + return nil, errors.New("unexpected proto") + } + + return fooInput{}, nil +} + +func (fooInput) Input(d types.MatchingData) (types.DataInputResult, error) { + testData, ok := d.(*pbtest.TestData) + if !ok { + return types.DataInputResult{}, errors.New("invlaid matching data type") + } + + return types.DataInputResult{ + Availability: types.AllDataAvailable, + Data: &testData.Foo, + }, nil +} + +type barInput struct { + index uint64 +} + +func barInputFactory(m proto.Message) (interface{}, error) { + p, ok := m.(*pbtest.BarInput) + if !ok { + return nil, errors.New("unexpected proto") + } + + return barInput{uint64(p.Index)}, nil +} + +func (t barInput) Input(d types.MatchingData) (types.DataInputResult, error) { + metricData, ok := d.(*pbtest.TestData) + if !ok { + return types.DataInputResult{}, errors.New("invalid matching data type") + } + + if t.index >= uint64(len(metricData.Bar)) { + return types.DataInputResult{ + Availability: types.AllDataAvailable, + Data: nil, + }, nil + + } + value := metricData.Bar[t.index] + return types.DataInputResult{ + Availability: types.AllDataAvailable, + Data: &value, + }, nil +} + +type moreDataAvailableInput struct{} + +func (moreDataAvailableInput) Input(d types.MatchingData) (types.DataInputResult, error) { + return types.DataInputResult{ + Availability: types.MoreDataMightBeAvailable, + Data: nil, + }, nil +} + +func moreDataAvailableInputFactory(m proto.Message) (interface{}, error) { + return moreDataAvailableInput{}, nil +} + +type errorInput struct{} + +func (errorInput) Input(d types.MatchingData) (types.DataInputResult, error) { + return types.DataInputResult{}, errors.New("test error") +} + +func errorInputFactory(m proto.Message) (interface{}, error) { + return errorInput{}, nil +} + +type noDataInput struct{} + +func (noDataInput) Input(d types.MatchingData) (types.DataInputResult, error) { + return types.DataInputResult{ + Availability: types.NotAvailable, + }, nil +} + +func noDataInputFactory(m proto.Message) (interface{}, error) { + return noDataInput{}, nil +} + +type matchAction struct{} + +func matchActionFactory(m proto.Message) (interface{}, error) { + _, ok := m.(*pbtest.MatchAction) + if !ok { + return nil, errors.New("unexpected proto") + } + + return matchAction{}, nil +} diff --git a/xdsmatcher/test/proto/test.pb.go b/xdsmatcher/test/proto/test.pb.go new file mode 100644 index 0000000000..d9bbeab86c --- /dev/null +++ b/xdsmatcher/test/proto/test.pb.go @@ -0,0 +1,529 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.26.0 +// protoc v3.17.3 +// source: test.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type TestData struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Foo string `protobuf:"bytes,1,opt,name=foo,proto3" json:"foo,omitempty"` + Bar []string `protobuf:"bytes,2,rep,name=bar,proto3" json:"bar,omitempty"` +} + +func (x *TestData) Reset() { + *x = TestData{} + if protoimpl.UnsafeEnabled { + mi := &file_test_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TestData) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TestData) ProtoMessage() {} + +func (x *TestData) ProtoReflect() protoreflect.Message { + mi := &file_test_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TestData.ProtoReflect.Descriptor instead. +func (*TestData) Descriptor() ([]byte, []int) { + return file_test_proto_rawDescGZIP(), []int{0} +} + +func (x *TestData) GetFoo() string { + if x != nil { + return x.Foo + } + return "" +} + +func (x *TestData) GetBar() []string { + if x != nil { + return x.Bar + } + return nil +} + +type FooInput struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *FooInput) Reset() { + *x = FooInput{} + if protoimpl.UnsafeEnabled { + mi := &file_test_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FooInput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FooInput) ProtoMessage() {} + +func (x *FooInput) ProtoReflect() protoreflect.Message { + mi := &file_test_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FooInput.ProtoReflect.Descriptor instead. +func (*FooInput) Descriptor() ([]byte, []int) { + return file_test_proto_rawDescGZIP(), []int{1} +} + +type BarInput struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Index uint32 `protobuf:"varint,1,opt,name=index,proto3" json:"index,omitempty"` +} + +func (x *BarInput) Reset() { + *x = BarInput{} + if protoimpl.UnsafeEnabled { + mi := &file_test_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *BarInput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BarInput) ProtoMessage() {} + +func (x *BarInput) ProtoReflect() protoreflect.Message { + mi := &file_test_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BarInput.ProtoReflect.Descriptor instead. +func (*BarInput) Descriptor() ([]byte, []int) { + return file_test_proto_rawDescGZIP(), []int{2} +} + +func (x *BarInput) GetIndex() uint32 { + if x != nil { + return x.Index + } + return 0 +} + +type MatchAction struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *MatchAction) Reset() { + *x = MatchAction{} + if protoimpl.UnsafeEnabled { + mi := &file_test_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *MatchAction) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MatchAction) ProtoMessage() {} + +func (x *MatchAction) ProtoReflect() protoreflect.Message { + mi := &file_test_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MatchAction.ProtoReflect.Descriptor instead. +func (*MatchAction) Descriptor() ([]byte, []int) { + return file_test_proto_rawDescGZIP(), []int{3} +} + +type MoreDataAvailableInput struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *MoreDataAvailableInput) Reset() { + *x = MoreDataAvailableInput{} + if protoimpl.UnsafeEnabled { + mi := &file_test_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *MoreDataAvailableInput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MoreDataAvailableInput) ProtoMessage() {} + +func (x *MoreDataAvailableInput) ProtoReflect() protoreflect.Message { + mi := &file_test_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MoreDataAvailableInput.ProtoReflect.Descriptor instead. +func (*MoreDataAvailableInput) Descriptor() ([]byte, []int) { + return file_test_proto_rawDescGZIP(), []int{4} +} + +type NoDataAvailableInput struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *NoDataAvailableInput) Reset() { + *x = NoDataAvailableInput{} + if protoimpl.UnsafeEnabled { + mi := &file_test_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *NoDataAvailableInput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NoDataAvailableInput) ProtoMessage() {} + +func (x *NoDataAvailableInput) ProtoReflect() protoreflect.Message { + mi := &file_test_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NoDataAvailableInput.ProtoReflect.Descriptor instead. +func (*NoDataAvailableInput) Descriptor() ([]byte, []int) { + return file_test_proto_rawDescGZIP(), []int{5} +} + +type ErrorInput struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *ErrorInput) Reset() { + *x = ErrorInput{} + if protoimpl.UnsafeEnabled { + mi := &file_test_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ErrorInput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ErrorInput) ProtoMessage() {} + +func (x *ErrorInput) ProtoReflect() protoreflect.Message { + mi := &file_test_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ErrorInput.ProtoReflect.Descriptor instead. +func (*ErrorInput) Descriptor() ([]byte, []int) { + return file_test_proto_rawDescGZIP(), []int{6} +} + +type NotRegistered struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *NotRegistered) Reset() { + *x = NotRegistered{} + if protoimpl.UnsafeEnabled { + mi := &file_test_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *NotRegistered) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NotRegistered) ProtoMessage() {} + +func (x *NotRegistered) ProtoReflect() protoreflect.Message { + mi := &file_test_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NotRegistered.ProtoReflect.Descriptor instead. +func (*NotRegistered) Descriptor() ([]byte, []int) { + return file_test_proto_rawDescGZIP(), []int{7} +} + +var File_test_proto protoreflect.FileDescriptor + +var file_test_proto_rawDesc = []byte{ + 0x0a, 0x0a, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x15, 0x78, 0x64, + 0x73, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x22, 0x2e, 0x0a, 0x08, 0x54, 0x65, 0x73, 0x74, 0x44, 0x61, 0x74, 0x61, 0x12, + 0x10, 0x0a, 0x03, 0x66, 0x6f, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x66, 0x6f, + 0x6f, 0x12, 0x10, 0x0a, 0x03, 0x62, 0x61, 0x72, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, + 0x62, 0x61, 0x72, 0x22, 0x0a, 0x0a, 0x08, 0x46, 0x6f, 0x6f, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x22, + 0x20, 0x0a, 0x08, 0x42, 0x61, 0x72, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x69, + 0x6e, 0x64, 0x65, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x69, 0x6e, 0x64, 0x65, + 0x78, 0x22, 0x0d, 0x0a, 0x0b, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x22, 0x18, 0x0a, 0x16, 0x4d, 0x6f, 0x72, 0x65, 0x44, 0x61, 0x74, 0x61, 0x41, 0x76, 0x61, 0x69, + 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x22, 0x16, 0x0a, 0x14, 0x4e, 0x6f, + 0x44, 0x61, 0x74, 0x61, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x49, 0x6e, 0x70, + 0x75, 0x74, 0x22, 0x0c, 0x0a, 0x0a, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x49, 0x6e, 0x70, 0x75, 0x74, + 0x22, 0x0f, 0x0a, 0x0d, 0x4e, 0x6f, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, + 0x64, 0x42, 0x3e, 0x5a, 0x3c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, + 0x65, 0x6e, 0x76, 0x6f, 0x79, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x67, 0x6f, 0x2d, 0x63, 0x6f, + 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x2d, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2f, 0x78, 0x64, 0x73, 0x6d, + 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_test_proto_rawDescOnce sync.Once + file_test_proto_rawDescData = file_test_proto_rawDesc +) + +func file_test_proto_rawDescGZIP() []byte { + file_test_proto_rawDescOnce.Do(func() { + file_test_proto_rawDescData = protoimpl.X.CompressGZIP(file_test_proto_rawDescData) + }) + return file_test_proto_rawDescData +} + +var file_test_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_test_proto_goTypes = []interface{}{ + (*TestData)(nil), // 0: xdsmatcher.test.proto.TestData + (*FooInput)(nil), // 1: xdsmatcher.test.proto.FooInput + (*BarInput)(nil), // 2: xdsmatcher.test.proto.BarInput + (*MatchAction)(nil), // 3: xdsmatcher.test.proto.MatchAction + (*MoreDataAvailableInput)(nil), // 4: xdsmatcher.test.proto.MoreDataAvailableInput + (*NoDataAvailableInput)(nil), // 5: xdsmatcher.test.proto.NoDataAvailableInput + (*ErrorInput)(nil), // 6: xdsmatcher.test.proto.ErrorInput + (*NotRegistered)(nil), // 7: xdsmatcher.test.proto.NotRegistered +} +var file_test_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_test_proto_init() } +func file_test_proto_init() { + if File_test_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_test_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TestData); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_test_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FooInput); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_test_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*BarInput); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_test_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*MatchAction); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_test_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*MoreDataAvailableInput); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_test_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*NoDataAvailableInput); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_test_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ErrorInput); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_test_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*NotRegistered); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_test_proto_rawDesc, + NumEnums: 0, + NumMessages: 8, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_test_proto_goTypes, + DependencyIndexes: file_test_proto_depIdxs, + MessageInfos: file_test_proto_msgTypes, + }.Build() + File_test_proto = out.File + file_test_proto_rawDesc = nil + file_test_proto_goTypes = nil + file_test_proto_depIdxs = nil +} diff --git a/xdsmatcher/test/proto/test.proto b/xdsmatcher/test/proto/test.proto new file mode 100644 index 0000000000..e9d06a87de --- /dev/null +++ b/xdsmatcher/test/proto/test.proto @@ -0,0 +1,27 @@ +syntax = "proto3"; + +package xdsmatcher.test.proto; + +option go_package = "github.com/envoyproxy/go-control-plane/xdsmatcher/test/proto"; + +// This file contains a number of proto definitions used for registration of actions and inputs for testing. +message TestData { + string foo = 1; + repeated string bar = 2; +} + +message FooInput {} + +message BarInput { + uint32 index = 1; +} + +message MatchAction {} + +message MoreDataAvailableInput {} + +message NoDataAvailableInput {} + +message ErrorInput {} + +message NotRegistered {} From 6e90e0b427535caf144904ef8967795fca67b966 Mon Sep 17 00:00:00 2001 From: Snow Pettersen Date: Thu, 11 Nov 2021 09:39:08 -0500 Subject: [PATCH 2/5] comments, readme Signed-off-by: Snow Pettersen --- xdsmatcher/README.md | 100 +++++++++++++++++++++++++ xdsmatcher/internal/proto/proto.go | 3 + xdsmatcher/pkg/matcher/matcher_test.go | 7 +- 3 files changed, 109 insertions(+), 1 deletion(-) diff --git a/xdsmatcher/README.md b/xdsmatcher/README.md index f26ce263e0..a5cc232df0 100644 --- a/xdsmatcher/README.md +++ b/xdsmatcher/README.md @@ -1,3 +1,103 @@ +# xDS Matching API Evaluation Framework + +This library provides a way to evaluate the +[xDS matching API](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/advanced/matching/matching_api) +using the same logic that's implemented within the Envoy proxy. The intended use case right now is +to support unit testing of generated configuration, but this may be extended to allow for the usage +of this matching API in other contexts. + +## Example Usage + +To use, first register the input and action extensions that will be used: + +```golang +func init() { + // Register the input type with the register, using the type URL of the describing protobuf. + registry.InputExtensions["type.googleapis.com/xdsmatcher.test.proto.FooInput"] = fooInputFactory +} + +// The implementation of the input. +type fooInput struct { +} + +// The factory method of the input, this converts the describing protobuf into the input object. +func fooInputFactory(m proto.Message) (interface{}, error) { + _, ok := m.(*pbtest.FooInput) + if !ok { + return nil, errors.New("unexpected proto") + } + + return fooInput{}, nil +} + +// Implement types.DataInput for the input type. +func (fooInput) Input(d types.MatchingData) (types.DataInputResult, error) { + // Cast to the expected data type, this should never fail. + testData, ok := d.(*pbtest.TestData) + if !ok { + return types.DataInputResult{}, errors.New("invalid matching data type") + } + + // Return the value to match on from the data, qualifying it with a data availability. + return types.DataInputResult{ + Availability: types.AllDataAvailable, + Data: &testData.Foo, + }, nil +} +``` + +```golang +func init() { + // Register the action with the register, using the type URL of the describing protobuf. + registry.Action["type.googleapis.com/xdsmatcher.test.proto.Action"] = actionFactory +} + +// The implementation of the action. +type action struct { +} + +// The factory method of the action, this converts the describing protobuf into the input object. +func actionFactory(m proto.Message) (interface{}, error) { + _, ok := m.(*pbtest.Action) + if !ok { + return nil, errors.New("unexpected proto") + } + + return action{}, nil +} +``` + +Once registered, a match tree can be created and evaluated: + +```golang + yaml := ` +matcher_tree: + input: + name: foo + typed_config: + "@type": "type.googleapis.com/xdsmatcher.test.proto.FooInput" + exact_match_map: + map: + "foo": + action: + name: action + typed_config: + "@type": type.googleapis.com/xdsmatcher.test.proto.MatchAction +` + matcher := &pbmatcher.Matcher{} + _ = iproto.ProtoFromYaml([]byte(yaml), matcher) + + m, _ := matcher.Create(matcher) + + data := &pbtest.TestData{} + r, _ := m.Match(data) + if r.MatchResult != nil && r.MatchResult.Action != nil { + // resulting action + action, ok := r.MatchResult.Action.(action) + } +``` + +See matcher_test.go for more examples of how to use the API. # Development To install the required dev tools on Linux: diff --git a/xdsmatcher/internal/proto/proto.go b/xdsmatcher/internal/proto/proto.go index 97926482ee..0ce95b5722 100644 --- a/xdsmatcher/internal/proto/proto.go +++ b/xdsmatcher/internal/proto/proto.go @@ -8,6 +8,7 @@ import ( "gopkg.in/yaml.v2" ) +// ProtoFromYaml converts a YAML string into the specific protobuf message. func ProtoFromYaml(s []byte, m proto.Message) error { var obj interface{} if err := yaml.Unmarshal(s, &obj); err != nil { @@ -29,6 +30,8 @@ func ProtoFromYaml(s []byte, m proto.Message) error { return nil } +// This is necessary because yaml.Unmarshal gives us map[interface{}]interface{} and we need +// to cast the types in order to make json.Marshal accept it. func convert(i interface{}) interface{} { switch x := i.(type) { case map[interface{}]interface{}: diff --git a/xdsmatcher/pkg/matcher/matcher_test.go b/xdsmatcher/pkg/matcher/matcher_test.go index b852da0731..49ea32190f 100644 --- a/xdsmatcher/pkg/matcher/matcher_test.go +++ b/xdsmatcher/pkg/matcher/matcher_test.go @@ -72,7 +72,7 @@ matcher_list: testHelper(t, conjunctionMatcherConfig) } -func TestDisjunctionMatch(t *testing.T) { +func TestDisjunction(t *testing.T) { disjunnctionMatcherConfig := func(inputType string) string { return fmt.Sprintf(` matcher_list: @@ -110,8 +110,13 @@ matcher_list: testHelper(t, disjunnctionMatcherConfig) } +// Helper that evaluates the provided configuration generator with various inputs to provide test coverage of a series of scenarios. +// The configurator should follow the following convention: +// when the provided type name is FooInput, the match should be succesful when ran against testData1, but fail when run against testData2. +// if the provided test type is not available, the matching should fail, allowing us to surface needsMoreData responses. func testHelper(t *testing.T, configurationGenerator func(typeName string) string) { t.Helper() + tcs := []struct { name string inputType string From d1733d087ef8b6275f044f40f5f7d676d40b000f Mon Sep 17 00:00:00 2001 From: Snow Pettersen Date: Thu, 11 Nov 2021 09:39:12 -0500 Subject: [PATCH 3/5] format Signed-off-by: Snow Pettersen --- xdsmatcher/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xdsmatcher/README.md b/xdsmatcher/README.md index a5cc232df0..e6e15d50d4 100644 --- a/xdsmatcher/README.md +++ b/xdsmatcher/README.md @@ -88,7 +88,7 @@ matcher_tree: _ = iproto.ProtoFromYaml([]byte(yaml), matcher) m, _ := matcher.Create(matcher) - + data := &pbtest.TestData{} r, _ := m.Match(data) if r.MatchResult != nil && r.MatchResult.Action != nil { From 991050c108341b34fc518ccc84be8142576bfd38 Mon Sep 17 00:00:00 2001 From: Snow Pettersen Date: Mon, 15 Nov 2021 10:27:23 -0500 Subject: [PATCH 4/5] move support to primarily supporting the new xds format Signed-off-by: Snow Pettersen --- xdsmatcher/go.mod | 1 + xdsmatcher/go.sum | 2 + xdsmatcher/pkg/matcher/matcher.go | 24 ++++++++-- xdsmatcher/pkg/matcher/matcher_test.go | 49 ++++++++++++++++----- xdsmatcher/pkg/matcher/registry/registry.go | 2 +- 5 files changed, 63 insertions(+), 15 deletions(-) diff --git a/xdsmatcher/go.mod b/xdsmatcher/go.mod index 7f21cf0d50..9cc388568f 100644 --- a/xdsmatcher/go.mod +++ b/xdsmatcher/go.mod @@ -3,6 +3,7 @@ module github.com/envoyproxy/go-control-plane/xdsmatcher go 1.15 require ( + github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1 github.com/davecgh/go-spew v1.1.1 // indirect github.com/envoyproxy/go-control-plane v0.9.9 github.com/golang/protobuf v1.5.2 // indirect diff --git a/xdsmatcher/go.sum b/xdsmatcher/go.sum index 408b094a3b..757c09becd 100644 --- a/xdsmatcher/go.sum +++ b/xdsmatcher/go.sum @@ -8,6 +8,8 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed h1:OZmjad4L3H8ncOIR8rnb5MREYqG8ixi5+WbeUsquF0c= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1 h1:zH8ljVhhq7yC0MIeUL/IviMtY8hx2mK8cN9wEYb8ggw= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/xdsmatcher/pkg/matcher/matcher.go b/xdsmatcher/pkg/matcher/matcher.go index 96e999625d..9b17744b52 100644 --- a/xdsmatcher/pkg/matcher/matcher.go +++ b/xdsmatcher/pkg/matcher/matcher.go @@ -6,10 +6,11 @@ import ( "regexp" "strings" - pbmatcher "github.com/envoyproxy/go-control-plane/envoy/config/common/matcher/v3" - pbtypematcher "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3" + pbmatcher "github.com/cncf/xds/go/xds/type/matcher/v3" + pblegacymatcher "github.com/envoyproxy/go-control-plane/envoy/config/common/matcher/v3" "github.com/envoyproxy/go-control-plane/xdsmatcher/pkg/matcher/registry" "github.com/envoyproxy/go-control-plane/xdsmatcher/pkg/matcher/types" + "google.golang.org/protobuf/proto" ) type MatcherTree struct { @@ -63,6 +64,21 @@ func Create(matcher *pbmatcher.Matcher) (*MatcherTree, error) { return create(matcher, &tracingLogger{}) } +func CreateLegacy(matcher *pblegacymatcher.Matcher) (*MatcherTree, error) { + // Wire cast the matcher to the new matcher format. + b, err := proto.Marshal(matcher) + if err != nil { + return nil, err + } + m := &pbmatcher.Matcher{} + err = proto.Unmarshal(b, m) + if err != nil { + return nil, err + } + + return Create(m) +} + func create(matcher *pbmatcher.Matcher, logger *tracingLogger) (*MatcherTree, error) { onNoMatch, err := createOnMatch(matcher.OnNoMatch, logger) if err != nil { @@ -281,9 +297,9 @@ func createSinglePredicate(predicate *pbmatcher.Matcher_MatcherList_Predicate_Si } } -func createValueMatcher(valueMatcher *pbtypematcher.StringMatcher) (simpleMatcher, error) { +func createValueMatcher(valueMatcher *pbmatcher.StringMatcher) (simpleMatcher, error) { switch m := valueMatcher.MatchPattern.(type) { - case *pbtypematcher.StringMatcher_SafeRegex: + case *pbmatcher.StringMatcher_SafeRegex: r, err := regexp.Compile(m.SafeRegex.Regex) if err != nil { return nil, err diff --git a/xdsmatcher/pkg/matcher/matcher_test.go b/xdsmatcher/pkg/matcher/matcher_test.go index 49ea32190f..faeeb3338c 100644 --- a/xdsmatcher/pkg/matcher/matcher_test.go +++ b/xdsmatcher/pkg/matcher/matcher_test.go @@ -7,7 +7,8 @@ import ( "github.com/stretchr/testify/assert" - pbmatcher "github.com/envoyproxy/go-control-plane/envoy/config/common/matcher/v3" + pbmatcher "github.com/cncf/xds/go/xds/type/matcher/v3" + pblegacymatcher "github.com/envoyproxy/go-control-plane/envoy/config/common/matcher/v3" iproto "github.com/envoyproxy/go-control-plane/xdsmatcher/internal/proto" _ "github.com/envoyproxy/go-control-plane/xdsmatcher/test" pbtest "github.com/envoyproxy/go-control-plane/xdsmatcher/test/proto" @@ -31,7 +32,28 @@ matcher_tree: `, inputType) } - testHelper(t, configuration) + testHelper(t, configuration, false) +} + +func TestSimpleLegacy(t *testing.T) { + configuration := func(inputType string) string { + return fmt.Sprintf(` +matcher_tree: + input: + name: foo + typed_config: + "@type": %s + exact_match_map: + map: + "foo": + action: + name: action + typed_config: + "@type": type.googleapis.com/xdsmatcher.test.proto.MatchAction +`, inputType) + } + + testHelper(t, configuration, true) } func TestConjunction(t *testing.T) { @@ -69,7 +91,7 @@ matcher_list: `, inputType) } - testHelper(t, conjunctionMatcherConfig) + testHelper(t, conjunctionMatcherConfig, false) } func TestDisjunction(t *testing.T) { @@ -107,14 +129,14 @@ matcher_list: `, inputType) } - testHelper(t, disjunnctionMatcherConfig) + testHelper(t, disjunnctionMatcherConfig, false) } // Helper that evaluates the provided configuration generator with various inputs to provide test coverage of a series of scenarios. // The configurator should follow the following convention: // when the provided type name is FooInput, the match should be succesful when ran against testData1, but fail when run against testData2. // if the provided test type is not available, the matching should fail, allowing us to surface needsMoreData responses. -func testHelper(t *testing.T, configurationGenerator func(typeName string) string) { +func testHelper(t *testing.T, configurationGenerator func(typeName string) string, legacy bool) { t.Helper() tcs := []struct { @@ -160,7 +182,7 @@ func testHelper(t *testing.T, configurationGenerator func(typeName string) strin for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { configuration := configurationGenerator(tc.inputType) - m, err := createMatcher(t, configuration) + m, err := createMatcher(t, configuration, legacy) assert.NoError(t, err) r, err := m.Match(tc.input) @@ -215,7 +237,7 @@ matcher_tree: "@type": type.googleapis.com/xdsmatcher.test.proto.MatchAction ` - m, err := createMatcher(t, configuration) + m, err := createMatcher(t, configuration, false) assert.NoError(t, err) r, err := m.Match(testData1) assert.NoError(t, err) @@ -241,7 +263,7 @@ matcher_tree: "@type": type.googleapis.com/xdsmatcher.test.proto.NotRegistered ` - m, err := createMatcher(t, config) + m, err := createMatcher(t, config, false) assert.Error(t, err) assert.Nil(t, m) @@ -260,7 +282,7 @@ matcher_tree: "@type": type.googleapis.com/xdsmatcher.test.proto.NotRegistered ` - m, err = createMatcher(t, config) + m, err = createMatcher(t, config, false) assert.Error(t, err) assert.Nil(t, m) } @@ -274,7 +296,14 @@ var testData2 = &pbtest.TestData{ Foo: "not-foo", } -func createMatcher(t *testing.T, yaml string) (*MatcherTree, error) { +func createMatcher(t *testing.T, yaml string, legacy bool) (*MatcherTree, error) { + if legacy { + matcher := &pblegacymatcher.Matcher{} + assert.NoError(t, iproto.ProtoFromYaml([]byte(yaml), matcher)) + + return CreateLegacy(matcher) + } + matcher := &pbmatcher.Matcher{} assert.NoError(t, iproto.ProtoFromYaml([]byte(yaml), matcher)) diff --git a/xdsmatcher/pkg/matcher/registry/registry.go b/xdsmatcher/pkg/matcher/registry/registry.go index 1ea2a7925d..455d8977c7 100644 --- a/xdsmatcher/pkg/matcher/registry/registry.go +++ b/xdsmatcher/pkg/matcher/registry/registry.go @@ -6,7 +6,7 @@ import ( "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/anypb" - pbcore "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + pbcore "github.com/cncf/xds/go/xds/core/v3" "github.com/envoyproxy/go-control-plane/xdsmatcher/pkg/matcher/types" ) From 0a40e3f55e9a99640383ecf49d391fca84dd6889 Mon Sep 17 00:00:00 2001 From: Snow Pettersen Date: Wed, 12 Jan 2022 14:25:44 -0500 Subject: [PATCH 5/5] feedback Signed-off-by: Snow Pettersen --- xdsmatcher/README.md | 2 +- xdsmatcher/pkg/matcher/matcher.go | 9 ++++++++- xdsmatcher/pkg/matcher/types/types.go | 22 ++++++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/xdsmatcher/README.md b/xdsmatcher/README.md index e6e15d50d4..5895a1b0c5 100644 --- a/xdsmatcher/README.md +++ b/xdsmatcher/README.md @@ -102,7 +102,7 @@ See matcher_test.go for more examples of how to use the API. # Development To install the required dev tools on Linux: -```` +``` apt-get install -y protobuf-compiler go install google.golang.org/protobuf/cmd/protoc-gen-go diff --git a/xdsmatcher/pkg/matcher/matcher.go b/xdsmatcher/pkg/matcher/matcher.go index 9b17744b52..84f0f9b59f 100644 --- a/xdsmatcher/pkg/matcher/matcher.go +++ b/xdsmatcher/pkg/matcher/matcher.go @@ -13,6 +13,7 @@ import ( "google.golang.org/protobuf/proto" ) +// MatcherTree is the constructed match tree, allowing for matching input data against the match tree. type MatcherTree struct { onNoMatch *types.OnMatch matchRoot types.Matcher @@ -20,6 +21,8 @@ type MatcherTree struct { logger *tracingLogger } +// Match attempts to match the input data against the match tree. +// An error is returned if something went wrong during match evaluation. func (m MatcherTree) Match(data types.MatchingData) (types.Result, error) { result, err := m.matchRoot.Match(data) if err != nil { @@ -60,10 +63,13 @@ func (m MatcherTree) Match(data types.MatchingData) (types.Result, error) { }, nil } +// Create creates a new match tree from the provided match tree configuration. func Create(matcher *pbmatcher.Matcher) (*MatcherTree, error) { return create(matcher, &tracingLogger{}) } +// CreateLegacy creates a new match tree from the provided match tree configuration, specified by the legacy +// format. func CreateLegacy(matcher *pblegacymatcher.Matcher) (*MatcherTree, error) { // Wire cast the matcher to the new matcher format. b, err := proto.Marshal(matcher) @@ -109,7 +115,6 @@ func create(matcher *pbmatcher.Matcher, logger *tracingLogger) (*MatcherTree, er }, nil default: return nil, errors.New("not implemented: matcher tree") - } } @@ -148,6 +153,8 @@ type predicateFunc interface { match(types.MatchingData) (matcherResult, error) } +// InputMatcher is the interface for a matcher implementation, specifying how +// an input string should be matched. type InputMatcher interface { match(value *string) (matcherResult, error) } diff --git a/xdsmatcher/pkg/matcher/types/types.go b/xdsmatcher/pkg/matcher/types/types.go index 20685d0347..8b3cafb01b 100644 --- a/xdsmatcher/pkg/matcher/types/types.go +++ b/xdsmatcher/pkg/matcher/types/types.go @@ -2,32 +2,54 @@ package types type DataInputAvailability int +// Matcher describes a type that can produce a match result from a set of matching data. type Matcher interface { Match(MatchingData) (Result, error) } +// OnMatch is a node in the match tree, either describing an action (leaf node) or +// the start of a subtree (internal node). type OnMatch struct { Matcher Matcher Action Action } + +// Action describes an opaque action that is the final result of a match. Implementations would likely +// need to cast this to a more appropriate type. type Action interface{} + +// MatchingData describes an opaque set of input data. type MatchingData interface{} + +// Result describes the result of evaluating the match tree. type Result struct { + // MatchResult is the final result, if NeedMoreData is false. This can be nil if the match tree completed + // but no action was resolved. MatchResult *OnMatch + // NeedMoreData specified whether the match tree failed to resolve due to input data not being available yet. + // This can imply that as more data is made available, a match might be found. NeedMoreData bool } const ( + // NotAvailable indicates that the data input is not available. NotAvailable DataInputAvailability = iota + // MoreDataMightBeAvailable indicates that there might be more data available. MoreDataMightBeAvailable DataInputAvailability = iota + // AllDataAvailable indicates that all data is present, no more data will be added. AllDataAvailable DataInputAvailability = iota ) +// DataInputResult describes the result of evaluating a DataInput. type DataInputResult struct { + // Availability describes the kind of data availability the associated data has. Availability DataInputAvailability + // Data is the resulting data. This might be nil if the data is not available or if the + // backing data is available but the requested value does not. Data *string } +// DataInput describes a type that can extract an input value from the MatchingData. type DataInput interface { Input(MatchingData) (DataInputResult, error) }