From 5dd096c21a38e733bc4836193bbc6d99d873a099 Mon Sep 17 00:00:00 2001 From: josvaz Date: Tue, 24 Sep 2024 11:44:07 +0200 Subject: [PATCH] CLOUDP-274027: Add CEL testing support helper (#1832) * CLOUDP-275027: Add CEL testing support helper Signed-off-by: jose.vazquez * Refactored and added some coverage --------- Signed-off-by: jose.vazquez --- .licenses-gomod.sha256 | 2 +- go.mod | 9 + go.sum | 22 ++ licenses.csv | 10 + .../atlasbackupcompliancepolicy_controller.go | 2 +- test/helper/cel/cel.go | 147 +++++++++++++ test/helper/cel/cel_test.go | 202 ++++++++++++++++++ test/helper/cel/fake/resource.go | 63 ++++++ .../cel/fake/test.mongodb.com_resources.yaml | 116 ++++++++++ test/helper/cel/fake/zz_generated.deepcopy.go | 119 +++++++++++ test/helper/cel/pattern.go | 133 ++++++++++++ 11 files changed, 823 insertions(+), 2 deletions(-) create mode 100644 test/helper/cel/cel.go create mode 100644 test/helper/cel/cel_test.go create mode 100644 test/helper/cel/fake/resource.go create mode 100644 test/helper/cel/fake/test.mongodb.com_resources.yaml create mode 100644 test/helper/cel/fake/zz_generated.deepcopy.go create mode 100644 test/helper/cel/pattern.go diff --git a/.licenses-gomod.sha256 b/.licenses-gomod.sha256 index 74dfafa9c7..c6b1377518 100644 --- a/.licenses-gomod.sha256 +++ b/.licenses-gomod.sha256 @@ -1 +1 @@ -100644 d384bc65000f700e9f677cc5e534d3a877e0988a go.mod +100644 77f19f7bab8469aee0ac39bb4dec136b25a89f7c go.mod diff --git a/go.mod b/go.mod index d384bc6500..77f19f7bab 100644 --- a/go.mod +++ b/go.mod @@ -37,6 +37,7 @@ require ( gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.31.1 k8s.io/apimachinery v0.31.1 + k8s.io/apiserver v0.31.1 k8s.io/client-go v0.31.1 sigs.k8s.io/controller-runtime v0.19.0 ) @@ -49,17 +50,24 @@ require ( cloud.google.com/go/longrunning v0.6.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect + github.com/antlr4-go/antlr/v4 v4.13.0 // indirect + github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect + github.com/blang/semver/v4 v4.0.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect + github.com/google/cel-go v0.20.1 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 // indirect github.com/google/s2a-go v0.1.8 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/spf13/cobra v1.8.1 // indirect + github.com/stoewer/go-strcase v1.2.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect @@ -71,6 +79,7 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + k8s.io/component-base v0.31.1 // indirect ) require ( diff --git a/go.sum b/go.sum index a00276c0b8..3598396a57 100644 --- a/go.sum +++ b/go.sum @@ -63,15 +63,22 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= +github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU= github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -139,6 +146,8 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/cel-go v0.20.1 h1:nDx9r8S3L4pE61eDdt8igGj8rf5kjYR3ILxWIpWNi84= +github.com/google/cel-go v0.20.1/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -169,6 +178,8 @@ github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDP github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= @@ -229,16 +240,22 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sethvargo/go-password v0.3.1 h1:WqrLTjo7X6AcVYfC6R7GtSyuUQR9hGyAj/f1PYQZCJU= github.com/sethvargo/go-password v0.3.1/go.mod h1:rXofC1zT54N7R8K/h1WDUdkf9BOx5OptoxrMBcrXzvs= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= @@ -423,6 +440,7 @@ gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSP gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= @@ -438,8 +456,12 @@ k8s.io/apiextensions-apiserver v0.31.1 h1:L+hwULvXx+nvTYX/MKM3kKMZyei+UiSXQWciX/ k8s.io/apiextensions-apiserver v0.31.1/go.mod h1:tWMPR3sgW+jsl2xm9v7lAyRF1rYEK71i9G5dRtkknoQ= k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U= k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/apiserver v0.31.1 h1:Sars5ejQDCRBY5f7R3QFHdqN3s61nhkpaX8/k1iEw1c= +k8s.io/apiserver v0.31.1/go.mod h1:lzDhpeToamVZJmmFlaLwdYZwd7zB+WYRYIboqA1kGxM= k8s.io/client-go v0.31.1 h1:f0ugtWSbWpxHR7sjVpQwuvw9a3ZKLXX0u0itkFXufb0= k8s.io/client-go v0.31.1/go.mod h1:sKI8871MJN2OyeqRlmA4W4KM9KBdBUpDLu/43eGemCg= +k8s.io/component-base v0.31.1 h1:UpOepcrX3rQ3ab5NB6g5iP0tvsgJWzxTyAo20sgYSy8= +k8s.io/component-base v0.31.1/go.mod h1:WGeaw7t/kTsqpVTaCoVEtillbqAhF2/JgvO0LDOMa0w= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= diff --git a/licenses.csv b/licenses.csv index 86bea75a4e..8467f18235 100644 --- a/licenses.csv +++ b/licenses.csv @@ -23,9 +23,12 @@ github.com/Azure/go-autorest/logger,https://github.com/Azure/go-autorest/blob/lo github.com/Azure/go-autorest/tracing,https://github.com/Azure/go-autorest/blob/tracing/v0.6.0/tracing/LICENSE,Apache-2.0 github.com/AzureAD/microsoft-authentication-library-for-go/apps,https://github.com/AzureAD/microsoft-authentication-library-for-go/blob/v1.2.2/LICENSE,MIT github.com/Masterminds/semver,https://github.com/Masterminds/semver/blob/v1.5.0/LICENSE.txt,MIT +github.com/antlr4-go/antlr/v4,https://github.com/antlr4-go/antlr/blob/v4.13.0/LICENSE,BSD-3-Clause +github.com/asaskevich/govalidator,https://github.com/asaskevich/govalidator/blob/f61b66f89f4a/LICENSE,MIT github.com/aws/aws-sdk-go,https://github.com/aws/aws-sdk-go/blob/v1.55.5/LICENSE.txt,Apache-2.0 github.com/aws/aws-sdk-go/internal/sync/singleflight,https://github.com/aws/aws-sdk-go/blob/v1.55.5/internal/sync/singleflight/LICENSE,BSD-3-Clause github.com/beorn7/perks/quantile,https://github.com/beorn7/perks/blob/v1.0.1/LICENSE,MIT +github.com/blang/semver/v4,https://github.com/blang/semver/blob/v4.0.0/v4/LICENSE,MIT github.com/cespare/xxhash/v2,https://github.com/cespare/xxhash/blob/v2.3.0/LICENSE.txt,MIT github.com/davecgh/go-spew/spew,https://github.com/davecgh/go-spew/blob/d8f796af33cc/LICENSE,ISC github.com/dimchansky/utfbom,https://github.com/dimchansky/utfbom/blob/v1.1.1/LICENSE,Apache-2.0 @@ -47,6 +50,7 @@ github.com/golang-jwt/jwt/v5,https://github.com/golang-jwt/jwt/blob/v5.2.1/LICEN github.com/golang/groupcache/lru,https://github.com/golang/groupcache/blob/41bb18bfe9da/LICENSE,Apache-2.0 github.com/golang/protobuf,https://github.com/golang/protobuf/blob/v1.5.4/LICENSE,BSD-3-Clause github.com/golang/snappy,https://github.com/golang/snappy/blob/v0.0.4/LICENSE,BSD-3-Clause +github.com/google/cel-go,https://github.com/google/cel-go/blob/v0.20.1/LICENSE,Apache-2.0 github.com/google/gnostic-models,https://github.com/google/gnostic-models/blob/v0.6.8/LICENSE,Apache-2.0 github.com/google/go-cmp/cmp,https://github.com/google/go-cmp/blob/v0.6.0/LICENSE,BSD-3-Clause github.com/google/go-querystring/query,https://github.com/google/go-querystring/blob/v1.1.0/LICENSE,BSD-3-Clause @@ -82,7 +86,9 @@ github.com/prometheus/client_model/go,https://github.com/prometheus/client_model github.com/prometheus/common,https://github.com/prometheus/common/blob/v0.55.0/LICENSE,Apache-2.0 github.com/prometheus/procfs,https://github.com/prometheus/procfs/blob/v0.15.1/LICENSE,Apache-2.0 github.com/sethvargo/go-password/password,https://github.com/sethvargo/go-password/blob/v0.3.1/LICENSE,MIT +github.com/spf13/cobra,https://github.com/spf13/cobra/blob/v1.8.1/LICENSE.txt,Apache-2.0 github.com/spf13/pflag,https://github.com/spf13/pflag/blob/v1.0.5/LICENSE,BSD-3-Clause +github.com/stoewer/go-strcase,https://github.com/stoewer/go-strcase/blob/v1.2.0/LICENSE,MIT github.com/stretchr/objx,https://github.com/stretchr/objx/blob/v0.5.2/LICENSE,MIT github.com/stretchr/testify,https://github.com/stretchr/testify/blob/v1.9.0/LICENSE,MIT github.com/x448/float16,https://github.com/x448/float16/blob/v0.8.4/LICENSE,MIT @@ -127,11 +133,15 @@ k8s.io/api,https://github.com/kubernetes/api/blob/v0.31.1/LICENSE,Apache-2.0 k8s.io/apiextensions-apiserver/pkg,https://github.com/kubernetes/apiextensions-apiserver/blob/v0.31.1/LICENSE,Apache-2.0 k8s.io/apimachinery/pkg,https://github.com/kubernetes/apimachinery/blob/v0.31.1/LICENSE,Apache-2.0 k8s.io/apimachinery/third_party/forked/golang,https://github.com/kubernetes/apimachinery/blob/v0.31.1/third_party/forked/golang/LICENSE,BSD-3-Clause +k8s.io/apiserver/pkg,https://github.com/kubernetes/apiserver/blob/v0.31.1/LICENSE,Apache-2.0 k8s.io/client-go,https://github.com/kubernetes/client-go/blob/v0.31.1/LICENSE,Apache-2.0 +k8s.io/component-base,https://github.com/kubernetes/component-base/blob/v0.31.1/LICENSE,Apache-2.0 k8s.io/klog/v2,https://github.com/kubernetes/klog/blob/v2.130.1/LICENSE,Apache-2.0 k8s.io/kube-openapi/pkg,https://github.com/kubernetes/kube-openapi/blob/70dd3763d340/LICENSE,Apache-2.0 k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json,https://github.com/kubernetes/kube-openapi/blob/70dd3763d340/pkg/internal/third_party/go-json-experiment/json/LICENSE,BSD-3-Clause +k8s.io/kube-openapi/pkg/validation/errors,https://github.com/kubernetes/kube-openapi/blob/70dd3763d340/pkg/validation/errors/LICENSE,Apache-2.0 k8s.io/kube-openapi/pkg/validation/spec,https://github.com/kubernetes/kube-openapi/blob/70dd3763d340/pkg/validation/spec/LICENSE,Apache-2.0 +k8s.io/kube-openapi/pkg/validation/strfmt,https://github.com/kubernetes/kube-openapi/blob/70dd3763d340/pkg/validation/strfmt/LICENSE,Apache-2.0 k8s.io/utils,https://github.com/kubernetes/utils/blob/18e509b52bc8/LICENSE,Apache-2.0 k8s.io/utils/internal/third_party/forked/golang/net,https://github.com/kubernetes/utils/blob/18e509b52bc8/internal/third_party/forked/golang/LICENSE,BSD-3-Clause sigs.k8s.io/controller-runtime,https://github.com/kubernetes-sigs/controller-runtime/blob/v0.19.0/LICENSE,Apache-2.0 diff --git a/pkg/controller/atlasbackupcompliancepolicy/atlasbackupcompliancepolicy_controller.go b/pkg/controller/atlasbackupcompliancepolicy/atlasbackupcompliancepolicy_controller.go index 04b07e30e0..31100dbbf9 100644 --- a/pkg/controller/atlasbackupcompliancepolicy/atlasbackupcompliancepolicy_controller.go +++ b/pkg/controller/atlasbackupcompliancepolicy/atlasbackupcompliancepolicy_controller.go @@ -47,7 +47,7 @@ type AtlasBackupCompliancePolicyReconciler struct { func (r *AtlasBackupCompliancePolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := r.Log.With("atlasbackupcompliancepolicy", req.NamespacedName) - log.Infow("-> Starting AtlasBackupCompliancePolicy reonciliation") + log.Infow("-> Starting AtlasBackupCompliancePolicy reconciliation") bcp := &akov2.AtlasBackupCompliancePolicy{} result := customresource.PrepareResource(ctx, r.Client, req, bcp, log) diff --git a/test/helper/cel/cel.go b/test/helper/cel/cel.go new file mode 100644 index 0000000000..7cbc033743 --- /dev/null +++ b/test/helper/cel/cel.go @@ -0,0 +1,147 @@ +/* +Copyright 2024 MongoDB. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +/* +Copyright 2022 The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cel + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apiextensions-apiserver/pkg/apiserver/schema" + "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel" + "k8s.io/apimachinery/pkg/util/validation/field" + celconfig "k8s.io/apiserver/pkg/apis/cel" +) + +// FieldValidatorsFromFile extracts the CEL validators by version and JSONPath from a CRD file and returns +// a validator func for testing against samples. +func FieldValidatorsFromFile(t *testing.T, crdFilePath string) (validatorsByVersionByJSONPath map[string]map[string]CELValidateFunc) { + crd := loadCRDFromFile(t, crdFilePath) + + ret := map[string]map[string]CELValidateFunc{} + for _, v := range crd.Spec.Versions { + var internalSchema apiextensions.JSONSchemaProps + err := apiextensionsv1.Convert_v1_JSONSchemaProps_To_apiextensions_JSONSchemaProps(v.Schema.OpenAPIV3Schema, &internalSchema, nil) + require.NoError(t, err, "failed to convert JSONSchemaProps for version %s: %v", v.Name, err) + structuralSchema, err := schema.NewStructural(&internalSchema) + require.NoError(t, err, "failed to create StructuralSchema for version %s: %v", v.Name, err) + + versionVals, err := findCEL(t, structuralSchema, true, field.NewPath("openAPIV3Schema")) + require.NoError(t, err, "failed to find CEL for version %s: %v", v.Name, err) + ret[v.Name] = versionVals + } + + return ret +} + +// VersionValidatorsFromFile extracts the CEL validators by version from a CRD file and returns +// a validator func for testing against samples. +func VersionValidatorsFromFile(t *testing.T, crdFilePath string) map[string]CELValidateFunc { + crd := loadCRDFromFile(t, crdFilePath) + ret := map[string]CELValidateFunc{} + for _, v := range crd.Spec.Versions { + var internalSchema apiextensions.JSONSchemaProps + err := apiextensionsv1.Convert_v1_JSONSchemaProps_To_apiextensions_JSONSchemaProps(v.Schema.OpenAPIV3Schema, &internalSchema, nil) + require.NoError(t, err, "failed to convert JSONSchemaProps for version %s: %v", v.Name, err) + structuralSchema, err := schema.NewStructural(&internalSchema) + require.NoError(t, err, "failed to create StructuralSchema for version %s: %v", v.Name, err) + ret[v.Name] = func(obj, old interface{}) field.ErrorList { + errs, _ := cel.NewValidator(structuralSchema, true, celconfig.RuntimeCELCostBudget).Validate(context.TODO(), nil, structuralSchema, obj, old, celconfig.PerCallLimit) + return errs + } + } + + return ret +} + +// VersionValidatorFromFile extracts the CEL validators for a given version from a CRD file and returns +// a validator func for testing against samples. +func VersionValidatorFromFile(t *testing.T, crdFilePath string, version string) (CELValidateFunc, error) { + vals := VersionValidatorsFromFile(t, crdFilePath) + if val, ok := vals[version]; ok { + return val, nil + } + return nil, fmt.Errorf("version %s not found", version) +} + +// CELValidateFunc tests a sample object against a CEL validator. +type CELValidateFunc func(obj, old interface{}) field.ErrorList + +func findCEL(t *testing.T, s *schema.Structural, root bool, pth *field.Path) (map[string]CELValidateFunc, error) { + t.Helper() + ret := map[string]CELValidateFunc{} + + if len(s.XValidations) > 0 { + s := *s + pth := *pth + ret[pth.String()] = func(obj, old interface{}) field.ErrorList { + errs, _ := cel.NewValidator(&s, root, celconfig.RuntimeCELCostBudget).Validate(context.TODO(), &pth, &s, obj, old, celconfig.PerCallLimit) + return errs + } + } + + for k, v := range s.Properties { + v := v + sub, err := findCEL(t, &v, false, pth.Child("properties").Child(k)) + if err != nil { + return nil, err + } + + for pth, val := range sub { + ret[pth] = val + } + } + if s.Items != nil { + sub, err := findCEL(t, s.Items, false, pth.Child("items")) + if err != nil { + return nil, err + } + for pth, val := range sub { + ret[pth] = val + } + } + if s.AdditionalProperties != nil && s.AdditionalProperties.Structural != nil { + sub, err := findCEL(t, s.AdditionalProperties.Structural, false, pth.Child("additionalProperties")) + if err != nil { + return nil, err + } + for pth, val := range sub { + ret[pth] = val + } + } + + return ret, nil +} diff --git a/test/helper/cel/cel_test.go b/test/helper/cel/cel_test.go new file mode 100644 index 0000000000..267862e458 --- /dev/null +++ b/test/helper/cel/cel_test.go @@ -0,0 +1,202 @@ +/* +Copyright 2024 MongoDB. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package cel_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + + "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/cel" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/cel/fake" +) + +const ( + crdFilePath = "./fake/test.mongodb.com_resources.yaml" + + crdVersion = "fake" +) + +func TestCEL(t *testing.T) { + testCases := []struct { + name string + current, old runtime.Object + wantErrs []string + }{ + { + // Note: It would be desirable if this case failed as well. + // This will become possible with CRD ratcheting and "optionalOldSelf: true" in the CRD declaration. + // See https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#field-optional-oldself. + name: "creating a fake Resource with deprecated set values succeeds", + old: nil, + current: &fake.Resource{ + Spec: fake.ResourceSpec{ + DeprecatedSet: []string{"foo"}, + }, + }, + }, + { + name: "updating a fake Resource and adding an empty deprecated set field succeeds", + old: &fake.Resource{ + Spec: fake.ResourceSpec{}, + }, + current: &fake.Resource{ + Spec: fake.ResourceSpec{ + DeprecatedSet: []string{}, + }, + }, + }, + { + name: "updating a fake Resource and adding a deprecated set with values fails", + old: &fake.Resource{ + Spec: fake.ResourceSpec{}, + }, + current: &fake.Resource{ + Spec: fake.ResourceSpec{ + DeprecatedSet: []string{"foo"}, + }, + }, + wantErrs: []string{ + `spec: Invalid value: "object": setting new deprecated set values is invalid: use the NewThing CRD instead.`, + }, + }, + { + name: "updating a fake Resource with an empty deprecated set field and adding values to it fails", + old: &fake.Resource{ + Spec: fake.ResourceSpec{ + DeprecatedSet: []string{}, + }, + }, + current: &fake.Resource{ + Spec: fake.ResourceSpec{ + DeprecatedSet: []string{"foo"}, + }, + }, + wantErrs: []string{ + `spec: Invalid value: "object": setting new deprecated set values is invalid: use the NewThing CRD instead.`, + }, + }, + { + name: "updating a fake Resource with existing deprecated set values and adding a custom role succeeds", + old: &fake.Resource{ + Spec: fake.ResourceSpec{ + DeprecatedSet: []string{"foo"}, + }, + }, + current: &fake.Resource{ + Spec: fake.ResourceSpec{ + DeprecatedSet: []string{"foo", "bar"}, + }, + }, + }, + { + name: "updating a fake Resource with existing deprecated set values and removing a custom role succeeds", + old: &fake.Resource{ + Spec: fake.ResourceSpec{ + DeprecatedSet: []string{"foo", "bar"}, + }, + }, + current: &fake.Resource{ + Spec: fake.ResourceSpec{ + DeprecatedSet: []string{"foo"}, + }, + }, + }, + { + name: "updating a fake Resource with existing deprecated set values and removing all deprecated set values succeeds", + old: &fake.Resource{ + Spec: fake.ResourceSpec{ + DeprecatedSet: []string{"foo", "bar"}, + }, + }, + current: &fake.Resource{ + Spec: fake.ResourceSpec{ + DeprecatedSet: []string{}, + }, + }, + }, + { + name: "updating a fake Resource with existing deprecated set values and removing the deprecated set values field succeeds", + old: &fake.Resource{ + Spec: fake.ResourceSpec{ + DeprecatedSet: []string{"foo", "bar"}, + }, + }, + current: &fake.Resource{ + Spec: fake.ResourceSpec{}, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + errs := ValidateFromFile(t, crdFilePath, crdVersion, tc.old, tc.current) + + if got := len(errs); got != len(tc.wantErrs) { + t.Errorf("expected errors %v, got %v", len(tc.wantErrs), len(errs)) + return + } + + for i := range tc.wantErrs { + got := errs[i].Error() + if got != tc.wantErrs[i] { + t.Errorf("want error %q, got %q", tc.wantErrs[i], got) + } + } + }) + } +} + +func ValidateFromFile(t *testing.T, crdFilePath string, version string, old, current runtime.Object) field.ErrorList { + t.Helper() + + var ( + err error + currentRaw, oldRaw map[string]interface{} + ) + + if current != nil { + currentRaw, err = runtime.DefaultUnstructuredConverter.ToUnstructured(current) + require.NoError(t, err) + } + if old != nil { + oldRaw, err = runtime.DefaultUnstructuredConverter.ToUnstructured(old) + require.NoError(t, err) + } + + validator, err := cel.VersionValidatorFromFile(t, crdFilePath, version) + require.NoError(t, err) + return validator(currentRaw, oldRaw) +} + +func TestFieldValidator(t *testing.T) { + result := cel.FieldValidatorsFromFile(t, crdFilePath) + assert.NotNil(t, result[crdVersion]) + assert.NotNil(t, result[crdVersion]["openAPIV3Schema.properties.spec"]) +} + +func TestPatterValidator(t *testing.T) { + result := cel.PatternValidatorsFromFile(t, crdFilePath) + assert.Equal(t, 1, len(result)) + assert.NotNil(t, result[crdVersion]) + validators := result[crdVersion] + assert.Equal(t, 2, len(validators)) + assert.NotNil(t, validators["openAPIV3Schema.properties.status.properties.conditions.items.properties.reason"]) + assert.NotNil(t, validators["openAPIV3Schema.properties.status.properties.conditions.items.properties.type"]) +} diff --git a/test/helper/cel/fake/resource.go b/test/helper/cel/fake/resource.go new file mode 100644 index 0000000000..59bd1724bf --- /dev/null +++ b/test/helper/cel/fake/resource.go @@ -0,0 +1,63 @@ +// Package fake contains API Schema definitions for the resource v1alpha1 API group +// +kubebuilder:object:generate=true +// +groupName=test.mongodb.com +package fake + +// Regenerate with: +// controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./test/helper/cel/fake/..." +// controller-gen "crd:crdVersions=v1,ignoreUnexportedFields=true" rbac:roleName=manager-role webhook paths="./test/helper/cel/fake/..." output:crd:artifacts:config=test/helper/cel/fake +// make fmt + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "test.mongodb.com", Version: "v1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) + +// ResourceSpec defines the desired state of Resource +// +kubebuilder:validation:XValidation:rule=!has(self.deprecatedSet) || has(oldSelf.deprecatedSet), message="setting new deprecated set values is invalid: use the NewThing CRD instead." +type ResourceSpec struct { + // DeprecatedField for the resource + DeprecatedSet []string `json:"deprecatedSet,omitempty"` +} + +// ResourceStatus defines the observed state of FakeRemote +type ResourceStatus struct { + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// Resource is the Schema for the resource API +type Resource struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ResourceSpec `json:"spec,omitempty"` + Status ResourceStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// ResourceList contains a list of Resource +type ResourceList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Resource `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Resource{}, &ResourceList{}) +} diff --git a/test/helper/cel/fake/test.mongodb.com_resources.yaml b/test/helper/cel/fake/test.mongodb.com_resources.yaml new file mode 100644 index 0000000000..14de9591fc --- /dev/null +++ b/test/helper/cel/fake/test.mongodb.com_resources.yaml @@ -0,0 +1,116 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.1 + name: resources.test.mongodb.com +spec: + group: test.mongodb.com + names: + kind: Resource + listKind: ResourceList + plural: resources + singular: resource + scope: Namespaced + versions: + - name: fake + schema: + openAPIV3Schema: + description: Resource is the Schema for the resource API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: ResourceSpec defines the desired state of Resource + properties: + deprecatedSet: + description: DeprecatedField for the resource + items: + type: string + type: array + type: object + x-kubernetes-validations: + - message: 'setting new deprecated set values is invalid: use the NewThing + CRD instead.' + rule: '!has(self.deprecatedSet) || has(oldSelf.deprecatedSet)' + status: + description: ResourceStatus defines the observed state of FakeRemote + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/test/helper/cel/fake/zz_generated.deepcopy.go b/test/helper/cel/fake/zz_generated.deepcopy.go new file mode 100644 index 0000000000..abeac07636 --- /dev/null +++ b/test/helper/cel/fake/zz_generated.deepcopy.go @@ -0,0 +1,119 @@ +//go:build !ignore_autogenerated + +/* +Copyright (C) MongoDB, Inc. 2020-present. + +Licensed under the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. You may obtain +a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package fake + +import ( + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Resource) DeepCopyInto(out *Resource) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Resource. +func (in *Resource) DeepCopy() *Resource { + if in == nil { + return nil + } + out := new(Resource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Resource) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourceList) DeepCopyInto(out *ResourceList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Resource, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceList. +func (in *ResourceList) DeepCopy() *ResourceList { + if in == nil { + return nil + } + out := new(ResourceList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ResourceList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourceSpec) DeepCopyInto(out *ResourceSpec) { + *out = *in + if in.DeprecatedSet != nil { + in, out := &in.DeprecatedSet, &out.DeprecatedSet + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceSpec. +func (in *ResourceSpec) DeepCopy() *ResourceSpec { + if in == nil { + return nil + } + out := new(ResourceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourceStatus) DeepCopyInto(out *ResourceStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceStatus. +func (in *ResourceStatus) DeepCopy() *ResourceStatus { + if in == nil { + return nil + } + out := new(ResourceStatus) + in.DeepCopyInto(out) + return out +} diff --git a/test/helper/cel/pattern.go b/test/helper/cel/pattern.go new file mode 100644 index 0000000000..94db2cd648 --- /dev/null +++ b/test/helper/cel/pattern.go @@ -0,0 +1,133 @@ +/* +Copyright 2024 MongoDB. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +/* +Copyright 2022 The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cel + +import ( + "errors" + "os" + "path/filepath" + "regexp" + "testing" + + "github.com/stretchr/testify/require" + + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apiextensions-apiserver/pkg/apiserver/schema" + "k8s.io/apimachinery/pkg/util/validation/field" + "sigs.k8s.io/yaml" +) + +// PatternValidatorsFromFile extracts the CEL validators by version and JSONPath from a CRD file and returns +// a validator func for testing against samples. +func PatternValidatorsFromFile(t *testing.T, crdFilePath string) (validatorsByVersionByJSONPath map[string]map[string]PatternValidateFunc) { + crd := loadCRDFromFile(t, crdFilePath) + + ret := map[string]map[string]PatternValidateFunc{} + for _, v := range crd.Spec.Versions { + var internalSchema apiextensions.JSONSchemaProps + err := apiextensionsv1.Convert_v1_JSONSchemaProps_To_apiextensions_JSONSchemaProps(v.Schema.OpenAPIV3Schema, &internalSchema, nil) + require.NoError(t, err, "failed to convert JSONSchemaProps for version %s: %v", v.Name, err) + structuralSchema, err := schema.NewStructural(&internalSchema) + require.NoError(t, err, "failed to create StructuralSchema for version %s: %v", v.Name, err) + + versionVals, err := findPattern(t, structuralSchema, field.NewPath("openAPIV3Schema")) + require.NoError(t, err, "failed to find CEL for version %s: %v", v.Name, err) + ret[v.Name] = versionVals + } + + return ret +} + +type PatternValidateFunc func(obj interface{}) error + +func findPattern(t *testing.T, s *schema.Structural, pth *field.Path) (map[string]PatternValidateFunc, error) { + t.Helper() + ret := map[string]PatternValidateFunc{} + + if len(s.ValueValidation.Pattern) > 0 { + s := *s + pth := *pth + ret[pth.String()] = func(obj interface{}) error { + p, err := regexp.Compile(s.ValueValidation.Pattern) + if err != nil { + return err + } + if p.MatchString(obj.(string)) { + return nil + } + return errors.New("pattern mismatch") + } + } + + for k, v := range s.Properties { + v := v + sub, err := findPattern(t, &v, pth.Child("properties").Child(k)) + if err != nil { + return nil, err + } + + for pth, val := range sub { + ret[pth] = val + } + } + if s.Items != nil { + sub, err := findPattern(t, s.Items, pth.Child("items")) + if err != nil { + return nil, err + } + for pth, val := range sub { + ret[pth] = val + } + } + if s.AdditionalProperties != nil && s.AdditionalProperties.Structural != nil { + sub, err := findPattern(t, s.AdditionalProperties.Structural, pth.Child("additionalProperties")) + if err != nil { + return nil, err + } + for pth, val := range sub { + ret[pth] = val + } + } + + return ret, nil +} + +func loadCRDFromFile(t *testing.T, crdFilePath string) *apiextensionsv1.CustomResourceDefinition { + data, err := os.ReadFile(filepath.Clean(crdFilePath)) + require.NoError(t, err) + + crd := apiextensionsv1.CustomResourceDefinition{} + err = yaml.Unmarshal(data, &crd) + require.NoError(t, err) + return &crd +}