Compare commits
245 Commits
v0.32.0-rc
...
master
Author | SHA1 | Date |
---|---|---|
|
04507a37f6 | |
|
50e39b11cd | |
|
7d108e8f2c | |
|
58c4eb072e | |
|
38a24e68ed | |
|
c04562bf9e | |
|
c91403d681 | |
|
b92abb2d81 | |
|
9b71f293aa | |
|
c58c5e474b | |
|
ada13e71d1 | |
|
d9c68830cd | |
|
76e5596af4 | |
|
fa4b7ae54f | |
|
3d4cbfbb07 | |
|
2e350ba61b | |
|
2ee6665f26 | |
|
a75d3d8a0f | |
|
3130ae84d8 | |
|
1ebcba2516 | |
|
8b4e5d086b | |
|
3b00b62c4b | |
|
c73d18dbc8 | |
|
d5a1ab82b5 | |
|
a3a3a5d4bd | |
|
ba86525683 | |
|
4962c2625e | |
|
5937fdff41 | |
|
b025858346 | |
|
c904feb835 | |
|
e6bf2975f4 | |
|
c904af427c | |
|
3bd79837f5 | |
|
b31e2b6043 | |
|
ed63805e81 | |
|
882f988ee5 | |
|
b18bb6a9e8 | |
|
cd1624cbbf | |
|
f90b6830ca | |
|
e76ac9371e | |
|
a9de165b70 | |
|
b86b632271 | |
|
7548d4da2f | |
|
45e4ebb75e | |
|
a864156cb7 | |
|
852f12619b | |
|
1880ea593e | |
|
7f21f07c1c | |
|
48f740b63c | |
|
10cef9d38d | |
|
6af1141f10 | |
|
b0db85e7d4 | |
|
cfeb5db0a7 | |
|
5c20c365a1 | |
|
d6651abdfe | |
|
f3d86859ab | |
|
e6ac37c004 | |
|
c21f374a5e | |
|
f545e563a3 | |
|
5f2744fe1e | |
|
ae7698643b | |
|
36c0fecd85 | |
|
7fecabd51e | |
|
0084b28976 | |
|
c58e197ee8 | |
|
d3c271abfa | |
|
07890ccffb | |
|
1025cde0fd | |
|
47b0b24556 | |
|
d2d60b8eb2 | |
|
6c33c81b53 | |
|
e0270fe44c | |
|
01cb050342 | |
|
9dd84ae6f3 | |
|
9ad036216e | |
|
e172d59e75 | |
|
a1eb4c90eb | |
|
e22a1dfe78 | |
|
d5745c2f38 | |
|
1ea980407b | |
|
90f1d78adf | |
|
e15237071b | |
|
4af193d7f5 | |
|
a925cd7fb3 | |
|
855a9556cb | |
|
251918081b | |
|
add5cf846c | |
|
09ff13941c | |
|
cb4ef17d4e | |
|
dc0ccb55f8 | |
|
52e0cea356 | |
|
460ba78fbe | |
|
da3bba9054 | |
|
0d18dbd72a | |
|
b482a2c0d9 | |
|
1bb0bf9dfa | |
|
f4fdaa0c9a | |
|
202cba0f14 | |
|
a49178112c | |
|
f7c4380031 | |
|
c4b7aee3fc | |
|
d56afd172a | |
|
37c3ecc10a | |
|
0a090da87e | |
|
2b45e0daa3 | |
|
e303f8a95d | |
|
e07849993d | |
|
863c50fec7 | |
|
512f488de3 | |
|
126acd266c | |
|
5b059a2317 | |
|
7b819a3b82 | |
|
b9dc8ce33c | |
|
1feb0afd7e | |
|
f639733311 | |
|
4390983008 | |
|
e514dfafab | |
|
43d4698fb9 | |
|
9386fa7b71 | |
|
4a2766af85 | |
|
b5d3fcfd1f | |
|
0aaa0d40ad | |
|
503b989732 | |
|
70566753bf | |
|
a0fdacfb32 | |
|
9549609199 | |
|
27a82ebc8b | |
|
7b4292bd2e | |
|
90c4280d8b | |
|
da1e88666e | |
|
89b3b23b4b | |
|
688a8fd9d9 | |
|
e8a77bd768 | |
|
7e8c77e774 | |
|
27fd3963bb | |
|
8bcc6f18d0 | |
|
6ce776c88d | |
|
f2c94d6c4b | |
|
b63ba07f71 | |
|
beddba44f4 | |
|
eaf4038701 | |
|
c8bf40462f | |
|
a04ff375ce | |
|
2eee037957 | |
|
2687636770 | |
|
f33bb5d4eb | |
|
18f464253a | |
|
a78ae8bc64 | |
|
39750cdbae | |
|
d9e6c50b14 | |
|
e79d000292 | |
|
87bb4f9a9b | |
|
e0ec8168b4 | |
|
a18d60b0df | |
|
6e3d6ca424 | |
|
56015c7e0a | |
|
40f26b37da | |
|
af97bd6ab1 | |
|
06dde8a0df | |
|
ee322b25b6 | |
|
e25aab096b | |
|
9b3d085fa5 | |
|
a18d7f2679 | |
|
78ddbb827e | |
|
ac04c7e419 | |
|
7802db1890 | |
|
a3f7d4eded | |
|
4dfd1a614e | |
|
a70cc77ccb | |
|
b5eba295a2 | |
|
e93b7f2f82 | |
|
39f6713fb4 | |
|
9dca0b5e16 | |
|
14ab9701c6 | |
|
93247ca898 | |
|
f6058d5a5b | |
|
77caaf986c | |
|
13b1842540 | |
|
4e966741ac | |
|
ded50ecb1b | |
|
e8d821eb16 | |
|
a3e3122ed9 | |
|
7d0dbe29a4 | |
|
609a76591d | |
|
6c5685cb7e | |
|
758f86daa8 | |
|
f7c9d8b15c | |
|
68b2a8167a | |
|
aac66c809b | |
|
ee1e055b7c | |
|
46d8d84c42 | |
|
fa95ab3e7c | |
|
a783532045 | |
|
0571dbf93e | |
|
da1b1a9e4c | |
|
e38241d3e7 | |
|
0a4167067c | |
|
47e7fa9a40 | |
|
11b535c1d8 | |
|
46c230ea8d | |
|
a19f1f8137 | |
|
d8c2c0ad39 | |
|
12352425f4 | |
|
a2cb7d3ca7 | |
|
4c615911e2 | |
|
d5dedd03f2 | |
|
f7fd8b3a38 | |
|
45d29dc4d6 | |
|
c74304d2a6 | |
|
f863467e6f | |
|
808ab771db | |
|
5036924849 | |
|
3e8e52d6a1 | |
|
c485e50d18 | |
|
675c4f7a80 | |
|
1e89b89bc5 | |
|
307a3ddd3c | |
|
a1a247aa25 | |
|
8d8a7acdf9 | |
|
767f17a6af | |
|
ff1373e940 | |
|
cfa44a126e | |
|
b6d30bdf92 | |
|
6091c6aa75 | |
|
5765d816ab | |
|
7249ce11e4 | |
|
f228881001 | |
|
d2cfef5abd | |
|
146e532b55 | |
|
6e120e1a46 | |
|
373952f922 | |
|
8c60292e48 | |
|
f9c043ade2 | |
|
a0ca8148bd | |
|
65d79dc805 | |
|
15f29b93dc | |
|
96b97de8d6 | |
|
aa837c28fd | |
|
b93cf3280f | |
|
fe80b851d8 | |
|
718670137b | |
|
72340d2cf7 | |
|
8aa42c4d2a | |
|
5b974f202d | |
|
ba7db196ed |
2
doc.go
2
doc.go
|
@ -14,4 +14,4 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package apimachinery // import "k8s.io/apimachinery"
|
||||
package apimachinery
|
||||
|
|
50
go.mod
50
go.mod
|
@ -2,35 +2,34 @@
|
|||
|
||||
module k8s.io/apimachinery
|
||||
|
||||
go 1.23.0
|
||||
go 1.24.0
|
||||
|
||||
godebug default=go1.23
|
||||
godebug default=go1.24
|
||||
|
||||
require (
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
|
||||
github.com/fxamacker/cbor/v2 v2.7.0
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/fxamacker/cbor/v2 v2.9.0
|
||||
github.com/gogo/protobuf v1.3.2
|
||||
github.com/golang/protobuf v1.5.4
|
||||
github.com/google/gnostic-models v0.6.8
|
||||
github.com/google/go-cmp v0.6.0
|
||||
github.com/google/gofuzz v1.2.0
|
||||
github.com/google/gnostic-models v0.7.0
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/moby/spdystream v0.5.0
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f
|
||||
github.com/onsi/ginkgo/v2 v2.21.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.9.0
|
||||
golang.org/x/net v0.30.0
|
||||
golang.org/x/time v0.7.0
|
||||
github.com/pmezard/go-difflib v1.0.0
|
||||
github.com/spf13/pflag v1.0.6
|
||||
github.com/stretchr/testify v1.10.0
|
||||
golang.org/x/net v0.38.0
|
||||
golang.org/x/time v0.9.0
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0
|
||||
gopkg.in/inf.v0 v0.9.1
|
||||
k8s.io/klog/v2 v2.130.1
|
||||
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.2
|
||||
sigs.k8s.io/yaml v1.4.0
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8
|
||||
sigs.k8s.io/randfill v1.0.0
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0
|
||||
sigs.k8s.io/yaml v1.6.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
@ -38,20 +37,19 @@ require (
|
|||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.21.0 // indirect
|
||||
github.com/onsi/gomega v1.35.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/text v0.19.0 // indirect
|
||||
golang.org/x/tools v0.26.0 // indirect
|
||||
google.golang.org/protobuf v1.35.1 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
84
go.sum
84
go.sum
|
@ -2,11 +2,10 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
|
|||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||
|
@ -21,16 +20,11 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
|
|||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
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.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
|
||||
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
|
@ -55,8 +49,9 @@ github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVO
|
|||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
|
||||
|
@ -65,13 +60,12 @@ github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
|
|||
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
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/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
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=
|
||||
|
@ -79,12 +73,16 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
|||
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=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
|
@ -94,22 +92,22 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
|||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
|
||||
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
|
@ -120,8 +118,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
|
|||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
|
||||
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
@ -134,13 +132,15 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
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-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=
|
||||
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4=
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4=
|
||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
Copyright 2025 The Kubernetes 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 apitesting
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Close and fail the test if it returns an error.
|
||||
func Close(t TestingT, c io.Closer) {
|
||||
t.Helper()
|
||||
assertNoError(t, c.Close())
|
||||
}
|
||||
|
||||
// CloseNoOp does nothing. Use as a replacement for Close when you
|
||||
// need to disable a defer.
|
||||
func CloseNoOp(TestingT, io.Closer) {}
|
||||
|
||||
// TestingT simulates assert.TestingT and assert.tHelper without adding
|
||||
// testify as a non-test dependency.
|
||||
type TestingT interface {
|
||||
Errorf(format string, args ...interface{})
|
||||
Helper()
|
||||
}
|
||||
|
||||
// Ensure that testing T & B satisfy the TestingT interface
|
||||
var _ TestingT = &testing.T{}
|
||||
var _ TestingT = &testing.B{}
|
||||
|
||||
// assertNoError simulates assert.NoError without adding testify as a
|
||||
// non-test dependency.
|
||||
//
|
||||
// In test files, use github.com/stretchr/testify/assert instead.
|
||||
func assertNoError(t TestingT, err error) {
|
||||
t.Helper()
|
||||
if err != nil {
|
||||
t.Errorf("Received unexpected error:\n%+v", err)
|
||||
}
|
||||
}
|
|
@ -21,19 +21,19 @@ import (
|
|||
"fmt"
|
||||
"math/rand"
|
||||
|
||||
"github.com/google/gofuzz"
|
||||
"sigs.k8s.io/randfill"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
kjson "k8s.io/apimachinery/pkg/util/json"
|
||||
)
|
||||
|
||||
// FuzzerFuncs returns a list of func(*SomeType, c fuzz.Continue) functions.
|
||||
// FuzzerFuncs returns a list of func(*SomeType, c randfill.Continue) functions.
|
||||
type FuzzerFuncs func(codecs runtimeserializer.CodecFactory) []interface{}
|
||||
|
||||
// FuzzerFor can randomly populate api objects that are destined for version.
|
||||
func FuzzerFor(funcs FuzzerFuncs, src rand.Source, codecs runtimeserializer.CodecFactory) *fuzz.Fuzzer {
|
||||
f := fuzz.New().NilChance(.5).NumElements(0, 1)
|
||||
func FuzzerFor(funcs FuzzerFuncs, src rand.Source, codecs runtimeserializer.CodecFactory) *randfill.Filler {
|
||||
f := randfill.New().NilChance(.5).NumElements(0, 1)
|
||||
if src != nil {
|
||||
f.RandSource(src)
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import (
|
|||
"bytes"
|
||||
gojson "encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
@ -115,7 +116,7 @@ func (c *CompatibilityTestOptions) Complete(t *testing.T) *CompatibilityTestOpti
|
|||
c.TestDataDir = "testdata"
|
||||
}
|
||||
if c.TestDataDirCurrentVersion == "" {
|
||||
c.TestDataDirCurrentVersion = filepath.Join(c.TestDataDir, "HEAD")
|
||||
c.TestDataDirCurrentVersion = filepath.Join(c.TestDataDir, http.MethodHead)
|
||||
}
|
||||
if c.TestDataDirsPreviousVersions == nil {
|
||||
dirs, err := filepath.Glob(filepath.Join(c.TestDataDir, "v*"))
|
||||
|
@ -199,7 +200,7 @@ func (c *CompatibilityTestOptions) Run(t *testing.T) {
|
|||
for _, gvk := range c.Kinds {
|
||||
t.Run(makeName(gvk), func(t *testing.T) {
|
||||
|
||||
t.Run("HEAD", func(t *testing.T) {
|
||||
t.Run(http.MethodHead, func(t *testing.T) {
|
||||
c.runCurrentVersionTest(t, gvk, usedHEADFixtures)
|
||||
})
|
||||
|
||||
|
|
|
@ -24,11 +24,9 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
//nolint:staticcheck //iccheck // SA1019 Keep using deprecated module; it still seems to be maintained and the api of the recommended replacement differs
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
fuzz "github.com/google/gofuzz"
|
||||
flag "github.com/spf13/pflag"
|
||||
"sigs.k8s.io/randfill"
|
||||
|
||||
apitesting "k8s.io/apimachinery/pkg/api/apitesting"
|
||||
"k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
|
||||
|
@ -122,15 +120,15 @@ func GlobalNonRoundTrippableTypes() sets.String {
|
|||
|
||||
// RoundTripTypesWithoutProtobuf applies the round-trip test to all round-trippable Kinds
|
||||
// in the scheme. It will skip all the GroupVersionKinds in the skip list.
|
||||
func RoundTripTypesWithoutProtobuf(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, nonRoundTrippableTypes map[schema.GroupVersionKind]bool) {
|
||||
func RoundTripTypesWithoutProtobuf(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *randfill.Filler, nonRoundTrippableTypes map[schema.GroupVersionKind]bool) {
|
||||
roundTripTypes(t, scheme, codecFactory, fuzzer, nonRoundTrippableTypes, true)
|
||||
}
|
||||
|
||||
func RoundTripTypes(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, nonRoundTrippableTypes map[schema.GroupVersionKind]bool) {
|
||||
func RoundTripTypes(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *randfill.Filler, nonRoundTrippableTypes map[schema.GroupVersionKind]bool) {
|
||||
roundTripTypes(t, scheme, codecFactory, fuzzer, nonRoundTrippableTypes, false)
|
||||
}
|
||||
|
||||
func roundTripTypes(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, nonRoundTrippableTypes map[schema.GroupVersionKind]bool, skipProtobuf bool) {
|
||||
func roundTripTypes(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *randfill.Filler, nonRoundTrippableTypes map[schema.GroupVersionKind]bool, skipProtobuf bool) {
|
||||
for _, group := range groupsFromScheme(scheme) {
|
||||
t.Logf("starting group %q", group)
|
||||
internalVersion := schema.GroupVersion{Group: group, Version: runtime.APIVersionInternal}
|
||||
|
@ -151,7 +149,7 @@ func roundTripTypes(t *testing.T, scheme *runtime.Scheme, codecFactory runtimese
|
|||
|
||||
// RoundTripExternalTypes applies the round-trip test to all external round-trippable Kinds
|
||||
// in the scheme. It will skip all the GroupVersionKinds in the nonRoundTripExternalTypes list .
|
||||
func RoundTripExternalTypes(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, nonRoundTrippableTypes map[schema.GroupVersionKind]bool) {
|
||||
func RoundTripExternalTypes(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *randfill.Filler, nonRoundTrippableTypes map[schema.GroupVersionKind]bool) {
|
||||
kinds := scheme.AllKnownTypes()
|
||||
for gvk := range kinds {
|
||||
if gvk.Version == runtime.APIVersionInternal || globalNonRoundTrippableTypes.Has(gvk.Kind) {
|
||||
|
@ -165,7 +163,7 @@ func RoundTripExternalTypes(t *testing.T, scheme *runtime.Scheme, codecFactory r
|
|||
|
||||
// RoundTripExternalTypesWithoutProtobuf applies the round-trip test to all external round-trippable Kinds
|
||||
// in the scheme. It will skip all the GroupVersionKinds in the nonRoundTripExternalTypes list.
|
||||
func RoundTripExternalTypesWithoutProtobuf(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, nonRoundTrippableTypes map[schema.GroupVersionKind]bool) {
|
||||
func RoundTripExternalTypesWithoutProtobuf(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *randfill.Filler, nonRoundTrippableTypes map[schema.GroupVersionKind]bool) {
|
||||
kinds := scheme.AllKnownTypes()
|
||||
for gvk := range kinds {
|
||||
if gvk.Version == runtime.APIVersionInternal || globalNonRoundTrippableTypes.Has(gvk.Kind) {
|
||||
|
@ -177,15 +175,15 @@ func RoundTripExternalTypesWithoutProtobuf(t *testing.T, scheme *runtime.Scheme,
|
|||
}
|
||||
}
|
||||
|
||||
func RoundTripSpecificKindWithoutProtobuf(t *testing.T, gvk schema.GroupVersionKind, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, nonRoundTrippableTypes map[schema.GroupVersionKind]bool) {
|
||||
func RoundTripSpecificKindWithoutProtobuf(t *testing.T, gvk schema.GroupVersionKind, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *randfill.Filler, nonRoundTrippableTypes map[schema.GroupVersionKind]bool) {
|
||||
roundTripSpecificKind(t, gvk, scheme, codecFactory, fuzzer, nonRoundTrippableTypes, true)
|
||||
}
|
||||
|
||||
func RoundTripSpecificKind(t *testing.T, gvk schema.GroupVersionKind, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, nonRoundTrippableTypes map[schema.GroupVersionKind]bool) {
|
||||
func RoundTripSpecificKind(t *testing.T, gvk schema.GroupVersionKind, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *randfill.Filler, nonRoundTrippableTypes map[schema.GroupVersionKind]bool) {
|
||||
roundTripSpecificKind(t, gvk, scheme, codecFactory, fuzzer, nonRoundTrippableTypes, false)
|
||||
}
|
||||
|
||||
func roundTripSpecificKind(t *testing.T, gvk schema.GroupVersionKind, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, nonRoundTrippableTypes map[schema.GroupVersionKind]bool, skipProtobuf bool) {
|
||||
func roundTripSpecificKind(t *testing.T, gvk schema.GroupVersionKind, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *randfill.Filler, nonRoundTrippableTypes map[schema.GroupVersionKind]bool, skipProtobuf bool) {
|
||||
if nonRoundTrippableTypes[gvk] {
|
||||
t.Logf("skipping %v", gvk)
|
||||
return
|
||||
|
@ -206,8 +204,8 @@ func roundTripSpecificKind(t *testing.T, gvk schema.GroupVersionKind, scheme *ru
|
|||
|
||||
// fuzzInternalObject fuzzes an arbitrary runtime object using the appropriate
|
||||
// fuzzer registered with the apitesting package.
|
||||
func fuzzInternalObject(t *testing.T, fuzzer *fuzz.Fuzzer, object runtime.Object) runtime.Object {
|
||||
fuzzer.Fuzz(object)
|
||||
func fuzzInternalObject(t *testing.T, fuzzer *randfill.Filler, object runtime.Object) runtime.Object {
|
||||
fuzzer.Fill(object)
|
||||
|
||||
j, err := apimeta.TypeAccessor(object)
|
||||
if err != nil {
|
||||
|
@ -227,7 +225,7 @@ func groupsFromScheme(scheme *runtime.Scheme) []string {
|
|||
return ret.List()
|
||||
}
|
||||
|
||||
func roundTripToAllExternalVersions(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, internalGVK schema.GroupVersionKind, nonRoundTrippableTypes map[schema.GroupVersionKind]bool, skipProtobuf bool) {
|
||||
func roundTripToAllExternalVersions(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *randfill.Filler, internalGVK schema.GroupVersionKind, nonRoundTrippableTypes map[schema.GroupVersionKind]bool, skipProtobuf bool) {
|
||||
object, err := scheme.New(internalGVK)
|
||||
if err != nil {
|
||||
t.Fatalf("Couldn't make a %v? %v", internalGVK, err)
|
||||
|
@ -264,7 +262,7 @@ func roundTripToAllExternalVersions(t *testing.T, scheme *runtime.Scheme, codecF
|
|||
}
|
||||
}
|
||||
|
||||
func roundTripOfExternalType(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *fuzz.Fuzzer, externalGVK schema.GroupVersionKind, skipProtobuf bool) {
|
||||
func roundTripOfExternalType(t *testing.T, scheme *runtime.Scheme, codecFactory runtimeserializer.CodecFactory, fuzzer *randfill.Filler, externalGVK schema.GroupVersionKind, skipProtobuf bool) {
|
||||
object, err := scheme.New(externalGVK)
|
||||
if err != nil {
|
||||
t.Fatalf("Couldn't make a %v? %v", externalGVK, err)
|
||||
|
@ -431,7 +429,6 @@ func dataAsString(data []byte) string {
|
|||
dataString := string(data)
|
||||
if !strings.HasPrefix(dataString, "{") {
|
||||
dataString = "\n" + hex.Dump(data)
|
||||
proto.NewBuffer(make([]byte, 0, 1024)).DebugPrint("decoded object", data)
|
||||
}
|
||||
return dataString
|
||||
}
|
||||
|
|
|
@ -92,13 +92,13 @@ func RoundtripToUnstructured(t *testing.T, scheme *runtime.Scheme, funcs fuzzer.
|
|||
}
|
||||
|
||||
if nointernal.Has(gvk) {
|
||||
fuzzer.Fuzz(item)
|
||||
fuzzer.Fill(item)
|
||||
} else {
|
||||
internalObj, err := scheme.New(gvk.GroupKind().WithVersion(runtime.APIVersionInternal))
|
||||
if err != nil {
|
||||
t.Fatalf("couldn't create internal object %v: %v", gvk.Kind, err)
|
||||
}
|
||||
fuzzer.Fuzz(internalObj)
|
||||
fuzzer.Fill(internalObj)
|
||||
|
||||
if err := scheme.Convert(internalObj, item, nil); err != nil {
|
||||
t.Fatalf("conversion for %v failed: %v", gvk.Kind, err)
|
||||
|
|
|
@ -15,4 +15,4 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
// Package errors provides detailed error types for api field validation.
|
||||
package errors // import "k8s.io/apimachinery/pkg/api/errors"
|
||||
package errors
|
||||
|
|
|
@ -438,7 +438,7 @@ func NewGenericServerResponse(code int, verb string, qualifiedResource schema.Gr
|
|||
message := fmt.Sprintf("the server responded with the status code %d but did not return more information", code)
|
||||
switch code {
|
||||
case http.StatusConflict:
|
||||
if verb == "POST" {
|
||||
if verb == http.MethodPost {
|
||||
reason = metav1.StatusReasonAlreadyExists
|
||||
} else {
|
||||
reason = metav1.StatusReasonConflict
|
||||
|
|
|
@ -110,13 +110,13 @@ func TestErrorNew(t *testing.T) {
|
|||
if time, ok := SuggestsClientDelay(NewTooManyRequests("doing something", 1)); time != 1 || !ok {
|
||||
t.Errorf("unexpected %d", time)
|
||||
}
|
||||
if time, ok := SuggestsClientDelay(NewGenericServerResponse(429, "get", resource("tests"), "test", "doing something", 10, true)); time != 10 || !ok {
|
||||
if time, ok := SuggestsClientDelay(NewGenericServerResponse(429, http.MethodGet, resource("tests"), "test", "doing something", 10, true)); time != 10 || !ok {
|
||||
t.Errorf("unexpected %d", time)
|
||||
}
|
||||
if time, ok := SuggestsClientDelay(NewGenericServerResponse(500, "get", resource("tests"), "test", "doing something", 10, true)); time != 10 || !ok {
|
||||
if time, ok := SuggestsClientDelay(NewGenericServerResponse(500, http.MethodGet, resource("tests"), "test", "doing something", 10, true)); time != 10 || !ok {
|
||||
t.Errorf("unexpected %d", time)
|
||||
}
|
||||
if time, ok := SuggestsClientDelay(NewGenericServerResponse(429, "get", resource("tests"), "test", "doing something", 0, true)); time != 0 || ok {
|
||||
if time, ok := SuggestsClientDelay(NewGenericServerResponse(429, http.MethodGet, resource("tests"), "test", "doing something", 0, true)); time != 0 || ok {
|
||||
t.Errorf("unexpected %d", time)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,4 +16,4 @@ limitations under the License.
|
|||
|
||||
// Package meta provides functions for retrieving API metadata from objects
|
||||
// belonging to the Kubernetes API
|
||||
package meta // import "k8s.io/apimachinery/pkg/api/meta"
|
||||
package meta
|
||||
|
|
|
@ -221,6 +221,9 @@ func extractList(obj runtime.Object, allocNew bool) ([]runtime.Object, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if items.IsNil() {
|
||||
return nil, nil
|
||||
}
|
||||
list := make([]runtime.Object, items.Len())
|
||||
if len(list) == 0 {
|
||||
return list, nil
|
||||
|
|
|
@ -25,15 +25,15 @@ import (
|
|||
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
fuzz "github.com/google/gofuzz"
|
||||
"sigs.k8s.io/randfill"
|
||||
)
|
||||
|
||||
func TestAsPartialObjectMetadata(t *testing.T) {
|
||||
f := fuzz.New().NilChance(.5).NumElements(0, 1).RandSource(rand.NewSource(1))
|
||||
f := randfill.New().NilChance(.5).NumElements(0, 1).RandSource(rand.NewSource(1))
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
m := &metav1.ObjectMeta{}
|
||||
f.Fuzz(m)
|
||||
f.Fill(m)
|
||||
partial := AsPartialObjectMetadata(m)
|
||||
if !reflect.DeepEqual(&partial.ObjectMeta, m) {
|
||||
t.Fatalf("incomplete partial object metadata: %s", cmp.Diff(&partial.ObjectMeta, m))
|
||||
|
@ -42,7 +42,7 @@ func TestAsPartialObjectMetadata(t *testing.T) {
|
|||
|
||||
for i := 0; i < 100; i++ {
|
||||
m := &metav1beta1.PartialObjectMetadata{}
|
||||
f.Fuzz(&m.ObjectMeta)
|
||||
f.Fill(&m.ObjectMeta)
|
||||
partial := AsPartialObjectMetadata(m)
|
||||
if !reflect.DeepEqual(&partial.ObjectMeta, &m.ObjectMeta) {
|
||||
t.Fatalf("incomplete partial object metadata: %s", cmp.Diff(&partial.ObjectMeta, &m.ObjectMeta))
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
Copyright 2024 The Kubernetes 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 operation
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Operation provides contextual information about a validation request and the API
|
||||
// operation being validated.
|
||||
// This type is intended for use with generate validation code and may be enhanced
|
||||
// in the future to include other information needed to validate requests.
|
||||
type Operation struct {
|
||||
// Type is the category of operation being validated. This does not
|
||||
// differentiate between HTTP verbs like PUT and PATCH, but rather merges
|
||||
// those into a single "Update" category.
|
||||
Type Type
|
||||
|
||||
// Options declare the options enabled for validation.
|
||||
//
|
||||
// Options should be set according to a resource validation strategy before validation
|
||||
// is performed, and must be treated as read-only during validation.
|
||||
//
|
||||
// Options are identified by string names. Option string names may match the name of a feature
|
||||
// gate, in which case the presence of the name in the set indicates that the feature is
|
||||
// considered enabled for the resource being validated. Note that a resource may have a
|
||||
// feature enabled even when the feature gate is disabled. This can happen when feature is
|
||||
// already in-use by a resource, often because the feature gate was enabled when the
|
||||
// resource first began using the feature.
|
||||
//
|
||||
// Unset options are disabled/false.
|
||||
Options []string
|
||||
|
||||
// Request provides information about the request being validated.
|
||||
Request Request
|
||||
}
|
||||
|
||||
// HasOption returns true if the given string is in the Options slice.
|
||||
func (o Operation) HasOption(option string) bool {
|
||||
return slices.Contains(o.Options, option)
|
||||
}
|
||||
|
||||
// Request provides information about the request being validated.
|
||||
type Request struct {
|
||||
// Subresources identifies the subresource path components of the request. For
|
||||
// example, Subresources for a request to `/api/v1/pods/my-pod/status` would be
|
||||
// `["status"]`. For `/api/v1/widget/my-widget/x/y/z`, it would be `["x", "y",
|
||||
// "z"]`. For a root resource (`/api/v1/pods/my-pod`), Subresources will be an
|
||||
// empty slice.
|
||||
//
|
||||
// Validation logic should only consult this field if the validation rules for a
|
||||
// particular field differ depending on whether the main resource or a specific
|
||||
// subresource is being accessed. For example:
|
||||
//
|
||||
// Updates to a Pod resource (`/`) normally cannot change container resource
|
||||
// requests/limits after the Pod is created (they are immutable). However, when
|
||||
// accessing the Pod's "resize" subresource (`/resize`), these specific fields
|
||||
// are allowed to be modified. In this scenario, the validation logic for
|
||||
// `spec.container[*].resources` must check `Subresources` to permit changes only
|
||||
// when the request targets the "resize" subresource.
|
||||
//
|
||||
// Note: This field should not be used to control which fields a subresource
|
||||
// operation is allowed to write. This is the responsibility of "field wiping".
|
||||
// Field wiping logic is expected to be handled in resource strategies by
|
||||
// modifying the incoming object before it is validated.
|
||||
Subresources []string
|
||||
}
|
||||
|
||||
// SubresourcePath returns the path is a slash-separated list of subresource
|
||||
// names. For example, `/status`, `/resize`, or `/x/y/z`.
|
||||
func (r Request) SubresourcePath() string {
|
||||
if len(r.Subresources) == 0 {
|
||||
return "/"
|
||||
}
|
||||
return "/" + strings.Join(r.Subresources, "/")
|
||||
}
|
||||
|
||||
// Code is the request operation to be validated.
|
||||
type Type uint32
|
||||
|
||||
const (
|
||||
// Create indicates the request being validated is for a resource create operation.
|
||||
Create Type = iota
|
||||
|
||||
// Update indicates the request being validated is for a resource update operation.
|
||||
Update
|
||||
)
|
|
@ -28,9 +28,9 @@ import (
|
|||
"unicode"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
fuzz "github.com/google/gofuzz"
|
||||
"github.com/spf13/pflag"
|
||||
inf "gopkg.in/inf.v0"
|
||||
"sigs.k8s.io/randfill"
|
||||
|
||||
cbor "k8s.io/apimachinery/pkg/runtime/serializer/cbor/direct"
|
||||
)
|
||||
|
@ -827,12 +827,12 @@ func TestQuantityParseEmit(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
var fuzzer = fuzz.New().Funcs(
|
||||
func(q *Quantity, c fuzz.Continue) {
|
||||
var fuzzer = randfill.New().Funcs(
|
||||
func(q *Quantity, c randfill.Continue) {
|
||||
q.i = Zero
|
||||
if c.RandBool() {
|
||||
if c.Bool() {
|
||||
q.Format = BinarySI
|
||||
if c.RandBool() {
|
||||
if c.Bool() {
|
||||
dec := &inf.Dec{}
|
||||
q.d = infDecAmount{Dec: dec}
|
||||
dec.SetScale(0)
|
||||
|
@ -846,12 +846,12 @@ var fuzzer = fuzz.New().Funcs(
|
|||
dec.SetUnscaled(c.Int63n(1024) << uint(10*c.Intn(5)))
|
||||
return
|
||||
}
|
||||
if c.RandBool() {
|
||||
if c.Bool() {
|
||||
q.Format = DecimalSI
|
||||
} else {
|
||||
q.Format = DecimalExponent
|
||||
}
|
||||
if c.RandBool() {
|
||||
if c.Bool() {
|
||||
dec := &inf.Dec{}
|
||||
q.d = infDecAmount{Dec: dec}
|
||||
dec.SetScale(inf.Scale(c.Intn(4)))
|
||||
|
@ -897,7 +897,7 @@ func TestQuantityDeepCopy(t *testing.T) {
|
|||
func TestJSON(t *testing.T) {
|
||||
for i := 0; i < 500; i++ {
|
||||
q := &Quantity{}
|
||||
fuzzer.Fuzz(q)
|
||||
fuzzer.Fill(q)
|
||||
b, err := json.Marshal(q)
|
||||
if err != nil {
|
||||
t.Errorf("error encoding %v: %v", q, err)
|
||||
|
@ -1801,7 +1801,7 @@ func TestQuantityUnmarshalCBOR(t *testing.T) {
|
|||
func TestQuantityRoundtripCBOR(t *testing.T) {
|
||||
for i := 0; i < 500; i++ {
|
||||
var initial, final Quantity
|
||||
fuzzer.Fuzz(&initial)
|
||||
fuzzer.Fill(&initial)
|
||||
b, err := cbor.Marshal(initial)
|
||||
if err != nil {
|
||||
t.Errorf("error encoding %v: %v", initial, err)
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
Copyright 2024 The Kubernetes 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 safe
|
||||
|
||||
// Field takes a pointer to any value (which may or may not be nil) and a
|
||||
// function that traverses to a target type R (a typical use case is to
|
||||
// dereference a field), and returns the result of the traversal, or the zero
|
||||
// value of the target type.
|
||||
//
|
||||
// This is roughly equivalent to:
|
||||
//
|
||||
// value != nil ? fn(value) : zero-value
|
||||
//
|
||||
// ...in languages that support the ternary operator.
|
||||
func Field[V any, R any](value *V, fn func(*V) R) R {
|
||||
if value == nil {
|
||||
var zero R
|
||||
return zero
|
||||
}
|
||||
o := fn(value)
|
||||
return o
|
||||
}
|
||||
|
||||
// Cast takes any value, attempts to cast it to T, and returns the T value if
|
||||
// the cast is successful, or else the zero value of T.
|
||||
func Cast[T any](value any) T {
|
||||
result, _ := value.(T)
|
||||
return result
|
||||
}
|
||||
|
||||
// Value takes a pointer to any value (which may or may not be nil) and a
|
||||
// function that returns a pointer to the same type. If the value is not nil,
|
||||
// it is returned, otherwise the result of the function is returned.
|
||||
//
|
||||
// This is roughly equivalent to:
|
||||
//
|
||||
// value != nil ? value : fn()
|
||||
//
|
||||
// ...in languages that support the ternary operator.
|
||||
func Value[T any](value *T, fn func() *T) *T {
|
||||
if value != nil {
|
||||
return value
|
||||
}
|
||||
return fn()
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
# API validation
|
||||
|
||||
This package holds functions which validate fields and types in the Kubernetes
|
||||
API. It may be useful beyond API validation, but this is the primary goal.
|
||||
|
||||
Most of the public functions here have signatures which adhere to the following
|
||||
pattern, which is assumed by automation and code-generation:
|
||||
|
||||
```
|
||||
import (
|
||||
"context"
|
||||
"k8s.io/apimachinery/pkg/api/operation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
func <Name>(ctx context.Context, op operation.Operation, fldPath *field.Path, value, oldValue <ValueType>, <OtherArgs...>) field.ErrorList
|
||||
```
|
||||
|
||||
The name of validator functions should consider that callers will generally be
|
||||
spelling out the package name and the function name, and so should aim for
|
||||
legibility. E.g. `validate.Concept()`.
|
||||
|
||||
The `ctx` argument is Go's usual Context.
|
||||
|
||||
The `opCtx` argument provides information about the API operation in question.
|
||||
|
||||
The `fldPath` argument indicates the path to the field in question, to be used
|
||||
in errors.
|
||||
|
||||
The `value` and `oldValue` arguments are the thing(s) being validated. For
|
||||
CREATE operations (`opCtx.Operation == operation.Create`), the `oldValue`
|
||||
argument will be nil. Many validators functions only look at the current value
|
||||
(`value`) and disregard `oldValue`.
|
||||
|
||||
The `value` and `oldValue` arguments are always nilable - pointers to primitive
|
||||
types, slices of any type, or maps of any type. Validator functions should
|
||||
avoid dereferencing nil. Callers are expected to not pass a nil `value` unless the
|
||||
API field itself was nilable. `oldValue` is always nil for CREATE operations and
|
||||
is also nil for UPDATE operations if the `value` is not correlated with an `oldValue`.
|
||||
|
||||
Simple content-validators may have no `<OtherArgs>`, but validator functions
|
||||
may take additional arguments. Some validator functions will be built as
|
||||
generics, e.g. to allow any integer type or to handle arbitrary slices.
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
// NonEmpty validates that a string is not empty.
|
||||
func NonEmpty(ctx context.Context, op operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList
|
||||
|
||||
// Even validates that a slice has an even number of items.
|
||||
func Even[T any](ctx context.Context, op operation.Operation, fldPath *field.Path, value, _ []T) field.ErrorList
|
||||
|
||||
// KeysMaxLen validates that all of the string keys in a map are under the
|
||||
// specified length.
|
||||
func KeysMaxLen[T any](ctx context.Context, op operation.Operation, fldPath *field.Path, value, _ map[string]T, maxLen int) field.ErrorList
|
||||
```
|
||||
|
||||
Validator functions always return an `ErrorList` where each item is a distinct
|
||||
validation failure and a zero-length return value (not just nil) indicates
|
||||
success.
|
||||
|
||||
Good validation failure messages follow the Kubernetes API conventions, for
|
||||
example using "must" instead of "should".
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
Copyright 2024 The Kubernetes 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 validate
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/operation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
// ValidateFunc is a function that validates a value, possibly considering the
|
||||
// old value (if any).
|
||||
type ValidateFunc[T any] func(ctx context.Context, op operation.Operation, fldPath *field.Path, newValue, oldValue T) field.ErrorList
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
Copyright 2025 The Kubernetes 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 constraints
|
||||
|
||||
// Signed is a constraint that permits any signed integer type.
|
||||
type Signed interface {
|
||||
~int | ~int8 | ~int16 | ~int32 | ~int64
|
||||
}
|
||||
|
||||
// Unsigned is a constraint that permits any unsigned integer type.
|
||||
type Unsigned interface {
|
||||
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
|
||||
}
|
||||
|
||||
// Integer is a constraint that permits any integer type.
|
||||
type Integer interface {
|
||||
Signed | Unsigned
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
Copyright 2014 The Kubernetes 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 content
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/validate/constraints"
|
||||
)
|
||||
|
||||
// MinError returns a string explanation of a "must be greater than or equal"
|
||||
// validation failure.
|
||||
func MinError[T constraints.Integer](min T) string {
|
||||
return fmt.Sprintf("must be greater than or equal to %d", min)
|
||||
}
|
||||
|
||||
// NEQError returns a string explanation of a "must not be equal to" validation failure.
|
||||
func NEQError[T any](disallowed T) string {
|
||||
format := "%v"
|
||||
if reflect.ValueOf(disallowed).Kind() == reflect.String {
|
||||
format = "%q"
|
||||
}
|
||||
return fmt.Sprintf("must not be equal to "+format, disallowed)
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
Copyright 2024 The Kubernetes 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 validate holds API validation functions which are designed for use
|
||||
// with the k8s.io/code-generator/cmd/validation-gen tool. Each validation
|
||||
// function has a similar fingerprint:
|
||||
//
|
||||
// func <Name>(ctx context.Context,
|
||||
// op operation.Operation,
|
||||
// fldPath *field.Path,
|
||||
// value, oldValue <nilable type>,
|
||||
// <other args...>) field.ErrorList
|
||||
//
|
||||
// The value and oldValue arguments will always be a nilable type. If the
|
||||
// original value was a string, these will be a *string. If the original value
|
||||
// was a slice or map, these will be the same slice or map type.
|
||||
//
|
||||
// For a CREATE operation, the oldValue will always be nil. For an UPDATE
|
||||
// operation, either value or oldValue may be nil, e.g. when adding or removing
|
||||
// a value in a list-map. Validators which care about UPDATE operations should
|
||||
// look at the opCtx argument to know which operation is being executed.
|
||||
//
|
||||
// Tightened validation (also known as ratcheting validation) is supported by
|
||||
// defining a new validation function. For example:
|
||||
//
|
||||
// func TightenedMaxLength(ctx context.Context, op operation.Operation, fldPath *field.Path, value, oldValue *string) field.ErrorList {
|
||||
// if oldValue != nil && len(MaxLength(ctx, op, fldPath, oldValue, nil)) > 0 {
|
||||
// // old value is not valid, so this value skips the tightened validation
|
||||
// return nil
|
||||
// }
|
||||
// return MaxLength(ctx, op, fldPath, value, nil)
|
||||
// }
|
||||
//
|
||||
// In general, we cannot distinguish a non-specified slice or map from one that
|
||||
// is specified but empty. Validators should not rely on nil values, but use
|
||||
// len() instead.
|
||||
package validate
|
|
@ -0,0 +1,171 @@
|
|||
/*
|
||||
Copyright 2024 The Kubernetes 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 validate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/equality"
|
||||
"k8s.io/apimachinery/pkg/api/operation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
// MatchFunc is a function that compares two values of the same type,
|
||||
// according to some criteria, and returns true if they match.
|
||||
type MatchFunc[T any] func(T, T) bool
|
||||
|
||||
// EachSliceVal performs validation on each element of newSlice using the provided validation function.
|
||||
//
|
||||
// For update operations, the match function finds corresponding values in oldSlice for each
|
||||
// value in newSlice. This comparison can be either full or partial (e.g., matching only
|
||||
// specific struct fields that serve as a unique identifier). If match is nil, validation
|
||||
// proceeds without considering old values, and the equiv function is not used.
|
||||
//
|
||||
// For update operations, the equiv function checks if a new value is equivalent to its
|
||||
// corresponding old value, enabling validation ratcheting. If equiv is nil but match is
|
||||
// provided, the match function is assumed to perform full value comparison.
|
||||
//
|
||||
// Note: The slice element type must be non-nilable.
|
||||
func EachSliceVal[T any](ctx context.Context, op operation.Operation, fldPath *field.Path, newSlice, oldSlice []T,
|
||||
match, equiv MatchFunc[T], validator ValidateFunc[*T]) field.ErrorList {
|
||||
var errs field.ErrorList
|
||||
for i, val := range newSlice {
|
||||
var old *T
|
||||
if match != nil && len(oldSlice) > 0 {
|
||||
old = lookup(oldSlice, val, match)
|
||||
}
|
||||
// If the operation is an update, for validation ratcheting, skip re-validating if the old
|
||||
// value exists and either:
|
||||
// 1. The match function provides full comparison (equiv is nil)
|
||||
// 2. The equiv function confirms the values are equivalent (either directly or semantically)
|
||||
//
|
||||
// The equiv function provides equality comparison when match uses partial comparison.
|
||||
if op.Type == operation.Update && old != nil && (equiv == nil || equiv(val, *old)) {
|
||||
continue
|
||||
}
|
||||
errs = append(errs, validator(ctx, op, fldPath.Index(i), &val, old)...)
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// lookup returns a pointer to the first element in the list that matches the
|
||||
// target, according to the provided comparison function, or else nil.
|
||||
func lookup[T any](list []T, target T, match MatchFunc[T]) *T {
|
||||
for i := range list {
|
||||
if match(list[i], target) {
|
||||
return &list[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EachMapVal validates each value in newMap using the specified validation
|
||||
// function, passing the corresponding old value from oldMap if the key exists in oldMap.
|
||||
// For update operations, it implements validation ratcheting by skipping validation
|
||||
// when the old value exists and the equiv function confirms the values are equivalent.
|
||||
// The value-type of the map is assumed to not be nilable.
|
||||
// If equiv is nil, value-based ratcheting is disabled and all values will be validated.
|
||||
func EachMapVal[K ~string, V any](ctx context.Context, op operation.Operation, fldPath *field.Path, newMap, oldMap map[K]V,
|
||||
equiv MatchFunc[V], validator ValidateFunc[*V]) field.ErrorList {
|
||||
var errs field.ErrorList
|
||||
for key, val := range newMap {
|
||||
var old *V
|
||||
if o, found := oldMap[key]; found {
|
||||
old = &o
|
||||
}
|
||||
// If the operation is an update, for validation ratcheting, skip re-validating if the old
|
||||
// value is found and the equiv function confirms the values are equivalent.
|
||||
if op.Type == operation.Update && old != nil && equiv != nil && equiv(val, *old) {
|
||||
continue
|
||||
}
|
||||
errs = append(errs, validator(ctx, op, fldPath.Key(string(key)), &val, old)...)
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// EachMapKey validates each element of newMap with the specified
|
||||
// validation function.
|
||||
func EachMapKey[K ~string, T any](ctx context.Context, op operation.Operation, fldPath *field.Path, newMap, oldMap map[K]T,
|
||||
validator ValidateFunc[*K]) field.ErrorList {
|
||||
var errs field.ErrorList
|
||||
for key := range newMap {
|
||||
var old *K
|
||||
if _, found := oldMap[key]; found {
|
||||
old = &key
|
||||
}
|
||||
// If the operation is an update, for validation ratcheting, skip re-validating if
|
||||
// the key is found in oldMap.
|
||||
if op.Type == operation.Update && old != nil {
|
||||
continue
|
||||
}
|
||||
// Note: the field path is the field, not the key.
|
||||
errs = append(errs, validator(ctx, op, fldPath, &key, nil)...)
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// Unique verifies that each element of newSlice is unique, according to the
|
||||
// match function. It compares every element of the slice with every other
|
||||
// element and returns errors for non-unique items.
|
||||
func Unique[T any](_ context.Context, _ operation.Operation, fldPath *field.Path, newSlice, _ []T, match MatchFunc[T]) field.ErrorList {
|
||||
var dups []int
|
||||
for i, val := range newSlice {
|
||||
for j := i + 1; j < len(newSlice); j++ {
|
||||
other := newSlice[j]
|
||||
if match(val, other) {
|
||||
if dups == nil {
|
||||
dups = make([]int, 0, len(newSlice))
|
||||
}
|
||||
if lookup(dups, j, func(a, b int) bool { return a == b }) == nil {
|
||||
dups = append(dups, j)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var errs field.ErrorList
|
||||
sort.Ints(dups)
|
||||
for _, i := range dups {
|
||||
var val any = newSlice[i]
|
||||
// TODO: we don't want the whole item to be logged in the error, just
|
||||
// the key(s). Unfortunately, the way errors are rendered, it comes out
|
||||
// as something like "map[string]any{...}" which is not very nice. Once
|
||||
// that is fixed, we can consider adding a way for this function to
|
||||
// specify that just the keys should be rendered in the error.
|
||||
errs = append(errs, field.Duplicate(fldPath.Index(i), val))
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// SemanticDeepEqual is a MatchFunc that uses equality.Semantic.DeepEqual to
|
||||
// compare two values.
|
||||
// This wrapper is needed because MatchFunc requires a function that takes two
|
||||
// arguments of specific type T, while equality.Semantic.DeepEqual takes
|
||||
// arguments of type interface{}/any. The wrapper satisfies the type
|
||||
// constraints of MatchFunc while leveraging the underlying semantic equality
|
||||
// logic. It can be used by any other function that needs to call DeepEqual.
|
||||
func SemanticDeepEqual[T any](a, b T) bool {
|
||||
return equality.Semantic.DeepEqual(a, b)
|
||||
}
|
||||
|
||||
// DirectEqual is a MatchFunc that uses the == operator to compare two values.
|
||||
// It can be used by any other function that needs to compare two values
|
||||
// directly.
|
||||
func DirectEqual[T comparable](a, b T) bool {
|
||||
return a == b
|
||||
}
|
|
@ -0,0 +1,492 @@
|
|||
/*
|
||||
Copyright 2025 The Kubernetes 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 validate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/operation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
type TestStruct struct {
|
||||
I int
|
||||
D string
|
||||
}
|
||||
|
||||
type TestStructWithKey struct {
|
||||
Key string
|
||||
I int
|
||||
D string
|
||||
}
|
||||
|
||||
type NonComparableKey struct {
|
||||
I *int
|
||||
}
|
||||
|
||||
type NonComparableStruct struct {
|
||||
I int
|
||||
S []string
|
||||
}
|
||||
|
||||
type NonComparableStructWithKey struct {
|
||||
Key string
|
||||
I int
|
||||
S []string
|
||||
}
|
||||
|
||||
type NonComparableStructWithPtr struct {
|
||||
I int
|
||||
P *int
|
||||
}
|
||||
|
||||
func TestEachSliceVal(t *testing.T) {
|
||||
testEachSliceVal(t, "valid", []int{11, 12, 13})
|
||||
testEachSliceVal(t, "valid", []string{"a", "b", "c"})
|
||||
testEachSliceVal(t, "valid", []TestStruct{{11, "a"}, {12, "b"}, {13, "c"}})
|
||||
|
||||
testEachSliceVal(t, "empty", []int{})
|
||||
testEachSliceVal(t, "empty", []string{})
|
||||
testEachSliceVal(t, "empty", []TestStruct{})
|
||||
|
||||
testEachSliceVal[int](t, "nil", nil)
|
||||
testEachSliceVal[string](t, "nil", nil)
|
||||
testEachSliceVal[TestStruct](t, "nil", nil)
|
||||
|
||||
testEachSliceValUpdate(t, "valid", []int{11, 12, 13})
|
||||
testEachSliceValUpdate(t, "valid", []string{"a", "b", "c"})
|
||||
testEachSliceValUpdate(t, "valid", []TestStruct{{11, "a"}, {12, "b"}, {13, "c"}})
|
||||
|
||||
testEachSliceValUpdate(t, "empty", []int{})
|
||||
testEachSliceValUpdate(t, "empty", []string{})
|
||||
testEachSliceValUpdate(t, "empty", []TestStruct{})
|
||||
|
||||
testEachSliceValUpdate[int](t, "nil", nil)
|
||||
testEachSliceValUpdate[string](t, "nil", nil)
|
||||
testEachSliceValUpdate[TestStruct](t, "nil", nil)
|
||||
}
|
||||
|
||||
func testEachSliceVal[T any](t *testing.T, name string, input []T) {
|
||||
t.Helper()
|
||||
var zero T
|
||||
t.Run(fmt.Sprintf("%s(%T)", name, zero), func(t *testing.T) {
|
||||
calls := 0
|
||||
vfn := func(ctx context.Context, op operation.Operation, fldPath *field.Path, newVal, oldVal *T) field.ErrorList {
|
||||
if oldVal != nil {
|
||||
t.Errorf("expected nil oldVal, got %v", *oldVal)
|
||||
}
|
||||
calls++
|
||||
return nil
|
||||
}
|
||||
_ = EachSliceVal(context.Background(), operation.Operation{}, field.NewPath("test"), input, nil, nil, nil, vfn)
|
||||
if calls != len(input) {
|
||||
t.Errorf("expected %d calls, got %d", len(input), calls)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func testEachSliceValUpdate[T any](t *testing.T, name string, input []T) {
|
||||
t.Helper()
|
||||
var zero T
|
||||
t.Run(fmt.Sprintf("%s(%T)", name, zero), func(t *testing.T) {
|
||||
calls := 0
|
||||
vfn := func(ctx context.Context, op operation.Operation, fldPath *field.Path, newVal, oldVal *T) field.ErrorList {
|
||||
if oldVal == nil {
|
||||
t.Fatalf("expected non-nil oldVal")
|
||||
}
|
||||
if !reflect.DeepEqual(*newVal, *oldVal) {
|
||||
t.Errorf("expected oldVal == newVal, got %v, %v", *oldVal, *newVal)
|
||||
}
|
||||
calls++
|
||||
return nil
|
||||
}
|
||||
old := make([]T, len(input))
|
||||
copy(old, input)
|
||||
slices.Reverse(old)
|
||||
match := func(a, b T) bool { return reflect.DeepEqual(a, b) }
|
||||
_ = EachSliceVal(context.Background(), operation.Operation{}, field.NewPath("test"), input, old, match, match, vfn)
|
||||
if calls != len(input) {
|
||||
t.Errorf("expected %d calls, got %d", len(input), calls)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestEachSliceValRatcheting(t *testing.T) {
|
||||
testEachSliceValRatcheting(t, "ComparableStruct same data different order",
|
||||
[]TestStruct{
|
||||
{11, "a"}, {12, "b"}, {13, "c"},
|
||||
},
|
||||
[]TestStruct{
|
||||
{11, "a"}, {13, "c"}, {12, "b"},
|
||||
},
|
||||
SemanticDeepEqual,
|
||||
nil,
|
||||
)
|
||||
testEachSliceValRatcheting(t, "ComparableStruct less data in new, exist in old",
|
||||
[]TestStruct{
|
||||
{11, "a"}, {12, "b"}, {13, "c"},
|
||||
},
|
||||
[]TestStruct{
|
||||
{11, "a"}, {13, "c"},
|
||||
},
|
||||
DirectEqual,
|
||||
nil,
|
||||
)
|
||||
testEachSliceValRatcheting(t, "Comparable struct with key same data different order",
|
||||
[]TestStructWithKey{
|
||||
{Key: "a", I: 11, D: "a"}, {Key: "b", I: 12, D: "b"}, {Key: "c", I: 13, D: "c"},
|
||||
},
|
||||
[]TestStructWithKey{
|
||||
{Key: "a", I: 11, D: "a"}, {Key: "c", I: 13, D: "c"}, {Key: "b", I: 12, D: "b"},
|
||||
},
|
||||
MatchFunc[TestStructWithKey](func(a, b TestStructWithKey) bool {
|
||||
return a.Key == b.Key
|
||||
}),
|
||||
DirectEqual,
|
||||
)
|
||||
testEachSliceValRatcheting(t, "Comparable struct with key less data in new, exist in old",
|
||||
[]TestStructWithKey{
|
||||
{Key: "a", I: 11, D: "a"}, {Key: "b", I: 12, D: "b"}, {Key: "c", I: 13, D: "c"},
|
||||
},
|
||||
[]TestStructWithKey{
|
||||
{Key: "a", I: 11, D: "a"}, {Key: "c", I: 13, D: "c"},
|
||||
},
|
||||
MatchFunc[TestStructWithKey](func(a, b TestStructWithKey) bool {
|
||||
return a.Key == b.Key
|
||||
}),
|
||||
DirectEqual,
|
||||
)
|
||||
testEachSliceValRatcheting(t, "NonComparableStruct same data different order",
|
||||
[]NonComparableStruct{
|
||||
{I: 11, S: []string{"a"}}, {I: 12, S: []string{"b"}}, {I: 13, S: []string{"c"}},
|
||||
},
|
||||
[]NonComparableStruct{
|
||||
{I: 11, S: []string{"a"}}, {I: 13, S: []string{"c"}}, {I: 12, S: []string{"b"}},
|
||||
},
|
||||
SemanticDeepEqual,
|
||||
nil,
|
||||
)
|
||||
testEachSliceValRatcheting(t, "NonComparableStructWithKey same data different order",
|
||||
[]NonComparableStructWithKey{
|
||||
{Key: "a", I: 11, S: []string{"a"}}, {Key: "b", I: 12, S: []string{"b"}}, {Key: "c", I: 13, S: []string{"c"}},
|
||||
},
|
||||
[]NonComparableStructWithKey{
|
||||
{Key: "a", I: 11, S: []string{"a"}}, {Key: "b", I: 12, S: []string{"b"}}, {Key: "c", I: 13, S: []string{"c"}},
|
||||
},
|
||||
MatchFunc[NonComparableStructWithKey](func(a, b NonComparableStructWithKey) bool {
|
||||
return a.Key == b.Key
|
||||
}),
|
||||
SemanticDeepEqual,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
func testEachSliceValRatcheting[T any](t *testing.T, name string, old, new []T, match, equiv MatchFunc[T]) {
|
||||
t.Helper()
|
||||
var zero T
|
||||
t.Run(fmt.Sprintf("%s(%T)", name, zero), func(t *testing.T) {
|
||||
vfn := func(ctx context.Context, op operation.Operation, fldPath *field.Path, newVal, oldVal *T) field.ErrorList {
|
||||
return field.ErrorList{field.Invalid(fldPath, *newVal, "expected no calls")}
|
||||
}
|
||||
errs := EachSliceVal(context.Background(), operation.Operation{Type: operation.Update}, field.NewPath("test"), new, old, match, equiv, vfn)
|
||||
if len(errs) > 0 {
|
||||
t.Errorf("expected no errors, got %d: %s", len(errs), fmtErrs(errs))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestEachMapVal(t *testing.T) {
|
||||
testEachMapVal(t, "valid", map[string]int{"one": 11, "two": 12, "three": 13})
|
||||
testEachMapVal(t, "valid", map[string]string{"A": "a", "B": "b", "C": "c"})
|
||||
testEachMapVal(t, "valid", map[string]TestStruct{"one": {11, "a"}, "two": {12, "b"}, "three": {13, "c"}})
|
||||
|
||||
testEachMapVal(t, "empty", map[string]int{})
|
||||
testEachMapVal(t, "empty", map[string]string{})
|
||||
testEachMapVal(t, "empty", map[string]TestStruct{})
|
||||
|
||||
testEachMapVal[int](t, "nil", nil)
|
||||
testEachMapVal[string](t, "nil", nil)
|
||||
testEachMapVal[TestStruct](t, "nil", nil)
|
||||
}
|
||||
|
||||
func testEachMapVal[T any](t *testing.T, name string, input map[string]T) {
|
||||
t.Helper()
|
||||
var zero T
|
||||
t.Run(fmt.Sprintf("%s(%T)", name, zero), func(t *testing.T) {
|
||||
calls := 0
|
||||
vfn := func(ctx context.Context, op operation.Operation, fldPath *field.Path, newVal, oldVal *T) field.ErrorList {
|
||||
if oldVal != nil {
|
||||
t.Errorf("expected nil oldVal, got %v", *oldVal)
|
||||
}
|
||||
calls++
|
||||
return nil
|
||||
}
|
||||
_ = EachMapVal(context.Background(), operation.Operation{}, field.NewPath("test"), input, nil, nil, vfn)
|
||||
if calls != len(input) {
|
||||
t.Errorf("expected %d calls, got %d", len(input), calls)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestEachMapValRatcheting(t *testing.T) {
|
||||
testEachMapValRatcheting(t, "primitive same data",
|
||||
map[string]int{"one": 11, "two": 12, "three": 13},
|
||||
map[string]int{"one": 11, "three": 13, "two": 12},
|
||||
DirectEqual,
|
||||
0,
|
||||
)
|
||||
testEachMapValRatcheting(t, "primitive less data in new, exist in old",
|
||||
map[string]int{"one": 11, "two": 12, "three": 13},
|
||||
map[string]int{"one": 11, "three": 13},
|
||||
DirectEqual,
|
||||
0,
|
||||
)
|
||||
testEachMapValRatcheting(t, "primitive new data, not exist in old",
|
||||
map[string]int{"one": 11, "two": 12, "three": 13},
|
||||
map[string]int{"one": 11, "three": 13, "two": 12, "four": 14},
|
||||
DirectEqual,
|
||||
1,
|
||||
)
|
||||
testEachMapValRatcheting(t, "non comparable value, same data",
|
||||
map[string]NonComparableStruct{
|
||||
"one": {I: 11, S: []string{"a"}},
|
||||
"two": {I: 12, S: []string{"b"}},
|
||||
"three": {I: 13, S: []string{"c"}},
|
||||
},
|
||||
map[string]NonComparableStruct{
|
||||
"one": {I: 11, S: []string{"a"}},
|
||||
"three": {I: 13, S: []string{"c"}},
|
||||
"two": {I: 12, S: []string{"b"}},
|
||||
},
|
||||
SemanticDeepEqual,
|
||||
0,
|
||||
)
|
||||
testEachMapValRatcheting(t, "non comparable value, less data in new, exist in old",
|
||||
map[string]NonComparableStruct{
|
||||
"one": {I: 11, S: []string{"a"}},
|
||||
"two": {I: 12, S: []string{"b"}},
|
||||
"three": {I: 13, S: []string{"c"}},
|
||||
},
|
||||
map[string]NonComparableStruct{
|
||||
"one": {I: 11, S: []string{"a"}},
|
||||
"three": {I: 13, S: []string{"c"}},
|
||||
},
|
||||
SemanticDeepEqual,
|
||||
0,
|
||||
)
|
||||
testEachMapValRatcheting(t, "non comparable value, new data, not exist in old",
|
||||
map[string]NonComparableStruct{
|
||||
"one": {I: 11, S: []string{"a"}},
|
||||
"two": {I: 12, S: []string{"b"}},
|
||||
"three": {I: 13, S: []string{"c"}},
|
||||
},
|
||||
map[string]NonComparableStruct{
|
||||
"one": {I: 11, S: []string{"a"}},
|
||||
"three": {I: 13, S: []string{"c"}},
|
||||
"two": {I: 12, S: []string{"b"}},
|
||||
"four": {I: 14, S: []string{"d"}},
|
||||
},
|
||||
SemanticDeepEqual,
|
||||
1,
|
||||
)
|
||||
testEachMapValRatcheting(t, "struct with pointer field, same value different pointer",
|
||||
map[string]NonComparableStructWithPtr{
|
||||
"one": {I: 11, P: ptr.To(1)},
|
||||
"two": {I: 12, P: ptr.To(2)},
|
||||
},
|
||||
map[string]NonComparableStructWithPtr{
|
||||
"one": {I: 11, P: ptr.To(1)},
|
||||
"two": {I: 12, P: ptr.To(2)},
|
||||
},
|
||||
SemanticDeepEqual,
|
||||
0,
|
||||
)
|
||||
testEachMapValRatcheting(t, "nil map to empty map",
|
||||
nil,
|
||||
map[string]int{},
|
||||
DirectEqual,
|
||||
0,
|
||||
)
|
||||
|
||||
testEachMapValRatcheting(t, "nil map to non-empty map",
|
||||
nil,
|
||||
map[string]int{"one": 1},
|
||||
DirectEqual,
|
||||
1, // Expect validation for new entry
|
||||
)
|
||||
|
||||
testEachMapValRatcheting(t, "empty map to nil map",
|
||||
map[string]int{},
|
||||
nil,
|
||||
DirectEqual,
|
||||
0,
|
||||
)
|
||||
|
||||
testEachMapValRatcheting(t, "non-empty map to nil map",
|
||||
map[string]int{"one": 1},
|
||||
nil,
|
||||
DirectEqual,
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
func testEachMapValRatcheting[K ~string, V any](t *testing.T, name string, old, new map[K]V, equiv MatchFunc[V], wantCalls int) {
|
||||
t.Helper()
|
||||
var zero V
|
||||
t.Run(fmt.Sprintf("%s(%T)", name, zero), func(t *testing.T) {
|
||||
calls := 0
|
||||
vfn := func(ctx context.Context, op operation.Operation, fldPath *field.Path, newVal, oldVal *V) field.ErrorList {
|
||||
calls++
|
||||
return nil
|
||||
}
|
||||
_ = EachMapVal(context.Background(), operation.Operation{Type: operation.Update}, field.NewPath("test"), new, old, equiv, vfn)
|
||||
if calls != wantCalls {
|
||||
t.Errorf("expected %d calls, got %d", wantCalls, calls)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type StringType string
|
||||
|
||||
func TestEachMapKey(t *testing.T) {
|
||||
testEachMapKey(t, "valid", map[string]int{"one": 11, "two": 12, "three": 13})
|
||||
testEachMapKey(t, "valid", map[StringType]string{"A": "a", "B": "b", "C": "c"})
|
||||
}
|
||||
|
||||
func testEachMapKey[K ~string, V any](t *testing.T, name string, input map[K]V) {
|
||||
t.Helper()
|
||||
var zero K
|
||||
t.Run(fmt.Sprintf("%s(%T)", name, zero), func(t *testing.T) {
|
||||
calls := 0
|
||||
vfn := func(ctx context.Context, op operation.Operation, fldPath *field.Path, newVal, oldVal *K) field.ErrorList {
|
||||
if oldVal != nil {
|
||||
t.Errorf("expected nil oldVal, got %v", *oldVal)
|
||||
}
|
||||
calls++
|
||||
return nil
|
||||
}
|
||||
_ = EachMapKey(context.Background(), operation.Operation{}, field.NewPath("test"), input, nil, vfn)
|
||||
if calls != len(input) {
|
||||
t.Errorf("expected %d calls, got %d", len(input), calls)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestEachMapKeyRatcheting(t *testing.T) {
|
||||
testEachMapKeyRatcheting(t, "same data, 0 validation calls",
|
||||
map[string]int{"one": 11, "two": 12, "three": 13},
|
||||
map[string]int{"one": 11, "three": 13, "two": 12},
|
||||
0,
|
||||
)
|
||||
testEachMapKeyRatcheting(t, "less data in new, exist in old, 0 validation calls",
|
||||
map[string]int{"one": 11, "two": 12, "three": 13},
|
||||
map[string]int{"one": 11, "three": 13},
|
||||
0,
|
||||
)
|
||||
testEachMapKeyRatcheting(t, "new data, not exist in old, 1 validation call",
|
||||
map[string]int{"one": 11, "two": 12, "three": 13},
|
||||
map[string]int{"one": 11, "three": 13, "two": 12, "four": 14},
|
||||
1,
|
||||
)
|
||||
}
|
||||
|
||||
func testEachMapKeyRatcheting[K ~string, V any](t *testing.T, name string, old, new map[K]V, wantCalls int) {
|
||||
t.Helper()
|
||||
var zero V
|
||||
t.Run(fmt.Sprintf("%s(%T)", name, zero), func(t *testing.T) {
|
||||
calls := 0
|
||||
vfn := func(ctx context.Context, op operation.Operation, fldPath *field.Path, newVal, oldVal *K) field.ErrorList {
|
||||
calls++
|
||||
return nil
|
||||
}
|
||||
_ = EachMapKey(context.Background(), operation.Operation{Type: operation.Update}, field.NewPath("test"), new, old, vfn)
|
||||
if calls != wantCalls {
|
||||
t.Errorf("expected %d calls, got %d", wantCalls, calls)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestUniqueComparableValues(t *testing.T) {
|
||||
testUnique(t, "int_nil", []int(nil), 0)
|
||||
testUnique(t, "int_empty", []int{}, 0)
|
||||
testUnique(t, "int_uniq", []int{1, 2, 3}, 0)
|
||||
testUnique(t, "int_dup", []int{1, 2, 3, 2, 1}, 2)
|
||||
|
||||
testUnique(t, "string_nil", []string(nil), 0)
|
||||
testUnique(t, "string_empty", []string{}, 0)
|
||||
testUnique(t, "string_uniq", []string{"a", "b", "c"}, 0)
|
||||
testUnique(t, "string_dup", []string{"a", "a", "c", "b", "a"}, 2)
|
||||
|
||||
type isComparable struct {
|
||||
I int
|
||||
S string
|
||||
}
|
||||
|
||||
testUnique(t, "struct_nil", []isComparable(nil), 0)
|
||||
testUnique(t, "struct_empty", []isComparable{}, 0)
|
||||
testUnique(t, "struct_uniq", []isComparable{{1, "a"}, {2, "b"}, {3, "c"}}, 0)
|
||||
testUnique(t, "struct_dup", []isComparable{{1, "a"}, {2, "b"}, {3, "c"}, {2, "b"}, {1, "a"}}, 2)
|
||||
}
|
||||
|
||||
func testUnique[T comparable](t *testing.T, name string, input []T, wantErrs int) {
|
||||
t.Helper()
|
||||
t.Run(fmt.Sprintf("%s(direct)", name), func(t *testing.T) {
|
||||
errs := Unique(context.Background(), operation.Operation{}, field.NewPath("test"), input, nil, DirectEqual)
|
||||
if len(errs) != wantErrs {
|
||||
t.Errorf("expected %d errors, got %d: %s", wantErrs, len(errs), fmtErrs(errs))
|
||||
}
|
||||
})
|
||||
t.Run(fmt.Sprintf("%s(reflect)", name), func(t *testing.T) {
|
||||
errs := Unique(context.Background(), operation.Operation{}, field.NewPath("test"), input, nil, SemanticDeepEqual)
|
||||
if len(errs) != wantErrs {
|
||||
t.Errorf("expected %d errors, got %d: %s", wantErrs, len(errs), fmtErrs(errs))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestUniqueNonComparableValues(t *testing.T) {
|
||||
type nonComparable struct {
|
||||
I int
|
||||
S []string
|
||||
}
|
||||
|
||||
testUniqueByReflect(t, "noncomp_nil", []nonComparable(nil), 0)
|
||||
testUniqueByReflect(t, "noncomp_empty", []nonComparable{}, 0)
|
||||
testUniqueByReflect(t, "noncomp_uniq", []nonComparable{{1, []string{"a"}}, {2, []string{"b"}}, {3, []string{"c"}}}, 0)
|
||||
testUniqueByReflect(t, "noncomp_dup", []nonComparable{
|
||||
{1, []string{"a"}},
|
||||
{2, []string{"b"}},
|
||||
{3, []string{"c"}},
|
||||
{2, []string{"b"}},
|
||||
{1, []string{"a"}}}, 2)
|
||||
}
|
||||
|
||||
func testUniqueByReflect[T any](t *testing.T, name string, input []T, wantErrs int) {
|
||||
t.Helper()
|
||||
var zero T
|
||||
t.Run(fmt.Sprintf("%s(%T)", name, zero), func(t *testing.T) {
|
||||
errs := Unique(context.Background(), operation.Operation{}, field.NewPath("test"), input, nil, SemanticDeepEqual)
|
||||
if len(errs) != wantErrs {
|
||||
t.Errorf("expected %d errors, got %d: %s", wantErrs, len(errs), fmtErrs(errs))
|
||||
}
|
||||
})
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
Copyright 2024 The Kubernetes 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 validate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/operation"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
// Enum verifies that the specified value is one of the valid symbols.
|
||||
// This is for string enums only.
|
||||
func Enum[T ~string](_ context.Context, op operation.Operation, fldPath *field.Path, value, _ *T, symbols sets.Set[T]) field.ErrorList {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
if !symbols.Has(*value) {
|
||||
symbolList := symbols.UnsortedList()
|
||||
slices.Sort(symbolList)
|
||||
return field.ErrorList{field.NotSupported[T](fldPath, *value, symbolList)}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
Copyright 2024 The Kubernetes 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 validate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/operation"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
func TestEnum(t *testing.T) {
|
||||
cases := []struct {
|
||||
value string
|
||||
valid sets.Set[string]
|
||||
err bool
|
||||
}{{
|
||||
value: "a",
|
||||
valid: sets.New("a", "b", "c"),
|
||||
err: false,
|
||||
}, {
|
||||
value: "x",
|
||||
valid: sets.New("c", "a", "b"),
|
||||
err: true,
|
||||
}}
|
||||
|
||||
for i, tc := range cases {
|
||||
result := Enum(context.Background(), operation.Operation{}, field.NewPath("fldpath"), &tc.value, nil, tc.valid)
|
||||
if len(result) > 0 && !tc.err {
|
||||
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
|
||||
continue
|
||||
}
|
||||
if len(result) == 0 && tc.err {
|
||||
t.Errorf("case %d: unexpected success", i)
|
||||
continue
|
||||
}
|
||||
if len(result) > 0 {
|
||||
if len(result) > 1 {
|
||||
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
|
||||
continue
|
||||
}
|
||||
if want, got := `supported values: "a", "b", "c"`, result[0].Detail; got != want {
|
||||
t.Errorf("case %d: wrong error, expected: %q, got: %q", i, want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnumTypedef(t *testing.T) {
|
||||
type StringType string
|
||||
const (
|
||||
NotStringFoo StringType = "foo"
|
||||
NotStringBar StringType = "bar"
|
||||
NotStringQux StringType = "qux"
|
||||
)
|
||||
|
||||
cases := []struct {
|
||||
value StringType
|
||||
valid sets.Set[StringType]
|
||||
err bool
|
||||
}{{
|
||||
value: "foo",
|
||||
valid: sets.New(NotStringFoo, NotStringBar, NotStringQux),
|
||||
err: false,
|
||||
}, {
|
||||
value: "x",
|
||||
valid: sets.New(NotStringFoo, NotStringBar, NotStringQux),
|
||||
err: true,
|
||||
}}
|
||||
|
||||
for i, tc := range cases {
|
||||
result := Enum(context.Background(), operation.Operation{}, field.NewPath("fldpath"), &tc.value, nil, tc.valid)
|
||||
if len(result) > 0 && !tc.err {
|
||||
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
|
||||
continue
|
||||
}
|
||||
if len(result) == 0 && tc.err {
|
||||
t.Errorf("case %d: unexpected success", i)
|
||||
continue
|
||||
}
|
||||
if len(result) > 0 {
|
||||
if len(result) > 1 {
|
||||
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
|
||||
continue
|
||||
}
|
||||
if want, got := `supported values: "bar", "foo", "qux"`, result[0].Detail; got != want {
|
||||
t.Errorf("case %d: wrong error, expected: %q, got: %q", i, want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
Copyright 2025 The Kubernetes 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 validate
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/operation"
|
||||
"k8s.io/apimachinery/pkg/api/validate/content"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
// NEQ validates that the specified comparable value is not equal to the disallowed value.
|
||||
func NEQ[T comparable](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *T, disallowed T) field.ErrorList {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
if *value == disallowed {
|
||||
return field.ErrorList{
|
||||
field.Invalid(fldPath, *value, content.NEQError(disallowed)).WithOrigin("neq"),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
Copyright 2025 The Kubernetes 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 validate
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/equality"
|
||||
"k8s.io/apimachinery/pkg/api/operation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
// ImmutableByCompare verifies that the specified value has not changed in the
|
||||
// course of an update operation. It does nothing if the old value is not
|
||||
// provided. If the caller needs to compare types that are not trivially
|
||||
// comparable, they should use ImmutableByReflect instead.
|
||||
//
|
||||
// Caution: structs with pointer fields satisfy comparable, but this function
|
||||
// will only compare pointer values. It does not compare the pointed-to
|
||||
// values.
|
||||
func ImmutableByCompare[T comparable](_ context.Context, op operation.Operation, fldPath *field.Path, value, oldValue *T) field.ErrorList {
|
||||
if op.Type != operation.Update {
|
||||
return nil
|
||||
}
|
||||
if value == nil && oldValue == nil {
|
||||
return nil
|
||||
}
|
||||
if value == nil || oldValue == nil || *value != *oldValue {
|
||||
return field.ErrorList{
|
||||
field.Forbidden(fldPath, "field is immutable"),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ImmutableByReflect verifies that the specified value has not changed in
|
||||
// the course of an update operation. It does nothing if the old value is not
|
||||
// provided. Unlike ImmutableByCompare, this function can be used with types that are
|
||||
// not directly comparable, at the cost of performance.
|
||||
func ImmutableByReflect[T any](_ context.Context, op operation.Operation, fldPath *field.Path, value, oldValue T) field.ErrorList {
|
||||
if op.Type != operation.Update {
|
||||
return nil
|
||||
}
|
||||
if !equality.Semantic.DeepEqual(value, oldValue) {
|
||||
return field.ErrorList{
|
||||
field.Forbidden(fldPath, "field is immutable"),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,250 @@
|
|||
/*
|
||||
Copyright 2025 The Kubernetes 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 validate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/operation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
type StructComparable struct {
|
||||
S string
|
||||
I int
|
||||
B bool
|
||||
}
|
||||
|
||||
func TestImmutableByCompare(t *testing.T) {
|
||||
structA := StructComparable{"abc", 123, true}
|
||||
structA2 := structA
|
||||
structB := StructComparable{"xyz", 456, false}
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
fn func(operation.Operation, *field.Path) field.ErrorList
|
||||
fail bool
|
||||
}{{
|
||||
name: "nil both values",
|
||||
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
|
||||
return ImmutableByCompare[int](context.Background(), op, fld, nil, nil)
|
||||
},
|
||||
}, {
|
||||
name: "nil value",
|
||||
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
|
||||
return ImmutableByCompare(context.Background(), op, fld, nil, ptr.To(123))
|
||||
},
|
||||
fail: true,
|
||||
}, {
|
||||
name: "nil oldValue",
|
||||
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
|
||||
return ImmutableByCompare(context.Background(), op, fld, ptr.To(123), nil)
|
||||
},
|
||||
fail: true,
|
||||
}, {
|
||||
name: "int",
|
||||
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
|
||||
return ImmutableByCompare(context.Background(), op, fld, ptr.To(123), ptr.To(123))
|
||||
},
|
||||
}, {
|
||||
name: "int fail",
|
||||
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
|
||||
return ImmutableByCompare(context.Background(), op, fld, ptr.To(123), ptr.To(456))
|
||||
},
|
||||
fail: true,
|
||||
}, {
|
||||
name: "string",
|
||||
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
|
||||
return ImmutableByCompare(context.Background(), op, fld, ptr.To("abc"), ptr.To("abc"))
|
||||
},
|
||||
}, {
|
||||
name: "string fail",
|
||||
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
|
||||
return ImmutableByCompare(context.Background(), op, fld, ptr.To("abc"), ptr.To("xyz"))
|
||||
},
|
||||
fail: true,
|
||||
}, {
|
||||
name: "bool",
|
||||
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
|
||||
return ImmutableByCompare(context.Background(), op, fld, ptr.To(true), ptr.To(true))
|
||||
},
|
||||
}, {
|
||||
name: "bool fail",
|
||||
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
|
||||
return ImmutableByCompare(context.Background(), op, fld, ptr.To(true), ptr.To(false))
|
||||
},
|
||||
fail: true,
|
||||
}, {
|
||||
name: "same struct",
|
||||
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
|
||||
return ImmutableByCompare(context.Background(), op, fld, ptr.To(structA), ptr.To(structA))
|
||||
},
|
||||
}, {
|
||||
name: "equal struct",
|
||||
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
|
||||
return ImmutableByCompare(context.Background(), op, fld, ptr.To(structA), ptr.To(structA2))
|
||||
},
|
||||
}, {
|
||||
name: "struct fail",
|
||||
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
|
||||
return ImmutableByCompare(context.Background(), op, fld, ptr.To(structA), ptr.To(structB))
|
||||
},
|
||||
fail: true,
|
||||
}} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
errs := tc.fn(operation.Operation{Type: operation.Create}, field.NewPath(""))
|
||||
if len(errs) != 0 { // Create should always succeed
|
||||
t.Errorf("case %q (create): expected success: %v", tc.name, errs)
|
||||
}
|
||||
errs = tc.fn(operation.Operation{Type: operation.Update}, field.NewPath(""))
|
||||
if tc.fail && len(errs) == 0 {
|
||||
t.Errorf("case %q (update): expected failure", tc.name)
|
||||
} else if !tc.fail && len(errs) != 0 {
|
||||
t.Errorf("case %q (update): expected success: %v", tc.name, errs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type StructNonComparable struct {
|
||||
S string
|
||||
SP *string
|
||||
I int
|
||||
IP *int
|
||||
B bool
|
||||
BP *bool
|
||||
SS []string
|
||||
MSS map[string]string
|
||||
}
|
||||
|
||||
func TestImmutableByReflect(t *testing.T) {
|
||||
structA := StructNonComparable{
|
||||
S: "abc",
|
||||
SP: ptr.To("abc"),
|
||||
I: 123,
|
||||
IP: ptr.To(123),
|
||||
B: true,
|
||||
BP: ptr.To(true),
|
||||
SS: []string{"a", "b", "c"},
|
||||
MSS: map[string]string{"a": "b", "c": "d"},
|
||||
}
|
||||
|
||||
structA2 := structA
|
||||
structA2.SP = ptr.To("abc")
|
||||
structA2.IP = ptr.To(123)
|
||||
structA2.BP = ptr.To(true)
|
||||
structA2.SS = []string{"a", "b", "c"}
|
||||
structA2.MSS = map[string]string{"a": "b", "c": "d"}
|
||||
|
||||
structB := StructNonComparable{
|
||||
S: "xyz",
|
||||
SP: ptr.To("xyz"),
|
||||
I: 456,
|
||||
IP: ptr.To(456),
|
||||
B: false,
|
||||
BP: ptr.To(false),
|
||||
SS: []string{"x", "y", "z"},
|
||||
MSS: map[string]string{"x": "X", "y": "Y"},
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
fn func(operation.Operation, *field.Path) field.ErrorList
|
||||
fail bool
|
||||
}{{
|
||||
name: "nil both values",
|
||||
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
|
||||
return ImmutableByReflect[*int](context.Background(), op, fld, nil, nil)
|
||||
},
|
||||
}, {
|
||||
name: "nil value",
|
||||
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
|
||||
return ImmutableByReflect(context.Background(), op, fld, nil, ptr.To(123))
|
||||
},
|
||||
fail: true,
|
||||
}, {
|
||||
name: "nil oldValue",
|
||||
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
|
||||
return ImmutableByReflect(context.Background(), op, fld, ptr.To(123), nil)
|
||||
},
|
||||
fail: true,
|
||||
}, {
|
||||
name: "int",
|
||||
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
|
||||
return ImmutableByReflect(context.Background(), op, fld, ptr.To(123), ptr.To(123))
|
||||
},
|
||||
}, {
|
||||
name: "int fail",
|
||||
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
|
||||
return ImmutableByReflect(context.Background(), op, fld, ptr.To(123), ptr.To(456))
|
||||
},
|
||||
fail: true,
|
||||
}, {
|
||||
name: "string",
|
||||
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
|
||||
return ImmutableByReflect(context.Background(), op, fld, ptr.To("abc"), ptr.To("abc"))
|
||||
},
|
||||
}, {
|
||||
name: "string fail",
|
||||
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
|
||||
return ImmutableByReflect(context.Background(), op, fld, ptr.To("abc"), ptr.To("xyz"))
|
||||
},
|
||||
fail: true,
|
||||
}, {
|
||||
name: "bool",
|
||||
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
|
||||
return ImmutableByReflect(context.Background(), op, fld, ptr.To(true), ptr.To(true))
|
||||
},
|
||||
}, {
|
||||
name: "bool fail",
|
||||
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
|
||||
return ImmutableByReflect(context.Background(), op, fld, ptr.To(true), ptr.To(false))
|
||||
},
|
||||
fail: true,
|
||||
}, {
|
||||
name: "same struct",
|
||||
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
|
||||
return ImmutableByReflect(context.Background(), op, fld, ptr.To(structA), ptr.To(structA))
|
||||
},
|
||||
}, {
|
||||
name: "equal struct",
|
||||
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
|
||||
return ImmutableByReflect(context.Background(), op, fld, ptr.To(structA), ptr.To(structA2))
|
||||
},
|
||||
}, {
|
||||
name: "struct fail",
|
||||
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
|
||||
return ImmutableByReflect(context.Background(), op, fld, ptr.To(structA), ptr.To(structB))
|
||||
},
|
||||
fail: true,
|
||||
}} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
errs := tc.fn(operation.Operation{Type: operation.Create}, field.NewPath(""))
|
||||
if len(errs) != 0 { // Create should always succeed
|
||||
t.Errorf("case %q (create): expected success: %v", tc.name, errs)
|
||||
}
|
||||
errs = tc.fn(operation.Operation{Type: operation.Update}, field.NewPath(""))
|
||||
if tc.fail && len(errs) == 0 {
|
||||
t.Errorf("case %q (update): expected failure", tc.name)
|
||||
} else if !tc.fail && len(errs) != 0 {
|
||||
t.Errorf("case %q (update): expected success: %v", tc.name, errs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
Copyright 2024 The Kubernetes 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 validate
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/operation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
// MatchItemFn takes a pointer to an item and returns true if it matches the criteria.
|
||||
type MatchItemFn[T any] func(*T) bool
|
||||
|
||||
// SliceItem finds the first item in newList that satisfies the 'matches' predicate,
|
||||
// and if found, also looks for a matching item in oldList. It then invokes
|
||||
// 'itemValidator' on these items.
|
||||
// The fldPath passed to itemValidator is indexed to the matched item's position in newList.
|
||||
// This function processes only the *first* matching item found in newList.
|
||||
// It assumes that the 'matches' predicate targets a unique identifier (primary key) and
|
||||
// will match at most one element per list.
|
||||
// If this assumption is violated, changes in list order can lead this function
|
||||
// to have inconsistent behavior.
|
||||
// This function does not validate items that were removed (present in oldList but not in newList).
|
||||
func SliceItem[TList ~[]TItem, TItem any](
|
||||
ctx context.Context, op operation.Operation, fldPath *field.Path,
|
||||
newList, oldList TList,
|
||||
matches MatchItemFn[TItem],
|
||||
equiv MatchFunc[TItem],
|
||||
itemValidator func(ctx context.Context, op operation.Operation, fldPath *field.Path, newObj, oldObj *TItem) field.ErrorList,
|
||||
) field.ErrorList {
|
||||
var matchedNew, matchedOld *TItem
|
||||
var newIndex int
|
||||
|
||||
for i := range newList {
|
||||
if matches(&newList[i]) {
|
||||
matchedNew = &newList[i]
|
||||
newIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if matchedNew == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for i := range oldList {
|
||||
if matches(&oldList[i]) {
|
||||
matchedOld = &oldList[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if op.Type == operation.Update && matchedOld != nil && equiv(*matchedNew, *matchedOld) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return itemValidator(ctx, op, fldPath.Index(newIndex), matchedNew, matchedOld)
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
Copyright 2025 The Kubernetes 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 validate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/operation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
type multiKeyItem struct {
|
||||
K1 string `json:"k1"`
|
||||
K2 string `json:"k2"`
|
||||
V int `json:"v"`
|
||||
}
|
||||
|
||||
func TestSliceItem(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
new []multiKeyItem
|
||||
old []multiKeyItem
|
||||
match MatchItemFn[multiKeyItem]
|
||||
validator func(context.Context, operation.Operation, *field.Path, *multiKeyItem, *multiKeyItem) field.ErrorList
|
||||
expected field.ErrorList
|
||||
}{
|
||||
{
|
||||
name: "no match",
|
||||
new: []multiKeyItem{
|
||||
{K1: "a", K2: "1", V: 1},
|
||||
},
|
||||
match: func(i *multiKeyItem) bool {
|
||||
return i.K1 == "target"
|
||||
},
|
||||
validator: func(_ context.Context, _ operation.Operation, fp *field.Path, _, _ *multiKeyItem) field.ErrorList {
|
||||
return field.ErrorList{field.Invalid(fp, nil, "err")}
|
||||
},
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "new item with matching keys",
|
||||
new: []multiKeyItem{
|
||||
{K1: "a", K2: "1", V: 1},
|
||||
{K1: "target", K2: "target2", V: 2},
|
||||
},
|
||||
match: func(i *multiKeyItem) bool {
|
||||
return i.K1 == "target" && i.K2 == "target2"
|
||||
},
|
||||
validator: func(_ context.Context, _ operation.Operation, fp *field.Path, n, o *multiKeyItem) field.ErrorList {
|
||||
if n != nil && o == nil {
|
||||
return field.ErrorList{field.Invalid(fp, n.K1, "added")}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
expected: field.ErrorList{field.Invalid(field.NewPath("").Index(1), "target", "added")},
|
||||
},
|
||||
{
|
||||
name: "updated item - same keys different values",
|
||||
new: []multiKeyItem{
|
||||
{K1: "a", K2: "1", V: 1},
|
||||
{K1: "update", K2: "target2", V: 20},
|
||||
},
|
||||
old: []multiKeyItem{
|
||||
{K1: "a", K2: "1", V: 1},
|
||||
{K1: "update", K2: "target2", V: 2},
|
||||
},
|
||||
match: func(i *multiKeyItem) bool {
|
||||
return i.K1 == "update" && i.K2 == "target2"
|
||||
},
|
||||
validator: func(_ context.Context, _ operation.Operation, fp *field.Path, n, o *multiKeyItem) field.ErrorList {
|
||||
if n != nil && o != nil && n.V != o.V {
|
||||
return field.ErrorList{field.Invalid(fp.Child("v"), n.V, "changed")}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
expected: field.ErrorList{field.Invalid(field.NewPath("").Index(1).Child("v"), 20, "changed")},
|
||||
},
|
||||
{
|
||||
// For completeness as listType=map && listKey=... required tags prevents dupes.
|
||||
name: "first match only - multiple items with same keys",
|
||||
new: []multiKeyItem{
|
||||
{K1: "dup", K2: "target2", V: 1},
|
||||
{K1: "dup", K2: "target2", V: 2},
|
||||
},
|
||||
old: []multiKeyItem{
|
||||
{K1: "dup", K2: "target2", V: 10},
|
||||
},
|
||||
match: func(i *multiKeyItem) bool {
|
||||
return i.K1 == "dup" && i.K2 == "target2"
|
||||
},
|
||||
validator: func(_ context.Context, _ operation.Operation, fp *field.Path, n, o *multiKeyItem) field.ErrorList {
|
||||
if n != nil && o != nil {
|
||||
return field.ErrorList{field.Invalid(fp, n.V, "value")}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
expected: field.ErrorList{field.Invalid(field.NewPath("").Index(0), 1, "value")},
|
||||
},
|
||||
{
|
||||
name: "nil new list",
|
||||
new: nil,
|
||||
old: []multiKeyItem{
|
||||
{K1: "exists", K2: "target2", V: 1},
|
||||
},
|
||||
match: func(i *multiKeyItem) bool {
|
||||
return i.K1 == "exists" && i.K2 == "target2"
|
||||
},
|
||||
validator: func(_ context.Context, _ operation.Operation, fp *field.Path, n, o *multiKeyItem) field.ErrorList {
|
||||
if n == nil && o != nil {
|
||||
return field.ErrorList{field.Invalid(fp, nil, "deleted")}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "empty lists",
|
||||
new: []multiKeyItem{},
|
||||
old: []multiKeyItem{},
|
||||
match: func(i *multiKeyItem) bool { return true },
|
||||
validator: func(_ context.Context, _ operation.Operation, fp *field.Path, _, _ *multiKeyItem) field.ErrorList {
|
||||
return field.ErrorList{field.Invalid(fp, nil, "err")}
|
||||
},
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "nil lists",
|
||||
new: nil,
|
||||
old: nil,
|
||||
match: func(i *multiKeyItem) bool { return true },
|
||||
validator: func(_ context.Context, _ operation.Operation, fp *field.Path, _, _ *multiKeyItem) field.ErrorList {
|
||||
return field.ErrorList{field.Invalid(fp, nil, "err")}
|
||||
},
|
||||
expected: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
op := operation.Operation{Type: operation.Update}
|
||||
fp := field.NewPath("")
|
||||
|
||||
got := SliceItem(ctx, op, fp, tc.new, tc.old, tc.match, SemanticDeepEqual, tc.validator)
|
||||
|
||||
if !reflect.DeepEqual(got, tc.expected) {
|
||||
t.Errorf("got %v want %v", got, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
Copyright 2024 The Kubernetes 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 validate
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/operation"
|
||||
"k8s.io/apimachinery/pkg/api/validate/constraints"
|
||||
"k8s.io/apimachinery/pkg/api/validate/content"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
// Minimum verifies that the specified value is greater than or equal to min.
|
||||
func Minimum[T constraints.Integer](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *T, min T) field.ErrorList {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
if *value < min {
|
||||
return field.ErrorList{field.Invalid(fldPath, *value, content.MinError(min)).WithOrigin("minimum")}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
Copyright 2024 The Kubernetes 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 validate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/operation"
|
||||
"k8s.io/apimachinery/pkg/api/validate/constraints"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
func TestMinimum(t *testing.T) {
|
||||
testMinimumPositive[int](t)
|
||||
testMinimumNegative[int](t)
|
||||
testMinimumPositive[int8](t)
|
||||
testMinimumNegative[int8](t)
|
||||
testMinimumPositive[int16](t)
|
||||
testMinimumNegative[int16](t)
|
||||
testMinimumPositive[int32](t)
|
||||
testMinimumNegative[int32](t)
|
||||
testMinimumPositive[int64](t)
|
||||
testMinimumNegative[int64](t)
|
||||
|
||||
testMinimumPositive[uint](t)
|
||||
testMinimumPositive[uint8](t)
|
||||
testMinimumPositive[uint16](t)
|
||||
testMinimumPositive[uint32](t)
|
||||
testMinimumPositive[uint64](t)
|
||||
}
|
||||
|
||||
type minimumTestCase[T constraints.Integer] struct {
|
||||
value T
|
||||
min T
|
||||
err string // regex
|
||||
}
|
||||
|
||||
func testMinimumPositive[T constraints.Integer](t *testing.T) {
|
||||
t.Helper()
|
||||
cases := []minimumTestCase[T]{{
|
||||
value: 0,
|
||||
min: 0,
|
||||
}, {
|
||||
value: 0,
|
||||
min: 1,
|
||||
err: "fldpath: Invalid value.*must be greater than or equal to",
|
||||
}, {
|
||||
value: 1,
|
||||
min: 1,
|
||||
}, {
|
||||
value: 1,
|
||||
min: 2,
|
||||
err: "fldpath: Invalid value.*must be greater than or equal to",
|
||||
}}
|
||||
doTestMinimum[T](t, cases)
|
||||
}
|
||||
|
||||
func testMinimumNegative[T constraints.Signed](t *testing.T) {
|
||||
t.Helper()
|
||||
cases := []minimumTestCase[T]{{
|
||||
value: -1,
|
||||
min: -1,
|
||||
}, {
|
||||
value: -2,
|
||||
min: -1,
|
||||
err: "fldpath: Invalid value.*must be greater than or equal to",
|
||||
}}
|
||||
|
||||
doTestMinimum[T](t, cases)
|
||||
}
|
||||
|
||||
func doTestMinimum[T constraints.Integer](t *testing.T, cases []minimumTestCase[T]) {
|
||||
t.Helper()
|
||||
for i, tc := range cases {
|
||||
v := tc.value
|
||||
result := Minimum(context.Background(), operation.Operation{}, field.NewPath("fldpath"), &v, nil, tc.min)
|
||||
if len(result) > 0 && tc.err == "" {
|
||||
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
|
||||
continue
|
||||
}
|
||||
if len(result) == 0 && tc.err != "" {
|
||||
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
|
||||
continue
|
||||
}
|
||||
if len(result) > 0 {
|
||||
if len(result) > 1 {
|
||||
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
|
||||
continue
|
||||
}
|
||||
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
|
||||
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
Copyright 2024 The Kubernetes 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 validate
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/operation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
// RequiredValue verifies that the specified value is not the zero-value for
|
||||
// its type.
|
||||
func RequiredValue[T comparable](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *T) field.ErrorList {
|
||||
var zero T
|
||||
if *value != zero {
|
||||
return nil
|
||||
}
|
||||
return field.ErrorList{field.Required(fldPath, "")}
|
||||
}
|
||||
|
||||
// RequiredPointer verifies that the specified pointer is not nil.
|
||||
func RequiredPointer[T any](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *T) field.ErrorList {
|
||||
if value != nil {
|
||||
return nil
|
||||
}
|
||||
return field.ErrorList{field.Required(fldPath, "")}
|
||||
}
|
||||
|
||||
// RequiredSlice verifies that the specified slice is not empty.
|
||||
func RequiredSlice[T any](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ []T) field.ErrorList {
|
||||
if len(value) > 0 {
|
||||
return nil
|
||||
}
|
||||
return field.ErrorList{field.Required(fldPath, "")}
|
||||
}
|
||||
|
||||
// RequiredMap verifies that the specified map is not empty.
|
||||
func RequiredMap[K comparable, T any](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ map[K]T) field.ErrorList {
|
||||
if len(value) > 0 {
|
||||
return nil
|
||||
}
|
||||
return field.ErrorList{field.Required(fldPath, "")}
|
||||
}
|
||||
|
||||
// ForbiddenValue verifies that the specified value is the zero-value for its
|
||||
// type.
|
||||
func ForbiddenValue[T comparable](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *T) field.ErrorList {
|
||||
var zero T
|
||||
if *value == zero {
|
||||
return nil
|
||||
}
|
||||
return field.ErrorList{field.Forbidden(fldPath, "")}
|
||||
}
|
||||
|
||||
// ForbiddenPointer verifies that the specified pointer is nil.
|
||||
func ForbiddenPointer[T any](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *T) field.ErrorList {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return field.ErrorList{field.Forbidden(fldPath, "")}
|
||||
}
|
||||
|
||||
// ForbiddenSlice verifies that the specified slice is empty.
|
||||
func ForbiddenSlice[T any](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ []T) field.ErrorList {
|
||||
if len(value) == 0 {
|
||||
return nil
|
||||
}
|
||||
return field.ErrorList{field.Forbidden(fldPath, "")}
|
||||
}
|
||||
|
||||
// ForbiddenMap verifies that the specified map is empty.
|
||||
func ForbiddenMap[K comparable, T any](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ map[K]T) field.ErrorList {
|
||||
if len(value) == 0 {
|
||||
return nil
|
||||
}
|
||||
return field.ErrorList{field.Forbidden(fldPath, "")}
|
||||
}
|
||||
|
||||
// OptionalValue verifies that the specified value is not the zero-value for
|
||||
// its type. This is identical to RequiredValue, but the caller should treat an
|
||||
// error here as an indication that the optional value was not specified.
|
||||
func OptionalValue[T comparable](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *T) field.ErrorList {
|
||||
var zero T
|
||||
if *value != zero {
|
||||
return nil
|
||||
}
|
||||
return field.ErrorList{field.Required(fldPath, "optional value was not specified")}
|
||||
}
|
||||
|
||||
// OptionalPointer verifies that the specified pointer is not nil. This is
|
||||
// identical to RequiredPointer, but the caller should treat an error here as an
|
||||
// indication that the optional value was not specified.
|
||||
func OptionalPointer[T any](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *T) field.ErrorList {
|
||||
if value != nil {
|
||||
return nil
|
||||
}
|
||||
return field.ErrorList{field.Required(fldPath, "optional value was not specified")}
|
||||
}
|
||||
|
||||
// OptionalSlice verifies that the specified slice is not empty. This is
|
||||
// identical to RequiredSlice, but the caller should treat an error here as an
|
||||
// indication that the optional value was not specified.
|
||||
func OptionalSlice[T any](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ []T) field.ErrorList {
|
||||
if len(value) > 0 {
|
||||
return nil
|
||||
}
|
||||
return field.ErrorList{field.Required(fldPath, "optional value was not specified")}
|
||||
}
|
||||
|
||||
// OptionalMap verifies that the specified map is not empty. This is identical
|
||||
// to RequiredMap, but the caller should treat an error here as an indication that
|
||||
// the optional value was not specified.
|
||||
func OptionalMap[K comparable, T any](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ map[K]T) field.ErrorList {
|
||||
if len(value) > 0 {
|
||||
return nil
|
||||
}
|
||||
return field.ErrorList{field.Required(fldPath, "optional value was not specified")}
|
||||
}
|
|
@ -0,0 +1,924 @@
|
|||
/*
|
||||
Copyright 2024 The Kubernetes 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 validate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/operation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
func TestRequiredValue(t *testing.T) {
|
||||
cases := []struct {
|
||||
fn func(op operation.Operation, fp *field.Path) field.ErrorList
|
||||
err string // regex
|
||||
}{{
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := "value"
|
||||
return RequiredValue(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := "" // zero-value
|
||||
return RequiredValue(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
err: "fldpath: Required value",
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := 123
|
||||
return RequiredValue(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := 0 // zero-value
|
||||
return RequiredValue(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
err: "fldpath: Required value",
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := true
|
||||
return RequiredValue(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := false // zero-value
|
||||
return RequiredValue(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
err: "fldpath: Required value",
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := struct{ S string }{"value"}
|
||||
return RequiredValue(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := struct{ S string }{} // zero-value
|
||||
return RequiredValue(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
err: "fldpath: Required value",
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := ptr.To("")
|
||||
return RequiredValue(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := (*string)(nil) // zero-value
|
||||
return RequiredValue(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
err: "fldpath: Required value",
|
||||
}}
|
||||
|
||||
for i, tc := range cases {
|
||||
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
|
||||
if len(result) > 0 && tc.err == "" {
|
||||
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
|
||||
continue
|
||||
}
|
||||
if len(result) == 0 && tc.err != "" {
|
||||
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
|
||||
continue
|
||||
}
|
||||
if len(result) > 0 {
|
||||
if len(result) > 1 {
|
||||
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
|
||||
continue
|
||||
}
|
||||
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
|
||||
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequiredPointer(t *testing.T) {
|
||||
cases := []struct {
|
||||
fn func(op operation.Operation, fp *field.Path) field.ErrorList
|
||||
err string // regex
|
||||
}{{
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := ""
|
||||
return RequiredPointer(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
pointer := (*string)(nil)
|
||||
return RequiredPointer(context.Background(), op, fp, pointer, nil)
|
||||
},
|
||||
err: "fldpath: Required value",
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := 0
|
||||
return RequiredPointer(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
pointer := (*int)(nil)
|
||||
return RequiredPointer(context.Background(), op, fp, pointer, nil)
|
||||
},
|
||||
err: "fldpath: Required value",
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := false
|
||||
return RequiredPointer(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
pointer := (*bool)(nil)
|
||||
return RequiredPointer(context.Background(), op, fp, pointer, nil)
|
||||
},
|
||||
err: "fldpath: Required value",
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := struct{ S string }{}
|
||||
return RequiredPointer(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
pointer := (*struct{ S string })(nil)
|
||||
return RequiredPointer(context.Background(), op, fp, pointer, nil)
|
||||
},
|
||||
err: "fldpath: Required value",
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := (*string)(nil)
|
||||
return RequiredPointer(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
pointer := (**string)(nil)
|
||||
return RequiredPointer(context.Background(), op, fp, pointer, nil)
|
||||
},
|
||||
err: "fldpath: Required value",
|
||||
}}
|
||||
|
||||
for i, tc := range cases {
|
||||
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
|
||||
if len(result) > 0 && tc.err == "" {
|
||||
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
|
||||
continue
|
||||
}
|
||||
if len(result) == 0 && tc.err != "" {
|
||||
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
|
||||
continue
|
||||
}
|
||||
if len(result) > 0 {
|
||||
if len(result) > 1 {
|
||||
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
|
||||
continue
|
||||
}
|
||||
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
|
||||
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequiredSlice(t *testing.T) {
|
||||
cases := []struct {
|
||||
fn func(op operation.Operation, fp *field.Path) field.ErrorList
|
||||
err string // regex
|
||||
}{{
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := []string{""}
|
||||
return RequiredSlice(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := []string{}
|
||||
return RequiredSlice(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
err: "fldpath: Required value",
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := []int{0}
|
||||
return RequiredSlice(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := []int{}
|
||||
return RequiredSlice(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
err: "fldpath: Required value",
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := []bool{false}
|
||||
return RequiredSlice(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := []bool{}
|
||||
return RequiredSlice(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
err: "fldpath: Required value",
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := []*string{nil}
|
||||
return RequiredSlice(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := []*string{}
|
||||
return RequiredSlice(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
err: "fldpath: Required value",
|
||||
}}
|
||||
|
||||
for i, tc := range cases {
|
||||
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
|
||||
if len(result) > 0 && tc.err == "" {
|
||||
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
|
||||
continue
|
||||
}
|
||||
if len(result) == 0 && tc.err != "" {
|
||||
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
|
||||
continue
|
||||
}
|
||||
if len(result) > 0 {
|
||||
if len(result) > 1 {
|
||||
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
|
||||
continue
|
||||
}
|
||||
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
|
||||
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequiredMap(t *testing.T) {
|
||||
cases := []struct {
|
||||
fn func(op operation.Operation, fp *field.Path) field.ErrorList
|
||||
err string // regex
|
||||
}{{
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := map[string]string{"": ""}
|
||||
return RequiredMap(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := map[string]string{}
|
||||
return RequiredMap(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
err: "fldpath: Required value",
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := map[int]int{0: 0}
|
||||
return RequiredMap(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := map[int]int{}
|
||||
return RequiredMap(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
err: "fldpath: Required value",
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := map[bool]bool{false: false}
|
||||
return RequiredMap(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := map[string]bool{}
|
||||
return RequiredMap(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
err: "fldpath: Required value",
|
||||
}}
|
||||
|
||||
for i, tc := range cases {
|
||||
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
|
||||
if len(result) > 0 && tc.err == "" {
|
||||
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
|
||||
continue
|
||||
}
|
||||
if len(result) == 0 && tc.err != "" {
|
||||
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
|
||||
continue
|
||||
}
|
||||
if len(result) > 0 {
|
||||
if len(result) > 1 {
|
||||
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
|
||||
continue
|
||||
}
|
||||
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
|
||||
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOptionalValue(t *testing.T) {
|
||||
cases := []struct {
|
||||
fn func(op operation.Operation, fp *field.Path) field.ErrorList
|
||||
err string // regex
|
||||
}{{
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := "value"
|
||||
return OptionalValue(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := "" // zero-value
|
||||
return OptionalValue(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
err: "fldpath:.*optional value was not specified",
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := 123
|
||||
return OptionalValue(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := 0 // zero-value
|
||||
return OptionalValue(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
err: "fldpath:.*optional value was not specified",
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := true
|
||||
return OptionalValue(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := false // zero-value
|
||||
return OptionalValue(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
err: "fldpath:.*optional value was not specified",
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := struct{ S string }{"value"}
|
||||
return OptionalValue(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := struct{ S string }{} // zero-value
|
||||
return OptionalValue(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
err: "fldpath:.*optional value was not specified",
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := ptr.To("")
|
||||
return OptionalValue(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := (*string)(nil) // zero-value
|
||||
return OptionalValue(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
err: "fldpath:.*optional value was not specified",
|
||||
}}
|
||||
|
||||
for i, tc := range cases {
|
||||
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
|
||||
if len(result) > 0 && tc.err == "" {
|
||||
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
|
||||
continue
|
||||
}
|
||||
if len(result) == 0 && tc.err != "" {
|
||||
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
|
||||
continue
|
||||
}
|
||||
if len(result) > 0 {
|
||||
if len(result) > 1 {
|
||||
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
|
||||
continue
|
||||
}
|
||||
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
|
||||
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOptionalPointer(t *testing.T) {
|
||||
cases := []struct {
|
||||
fn func(op operation.Operation, fp *field.Path) field.ErrorList
|
||||
err string // regex
|
||||
}{{
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := ""
|
||||
return OptionalPointer(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
pointer := (*string)(nil)
|
||||
return OptionalPointer(context.Background(), op, fp, pointer, nil)
|
||||
},
|
||||
err: "fldpath:.*optional value was not specified",
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := 0
|
||||
return OptionalPointer(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
pointer := (*int)(nil)
|
||||
return OptionalPointer(context.Background(), op, fp, pointer, nil)
|
||||
},
|
||||
err: "fldpath:.*optional value was not specified",
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := false
|
||||
return OptionalPointer(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
pointer := (*bool)(nil)
|
||||
return OptionalPointer(context.Background(), op, fp, pointer, nil)
|
||||
},
|
||||
err: "fldpath:.*optional value was not specified",
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := struct{ S string }{}
|
||||
return OptionalPointer(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
pointer := (*struct{ S string })(nil)
|
||||
return OptionalPointer(context.Background(), op, fp, pointer, nil)
|
||||
},
|
||||
err: "fldpath:.*optional value was not specified",
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := (*string)(nil)
|
||||
return OptionalPointer(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
pointer := (**string)(nil)
|
||||
return OptionalPointer(context.Background(), op, fp, pointer, nil)
|
||||
},
|
||||
err: "fldpath:.*optional value was not specified",
|
||||
}}
|
||||
|
||||
for i, tc := range cases {
|
||||
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
|
||||
if len(result) > 0 && tc.err == "" {
|
||||
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
|
||||
continue
|
||||
}
|
||||
if len(result) == 0 && tc.err != "" {
|
||||
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
|
||||
continue
|
||||
}
|
||||
if len(result) > 0 {
|
||||
if len(result) > 1 {
|
||||
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
|
||||
continue
|
||||
}
|
||||
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
|
||||
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOptionalSlice(t *testing.T) {
|
||||
cases := []struct {
|
||||
fn func(op operation.Operation, fp *field.Path) field.ErrorList
|
||||
err string // regex
|
||||
}{{
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := []string{""}
|
||||
return OptionalSlice(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := []string{}
|
||||
return OptionalSlice(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
err: "fldpath:.*optional value was not specified",
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := []int{0}
|
||||
return OptionalSlice(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := []int{}
|
||||
return OptionalSlice(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
err: "fldpath:.*optional value was not specified",
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := []bool{false}
|
||||
return OptionalSlice(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := []bool{}
|
||||
return OptionalSlice(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
err: "fldpath:.*optional value was not specified",
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := []*string{nil}
|
||||
return OptionalSlice(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := []*string{}
|
||||
return OptionalSlice(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
err: "fldpath:.*optional value was not specified",
|
||||
}}
|
||||
|
||||
for i, tc := range cases {
|
||||
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
|
||||
if len(result) > 0 && tc.err == "" {
|
||||
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
|
||||
continue
|
||||
}
|
||||
if len(result) == 0 && tc.err != "" {
|
||||
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
|
||||
continue
|
||||
}
|
||||
if len(result) > 0 {
|
||||
if len(result) > 1 {
|
||||
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
|
||||
continue
|
||||
}
|
||||
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
|
||||
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOptionalMap(t *testing.T) {
|
||||
cases := []struct {
|
||||
fn func(op operation.Operation, fp *field.Path) field.ErrorList
|
||||
err string // regex
|
||||
}{{
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := map[string]string{"": ""}
|
||||
return OptionalMap(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := map[string]string{}
|
||||
return OptionalMap(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
err: "fldpath:.*optional value was not specified",
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := map[int]int{0: 0}
|
||||
return OptionalMap(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := map[int]int{}
|
||||
return OptionalMap(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
err: "fldpath:.*optional value was not specified",
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := map[bool]bool{false: false}
|
||||
return OptionalMap(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := map[string]bool{}
|
||||
return OptionalMap(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
err: "fldpath:.*optional value was not specified",
|
||||
}}
|
||||
|
||||
for i, tc := range cases {
|
||||
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
|
||||
if len(result) > 0 && tc.err == "" {
|
||||
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
|
||||
continue
|
||||
}
|
||||
if len(result) == 0 && tc.err != "" {
|
||||
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
|
||||
continue
|
||||
}
|
||||
if len(result) > 0 {
|
||||
if len(result) > 1 {
|
||||
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
|
||||
continue
|
||||
}
|
||||
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
|
||||
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestForbiddenValue(t *testing.T) {
|
||||
cases := []struct {
|
||||
fn func(op operation.Operation, fp *field.Path) field.ErrorList
|
||||
err string // regex
|
||||
}{{
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := ""
|
||||
return ForbiddenValue(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := "value"
|
||||
return ForbiddenValue(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
err: "fldpath: Forbidden",
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := 0
|
||||
return ForbiddenValue(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := 123
|
||||
return ForbiddenValue(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
err: "fldpath: Forbidden",
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := false
|
||||
return ForbiddenValue(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := true
|
||||
return ForbiddenValue(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
err: "fldpath: Forbidden",
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := struct{ S string }{}
|
||||
return ForbiddenValue(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := struct{ S string }{"value"}
|
||||
return ForbiddenValue(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
err: "fldpath: Forbidden",
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := (*string)(nil)
|
||||
return ForbiddenValue(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := ptr.To("")
|
||||
return ForbiddenValue(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
err: "fldpath: Forbidden",
|
||||
}}
|
||||
|
||||
for i, tc := range cases {
|
||||
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
|
||||
if len(result) > 0 && tc.err == "" {
|
||||
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
|
||||
continue
|
||||
}
|
||||
if len(result) == 0 && tc.err != "" {
|
||||
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
|
||||
continue
|
||||
}
|
||||
if len(result) > 0 {
|
||||
if len(result) > 1 {
|
||||
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
|
||||
continue
|
||||
}
|
||||
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
|
||||
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestForbiddenPointer(t *testing.T) {
|
||||
cases := []struct {
|
||||
fn func(op operation.Operation, fp *field.Path) field.ErrorList
|
||||
err string // regex
|
||||
}{{
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
pointer := (*string)(nil)
|
||||
return ForbiddenPointer(context.Background(), op, fp, pointer, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := ""
|
||||
return ForbiddenPointer(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
err: "fldpath: Forbidden",
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
pointer := (*int)(nil)
|
||||
return ForbiddenPointer(context.Background(), op, fp, pointer, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := 0
|
||||
return ForbiddenPointer(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
err: "fldpath: Forbidden",
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
pointer := (*bool)(nil)
|
||||
return ForbiddenPointer(context.Background(), op, fp, pointer, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := false
|
||||
return ForbiddenPointer(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
err: "fldpath: Forbidden",
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
pointer := (*struct{ S string })(nil)
|
||||
return ForbiddenPointer(context.Background(), op, fp, pointer, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := struct{ S string }{}
|
||||
return ForbiddenPointer(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
err: "fldpath: Forbidden",
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
pointer := (**string)(nil)
|
||||
return ForbiddenPointer(context.Background(), op, fp, pointer, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := (*string)(nil)
|
||||
return ForbiddenPointer(context.Background(), op, fp, &value, nil)
|
||||
},
|
||||
err: "fldpath: Forbidden",
|
||||
}}
|
||||
|
||||
for i, tc := range cases {
|
||||
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
|
||||
if len(result) > 0 && tc.err == "" {
|
||||
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
|
||||
continue
|
||||
}
|
||||
if len(result) == 0 && tc.err != "" {
|
||||
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
|
||||
continue
|
||||
}
|
||||
if len(result) > 0 {
|
||||
if len(result) > 1 {
|
||||
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
|
||||
continue
|
||||
}
|
||||
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
|
||||
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestForbiddenSlice(t *testing.T) {
|
||||
cases := []struct {
|
||||
fn func(op operation.Operation, fp *field.Path) field.ErrorList
|
||||
err string // regex
|
||||
}{{
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := []string{}
|
||||
return ForbiddenSlice(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := []string{""}
|
||||
return ForbiddenSlice(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
err: "fldpath: Forbidden",
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := []int{}
|
||||
return ForbiddenSlice(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := []int{0}
|
||||
return ForbiddenSlice(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
err: "fldpath: Forbidden",
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := []bool{}
|
||||
return ForbiddenSlice(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := []bool{false}
|
||||
return ForbiddenSlice(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
err: "fldpath: Forbidden",
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := []*string{}
|
||||
return ForbiddenSlice(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := []*string{nil}
|
||||
return ForbiddenSlice(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
err: "fldpath: Forbidden",
|
||||
}}
|
||||
|
||||
for i, tc := range cases {
|
||||
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
|
||||
if len(result) > 0 && tc.err == "" {
|
||||
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
|
||||
continue
|
||||
}
|
||||
if len(result) == 0 && tc.err != "" {
|
||||
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
|
||||
continue
|
||||
}
|
||||
if len(result) > 0 {
|
||||
if len(result) > 1 {
|
||||
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
|
||||
continue
|
||||
}
|
||||
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
|
||||
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestForbiddenMap(t *testing.T) {
|
||||
cases := []struct {
|
||||
fn func(op operation.Operation, fp *field.Path) field.ErrorList
|
||||
err string // regex
|
||||
}{{
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := map[string]string{}
|
||||
return ForbiddenMap(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := map[string]string{"": ""}
|
||||
return ForbiddenMap(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
err: "fldpath: Forbidden",
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := map[int]int{}
|
||||
return ForbiddenMap(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := map[int]int{0: 0}
|
||||
return ForbiddenMap(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
err: "fldpath: Forbidden",
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := map[string]bool{}
|
||||
return ForbiddenMap(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
}, {
|
||||
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||
value := map[bool]bool{false: false}
|
||||
return ForbiddenMap(context.Background(), op, fp, value, nil)
|
||||
},
|
||||
err: "fldpath: Forbidden",
|
||||
}}
|
||||
|
||||
for i, tc := range cases {
|
||||
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
|
||||
if len(result) > 0 && tc.err == "" {
|
||||
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
|
||||
continue
|
||||
}
|
||||
if len(result) == 0 && tc.err != "" {
|
||||
t.Errorf("case %d: unexpected success: expected %q", i, tc.err)
|
||||
continue
|
||||
}
|
||||
if len(result) > 0 {
|
||||
if len(result) > 1 {
|
||||
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
|
||||
continue
|
||||
}
|
||||
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
|
||||
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
Copyright 2025 The Kubernetes 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 validate
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/operation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
// GetFieldFunc is a function that extracts a field from a type and returns a
|
||||
// nilable value.
|
||||
type GetFieldFunc[Tstruct any, Tfield any] func(*Tstruct) Tfield
|
||||
|
||||
// Subfield validates a subfield of a struct against a validator function.
|
||||
func Subfield[Tstruct any, Tfield any](ctx context.Context, op operation.Operation, fldPath *field.Path, newStruct, oldStruct *Tstruct,
|
||||
fldName string, getField GetFieldFunc[Tstruct, Tfield], validator ValidateFunc[Tfield]) field.ErrorList {
|
||||
var errs field.ErrorList
|
||||
newVal := getField(newStruct)
|
||||
var oldVal Tfield
|
||||
if oldStruct != nil {
|
||||
oldVal = getField(oldStruct)
|
||||
}
|
||||
// TODO: passing an equiv function to Subfield for direct comparison instead of
|
||||
// SemanticDeepEqual if fields can be compared directly, to improve performance.
|
||||
if op.Type == operation.Update && SemanticDeepEqual(newVal, oldVal) {
|
||||
return nil
|
||||
}
|
||||
errs = append(errs, validator(ctx, op, fldPath.Child(fldName), newVal, oldVal)...)
|
||||
return errs
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
Copyright 2014 The Kubernetes 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 validate
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/operation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
// FixedResult asserts a fixed boolean result. This is mostly useful for
|
||||
// testing.
|
||||
func FixedResult[T any](_ context.Context, op operation.Operation, fldPath *field.Path, value, _ T, result bool, arg string) field.ErrorList {
|
||||
if result {
|
||||
return nil
|
||||
}
|
||||
return field.ErrorList{
|
||||
field.Invalid(fldPath, value, "forced failure: "+arg).WithOrigin("validateFalse"),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
Copyright 2024 The Kubernetes 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 validate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/operation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
func TestFixedResult(t *testing.T) {
|
||||
cases := []struct {
|
||||
value any
|
||||
pass bool
|
||||
}{{
|
||||
value: "",
|
||||
pass: false,
|
||||
}, {
|
||||
value: "",
|
||||
pass: true,
|
||||
}, {
|
||||
value: "nonempty",
|
||||
pass: false,
|
||||
}, {
|
||||
value: "nonempty",
|
||||
pass: true,
|
||||
}, {
|
||||
value: 0,
|
||||
pass: false,
|
||||
}, {
|
||||
value: 0,
|
||||
pass: true,
|
||||
}, {
|
||||
value: 1,
|
||||
pass: false,
|
||||
}, {
|
||||
value: 1,
|
||||
pass: true,
|
||||
}, {
|
||||
value: false,
|
||||
pass: false,
|
||||
}, {
|
||||
value: false,
|
||||
pass: true,
|
||||
}, {
|
||||
value: true,
|
||||
pass: false,
|
||||
}, {
|
||||
value: true,
|
||||
pass: true,
|
||||
}, {
|
||||
value: nil,
|
||||
pass: false,
|
||||
}, {
|
||||
value: nil,
|
||||
pass: true,
|
||||
}, {
|
||||
value: ptr.To(""),
|
||||
pass: false,
|
||||
}, {
|
||||
value: ptr.To(""),
|
||||
pass: true,
|
||||
}, {
|
||||
value: ptr.To("nonempty"),
|
||||
pass: false,
|
||||
}, {
|
||||
value: ptr.To("nonempty"),
|
||||
pass: true,
|
||||
}, {
|
||||
value: []string(nil),
|
||||
pass: false,
|
||||
}, {
|
||||
value: []string(nil),
|
||||
pass: true,
|
||||
}, {
|
||||
value: []string{},
|
||||
pass: false,
|
||||
}, {
|
||||
value: []string{},
|
||||
pass: true,
|
||||
}, {
|
||||
value: []string{"s"},
|
||||
pass: false,
|
||||
}, {
|
||||
value: []string{"s"},
|
||||
pass: true,
|
||||
}, {
|
||||
value: map[string]string(nil),
|
||||
pass: false,
|
||||
}, {
|
||||
value: map[string]string(nil),
|
||||
pass: true,
|
||||
}, {
|
||||
value: map[string]string{},
|
||||
pass: false,
|
||||
}, {
|
||||
value: map[string]string{},
|
||||
pass: true,
|
||||
}, {
|
||||
value: map[string]string{"k": "v"},
|
||||
pass: false,
|
||||
}, {
|
||||
value: map[string]string{"k": "v"},
|
||||
pass: true,
|
||||
}}
|
||||
|
||||
matcher := field.ErrorMatcher{}.ByOrigin().ByDetailExact()
|
||||
for i, tc := range cases {
|
||||
result := FixedResult(context.Background(), operation.Operation{}, field.NewPath("fldpath"), tc.value, nil, tc.pass, "detail string")
|
||||
if len(result) != 0 && tc.pass {
|
||||
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
|
||||
continue
|
||||
}
|
||||
if len(result) == 0 && !tc.pass {
|
||||
t.Errorf("case %d: unexpected success", i)
|
||||
continue
|
||||
}
|
||||
if len(result) > 0 {
|
||||
if len(result) > 1 {
|
||||
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
|
||||
continue
|
||||
}
|
||||
wantErrorList := field.ErrorList{
|
||||
field.Invalid(field.NewPath("fldpath"), tc.value, "forced failure: detail string").WithOrigin("validateFalse"),
|
||||
}
|
||||
matcher.Test(t, wantErrorList, result)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,212 @@
|
|||
/*
|
||||
Copyright 2024 The Kubernetes 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 validate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/operation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
// ExtractorFn extracts a value from a parent object. Depending on the context,
|
||||
// that could be the value of a field or just whether that field was set or
|
||||
// not.
|
||||
// Note: obj is not guaranteed to be non-nil, need to handle nil obj in the
|
||||
// extractor.
|
||||
type ExtractorFn[T, V any] func(obj T) V
|
||||
|
||||
// UnionValidationOptions configures how union validation behaves
|
||||
type UnionValidationOptions struct {
|
||||
// ErrorForEmpty returns error when no fields are set (nil means no error)
|
||||
ErrorForEmpty func(fldPath *field.Path, allFields []string) *field.Error
|
||||
|
||||
// ErrorForMultiple returns error when multiple fields are set (nil means no error)
|
||||
ErrorForMultiple func(fldPath *field.Path, specifiedFields []string, allFields []string) *field.Error
|
||||
}
|
||||
|
||||
// Union verifies that exactly one member of a union is specified.
|
||||
//
|
||||
// UnionMembership must define all the members of the union.
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// var UnionMembershipForABC := validate.NewUnionMembership([2]string{"a", "A"}, [2]string{"b", "B"}, [2]string{"c", "C"})
|
||||
// func ValidateABC(ctx context.Context, op operation.Operation, fldPath *field.Path, in *ABC) (errs fields.ErrorList) {
|
||||
// errs = append(errs, Union(ctx, op, fldPath, in, oldIn, UnionMembershipForABC,
|
||||
// func(in *ABC) bool { return in.A != nil },
|
||||
// func(in *ABC) bool { return in.B != ""},
|
||||
// func(in *ABC) bool { return in.C != 0 },
|
||||
// )...)
|
||||
// return errs
|
||||
// }
|
||||
func Union[T any](_ context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj T, union *UnionMembership, isSetFns ...ExtractorFn[T, bool]) field.ErrorList {
|
||||
options := UnionValidationOptions{
|
||||
ErrorForEmpty: func(fldPath *field.Path, allFields []string) *field.Error {
|
||||
return field.Invalid(fldPath, "",
|
||||
fmt.Sprintf("must specify one of: %s", strings.Join(allFields, ", ")))
|
||||
},
|
||||
ErrorForMultiple: func(fldPath *field.Path, specifiedFields []string, allFields []string) *field.Error {
|
||||
return field.Invalid(fldPath, fmt.Sprintf("{%s}", strings.Join(specifiedFields, ", ")),
|
||||
fmt.Sprintf("must specify exactly one of: %s", strings.Join(allFields, ", ")))
|
||||
},
|
||||
}
|
||||
|
||||
return unionValidate(op, fldPath, obj, oldObj, union, options, isSetFns...)
|
||||
}
|
||||
|
||||
// DiscriminatedUnion verifies specified union member matches the discriminator.
|
||||
//
|
||||
// UnionMembership must define all the members of the union and the discriminator.
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// var UnionMembershipForABC = validate.NewDiscriminatedUnionMembership("type", [2]string{"a", "A"}, [2]string{"b", "B"}, [2]string{"c", "C"})
|
||||
// func ValidateABC(ctx context.Context, op operation.Operation, fldPath *field.Path, in *ABC) (errs field.ErrorList) {
|
||||
// errs = append(errs, DiscriminatedUnion(ctx, op, fldPath, in, oldIn, UnionMembershipForABC,
|
||||
// func(in *ABC) string { return string(in.Type) },
|
||||
// func(in *ABC) bool { return in.A != nil },
|
||||
// func(in *ABC) bool { return in.B != ""},
|
||||
// func(in *ABC) bool { return in.C != 0 },
|
||||
// )...)
|
||||
// return errs
|
||||
// }
|
||||
//
|
||||
// It is not an error for the discriminatorValue to be unknown. That must be
|
||||
// validated on its own.
|
||||
func DiscriminatedUnion[T any, D ~string](_ context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj T, union *UnionMembership, discriminatorExtractor ExtractorFn[T, D], isSetFns ...ExtractorFn[T, bool]) (errs field.ErrorList) {
|
||||
if len(union.members) != len(isSetFns) {
|
||||
return field.ErrorList{
|
||||
field.InternalError(fldPath,
|
||||
fmt.Errorf("number of extractors (%d) does not match number of union members (%d)",
|
||||
len(isSetFns), len(union.members))),
|
||||
}
|
||||
}
|
||||
var changed bool
|
||||
discriminatorValue := discriminatorExtractor(obj)
|
||||
if op.Type == operation.Update {
|
||||
oldDiscriminatorValue := discriminatorExtractor(oldObj)
|
||||
changed = discriminatorValue != oldDiscriminatorValue
|
||||
}
|
||||
|
||||
for i, fieldIsSet := range isSetFns {
|
||||
member := union.members[i]
|
||||
isDiscriminatedMember := string(discriminatorValue) == member.discriminatorValue
|
||||
newIsSet := fieldIsSet(obj)
|
||||
if op.Type == operation.Update && !changed {
|
||||
oldIsSet := fieldIsSet(oldObj)
|
||||
changed = changed || newIsSet != oldIsSet
|
||||
}
|
||||
if newIsSet && !isDiscriminatedMember {
|
||||
errs = append(errs, field.Invalid(fldPath.Child(member.fieldName), "",
|
||||
fmt.Sprintf("may only be specified when `%s` is %q", union.discriminatorName, member.discriminatorValue)))
|
||||
} else if !newIsSet && isDiscriminatedMember {
|
||||
errs = append(errs, field.Invalid(fldPath.Child(member.fieldName), "",
|
||||
fmt.Sprintf("must be specified when `%s` is %q", union.discriminatorName, discriminatorValue)))
|
||||
}
|
||||
}
|
||||
// If the union discriminator and membership is unchanged, we don't need to
|
||||
// re-validate.
|
||||
if op.Type == operation.Update && !changed {
|
||||
return nil
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
type member struct {
|
||||
fieldName, discriminatorValue string
|
||||
}
|
||||
|
||||
// UnionMembership represents an ordered list of field union memberships.
|
||||
type UnionMembership struct {
|
||||
discriminatorName string
|
||||
members []member
|
||||
}
|
||||
|
||||
// NewUnionMembership returns a new UnionMembership for the given list of members.
|
||||
//
|
||||
// Each member is a [2]string to provide a fieldName and discriminatorValue pair, where
|
||||
// [0] identifies the field name and [1] identifies the union member Name.
|
||||
//
|
||||
// Field names must be unique.
|
||||
func NewUnionMembership(member ...[2]string) *UnionMembership {
|
||||
return NewDiscriminatedUnionMembership("", member...)
|
||||
}
|
||||
|
||||
// NewDiscriminatedUnionMembership returns a new UnionMembership for the given discriminator field and list of members.
|
||||
// members are provided in the same way as for NewUnionMembership.
|
||||
func NewDiscriminatedUnionMembership(discriminatorFieldName string, members ...[2]string) *UnionMembership {
|
||||
u := &UnionMembership{}
|
||||
u.discriminatorName = discriminatorFieldName
|
||||
for _, fieldName := range members {
|
||||
u.members = append(u.members, member{fieldName: fieldName[0], discriminatorValue: fieldName[1]})
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
// allFields returns a string listing all the field names of the member of a union for use in error reporting.
|
||||
func (u UnionMembership) allFields() []string {
|
||||
memberNames := make([]string, 0, len(u.members))
|
||||
for _, f := range u.members {
|
||||
memberNames = append(memberNames, fmt.Sprintf("`%s`", f.fieldName))
|
||||
}
|
||||
return memberNames
|
||||
}
|
||||
|
||||
func unionValidate[T any](op operation.Operation, fldPath *field.Path,
|
||||
obj, oldObj T, union *UnionMembership, options UnionValidationOptions, isSetFns ...ExtractorFn[T, bool],
|
||||
) field.ErrorList {
|
||||
if len(union.members) != len(isSetFns) {
|
||||
return field.ErrorList{
|
||||
field.InternalError(fldPath,
|
||||
fmt.Errorf("number of extractors (%d) does not match number of union members (%d)",
|
||||
len(isSetFns), len(union.members))),
|
||||
}
|
||||
}
|
||||
|
||||
var specifiedFields []string
|
||||
var changed bool
|
||||
for i, fieldIsSet := range isSetFns {
|
||||
newIsSet := fieldIsSet(obj)
|
||||
if op.Type == operation.Update && !changed {
|
||||
oldIsSet := fieldIsSet(oldObj)
|
||||
changed = changed || newIsSet != oldIsSet
|
||||
}
|
||||
if newIsSet {
|
||||
specifiedFields = append(specifiedFields, union.members[i].fieldName)
|
||||
}
|
||||
}
|
||||
|
||||
// If the union membership is unchanged, we don't need to re-validate.
|
||||
if op.Type == operation.Update && !changed {
|
||||
return nil
|
||||
}
|
||||
|
||||
var errs field.ErrorList
|
||||
|
||||
if len(specifiedFields) > 1 && options.ErrorForMultiple != nil {
|
||||
errs = append(errs, options.ErrorForMultiple(fldPath, specifiedFields, union.allFields()))
|
||||
}
|
||||
|
||||
if len(specifiedFields) == 0 && options.ErrorForEmpty != nil {
|
||||
errs = append(errs, options.ErrorForEmpty(fldPath, union.allFields()))
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
|
@ -0,0 +1,328 @@
|
|||
/*
|
||||
Copyright 2024 The Kubernetes 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 validate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/operation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
type testMember struct{}
|
||||
|
||||
func TestUnion(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
fields [][2]string
|
||||
fieldValues []bool
|
||||
expected field.ErrorList
|
||||
}{
|
||||
{
|
||||
name: "one member set",
|
||||
fields: [][2]string{{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}},
|
||||
fieldValues: []bool{false, false, false, true},
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "two members set",
|
||||
fields: [][2]string{{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}},
|
||||
fieldValues: []bool{false, true, false, true},
|
||||
expected: field.ErrorList{field.Invalid(nil, "{b, d}", "must specify exactly one of: `a`, `b`, `c`, `d`")},
|
||||
},
|
||||
{
|
||||
name: "all members set",
|
||||
fields: [][2]string{{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}},
|
||||
fieldValues: []bool{true, true, true, true},
|
||||
expected: field.ErrorList{field.Invalid(nil, "{a, b, c, d}", "must specify exactly one of: `a`, `b`, `c`, `d`")},
|
||||
},
|
||||
{
|
||||
name: "no member set",
|
||||
fields: [][2]string{{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}},
|
||||
fieldValues: []bool{false, false, false, false},
|
||||
expected: field.ErrorList{field.Invalid(nil, "", "must specify one of: `a`, `b`, `c`, `d`")},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Create mock extractors that return predefined values instead of
|
||||
// actually extracting from the object.
|
||||
extractors := make([]ExtractorFn[*testMember, bool], len(tc.fieldValues))
|
||||
for i, val := range tc.fieldValues {
|
||||
extractors[i] = func(_ *testMember) bool { return val }
|
||||
}
|
||||
|
||||
got := Union(context.Background(), operation.Operation{}, nil, &testMember{}, nil, NewUnionMembership(tc.fields...), extractors...)
|
||||
if !reflect.DeepEqual(got, tc.expected) {
|
||||
t.Errorf("got %v want %v", got, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscriminatedUnion(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
discriminatorField string
|
||||
fields [][2]string
|
||||
discriminatorValue string
|
||||
fieldValues []bool
|
||||
expected field.ErrorList
|
||||
}{
|
||||
{
|
||||
name: "valid discriminated union A",
|
||||
discriminatorField: "d",
|
||||
fields: [][2]string{{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}},
|
||||
discriminatorValue: "A",
|
||||
fieldValues: []bool{true, false, false, false},
|
||||
},
|
||||
{
|
||||
name: "valid discriminated union C",
|
||||
discriminatorField: "d",
|
||||
fields: [][2]string{{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}},
|
||||
discriminatorValue: "C",
|
||||
fieldValues: []bool{false, false, true, false},
|
||||
},
|
||||
{
|
||||
name: "invalid, discriminator not set to member that is specified",
|
||||
discriminatorField: "type",
|
||||
fields: [][2]string{{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}},
|
||||
discriminatorValue: "C",
|
||||
fieldValues: []bool{false, true, false, false},
|
||||
expected: field.ErrorList{
|
||||
field.Invalid(field.NewPath("b"), "", "may only be specified when `type` is \"B\""),
|
||||
field.Invalid(field.NewPath("c"), "", "must be specified when `type` is \"C\""),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid, discriminator correct, multiple members set",
|
||||
discriminatorField: "type",
|
||||
fields: [][2]string{{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}},
|
||||
discriminatorValue: "C",
|
||||
fieldValues: []bool{false, true, true, true},
|
||||
expected: field.ErrorList{
|
||||
field.Invalid(field.NewPath("b"), "", "may only be specified when `type` is \"B\""),
|
||||
field.Invalid(field.NewPath("d"), "", "may only be specified when `type` is \"D\""),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
discriminatorExtractor := func(_ *testMember) string { return tc.discriminatorValue }
|
||||
|
||||
// Create mock extractors that return predefined values instead of
|
||||
// actually extracting from the object.
|
||||
extractors := make([]ExtractorFn[*testMember, bool], len(tc.fieldValues))
|
||||
for i, val := range tc.fieldValues {
|
||||
extractors[i] = func(_ *testMember) bool { return val }
|
||||
}
|
||||
|
||||
got := DiscriminatedUnion(context.Background(), operation.Operation{}, nil, &testMember{}, nil, NewDiscriminatedUnionMembership(tc.discriminatorField, tc.fields...), discriminatorExtractor, extractors...)
|
||||
if !reflect.DeepEqual(got, tc.expected) {
|
||||
t.Errorf("got %v want %v", got.ToAggregate(), tc.expected.ToAggregate())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type testStruct struct {
|
||||
M1 *m1 `json:"m1"`
|
||||
M2 *m2 `json:"m2"`
|
||||
}
|
||||
|
||||
type m1 struct{}
|
||||
type m2 struct{}
|
||||
|
||||
var extractors = []ExtractorFn[*testStruct, bool]{
|
||||
func(s *testStruct) bool {
|
||||
if s == nil {
|
||||
return false
|
||||
}
|
||||
return s.M1 != nil
|
||||
},
|
||||
func(s *testStruct) bool {
|
||||
if s == nil {
|
||||
return false
|
||||
}
|
||||
return s.M2 != nil
|
||||
},
|
||||
}
|
||||
|
||||
func TestUnionRatcheting(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
oldStruct *testStruct
|
||||
newStruct *testStruct
|
||||
expected field.ErrorList
|
||||
}{
|
||||
{
|
||||
name: "both nil",
|
||||
oldStruct: nil,
|
||||
newStruct: nil,
|
||||
},
|
||||
{
|
||||
name: "both empty struct",
|
||||
oldStruct: &testStruct{},
|
||||
newStruct: &testStruct{},
|
||||
},
|
||||
{
|
||||
name: "both have more than one member",
|
||||
oldStruct: &testStruct{
|
||||
M1: &m1{},
|
||||
M2: &m2{},
|
||||
},
|
||||
newStruct: &testStruct{
|
||||
M1: &m1{},
|
||||
M2: &m2{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "change to invalid",
|
||||
oldStruct: &testStruct{
|
||||
M1: &m1{},
|
||||
},
|
||||
newStruct: &testStruct{
|
||||
M1: &m1{},
|
||||
M2: &m2{},
|
||||
},
|
||||
expected: field.ErrorList{
|
||||
field.Invalid(nil, "{m1, m2}", "must specify exactly one of: `m1`, `m2`"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := Union(context.Background(), operation.Operation{Type: operation.Update}, nil, tc.newStruct, tc.oldStruct, NewUnionMembership([][2]string{{"m1", "m1"}, {"m2", "m2"}}...), extractors...)
|
||||
if !reflect.DeepEqual(got, tc.expected) {
|
||||
t.Errorf("got %v want %v", got, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type testDiscriminatedStruct struct {
|
||||
D string `json:"d"`
|
||||
M1 *m1 `json:"m1"`
|
||||
M2 *m2 `json:"m2"`
|
||||
}
|
||||
|
||||
var testDiscriminatorExtractor = func(s *testDiscriminatedStruct) string {
|
||||
if s != nil {
|
||||
return s.D
|
||||
}
|
||||
return ""
|
||||
}
|
||||
var testDiscriminatedExtractors = []ExtractorFn[*testDiscriminatedStruct, bool]{
|
||||
func(s *testDiscriminatedStruct) bool {
|
||||
if s == nil {
|
||||
return false
|
||||
}
|
||||
return s.M1 != nil
|
||||
},
|
||||
func(s *testDiscriminatedStruct) bool {
|
||||
if s == nil {
|
||||
return false
|
||||
}
|
||||
return s.M2 != nil
|
||||
},
|
||||
}
|
||||
|
||||
func TestDiscriminatedUnionRatcheting(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
oldStruct *testDiscriminatedStruct
|
||||
newStruct *testDiscriminatedStruct
|
||||
expected field.ErrorList
|
||||
}{
|
||||
{
|
||||
name: "pass with both nil",
|
||||
},
|
||||
{
|
||||
name: "pass with both empty struct",
|
||||
oldStruct: &testDiscriminatedStruct{},
|
||||
newStruct: &testDiscriminatedStruct{},
|
||||
},
|
||||
{
|
||||
name: "pass with both not set to member that is specified",
|
||||
oldStruct: &testDiscriminatedStruct{
|
||||
D: "m1",
|
||||
M2: &m2{},
|
||||
},
|
||||
newStruct: &testDiscriminatedStruct{
|
||||
D: "m1",
|
||||
M2: &m2{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pass with both set to more than one member",
|
||||
oldStruct: &testDiscriminatedStruct{
|
||||
D: "m1",
|
||||
M1: &m1{},
|
||||
M2: &m2{},
|
||||
},
|
||||
newStruct: &testDiscriminatedStruct{
|
||||
D: "m1",
|
||||
M1: &m1{},
|
||||
M2: &m2{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "fail on changing to invalid with both set",
|
||||
oldStruct: &testDiscriminatedStruct{
|
||||
D: "m1",
|
||||
M1: &m1{},
|
||||
},
|
||||
newStruct: &testDiscriminatedStruct{
|
||||
D: "m1",
|
||||
M1: &m1{},
|
||||
M2: &m2{},
|
||||
},
|
||||
expected: field.ErrorList{
|
||||
field.Invalid(field.NewPath("m2"), "", "may only be specified when `d` is \"m2\""),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "fail on changing the discriminator",
|
||||
oldStruct: &testDiscriminatedStruct{
|
||||
D: "m1",
|
||||
M1: &m1{},
|
||||
},
|
||||
newStruct: &testDiscriminatedStruct{
|
||||
D: "m2",
|
||||
M1: &m1{},
|
||||
},
|
||||
expected: field.ErrorList{
|
||||
field.Invalid(field.NewPath("m1"), "", "may only be specified when `d` is \"m1\""),
|
||||
field.Invalid(field.NewPath("m2"), "", "must be specified when `d` is \"m2\""),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := DiscriminatedUnion(context.Background(), operation.Operation{Type: operation.Update}, nil, tc.newStruct, tc.oldStruct, NewDiscriminatedUnionMembership("d", [][2]string{{"m1", "m1"}, {"m2", "m2"}}...), testDiscriminatorExtractor, testDiscriminatedExtractors...)
|
||||
if !reflect.DeepEqual(got, tc.expected) {
|
||||
t.Errorf("got %v want %v", got, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
Copyright 2024 The Kubernetes 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 validate
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strconv"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
// fmtErrs is a helper for nicer test output. It will use multiple lines if
|
||||
// errs has more than 1 item.
|
||||
func fmtErrs(errs field.ErrorList) string {
|
||||
if len(errs) == 0 {
|
||||
return "<no errors>"
|
||||
}
|
||||
if len(errs) == 1 {
|
||||
return strconv.Quote(errs[0].Error())
|
||||
}
|
||||
buf := bytes.Buffer{}
|
||||
for _, e := range errs {
|
||||
buf.WriteString("\n")
|
||||
buf.WriteString(strconv.Quote(e.Error()))
|
||||
}
|
||||
return buf.String()
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
Copyright 2024 The Kubernetes 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 validate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/operation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
// ZeroOrOneOfUnion verifies that at most one member of a union is specified.
|
||||
//
|
||||
// ZeroOrOneOfMembership must define all the members of the union.
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// var ZeroOrOneOfMembershipForABC = validate.NewUnionMembership([2]string{"a", "A"}, [2]string{"b", "B"}, [2]string{"c", "C"})
|
||||
// func ValidateABC(ctx context.Context, op operation.Operation, fldPath *field.Path, in *ABC) (errs field.ErrorList) {
|
||||
// errs = append(errs, ZeroOrOneOfUnion(ctx, op, fldPath, in, oldIn, UnionMembershipForABC,
|
||||
// func(in *ABC) bool { return in.A != nil },
|
||||
// func(in *ABC) bool { return in.B != ""},
|
||||
// func(in *ABC) bool { return in.C != 0 },
|
||||
// )...)
|
||||
// return errs
|
||||
// }
|
||||
func ZeroOrOneOfUnion[T any](_ context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj T, union *UnionMembership, isSetFns ...ExtractorFn[T, bool]) field.ErrorList {
|
||||
options := UnionValidationOptions{
|
||||
ErrorForEmpty: nil,
|
||||
ErrorForMultiple: func(fldPath *field.Path, specifiedFields []string, allFields []string) *field.Error {
|
||||
return field.Invalid(fldPath, fmt.Sprintf("{%s}", strings.Join(specifiedFields, ", ")),
|
||||
fmt.Sprintf("must specify at most one of: %s", strings.Join(allFields, ", "))).WithOrigin("zeroOrOneOf")
|
||||
},
|
||||
}
|
||||
|
||||
errs := unionValidate(op, fldPath, obj, oldObj, union, options, isSetFns...)
|
||||
return errs
|
||||
}
|
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
Copyright 2024 The Kubernetes 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 validate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/operation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
func TestZeroOrOneOfUnion(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
fields [][2]string
|
||||
fieldValues []bool
|
||||
expected field.ErrorList
|
||||
}{
|
||||
{
|
||||
name: "one member set",
|
||||
fields: [][2]string{{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}},
|
||||
fieldValues: []bool{false, false, false, true},
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "two members set",
|
||||
fields: [][2]string{{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}},
|
||||
fieldValues: []bool{false, true, false, true},
|
||||
expected: field.ErrorList{field.Invalid(nil, "{b, d}", "must specify at most one of: `a`, `b`, `c`, `d`").WithOrigin("zeroOrOneOf")},
|
||||
},
|
||||
{
|
||||
name: "all members set",
|
||||
fields: [][2]string{{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}},
|
||||
fieldValues: []bool{true, true, true, true},
|
||||
expected: field.ErrorList{field.Invalid(nil, "{a, b, c, d}", "must specify at most one of: `a`, `b`, `c`, `d`").WithOrigin("zeroOrOneOf")},
|
||||
},
|
||||
{
|
||||
name: "no member set - allowed for ZeroOrOneOf",
|
||||
fields: [][2]string{{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}},
|
||||
fieldValues: []bool{false, false, false, false},
|
||||
expected: nil, // This is the key difference from Union
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Create mock extractors that return predefined values instead of
|
||||
// actually extracting from the object.
|
||||
extractors := make([]ExtractorFn[*testMember, bool], len(tc.fieldValues))
|
||||
for i, val := range tc.fieldValues {
|
||||
extractors[i] = func(_ *testMember) bool { return val }
|
||||
}
|
||||
|
||||
got := ZeroOrOneOfUnion(context.Background(), operation.Operation{}, nil, &testMember{}, nil, NewUnionMembership(tc.fields...), extractors...)
|
||||
if !reflect.DeepEqual(got, tc.expected) {
|
||||
t.Errorf("got %v want %v", got, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestZeroOrOneOfUnionRatcheting(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
oldStruct *testStruct
|
||||
newStruct *testStruct
|
||||
expected field.ErrorList
|
||||
}{
|
||||
{
|
||||
name: "both nil",
|
||||
oldStruct: nil,
|
||||
newStruct: nil,
|
||||
},
|
||||
{
|
||||
name: "both empty struct - allowed for ZeroOrOneOf",
|
||||
oldStruct: &testStruct{},
|
||||
newStruct: &testStruct{},
|
||||
},
|
||||
{
|
||||
name: "both have more than one member",
|
||||
oldStruct: &testStruct{
|
||||
M1: &m1{},
|
||||
M2: &m2{},
|
||||
},
|
||||
newStruct: &testStruct{
|
||||
M1: &m1{},
|
||||
M2: &m2{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "change to invalid",
|
||||
oldStruct: &testStruct{
|
||||
M1: &m1{},
|
||||
},
|
||||
newStruct: &testStruct{
|
||||
M1: &m1{},
|
||||
M2: &m2{},
|
||||
},
|
||||
expected: field.ErrorList{
|
||||
field.Invalid(nil, "{m1, m2}", "must specify at most one of: `m1`, `m2`").WithOrigin("zeroOrOneOf"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "change from empty to one member - allowed",
|
||||
oldStruct: &testStruct{},
|
||||
newStruct: &testStruct{
|
||||
M1: &m1{},
|
||||
},
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "change from one member to empty - allowed",
|
||||
oldStruct: &testStruct{
|
||||
M1: &m1{},
|
||||
},
|
||||
newStruct: &testStruct{},
|
||||
expected: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := ZeroOrOneOfUnion(context.Background(), operation.Operation{Type: operation.Update}, nil, tc.newStruct, tc.oldStruct, NewUnionMembership([][2]string{{"m1", "m1"}, {"m2", "m2"}}...), extractors...)
|
||||
if !reflect.DeepEqual(got, tc.expected) {
|
||||
t.Errorf("got %v want %v", got, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -15,4 +15,4 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
// Package validation contains generic api type validation functions.
|
||||
package validation // import "k8s.io/apimachinery/pkg/api/validation"
|
||||
package validation
|
||||
|
|
|
@ -82,7 +82,7 @@ func maskTrailingDash(name string) string {
|
|||
func ValidateNonnegativeField(value int64, fldPath *field.Path) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
if value < 0 {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, value, IsNegativeErrorMsg))
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, value, IsNegativeErrorMsg).WithOrigin("minimum"))
|
||||
}
|
||||
return allErrs
|
||||
}
|
||||
|
|
|
@ -74,13 +74,13 @@ func validateOwnerReference(ownerReference metav1.OwnerReference, fldPath *field
|
|||
allErrs = append(allErrs, field.Invalid(fldPath.Child("apiVersion"), ownerReference.APIVersion, "version must not be empty"))
|
||||
}
|
||||
if len(gvk.Kind) == 0 {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("kind"), ownerReference.Kind, "kind must not be empty"))
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("kind"), ownerReference.Kind, "must not be empty"))
|
||||
}
|
||||
if len(ownerReference.Name) == 0 {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("name"), ownerReference.Name, "name must not be empty"))
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("name"), ownerReference.Name, "must not be empty"))
|
||||
}
|
||||
if len(ownerReference.UID) == 0 {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("uid"), ownerReference.UID, "uid must not be empty"))
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("uid"), ownerReference.UID, "must not be empty"))
|
||||
}
|
||||
if _, ok := BannedOwners[gvk]; ok {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, ownerReference, fmt.Sprintf("%s is disallowed from being an owner", gvk)))
|
||||
|
|
|
@ -328,19 +328,19 @@ func TestValidateObjectMetaUpdatePreventsDeletionFieldMutation(t *testing.T) {
|
|||
Old: metav1.ObjectMeta{Name: "test", ResourceVersion: "1"},
|
||||
New: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionTimestamp: &now},
|
||||
ExpectedNew: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionTimestamp: &now},
|
||||
ExpectedErrs: []string{"field.deletionTimestamp: Invalid value: 1970-01-01 00:16:40 +0000 UTC: field is immutable"},
|
||||
ExpectedErrs: []string{"field.deletionTimestamp: Invalid value: \"1970-01-01T00:16:40Z\": field is immutable"},
|
||||
},
|
||||
"invalid clear deletionTimestamp": {
|
||||
Old: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionTimestamp: &now},
|
||||
New: metav1.ObjectMeta{Name: "test", ResourceVersion: "1"},
|
||||
ExpectedNew: metav1.ObjectMeta{Name: "test", ResourceVersion: "1"},
|
||||
ExpectedErrs: []string{"field.deletionTimestamp: Invalid value: \"null\": field is immutable"},
|
||||
ExpectedErrs: []string{"field.deletionTimestamp: Invalid value: null: field is immutable"},
|
||||
},
|
||||
"invalid change deletionTimestamp": {
|
||||
Old: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionTimestamp: &now},
|
||||
New: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionTimestamp: &later},
|
||||
ExpectedNew: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionTimestamp: &later},
|
||||
ExpectedErrs: []string{"field.deletionTimestamp: Invalid value: 1970-01-01 00:33:20 +0000 UTC: field is immutable"},
|
||||
ExpectedErrs: []string{"field.deletionTimestamp: Invalid value: \"1970-01-01T00:33:20Z\": field is immutable"},
|
||||
},
|
||||
|
||||
"invalid set deletionGracePeriodSeconds": {
|
||||
|
@ -353,7 +353,7 @@ func TestValidateObjectMetaUpdatePreventsDeletionFieldMutation(t *testing.T) {
|
|||
Old: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionGracePeriodSeconds: &gracePeriodShort},
|
||||
New: metav1.ObjectMeta{Name: "test", ResourceVersion: "1"},
|
||||
ExpectedNew: metav1.ObjectMeta{Name: "test", ResourceVersion: "1"},
|
||||
ExpectedErrs: []string{"field.deletionGracePeriodSeconds: Invalid value: \"null\": field is immutable"},
|
||||
ExpectedErrs: []string{"field.deletionGracePeriodSeconds: Invalid value: null: field is immutable"},
|
||||
},
|
||||
"invalid change deletionGracePeriodSeconds": {
|
||||
Old: metav1.ObjectMeta{Name: "test", ResourceVersion: "1", DeletionGracePeriodSeconds: &gracePeriodShort},
|
||||
|
@ -373,7 +373,7 @@ func TestValidateObjectMetaUpdatePreventsDeletionFieldMutation(t *testing.T) {
|
|||
}
|
||||
for i := range errs {
|
||||
if errs[i].Error() != tc.ExpectedErrs[i] {
|
||||
t.Errorf("%s: error #%d: expected %q, got %q", k, i, tc.ExpectedErrs[i], errs[i].Error())
|
||||
t.Errorf("%s: error #%d:\n expected: %q\n got: %q", k, i, tc.ExpectedErrs[i], errs[i].Error())
|
||||
}
|
||||
}
|
||||
if !reflect.DeepEqual(tc.New, tc.ExpectedNew) {
|
||||
|
@ -419,7 +419,7 @@ func TestObjectMetaGenerationUpdate(t *testing.T) {
|
|||
}
|
||||
for i := range errList {
|
||||
if errList[i] != tc.ExpectedErrs[i] {
|
||||
t.Errorf("%s: error #%d: expected %q, got %q", k, i, tc.ExpectedErrs[i], errList[i])
|
||||
t.Errorf("%s: error #%d:\n expected: %q\n got: %q", k, i, tc.ExpectedErrs[i], errs[i].Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
Copyright 2024 The Kubernetes 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 asn1
|
||||
|
||||
import "encoding/asn1"
|
||||
|
||||
// These constants store suffixes for use with the CNCF Private Enterprise Number allocated to Kubernetes:
|
||||
// https://www.iana.org/assignments/enterprise-numbers.txt
|
||||
//
|
||||
// Root: 1.3.6.1.4.1.57683
|
||||
//
|
||||
// Cloud Native Computing Foundation
|
||||
const (
|
||||
// single-value, string value
|
||||
x509UIDSuffix = 2
|
||||
)
|
||||
|
||||
func makeOID(suffix int) asn1.ObjectIdentifier {
|
||||
return asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57683, suffix}
|
||||
}
|
||||
|
||||
// X509UID returns an OID (1.3.6.1.4.1.57683.2) for an element of an x509 distinguished name representing a user UID.
|
||||
// The UID is a unique value for a particular user that will change if the user is removed from the system
|
||||
// and another user is added with the same username.
|
||||
//
|
||||
// This element must not appear more than once in a distinguished name, and the value must be a string
|
||||
func X509UID() asn1.ObjectIdentifier {
|
||||
return makeOID(x509UIDSuffix)
|
||||
}
|
|
@ -23,7 +23,7 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
fuzz "github.com/google/gofuzz"
|
||||
"sigs.k8s.io/randfill"
|
||||
|
||||
apitesting "k8s.io/apimachinery/pkg/api/apitesting"
|
||||
"k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
|
||||
|
@ -38,29 +38,29 @@ import (
|
|||
|
||||
func genericFuzzerFuncs(codecs runtimeserializer.CodecFactory) []interface{} {
|
||||
return []interface{}{
|
||||
func(q *resource.Quantity, c fuzz.Continue) {
|
||||
func(q *resource.Quantity, c randfill.Continue) {
|
||||
*q = *resource.NewQuantity(c.Int63n(1000), resource.DecimalExponent)
|
||||
},
|
||||
func(j *int, c fuzz.Continue) {
|
||||
func(j *int, c randfill.Continue) {
|
||||
*j = int(c.Int31())
|
||||
},
|
||||
func(j **int, c fuzz.Continue) {
|
||||
if c.RandBool() {
|
||||
func(j **int, c randfill.Continue) {
|
||||
if c.Bool() {
|
||||
i := int(c.Int31())
|
||||
*j = &i
|
||||
} else {
|
||||
*j = nil
|
||||
}
|
||||
},
|
||||
func(j *runtime.TypeMeta, c fuzz.Continue) {
|
||||
func(j *runtime.TypeMeta, c randfill.Continue) {
|
||||
// We have to customize the randomization of TypeMetas because their
|
||||
// APIVersion and Kind must remain blank in memory.
|
||||
j.APIVersion = ""
|
||||
j.Kind = ""
|
||||
},
|
||||
func(j *runtime.Object, c fuzz.Continue) {
|
||||
func(j *runtime.Object, c randfill.Continue) {
|
||||
// TODO: uncomment when round trip starts from a versioned object
|
||||
if true { //c.RandBool() {
|
||||
if true { // c.Bool() {
|
||||
*j = &runtime.Unknown{
|
||||
// We do not set TypeMeta here because it is not carried through a round trip
|
||||
Raw: []byte(`{"apiVersion":"unknown.group/unknown","kind":"Something","someKey":"someValue"}`),
|
||||
|
@ -69,15 +69,15 @@ func genericFuzzerFuncs(codecs runtimeserializer.CodecFactory) []interface{} {
|
|||
} else {
|
||||
types := []runtime.Object{&metav1.Status{}, &metav1.APIGroup{}}
|
||||
t := types[c.Rand.Intn(len(types))]
|
||||
c.Fuzz(t)
|
||||
c.Fill(t)
|
||||
*j = t
|
||||
}
|
||||
},
|
||||
func(r *runtime.RawExtension, c fuzz.Continue) {
|
||||
func(r *runtime.RawExtension, c randfill.Continue) {
|
||||
// Pick an arbitrary type and fuzz it
|
||||
types := []runtime.Object{&metav1.Status{}, &metav1.APIGroup{}}
|
||||
obj := types[c.Rand.Intn(len(types))]
|
||||
c.Fuzz(obj)
|
||||
c.Fill(obj)
|
||||
|
||||
// Find a codec for converting the object to raw bytes. This is necessary for the
|
||||
// api version and kind to be correctly set be serialization.
|
||||
|
@ -100,7 +100,7 @@ func genericFuzzerFuncs(codecs runtimeserializer.CodecFactory) []interface{} {
|
|||
}
|
||||
}
|
||||
|
||||
// taken from gofuzz internals for RandString
|
||||
// taken from randfill (nee gofuzz) internals for RandString
|
||||
type charRange struct {
|
||||
first, last rune
|
||||
}
|
||||
|
@ -114,7 +114,7 @@ func (c *charRange) choose(r *rand.Rand) rune {
|
|||
|
||||
// randomLabelPart produces a valid random label value or name-part
|
||||
// of a label key.
|
||||
func randomLabelPart(c fuzz.Continue, canBeEmpty bool) string {
|
||||
func randomLabelPart(c randfill.Continue, canBeEmpty bool) string {
|
||||
validStartEnd := []charRange{{'0', '9'}, {'a', 'z'}, {'A', 'Z'}}
|
||||
validMiddle := []charRange{{'0', '9'}, {'a', 'z'}, {'A', 'Z'},
|
||||
{'.', '.'}, {'-', '-'}, {'_', '_'}}
|
||||
|
@ -138,7 +138,7 @@ func randomLabelPart(c fuzz.Continue, canBeEmpty bool) string {
|
|||
return string(runes)
|
||||
}
|
||||
|
||||
func randomDNSLabel(c fuzz.Continue) string {
|
||||
func randomDNSLabel(c randfill.Continue) string {
|
||||
validStartEnd := []charRange{{'0', '9'}, {'a', 'z'}}
|
||||
validMiddle := []charRange{{'0', '9'}, {'a', 'z'}, {'-', '-'}}
|
||||
|
||||
|
@ -154,11 +154,11 @@ func randomDNSLabel(c fuzz.Continue) string {
|
|||
return string(runes)
|
||||
}
|
||||
|
||||
func randomLabelKey(c fuzz.Continue) string {
|
||||
func randomLabelKey(c randfill.Continue) string {
|
||||
namePart := randomLabelPart(c, false)
|
||||
prefixPart := ""
|
||||
|
||||
usePrefix := c.RandBool()
|
||||
usePrefix := c.Bool()
|
||||
if usePrefix {
|
||||
// we can fit, with dots, at most 3 labels in the 253 allotted characters
|
||||
prefixPartsLen := c.Rand.Intn(2) + 1
|
||||
|
@ -175,28 +175,28 @@ func randomLabelKey(c fuzz.Continue) string {
|
|||
func v1FuzzerFuncs(codecs runtimeserializer.CodecFactory) []interface{} {
|
||||
|
||||
return []interface{}{
|
||||
func(j *metav1.TypeMeta, c fuzz.Continue) {
|
||||
func(j *metav1.TypeMeta, c randfill.Continue) {
|
||||
// We have to customize the randomization of TypeMetas because their
|
||||
// APIVersion and Kind must remain blank in memory.
|
||||
j.APIVersion = ""
|
||||
j.Kind = ""
|
||||
},
|
||||
func(j *metav1.ObjectMeta, c fuzz.Continue) {
|
||||
c.FuzzNoCustom(j)
|
||||
func(j *metav1.ObjectMeta, c randfill.Continue) {
|
||||
c.FillNoCustom(j)
|
||||
|
||||
j.ResourceVersion = strconv.FormatUint(c.RandUint64(), 10)
|
||||
j.UID = types.UID(c.RandString())
|
||||
j.ResourceVersion = strconv.FormatUint(c.Uint64(), 10)
|
||||
j.UID = types.UID(c.String(0))
|
||||
|
||||
// Fuzzing sec and nsec in a smaller range (uint32 instead of int64),
|
||||
// so that the result Unix time is a valid date and can be parsed into RFC3339 format.
|
||||
var sec, nsec uint32
|
||||
c.Fuzz(&sec)
|
||||
c.Fuzz(&nsec)
|
||||
c.Fill(&sec)
|
||||
c.Fill(&nsec)
|
||||
j.CreationTimestamp = metav1.Unix(int64(sec), int64(nsec)).Rfc3339Copy()
|
||||
|
||||
if j.DeletionTimestamp != nil {
|
||||
c.Fuzz(&sec)
|
||||
c.Fuzz(&nsec)
|
||||
c.Fill(&sec)
|
||||
c.Fill(&nsec)
|
||||
t := metav1.Unix(int64(sec), int64(nsec)).Rfc3339Copy()
|
||||
j.DeletionTimestamp = &t
|
||||
}
|
||||
|
@ -218,16 +218,16 @@ func v1FuzzerFuncs(codecs runtimeserializer.CodecFactory) []interface{} {
|
|||
j.Finalizers = nil
|
||||
}
|
||||
},
|
||||
func(j *metav1.ResourceVersionMatch, c fuzz.Continue) {
|
||||
func(j *metav1.ResourceVersionMatch, c randfill.Continue) {
|
||||
matches := []metav1.ResourceVersionMatch{"", metav1.ResourceVersionMatchExact, metav1.ResourceVersionMatchNotOlderThan}
|
||||
*j = matches[c.Rand.Intn(len(matches))]
|
||||
},
|
||||
func(j *metav1.ListMeta, c fuzz.Continue) {
|
||||
j.ResourceVersion = strconv.FormatUint(c.RandUint64(), 10)
|
||||
j.SelfLink = c.RandString()
|
||||
func(j *metav1.ListMeta, c randfill.Continue) {
|
||||
j.ResourceVersion = strconv.FormatUint(c.Uint64(), 10)
|
||||
j.SelfLink = c.String(0) //nolint:staticcheck // SA1019 backwards compatibility
|
||||
},
|
||||
func(j *metav1.LabelSelector, c fuzz.Continue) {
|
||||
c.FuzzNoCustom(j)
|
||||
func(j *metav1.LabelSelector, c randfill.Continue) {
|
||||
c.FillNoCustom(j)
|
||||
// we can't have an entirely empty selector, so force
|
||||
// use of MatchExpression if necessary
|
||||
if len(j.MatchLabels) == 0 && len(j.MatchExpressions) == 0 {
|
||||
|
@ -257,7 +257,7 @@ func v1FuzzerFuncs(codecs runtimeserializer.CodecFactory) []interface{} {
|
|||
|
||||
for i := range j.MatchExpressions {
|
||||
req := metav1.LabelSelectorRequirement{}
|
||||
c.Fuzz(&req)
|
||||
c.Fill(&req)
|
||||
req.Key = randomLabelKey(c)
|
||||
req.Operator = validOperators[c.Rand.Intn(len(validOperators))]
|
||||
if req.Operator == metav1.LabelSelectorOpIn || req.Operator == metav1.LabelSelectorOpNotIn {
|
||||
|
@ -278,8 +278,8 @@ func v1FuzzerFuncs(codecs runtimeserializer.CodecFactory) []interface{} {
|
|||
sort.Slice(j.MatchExpressions, func(a, b int) bool { return j.MatchExpressions[a].Key < j.MatchExpressions[b].Key })
|
||||
}
|
||||
},
|
||||
func(j *metav1.ManagedFieldsEntry, c fuzz.Continue) {
|
||||
c.FuzzNoCustom(j)
|
||||
func(j *metav1.ManagedFieldsEntry, c randfill.Continue) {
|
||||
c.FillNoCustom(j)
|
||||
j.FieldsV1 = nil
|
||||
},
|
||||
}
|
||||
|
@ -287,15 +287,15 @@ func v1FuzzerFuncs(codecs runtimeserializer.CodecFactory) []interface{} {
|
|||
|
||||
func v1beta1FuzzerFuncs(codecs runtimeserializer.CodecFactory) []interface{} {
|
||||
return []interface{}{
|
||||
func(r *metav1beta1.TableOptions, c fuzz.Continue) {
|
||||
c.FuzzNoCustom(r)
|
||||
func(r *metav1beta1.TableOptions, c randfill.Continue) {
|
||||
c.FillNoCustom(r)
|
||||
// NoHeaders is not serialized to the wire but is allowed within the versioned
|
||||
// type because we don't use meta internal types in the client and API server.
|
||||
r.NoHeaders = false
|
||||
},
|
||||
func(r *metav1beta1.TableRow, c fuzz.Continue) {
|
||||
c.Fuzz(&r.Object)
|
||||
c.Fuzz(&r.Conditions)
|
||||
func(r *metav1beta1.TableRow, c randfill.Continue) {
|
||||
c.Fill(&r.Object)
|
||||
c.Fill(&r.Conditions)
|
||||
if len(r.Conditions) == 0 {
|
||||
r.Conditions = nil
|
||||
}
|
||||
|
@ -307,15 +307,15 @@ func v1beta1FuzzerFuncs(codecs runtimeserializer.CodecFactory) []interface{} {
|
|||
t := c.Intn(6)
|
||||
switch t {
|
||||
case 0:
|
||||
r.Cells[i] = c.RandString()
|
||||
r.Cells[i] = c.String(0)
|
||||
case 1:
|
||||
r.Cells[i] = c.Int63()
|
||||
case 2:
|
||||
r.Cells[i] = c.RandBool()
|
||||
r.Cells[i] = c.Bool()
|
||||
case 3:
|
||||
x := map[string]interface{}{}
|
||||
for j := c.Intn(10) + 1; j >= 0; j-- {
|
||||
x[c.RandString()] = c.RandString()
|
||||
x[c.String(0)] = c.String(0)
|
||||
}
|
||||
r.Cells[i] = x
|
||||
case 4:
|
||||
|
|
|
@ -25,13 +25,10 @@ import (
|
|||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
func TestSetListOptionsDefaults(t *testing.T) {
|
||||
boolPtrFn := func(b bool) *bool {
|
||||
return &b
|
||||
}
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
watchListFeatureEnabled bool
|
||||
|
@ -47,8 +44,8 @@ func TestSetListOptionsDefaults(t *testing.T) {
|
|||
{
|
||||
name: "no-op, SendInitialEvents set",
|
||||
watchListFeatureEnabled: true,
|
||||
targetObj: ListOptions{LabelSelector: labels.Everything(), FieldSelector: fields.Everything(), Watch: true, SendInitialEvents: boolPtrFn(true)},
|
||||
expectedObj: ListOptions{LabelSelector: labels.Everything(), FieldSelector: fields.Everything(), Watch: true, SendInitialEvents: boolPtrFn(true)},
|
||||
targetObj: ListOptions{LabelSelector: labels.Everything(), FieldSelector: fields.Everything(), Watch: true, SendInitialEvents: ptr.To(true)},
|
||||
expectedObj: ListOptions{LabelSelector: labels.Everything(), FieldSelector: fields.Everything(), Watch: true, SendInitialEvents: ptr.To(true)},
|
||||
},
|
||||
{
|
||||
name: "no-op, ResourceVersionMatch set",
|
||||
|
@ -66,13 +63,13 @@ func TestSetListOptionsDefaults(t *testing.T) {
|
|||
name: "defaults applied, match on empty RV",
|
||||
watchListFeatureEnabled: true,
|
||||
targetObj: ListOptions{LabelSelector: labels.Everything(), FieldSelector: fields.Everything(), Watch: true},
|
||||
expectedObj: ListOptions{LabelSelector: labels.Everything(), FieldSelector: fields.Everything(), Watch: true, SendInitialEvents: boolPtrFn(true), ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan},
|
||||
expectedObj: ListOptions{LabelSelector: labels.Everything(), FieldSelector: fields.Everything(), Watch: true, SendInitialEvents: ptr.To(true), ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan},
|
||||
},
|
||||
{
|
||||
name: "defaults applied, match on RV=0",
|
||||
watchListFeatureEnabled: true,
|
||||
targetObj: ListOptions{LabelSelector: labels.Everything(), FieldSelector: fields.Everything(), Watch: true, ResourceVersion: "0"},
|
||||
expectedObj: ListOptions{LabelSelector: labels.Everything(), FieldSelector: fields.Everything(), Watch: true, ResourceVersion: "0", SendInitialEvents: boolPtrFn(true), ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan},
|
||||
expectedObj: ListOptions{LabelSelector: labels.Everything(), FieldSelector: fields.Everything(), Watch: true, ResourceVersion: "0", SendInitialEvents: ptr.To(true), ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan},
|
||||
},
|
||||
{
|
||||
name: "no-op, match on empty RV but watch-list fg is off",
|
||||
|
@ -82,14 +79,14 @@ func TestSetListOptionsDefaults(t *testing.T) {
|
|||
{
|
||||
name: "no-op, match on empty RV but SendInitialEvents is on",
|
||||
watchListFeatureEnabled: true,
|
||||
targetObj: ListOptions{LabelSelector: labels.Everything(), FieldSelector: fields.Everything(), Watch: true, SendInitialEvents: boolPtrFn(true)},
|
||||
expectedObj: ListOptions{LabelSelector: labels.Everything(), FieldSelector: fields.Everything(), Watch: true, SendInitialEvents: boolPtrFn(true)},
|
||||
targetObj: ListOptions{LabelSelector: labels.Everything(), FieldSelector: fields.Everything(), Watch: true, SendInitialEvents: ptr.To(true)},
|
||||
expectedObj: ListOptions{LabelSelector: labels.Everything(), FieldSelector: fields.Everything(), Watch: true, SendInitialEvents: ptr.To(true)},
|
||||
},
|
||||
{
|
||||
name: "no-op, match on empty RV but SendInitialEvents is off",
|
||||
watchListFeatureEnabled: true,
|
||||
targetObj: ListOptions{LabelSelector: labels.Everything(), FieldSelector: fields.Everything(), Watch: true, SendInitialEvents: boolPtrFn(false)},
|
||||
expectedObj: ListOptions{LabelSelector: labels.Everything(), FieldSelector: fields.Everything(), Watch: true, SendInitialEvents: boolPtrFn(false)},
|
||||
targetObj: ListOptions{LabelSelector: labels.Everything(), FieldSelector: fields.Everything(), Watch: true, SendInitialEvents: ptr.To(false)},
|
||||
expectedObj: ListOptions{LabelSelector: labels.Everything(), FieldSelector: fields.Everything(), Watch: true, SendInitialEvents: ptr.To(false)},
|
||||
},
|
||||
{
|
||||
name: "no-op, match on empty RV but ResourceVersionMatch set",
|
||||
|
|
|
@ -17,4 +17,4 @@ limitations under the License.
|
|||
// +k8s:deepcopy-gen=package
|
||||
// +k8s:conversion-gen=k8s.io/apimachinery/pkg/apis/meta/v1
|
||||
|
||||
package internalversion // import "k8s.io/apimachinery/pkg/apis/meta/internalversion"
|
||||
package internalversion
|
||||
|
|
|
@ -14,4 +14,4 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package scheme // import "k8s.io/apimachinery/pkg/apis/meta/internalversion/scheme"
|
||||
package scheme
|
||||
|
|
|
@ -41,8 +41,6 @@ type ListOptions struct {
|
|||
// assume bookmarks are returned at any specific interval, nor may they
|
||||
// assume the server will send any BOOKMARK event during a session.
|
||||
// If this is not a watch, this field is ignored.
|
||||
// If the feature gate WatchBookmarks is not enabled in apiserver,
|
||||
// this field is ignored.
|
||||
AllowWatchBookmarks bool
|
||||
// resourceVersion sets a constraint on what resource versions a request may be served from.
|
||||
// See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for
|
||||
|
|
|
@ -21,13 +21,10 @@ import (
|
|||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
func TestValidateListOptions(t *testing.T) {
|
||||
boolPtrFn := func(b bool) *bool {
|
||||
return &b
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
opts internalversion.ListOptions
|
||||
|
@ -65,7 +62,7 @@ func TestValidateListOptions(t *testing.T) {
|
|||
}, {
|
||||
name: "list-sendInitialEvents-forbidden",
|
||||
opts: internalversion.ListOptions{
|
||||
SendInitialEvents: boolPtrFn(true),
|
||||
SendInitialEvents: ptr.To(true),
|
||||
},
|
||||
expectErrors: []string{"sendInitialEvents: Forbidden: sendInitialEvents is forbidden for list"},
|
||||
}, {
|
||||
|
@ -77,7 +74,7 @@ func TestValidateListOptions(t *testing.T) {
|
|||
name: "valid-watch-sendInitialEvents-on",
|
||||
opts: internalversion.ListOptions{
|
||||
Watch: true,
|
||||
SendInitialEvents: boolPtrFn(true),
|
||||
SendInitialEvents: ptr.To(true),
|
||||
ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan,
|
||||
AllowWatchBookmarks: true,
|
||||
},
|
||||
|
@ -86,7 +83,7 @@ func TestValidateListOptions(t *testing.T) {
|
|||
name: "valid-watch-sendInitialEvents-off",
|
||||
opts: internalversion.ListOptions{
|
||||
Watch: true,
|
||||
SendInitialEvents: boolPtrFn(false),
|
||||
SendInitialEvents: ptr.To(false),
|
||||
ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan,
|
||||
AllowWatchBookmarks: true,
|
||||
},
|
||||
|
@ -102,14 +99,14 @@ func TestValidateListOptions(t *testing.T) {
|
|||
name: "watch-sendInitialEvents-without-resourceversionmatch-forbidden",
|
||||
opts: internalversion.ListOptions{
|
||||
Watch: true,
|
||||
SendInitialEvents: boolPtrFn(true),
|
||||
SendInitialEvents: ptr.To(true),
|
||||
},
|
||||
expectErrors: []string{"resourceVersionMatch: Forbidden: sendInitialEvents requires setting resourceVersionMatch to NotOlderThan", "sendInitialEvents: Forbidden: sendInitialEvents is forbidden for watch unless the WatchList feature gate is enabled"},
|
||||
}, {
|
||||
name: "watch-sendInitialEvents-with-exact-resourceversionmatch-forbidden",
|
||||
opts: internalversion.ListOptions{
|
||||
Watch: true,
|
||||
SendInitialEvents: boolPtrFn(true),
|
||||
SendInitialEvents: ptr.To(true),
|
||||
ResourceVersionMatch: metav1.ResourceVersionMatchExact,
|
||||
AllowWatchBookmarks: true,
|
||||
},
|
||||
|
@ -119,7 +116,7 @@ func TestValidateListOptions(t *testing.T) {
|
|||
name: "watch-sendInitialEvents-on-with-empty-resourceversionmatch-forbidden",
|
||||
opts: internalversion.ListOptions{
|
||||
Watch: true,
|
||||
SendInitialEvents: boolPtrFn(true),
|
||||
SendInitialEvents: ptr.To(true),
|
||||
ResourceVersionMatch: "",
|
||||
},
|
||||
expectErrors: []string{"resourceVersionMatch: Forbidden: sendInitialEvents requires setting resourceVersionMatch to NotOlderThan", "sendInitialEvents: Forbidden: sendInitialEvents is forbidden for watch unless the WatchList feature gate is enabled"},
|
||||
|
@ -127,7 +124,7 @@ func TestValidateListOptions(t *testing.T) {
|
|||
name: "watch-sendInitialEvents-off-with-empty-resourceversionmatch-forbidden",
|
||||
opts: internalversion.ListOptions{
|
||||
Watch: true,
|
||||
SendInitialEvents: boolPtrFn(false),
|
||||
SendInitialEvents: ptr.To(false),
|
||||
ResourceVersionMatch: "",
|
||||
},
|
||||
expectErrors: []string{"resourceVersionMatch: Forbidden: sendInitialEvents requires setting resourceVersionMatch to NotOlderThan", "sendInitialEvents: Forbidden: sendInitialEvents is forbidden for watch unless the WatchList feature gate is enabled"},
|
||||
|
@ -135,7 +132,7 @@ func TestValidateListOptions(t *testing.T) {
|
|||
name: "watch-sendInitialEvents-with-incorrect-resourceversionmatch-forbidden",
|
||||
opts: internalversion.ListOptions{
|
||||
Watch: true,
|
||||
SendInitialEvents: boolPtrFn(true),
|
||||
SendInitialEvents: ptr.To(true),
|
||||
ResourceVersionMatch: "incorrect",
|
||||
AllowWatchBookmarks: true,
|
||||
},
|
||||
|
@ -147,7 +144,7 @@ func TestValidateListOptions(t *testing.T) {
|
|||
name: "watch-sendInitialEvents-no-allowWatchBookmark",
|
||||
opts: internalversion.ListOptions{
|
||||
Watch: true,
|
||||
SendInitialEvents: boolPtrFn(true),
|
||||
SendInitialEvents: ptr.To(true),
|
||||
ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan,
|
||||
},
|
||||
watchListFeatureEnabled: true,
|
||||
|
@ -155,7 +152,7 @@ func TestValidateListOptions(t *testing.T) {
|
|||
name: "watch-sendInitialEvents-no-watchlist-fg-disabled",
|
||||
opts: internalversion.ListOptions{
|
||||
Watch: true,
|
||||
SendInitialEvents: boolPtrFn(true),
|
||||
SendInitialEvents: ptr.To(true),
|
||||
ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan,
|
||||
AllowWatchBookmarks: true,
|
||||
},
|
||||
|
@ -164,7 +161,7 @@ func TestValidateListOptions(t *testing.T) {
|
|||
name: "watch-sendInitialEvents-no-watchlist-fg-disabled",
|
||||
opts: internalversion.ListOptions{
|
||||
Watch: true,
|
||||
SendInitialEvents: boolPtrFn(true),
|
||||
SendInitialEvents: ptr.To(true),
|
||||
ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan,
|
||||
AllowWatchBookmarks: true,
|
||||
Continue: "123",
|
||||
|
|
|
@ -21,4 +21,4 @@ limitations under the License.
|
|||
|
||||
// +groupName=meta.k8s.io
|
||||
|
||||
package v1 // import "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
package v1
|
||||
|
|
|
@ -23,9 +23,9 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
fuzz "github.com/google/gofuzz"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/randfill"
|
||||
)
|
||||
|
||||
func TestLabelSelectorAsSelector(t *testing.T) {
|
||||
|
@ -188,8 +188,8 @@ func TestResetObjectMetaForStatus(t *testing.T) {
|
|||
existingMeta := &ObjectMeta{}
|
||||
|
||||
// fuzz the existingMeta to set every field, no nils
|
||||
f := fuzz.New().NilChance(0).NumElements(1, 1).MaxDepth(10)
|
||||
f.Fuzz(existingMeta)
|
||||
f := randfill.New().NilChance(0).NumElements(1, 1).MaxDepth(10)
|
||||
f.Fill(existingMeta)
|
||||
ResetObjectMetaForStatus(meta, existingMeta)
|
||||
|
||||
// not all fields are stomped during the reset. These fields should not have been set. False
|
||||
|
|
|
@ -20,21 +20,22 @@ limitations under the License.
|
|||
package v1
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
fuzz "github.com/google/gofuzz"
|
||||
"sigs.k8s.io/randfill"
|
||||
)
|
||||
|
||||
// Fuzz satisfies fuzz.Interface.
|
||||
func (t *MicroTime) Fuzz(c fuzz.Continue) {
|
||||
// Fuzz satisfies randfill.SimpleSelfFiller.
|
||||
func (t *MicroTime) RandFill(r *rand.Rand) {
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
// Allow for about 1000 years of randomness. Accurate to a tenth of
|
||||
// micro second. Leave off nanoseconds because JSON doesn't
|
||||
// represent them so they can't round-trip properly.
|
||||
t.Time = time.Unix(c.Rand.Int63n(1000*365*24*60*60), 1000*c.Rand.Int63n(1000000))
|
||||
t.Time = time.Unix(r.Int63n(1000*365*24*60*60), 1000*r.Int63n(1000000))
|
||||
}
|
||||
|
||||
// ensure MicroTime implements fuzz.Interface
|
||||
var _ fuzz.Interface = &MicroTime{}
|
||||
// ensure MicroTime implements randfill.Interface
|
||||
var _ randfill.SimpleSelfFiller = &MicroTime{}
|
||||
|
|
|
@ -27,7 +27,7 @@ import (
|
|||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
fuzz "github.com/google/gofuzz"
|
||||
"sigs.k8s.io/randfill"
|
||||
)
|
||||
|
||||
type MicroTimeHolder struct {
|
||||
|
@ -378,10 +378,10 @@ func TestMicroTimeProtoUnmarshalRaw(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMicroTimeRoundtripCBOR(t *testing.T) {
|
||||
fuzzer := fuzz.New()
|
||||
fuzzer := randfill.New()
|
||||
for i := 0; i < 500; i++ {
|
||||
var initial, final MicroTime
|
||||
fuzzer.Fuzz(&initial)
|
||||
fuzzer.Fill(&initial)
|
||||
b, err := cbor.Marshal(initial)
|
||||
if err != nil {
|
||||
t.Errorf("error encoding %v: %v", initial, err)
|
||||
|
|
|
@ -22,15 +22,15 @@ import (
|
|||
"reflect"
|
||||
"testing"
|
||||
|
||||
fuzz "github.com/google/gofuzz"
|
||||
"sigs.k8s.io/randfill"
|
||||
)
|
||||
|
||||
func TestPatchOptionsIsSuperSetOfUpdateOptions(t *testing.T) {
|
||||
f := fuzz.New()
|
||||
f := randfill.New()
|
||||
for i := 0; i < 1000; i++ {
|
||||
t.Run(fmt.Sprintf("Run %d/1000", i), func(t *testing.T) {
|
||||
update := UpdateOptions{}
|
||||
f.Fuzz(&update)
|
||||
f.Fill(&update)
|
||||
|
||||
b, err := json.Marshal(update)
|
||||
if err != nil {
|
||||
|
|
|
@ -20,21 +20,22 @@ limitations under the License.
|
|||
package v1
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
fuzz "github.com/google/gofuzz"
|
||||
"sigs.k8s.io/randfill"
|
||||
)
|
||||
|
||||
// Fuzz satisfies fuzz.Interface.
|
||||
func (t *Time) Fuzz(c fuzz.Continue) {
|
||||
// Fuzz satisfies randfill.SimpleSelfFiller.
|
||||
func (t *Time) RandFill(r *rand.Rand) {
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
// Allow for about 1000 years of randomness. Leave off nanoseconds
|
||||
// because JSON doesn't represent them so they can't round-trip
|
||||
// properly.
|
||||
t.Time = time.Unix(c.Rand.Int63n(1000*365*24*60*60), 0)
|
||||
t.Time = time.Unix(r.Int63n(1000*365*24*60*60), 0)
|
||||
}
|
||||
|
||||
// ensure Time implements fuzz.Interface
|
||||
var _ fuzz.Interface = &Time{}
|
||||
// ensure Time implements randfill.SimpleSelfFiller
|
||||
var _ randfill.SimpleSelfFiller = &Time{}
|
||||
|
|
|
@ -27,7 +27,7 @@ import (
|
|||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
fuzz "github.com/google/gofuzz"
|
||||
"sigs.k8s.io/randfill"
|
||||
)
|
||||
|
||||
type TimeHolder struct {
|
||||
|
@ -303,10 +303,10 @@ func TestTimeIsZero(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestTimeRoundtripCBOR(t *testing.T) {
|
||||
fuzzer := fuzz.New()
|
||||
fuzzer := randfill.New()
|
||||
for i := 0; i < 500; i++ {
|
||||
var initial, final Time
|
||||
fuzzer.Fuzz(&initial)
|
||||
fuzzer.Fill(&initial)
|
||||
b, err := cbor.Marshal(initial)
|
||||
if err != nil {
|
||||
t.Errorf("error encoding %v: %v", initial, err)
|
||||
|
|
|
@ -185,7 +185,7 @@ type ObjectMeta struct {
|
|||
// Null for lists.
|
||||
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
|
||||
// +optional
|
||||
CreationTimestamp Time `json:"creationTimestamp,omitempty" protobuf:"bytes,8,opt,name=creationTimestamp"`
|
||||
CreationTimestamp Time `json:"creationTimestamp,omitempty,omitzero" protobuf:"bytes,8,opt,name=creationTimestamp"`
|
||||
|
||||
// DeletionTimestamp is RFC 3339 date and time at which this resource will be deleted. This
|
||||
// field is set by the server when a graceful deletion is requested by the user, and is not
|
||||
|
@ -439,20 +439,6 @@ const (
|
|||
//
|
||||
// The annotation is added to a "Bookmark" event.
|
||||
InitialEventsAnnotationKey = "k8s.io/initial-events-end"
|
||||
|
||||
// InitialEventsListBlueprintAnnotationKey is the name of the key
|
||||
// where an empty, versioned list is encoded in the requested format
|
||||
// (e.g., protobuf, JSON, CBOR), then base64-encoded and stored as a string.
|
||||
//
|
||||
// This encoding matches the request encoding format, which may be
|
||||
// protobuf, JSON, CBOR, or others, depending on what the client requested.
|
||||
// This ensures that the reconstructed list can be processed through the
|
||||
// same decoder chain that would handle a standard LIST call response.
|
||||
//
|
||||
// The annotation is added to a "Bookmark" event and is used by clients
|
||||
// to guarantee the format consistency when reconstructing
|
||||
// the list during WatchList processing.
|
||||
InitialEventsListBlueprintAnnotationKey = "kubernetes.io/initial-events-list-blueprint"
|
||||
)
|
||||
|
||||
// resourceVersionMatch specifies how the resourceVersion parameter is applied. resourceVersionMatch
|
||||
|
|
|
@ -188,7 +188,7 @@ func NestedSlice(obj map[string]interface{}, fields ...string) ([]interface{}, b
|
|||
// NestedStringMap returns a copy of map[string]string value of a nested field.
|
||||
// Returns false if value is not found and an error if not a map[string]interface{} or contains non-string values in the map.
|
||||
func NestedStringMap(obj map[string]interface{}, fields ...string) (map[string]string, bool, error) {
|
||||
m, found, err := nestedMapNoCopy(obj, fields...)
|
||||
m, found, err := nestedMapNoCopy(obj, false, fields...)
|
||||
if !found || err != nil {
|
||||
return nil, found, err
|
||||
}
|
||||
|
@ -203,10 +203,32 @@ func NestedStringMap(obj map[string]interface{}, fields ...string) (map[string]s
|
|||
return strMap, true, nil
|
||||
}
|
||||
|
||||
// NestedNullCoercingStringMap returns a copy of map[string]string value of a nested field.
|
||||
// Returns `nil, true, nil` if the value exists and is explicitly null.
|
||||
// Returns `nil, false, err` if the value is not a map or a null value, or is a map and contains non-string non-null values.
|
||||
// Null values in the map are coerced to "" to match json decoding behavior.
|
||||
func NestedNullCoercingStringMap(obj map[string]interface{}, fields ...string) (map[string]string, bool, error) {
|
||||
m, found, err := nestedMapNoCopy(obj, true, fields...)
|
||||
if !found || err != nil || m == nil {
|
||||
return nil, found, err
|
||||
}
|
||||
strMap := make(map[string]string, len(m))
|
||||
for k, v := range m {
|
||||
if str, ok := v.(string); ok {
|
||||
strMap[k] = str
|
||||
} else if v == nil {
|
||||
strMap[k] = ""
|
||||
} else {
|
||||
return nil, false, fmt.Errorf("%v accessor error: contains non-string value in the map under key %q: %v is of the type %T, expected string", jsonPath(fields), k, v, v)
|
||||
}
|
||||
}
|
||||
return strMap, true, nil
|
||||
}
|
||||
|
||||
// NestedMap returns a deep copy of map[string]interface{} value of a nested field.
|
||||
// Returns false if value is not found and an error if not a map[string]interface{}.
|
||||
func NestedMap(obj map[string]interface{}, fields ...string) (map[string]interface{}, bool, error) {
|
||||
m, found, err := nestedMapNoCopy(obj, fields...)
|
||||
m, found, err := nestedMapNoCopy(obj, false, fields...)
|
||||
if !found || err != nil {
|
||||
return nil, found, err
|
||||
}
|
||||
|
@ -215,11 +237,14 @@ func NestedMap(obj map[string]interface{}, fields ...string) (map[string]interfa
|
|||
|
||||
// nestedMapNoCopy returns a map[string]interface{} value of a nested field.
|
||||
// Returns false if value is not found and an error if not a map[string]interface{}.
|
||||
func nestedMapNoCopy(obj map[string]interface{}, fields ...string) (map[string]interface{}, bool, error) {
|
||||
func nestedMapNoCopy(obj map[string]interface{}, tolerateNil bool, fields ...string) (map[string]interface{}, bool, error) {
|
||||
val, found, err := NestedFieldNoCopy(obj, fields...)
|
||||
if !found || err != nil {
|
||||
return nil, found, err
|
||||
}
|
||||
if val == nil && tolerateNil {
|
||||
return nil, true, nil
|
||||
}
|
||||
m, ok := val.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, false, fmt.Errorf("%v accessor error: %v is of the type %T, expected map[string]interface{}", jsonPath(fields), val, val)
|
||||
|
|
|
@ -19,6 +19,8 @@ package unstructured
|
|||
import (
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
|
@ -297,3 +299,90 @@ func TestNestedNumberAsFloat64(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNestedNullCoercingStringMap(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
obj map[string]interface{}
|
||||
path []string
|
||||
wantObj map[string]string
|
||||
wantFound bool
|
||||
wantErrMessage string
|
||||
}{
|
||||
{
|
||||
name: "missing map",
|
||||
obj: nil,
|
||||
path: []string{"path"},
|
||||
wantObj: nil,
|
||||
wantFound: false,
|
||||
wantErrMessage: "",
|
||||
},
|
||||
{
|
||||
name: "null map",
|
||||
obj: map[string]interface{}{"path": nil},
|
||||
path: []string{"path"},
|
||||
wantObj: nil,
|
||||
wantFound: true,
|
||||
wantErrMessage: "",
|
||||
},
|
||||
{
|
||||
name: "non map",
|
||||
obj: map[string]interface{}{"path": 0},
|
||||
path: []string{"path"},
|
||||
wantObj: nil,
|
||||
wantFound: false,
|
||||
wantErrMessage: "type int",
|
||||
},
|
||||
{
|
||||
name: "empty map",
|
||||
obj: map[string]interface{}{"path": map[string]interface{}{}},
|
||||
path: []string{"path"},
|
||||
wantObj: map[string]string{},
|
||||
wantFound: true,
|
||||
wantErrMessage: "",
|
||||
},
|
||||
{
|
||||
name: "string value",
|
||||
obj: map[string]interface{}{"path": map[string]interface{}{"a": "1", "b": "2"}},
|
||||
path: []string{"path"},
|
||||
wantObj: map[string]string{"a": "1", "b": "2"},
|
||||
wantFound: true,
|
||||
wantErrMessage: "",
|
||||
},
|
||||
{
|
||||
name: "null value",
|
||||
obj: map[string]interface{}{"path": map[string]interface{}{"a": "1", "b": nil}},
|
||||
path: []string{"path"},
|
||||
wantObj: map[string]string{"a": "1", "b": ""},
|
||||
wantFound: true,
|
||||
wantErrMessage: "",
|
||||
},
|
||||
{
|
||||
name: "invalid value",
|
||||
obj: map[string]interface{}{"path": map[string]interface{}{"a": "1", "b": nil, "c": 0}},
|
||||
path: []string{"path"},
|
||||
wantObj: nil,
|
||||
wantFound: false,
|
||||
wantErrMessage: `key "c": 0`,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
gotObj, gotFound, gotErr := NestedNullCoercingStringMap(tc.obj, tc.path...)
|
||||
if !reflect.DeepEqual(gotObj, tc.wantObj) {
|
||||
t.Errorf("got %#v, wanted %#v", gotObj, tc.wantObj)
|
||||
}
|
||||
if gotFound != tc.wantFound {
|
||||
t.Errorf("got %v, wanted %v", gotFound, tc.wantFound)
|
||||
}
|
||||
if tc.wantErrMessage != "" {
|
||||
if gotErr == nil {
|
||||
t.Errorf("got nil error, wanted %s", tc.wantErrMessage)
|
||||
} else if gotErrMessage := gotErr.Error(); !strings.Contains(gotErrMessage, tc.wantErrMessage) {
|
||||
t.Errorf("wanted error %q, got: %v", gotErrMessage, tc.wantErrMessage)
|
||||
}
|
||||
} else if gotErr != nil {
|
||||
t.Errorf("wanted nil error, got %v", gotErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -397,7 +397,7 @@ func (u *Unstructured) SetDeletionGracePeriodSeconds(deletionGracePeriodSeconds
|
|||
}
|
||||
|
||||
func (u *Unstructured) GetLabels() map[string]string {
|
||||
m, _, _ := NestedStringMap(u.Object, "metadata", "labels")
|
||||
m, _, _ := NestedNullCoercingStringMap(u.Object, "metadata", "labels")
|
||||
return m
|
||||
}
|
||||
|
||||
|
@ -410,7 +410,7 @@ func (u *Unstructured) SetLabels(labels map[string]string) {
|
|||
}
|
||||
|
||||
func (u *Unstructured) GetAnnotations() map[string]string {
|
||||
m, _, _ := NestedStringMap(u.Object, "metadata", "annotations")
|
||||
m, _, _ := NestedNullCoercingStringMap(u.Object, "metadata", "annotations")
|
||||
return m
|
||||
}
|
||||
|
||||
|
|
|
@ -55,9 +55,7 @@ func TestObjectToUnstructuredConversion(t *testing.T) {
|
|||
Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Carp",
|
||||
"metadata": map[string]interface{}{
|
||||
"creationTimestamp": nil,
|
||||
},
|
||||
"metadata": map[string]interface{}{},
|
||||
"spec": map[string]interface{}{},
|
||||
"status": map[string]interface{}{},
|
||||
},
|
||||
|
@ -78,7 +76,6 @@ func TestObjectToUnstructuredConversion(t *testing.T) {
|
|||
"apiVersion": "v1",
|
||||
"kind": "Carp",
|
||||
"metadata": map[string]interface{}{
|
||||
"creationTimestamp": nil,
|
||||
"name": "noxu",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
|
@ -155,7 +152,6 @@ func TestUnstructuredToObjectConversion(t *testing.T) {
|
|||
"apiVersion": "v1",
|
||||
"kind": "Carp",
|
||||
"metadata": map[string]interface{}{
|
||||
"creationTimestamp": nil,
|
||||
"name": "noxu",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
|
@ -181,7 +177,6 @@ func TestUnstructuredToObjectConversion(t *testing.T) {
|
|||
"apiVersion": "v1",
|
||||
"kind": "Carp",
|
||||
"metadata": map[string]interface{}{
|
||||
"creationTimestamp": nil,
|
||||
"name": "noxu",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
|
@ -207,7 +202,6 @@ func TestUnstructuredToObjectConversion(t *testing.T) {
|
|||
"apiVersion": "v9",
|
||||
"kind": "Carp",
|
||||
"metadata": map[string]interface{}{
|
||||
"creationTimestamp": nil,
|
||||
"name": "noxu",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
|
@ -232,7 +226,6 @@ func TestUnstructuredToObjectConversion(t *testing.T) {
|
|||
"apiVersion": "v1",
|
||||
"kind": "Carp",
|
||||
"metadata": map[string]interface{}{
|
||||
"creationTimestamp": nil,
|
||||
"name": "noxu",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
|
@ -358,7 +351,6 @@ func TestUnstructuredToGVConversion(t *testing.T) {
|
|||
"apiVersion": "v1",
|
||||
"kind": "Carp",
|
||||
"metadata": map[string]interface{}{
|
||||
"creationTimestamp": nil,
|
||||
"name": "noxu",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
|
@ -388,7 +380,6 @@ func TestUnstructuredToGVConversion(t *testing.T) {
|
|||
"apiVersion": "v1",
|
||||
"kind": "Carp",
|
||||
"metadata": map[string]interface{}{
|
||||
"creationTimestamp": nil,
|
||||
"name": "noxu",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
|
@ -414,7 +405,6 @@ func TestUnstructuredToGVConversion(t *testing.T) {
|
|||
"apiVersion": "v1",
|
||||
"kind": "Carp",
|
||||
"metadata": map[string]interface{}{
|
||||
"creationTimestamp": nil,
|
||||
"name": "noxu",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
|
|
|
@ -28,8 +28,8 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
fuzz "github.com/google/gofuzz"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"sigs.k8s.io/randfill"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
|
||||
"k8s.io/apimachinery/pkg/api/equality"
|
||||
|
@ -67,7 +67,7 @@ func TestUnstructuredMetadataRoundTrip(t *testing.T) {
|
|||
u := &unstructured.Unstructured{Object: map[string]interface{}{}}
|
||||
uCopy := u.DeepCopy()
|
||||
metadata := &metav1.ObjectMeta{}
|
||||
fuzzer.Fuzz(metadata)
|
||||
fuzzer.Fill(metadata)
|
||||
|
||||
if err := setObjectMeta(u, metadata); err != nil {
|
||||
t.Fatalf("unexpected error setting fuzzed ObjectMeta: %v", err)
|
||||
|
@ -94,7 +94,7 @@ func TestUnstructuredMetadataOmitempty(t *testing.T) {
|
|||
// fuzz to make sure we don't miss any function calls below
|
||||
u := &unstructured.Unstructured{Object: map[string]interface{}{}}
|
||||
metadata := &metav1.ObjectMeta{}
|
||||
fuzzer.Fuzz(metadata)
|
||||
fuzzer.Fill(metadata)
|
||||
if err := setObjectMeta(u, metadata); err != nil {
|
||||
t.Fatalf("unexpected error setting fuzzed ObjectMeta: %v", err)
|
||||
}
|
||||
|
@ -184,7 +184,7 @@ func roundtripType[U runtime.Unstructured](t *testing.T) {
|
|||
|
||||
for i := 0; i < 50; i++ {
|
||||
original := reflect.New(reflect.TypeFor[U]().Elem()).Interface().(runtime.Unstructured)
|
||||
fuzzer.Fuzz(original)
|
||||
fuzzer.Fill(original)
|
||||
// unstructured -> JSON > unstructured > CBOR -> unstructured -> JSON -> unstructured
|
||||
roundtrip(t, original, jS, cS)
|
||||
// unstructured -> CBOR > unstructured > JSON -> unstructured -> CBOR -> unstructured
|
||||
|
@ -285,25 +285,25 @@ const (
|
|||
|
||||
func unstructuredFuzzerFuncs(codecs serializer.CodecFactory) []interface{} {
|
||||
return []interface{}{
|
||||
func(u *unstructured.Unstructured, c fuzz.Continue) {
|
||||
func(u *unstructured.Unstructured, c randfill.Continue) {
|
||||
obj := make(map[string]interface{})
|
||||
obj["apiVersion"] = generateValidAPIVersionString(c)
|
||||
obj["kind"] = generateNonEmptyString(c)
|
||||
for j := c.Intn(maxUnstructuredFanOut); j >= 0; j-- {
|
||||
obj[c.RandString()] = generateRandomTypeValue(maxUnstructuredDepth, c)
|
||||
obj[c.String(0)] = generateRandomTypeValue(maxUnstructuredDepth, c)
|
||||
}
|
||||
u.Object = obj
|
||||
},
|
||||
func(ul *unstructured.UnstructuredList, c fuzz.Continue) {
|
||||
func(ul *unstructured.UnstructuredList, c randfill.Continue) {
|
||||
obj := make(map[string]interface{})
|
||||
obj["apiVersion"] = generateValidAPIVersionString(c)
|
||||
obj["kind"] = generateNonEmptyString(c)
|
||||
for j := c.Intn(maxUnstructuredFanOut); j >= 0; j-- {
|
||||
obj[c.RandString()] = generateRandomTypeValue(maxUnstructuredDepth, c)
|
||||
obj[c.String(0)] = generateRandomTypeValue(maxUnstructuredDepth, c)
|
||||
}
|
||||
for j := c.Intn(maxUnstructuredFanOut); j >= 0; j-- {
|
||||
var item = unstructured.Unstructured{}
|
||||
c.Fuzz(&item)
|
||||
c.Fill(&item)
|
||||
ul.Items = append(ul.Items, item)
|
||||
}
|
||||
ul.Object = obj
|
||||
|
@ -311,16 +311,16 @@ func unstructuredFuzzerFuncs(codecs serializer.CodecFactory) []interface{} {
|
|||
}
|
||||
}
|
||||
|
||||
func generateNonEmptyString(c fuzz.Continue) string {
|
||||
temp := c.RandString()
|
||||
func generateNonEmptyString(c randfill.Continue) string {
|
||||
temp := c.String(0)
|
||||
for len(temp) == 0 {
|
||||
temp = c.RandString()
|
||||
temp = c.String(0)
|
||||
}
|
||||
return temp
|
||||
}
|
||||
|
||||
// generateNonEmptyNoSlashString generates a non-empty string without any slashes
|
||||
func generateNonEmptyNoSlashString(c fuzz.Continue) string {
|
||||
func generateNonEmptyNoSlashString(c randfill.Continue) string {
|
||||
temp := strings.ReplaceAll(generateNonEmptyString(c), "/", "")
|
||||
for len(temp) == 0 {
|
||||
temp = strings.ReplaceAll(generateNonEmptyString(c), "/", "")
|
||||
|
@ -330,8 +330,8 @@ func generateNonEmptyNoSlashString(c fuzz.Continue) string {
|
|||
|
||||
// generateValidAPIVersionString generates valid apiVersion string with formats:
|
||||
// <string>/<string> or <string>
|
||||
func generateValidAPIVersionString(c fuzz.Continue) string {
|
||||
if c.RandBool() {
|
||||
func generateValidAPIVersionString(c randfill.Continue) string {
|
||||
if c.Bool() {
|
||||
return generateNonEmptyNoSlashString(c) + "/" + generateNonEmptyNoSlashString(c)
|
||||
} else {
|
||||
return generateNonEmptyNoSlashString(c)
|
||||
|
@ -355,7 +355,7 @@ func generateValidAPIVersionString(c fuzz.Continue) string {
|
|||
// All external-versioned builtin types are exercised through RoundtripToUnstructured
|
||||
// in apitesting package. Types like metav1.Time are implicitly being exercised
|
||||
// because they appear as fields in those types.
|
||||
func generateRandomTypeValue(depth int, c fuzz.Continue) interface{} {
|
||||
func generateRandomTypeValue(depth int, c randfill.Continue) interface{} {
|
||||
t := c.Rand.Intn(120)
|
||||
// If the max depth for unstructured is reached, only add non-recursive types
|
||||
// which is 20+ in range
|
||||
|
@ -373,21 +373,21 @@ func generateRandomTypeValue(depth int, c fuzz.Continue) interface{} {
|
|||
case t < 20:
|
||||
item := map[string]interface{}{}
|
||||
for j := c.Intn(maxUnstructuredFanOut); j >= 0; j-- {
|
||||
item[c.RandString()] = generateRandomTypeValue(depth-1, c)
|
||||
item[c.String(0)] = generateRandomTypeValue(depth-1, c)
|
||||
}
|
||||
return item
|
||||
case t < 40:
|
||||
// Only valid UTF-8 encodings
|
||||
var item string
|
||||
c.Fuzz(&item)
|
||||
c.Fill(&item)
|
||||
return item
|
||||
case t < 60:
|
||||
var item int64
|
||||
c.Fuzz(&item)
|
||||
c.Fill(&item)
|
||||
return item
|
||||
case t < 80:
|
||||
var item bool
|
||||
c.Fuzz(&item)
|
||||
c.Fill(&item)
|
||||
return item
|
||||
case t < 100:
|
||||
return c.Rand.NormFloat64()
|
||||
|
|
|
@ -104,7 +104,7 @@ func ValidateLabelSelectorRequirement(sr metav1.LabelSelectorRequirement, opts L
|
|||
func ValidateLabelName(labelName string, fldPath *field.Path) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
for _, msg := range validation.IsQualifiedName(labelName) {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, labelName, msg))
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, labelName, msg).WithOrigin("labelKey"))
|
||||
}
|
||||
return allErrs
|
||||
}
|
||||
|
@ -328,11 +328,11 @@ func ValidateCondition(condition metav1.Condition, fldPath *field.Path) field.Er
|
|||
}
|
||||
|
||||
if condition.LastTransitionTime.IsZero() {
|
||||
allErrs = append(allErrs, field.Required(fldPath.Child("lastTransitionTime"), "must be set"))
|
||||
allErrs = append(allErrs, field.Required(fldPath.Child("lastTransitionTime"), ""))
|
||||
}
|
||||
|
||||
if len(condition.Reason) == 0 {
|
||||
allErrs = append(allErrs, field.Required(fldPath.Child("reason"), "must be set"))
|
||||
allErrs = append(allErrs, field.Required(fldPath.Child("reason"), ""))
|
||||
} else {
|
||||
for _, currErr := range isValidConditionReason(condition.Reason) {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("reason"), condition.Reason, currErr))
|
||||
|
|
|
@ -132,10 +132,6 @@ func TestInvalidDryRun(t *testing.T) {
|
|||
|
||||
}
|
||||
|
||||
func boolPtr(b bool) *bool {
|
||||
return &b
|
||||
}
|
||||
|
||||
func TestValidateDeleteOptionsWithIgnoreStoreReadError(t *testing.T) {
|
||||
fieldPath := field.NewPath("ignoreStoreReadErrorWithClusterBreakingPotential")
|
||||
tests := []struct {
|
||||
|
@ -232,7 +228,7 @@ func TestValidPatchOptions(t *testing.T) {
|
|||
patchType types.PatchType
|
||||
}{{
|
||||
opts: metav1.PatchOptions{
|
||||
Force: boolPtr(true),
|
||||
Force: ptr.To(true),
|
||||
FieldManager: "kubectl",
|
||||
},
|
||||
patchType: types.ApplyYAMLPatchType,
|
||||
|
@ -243,7 +239,7 @@ func TestValidPatchOptions(t *testing.T) {
|
|||
patchType: types.ApplyYAMLPatchType,
|
||||
}, {
|
||||
opts: metav1.PatchOptions{
|
||||
Force: boolPtr(true),
|
||||
Force: ptr.To(true),
|
||||
FieldManager: "kubectl",
|
||||
},
|
||||
patchType: types.ApplyCBORPatchType,
|
||||
|
@ -290,7 +286,7 @@ func TestInvalidPatchOptions(t *testing.T) {
|
|||
// force on non-apply
|
||||
{
|
||||
opts: metav1.PatchOptions{
|
||||
Force: boolPtr(true),
|
||||
Force: ptr.To(true),
|
||||
},
|
||||
patchType: types.MergePatchType,
|
||||
},
|
||||
|
@ -298,7 +294,7 @@ func TestInvalidPatchOptions(t *testing.T) {
|
|||
{
|
||||
opts: metav1.PatchOptions{
|
||||
FieldManager: "kubectl",
|
||||
Force: boolPtr(false),
|
||||
Force: ptr.To(false),
|
||||
},
|
||||
patchType: types.MergePatchType,
|
||||
},
|
||||
|
@ -442,7 +438,7 @@ func TestValidateConditions(t *testing.T) {
|
|||
if !hasError(errs, needle) {
|
||||
t.Errorf("missing %q in\n%v", needle, errorsAsString(errs))
|
||||
}
|
||||
needle = `status.conditions[0].lastTransitionTime: Required value: must be set`
|
||||
needle = `status.conditions[0].lastTransitionTime: Required value`
|
||||
if !hasError(errs, needle) {
|
||||
t.Errorf("missing %q in\n%v", needle, errorsAsString(errs))
|
||||
}
|
||||
|
|
|
@ -20,4 +20,4 @@ limitations under the License.
|
|||
|
||||
// +groupName=meta.k8s.io
|
||||
|
||||
package v1beta1 // import "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
|
||||
package v1beta1
|
||||
|
|
|
@ -19,4 +19,4 @@ limitations under the License.
|
|||
//
|
||||
// package testapigroup contains an testapigroup API used to demonstrate how to create api groups. Moreover, this is
|
||||
// used within tests.
|
||||
package testapigroup // import "k8s.io/apimachinery/pkg/apis/testapigroup"
|
||||
package testapigroup
|
||||
|
|
|
@ -19,12 +19,12 @@ package fuzzer
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/google/gofuzz"
|
||||
"sigs.k8s.io/randfill"
|
||||
|
||||
apitesting "k8s.io/apimachinery/pkg/api/apitesting"
|
||||
"k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
|
||||
"k8s.io/apimachinery/pkg/apis/testapigroup"
|
||||
"k8s.io/apimachinery/pkg/apis/testapigroup/v1"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
)
|
||||
|
@ -33,9 +33,9 @@ import (
|
|||
// values in a Kubernetes context.
|
||||
func overrideMetaFuncs(codecs runtimeserializer.CodecFactory) []interface{} {
|
||||
return []interface{}{
|
||||
func(j *runtime.Object, c fuzz.Continue) {
|
||||
func(j *runtime.Object, c randfill.Continue) {
|
||||
// TODO: uncomment when round trip starts from a versioned object
|
||||
if true { //c.RandBool() {
|
||||
if true { // c.Bool() {
|
||||
*j = &runtime.Unknown{
|
||||
// We do not set TypeMeta here because it is not carried through a round trip
|
||||
Raw: []byte(`{"apiVersion":"unknown.group/unknown","kind":"Something","someKey":"someValue"}`),
|
||||
|
@ -44,15 +44,15 @@ func overrideMetaFuncs(codecs runtimeserializer.CodecFactory) []interface{} {
|
|||
} else {
|
||||
types := []runtime.Object{&testapigroup.Carp{}}
|
||||
t := types[c.Rand.Intn(len(types))]
|
||||
c.Fuzz(t)
|
||||
c.Fill(t)
|
||||
*j = t
|
||||
}
|
||||
},
|
||||
func(r *runtime.RawExtension, c fuzz.Continue) {
|
||||
func(r *runtime.RawExtension, c randfill.Continue) {
|
||||
// Pick an arbitrary type and fuzz it
|
||||
types := []runtime.Object{&testapigroup.Carp{}}
|
||||
obj := types[c.Rand.Intn(len(types))]
|
||||
c.Fuzz(obj)
|
||||
c.Fill(obj)
|
||||
|
||||
// Convert the object to raw bytes
|
||||
bytes, err := runtime.Encode(apitesting.TestCodec(codecs, v1.SchemeGroupVersion), obj)
|
||||
|
@ -68,11 +68,11 @@ func overrideMetaFuncs(codecs runtimeserializer.CodecFactory) []interface{} {
|
|||
|
||||
func testapigroupFuncs(codecs runtimeserializer.CodecFactory) []interface{} {
|
||||
return []interface{}{
|
||||
func(s *testapigroup.CarpSpec, c fuzz.Continue) {
|
||||
c.FuzzNoCustom(s)
|
||||
func(s *testapigroup.CarpSpec, c randfill.Continue) {
|
||||
c.FillNoCustom(s)
|
||||
// has a default value
|
||||
ttl := int64(30)
|
||||
if c.RandBool() {
|
||||
if c.Bool() {
|
||||
ttl = int64(c.Uint32())
|
||||
}
|
||||
s.TerminationGracePeriodSeconds = &ttl
|
||||
|
@ -81,11 +81,11 @@ func testapigroupFuncs(codecs runtimeserializer.CodecFactory) []interface{} {
|
|||
s.SchedulerName = "default-scheduler"
|
||||
}
|
||||
},
|
||||
func(j *testapigroup.CarpPhase, c fuzz.Continue) {
|
||||
func(j *testapigroup.CarpPhase, c randfill.Continue) {
|
||||
statuses := []testapigroup.CarpPhase{"Pending", "Running", "Succeeded", "Failed", "Unknown"}
|
||||
*j = statuses[c.Rand.Intn(len(statuses))]
|
||||
},
|
||||
func(rp *testapigroup.RestartPolicy, c fuzz.Continue) {
|
||||
func(rp *testapigroup.RestartPolicy, c randfill.Continue) {
|
||||
policies := []testapigroup.RestartPolicy{"Always", "Never", "OnFailure"}
|
||||
*rp = policies[c.Rand.Intn(len(policies))]
|
||||
},
|
||||
|
|
|
@ -46,6 +46,7 @@ func Resource(resource string) schema.GroupResource {
|
|||
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
&Carp{},
|
||||
&CarpList{},
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -68,6 +68,14 @@ type CarpStatus struct {
|
|||
// This is before the Kubelet pulled the container image(s) for the carp.
|
||||
// +optional
|
||||
StartTime *metav1.Time
|
||||
|
||||
// Carp infos are provided by different clients, hence the map type.
|
||||
//
|
||||
// +listType=map
|
||||
// +listKey=a
|
||||
// +listKey=b
|
||||
// +listKey=c
|
||||
Infos []CarpInfo
|
||||
}
|
||||
|
||||
type CarpCondition struct {
|
||||
|
@ -83,6 +91,21 @@ type CarpCondition struct {
|
|||
Message string
|
||||
}
|
||||
|
||||
type CarpInfo struct {
|
||||
// A is the first map key.
|
||||
// +required
|
||||
A int64
|
||||
// B is the second map key.
|
||||
// +required
|
||||
B string
|
||||
// C is the third, optional map key
|
||||
// +optional
|
||||
C *string
|
||||
|
||||
// Some data for each pair of A and B.
|
||||
Data string
|
||||
}
|
||||
|
||||
// CarpSpec is a description of a carp
|
||||
type CarpSpec struct {
|
||||
// +optional
|
||||
|
|
|
@ -14,11 +14,13 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
// +k8s:deepcopy-gen=package
|
||||
// +k8s:conversion-gen=k8s.io/apimachinery/pkg/apis/testapigroup
|
||||
// +k8s:openapi-gen=false
|
||||
// +k8s:defaulter-gen=TypeMeta
|
||||
|
||||
// +k8s:prerelease-lifecycle-gen=true
|
||||
// +groupName=testapigroup.apimachinery.k8s.io
|
||||
|
||||
package v1 // import "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
|
||||
package v1
|
||||
|
|
|
@ -101,10 +101,38 @@ func (m *CarpCondition) XXX_DiscardUnknown() {
|
|||
|
||||
var xxx_messageInfo_CarpCondition proto.InternalMessageInfo
|
||||
|
||||
func (m *CarpInfo) Reset() { *m = CarpInfo{} }
|
||||
func (*CarpInfo) ProtoMessage() {}
|
||||
func (*CarpInfo) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_83e19b543dd132db, []int{2}
|
||||
}
|
||||
func (m *CarpInfo) XXX_Unmarshal(b []byte) error {
|
||||
return m.Unmarshal(b)
|
||||
}
|
||||
func (m *CarpInfo) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
b = b[:cap(b)]
|
||||
n, err := m.MarshalToSizedBuffer(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b[:n], nil
|
||||
}
|
||||
func (m *CarpInfo) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_CarpInfo.Merge(m, src)
|
||||
}
|
||||
func (m *CarpInfo) XXX_Size() int {
|
||||
return m.Size()
|
||||
}
|
||||
func (m *CarpInfo) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_CarpInfo.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_CarpInfo proto.InternalMessageInfo
|
||||
|
||||
func (m *CarpList) Reset() { *m = CarpList{} }
|
||||
func (*CarpList) ProtoMessage() {}
|
||||
func (*CarpList) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_83e19b543dd132db, []int{2}
|
||||
return fileDescriptor_83e19b543dd132db, []int{3}
|
||||
}
|
||||
func (m *CarpList) XXX_Unmarshal(b []byte) error {
|
||||
return m.Unmarshal(b)
|
||||
|
@ -132,7 +160,7 @@ var xxx_messageInfo_CarpList proto.InternalMessageInfo
|
|||
func (m *CarpSpec) Reset() { *m = CarpSpec{} }
|
||||
func (*CarpSpec) ProtoMessage() {}
|
||||
func (*CarpSpec) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_83e19b543dd132db, []int{3}
|
||||
return fileDescriptor_83e19b543dd132db, []int{4}
|
||||
}
|
||||
func (m *CarpSpec) XXX_Unmarshal(b []byte) error {
|
||||
return m.Unmarshal(b)
|
||||
|
@ -160,7 +188,7 @@ var xxx_messageInfo_CarpSpec proto.InternalMessageInfo
|
|||
func (m *CarpStatus) Reset() { *m = CarpStatus{} }
|
||||
func (*CarpStatus) ProtoMessage() {}
|
||||
func (*CarpStatus) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_83e19b543dd132db, []int{4}
|
||||
return fileDescriptor_83e19b543dd132db, []int{5}
|
||||
}
|
||||
func (m *CarpStatus) XXX_Unmarshal(b []byte) error {
|
||||
return m.Unmarshal(b)
|
||||
|
@ -188,6 +216,7 @@ var xxx_messageInfo_CarpStatus proto.InternalMessageInfo
|
|||
func init() {
|
||||
proto.RegisterType((*Carp)(nil), "k8s.io.apimachinery.pkg.apis.testapigroup.v1.Carp")
|
||||
proto.RegisterType((*CarpCondition)(nil), "k8s.io.apimachinery.pkg.apis.testapigroup.v1.CarpCondition")
|
||||
proto.RegisterType((*CarpInfo)(nil), "k8s.io.apimachinery.pkg.apis.testapigroup.v1.CarpInfo")
|
||||
proto.RegisterType((*CarpList)(nil), "k8s.io.apimachinery.pkg.apis.testapigroup.v1.CarpList")
|
||||
proto.RegisterType((*CarpSpec)(nil), "k8s.io.apimachinery.pkg.apis.testapigroup.v1.CarpSpec")
|
||||
proto.RegisterMapType((map[string]string)(nil), "k8s.io.apimachinery.pkg.apis.testapigroup.v1.CarpSpec.NodeSelectorEntry")
|
||||
|
@ -199,72 +228,77 @@ func init() {
|
|||
}
|
||||
|
||||
var fileDescriptor_83e19b543dd132db = []byte{
|
||||
// 1037 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x56, 0xc1, 0x6e, 0xdb, 0x46,
|
||||
0x13, 0x36, 0x2d, 0xc9, 0x96, 0xd6, 0x56, 0x62, 0x6f, 0x62, 0x80, 0xbf, 0x81, 0x48, 0x8e, 0x0f,
|
||||
0x86, 0xff, 0xc2, 0xa5, 0x62, 0xa3, 0x09, 0xdc, 0xa6, 0x40, 0x11, 0xda, 0x45, 0xa5, 0xc2, 0x71,
|
||||
0x84, 0x95, 0x81, 0x14, 0x45, 0x0f, 0x59, 0x51, 0x5b, 0x8a, 0x95, 0xc8, 0x25, 0x76, 0x57, 0x2a,
|
||||
0x74, 0x2b, 0xfa, 0x04, 0x7d, 0x88, 0xde, 0x7a, 0xee, 0x03, 0xf4, 0x50, 0xc0, 0xc7, 0x1c, 0x73,
|
||||
0x12, 0x6a, 0xf5, 0x2d, 0x7c, 0x2a, 0x76, 0xb9, 0xa4, 0x48, 0x4b, 0x55, 0xa3, 0xdc, 0xb8, 0x33,
|
||||
0xdf, 0xf7, 0xcd, 0xec, 0xce, 0x68, 0x46, 0xe0, 0xf3, 0xde, 0x29, 0xb7, 0x3c, 0x5a, 0xc3, 0xa1,
|
||||
0xe7, 0x63, 0xa7, 0xeb, 0x05, 0x84, 0x8d, 0x6a, 0x61, 0xcf, 0x95, 0x06, 0x5e, 0x13, 0x84, 0x0b,
|
||||
0x1c, 0x7a, 0x2e, 0xa3, 0x83, 0xb0, 0x36, 0x3c, 0xae, 0xb9, 0x24, 0x20, 0x0c, 0x0b, 0xd2, 0xb1,
|
||||
0x42, 0x46, 0x05, 0x85, 0x47, 0x11, 0xdb, 0x4a, 0xb3, 0xad, 0xb0, 0xe7, 0x4a, 0x03, 0xb7, 0xd2,
|
||||
0x6c, 0x6b, 0x78, 0xbc, 0xfb, 0xb1, 0xeb, 0x89, 0xee, 0xa0, 0x6d, 0x39, 0xd4, 0xaf, 0xb9, 0xd4,
|
||||
0xa5, 0x35, 0x25, 0xd2, 0x1e, 0x7c, 0xaf, 0x4e, 0xea, 0xa0, 0xbe, 0x22, 0xf1, 0xdd, 0x4f, 0x16,
|
||||
0xa6, 0xe6, 0x13, 0x81, 0xe7, 0xa4, 0xb4, 0x5b, 0xfb, 0x37, 0x16, 0x1b, 0x04, 0xc2, 0xf3, 0xc9,
|
||||
0x0c, 0xe1, 0xd9, 0x7f, 0x11, 0xb8, 0xd3, 0x25, 0x3e, 0xbe, 0xcb, 0xdb, 0xff, 0x75, 0x15, 0xe4,
|
||||
0xcf, 0x30, 0x0b, 0xe1, 0x1b, 0x50, 0x94, 0xc9, 0x74, 0xb0, 0xc0, 0xa6, 0xb1, 0x67, 0x1c, 0x6e,
|
||||
0x9c, 0x3c, 0xb1, 0x16, 0xbe, 0x8b, 0x44, 0x5b, 0xc3, 0x63, 0xeb, 0x55, 0xfb, 0x07, 0xe2, 0x88,
|
||||
0x97, 0x44, 0x60, 0x1b, 0x5e, 0x8f, 0xab, 0x2b, 0x93, 0x71, 0x15, 0x4c, 0x6d, 0x28, 0x51, 0x85,
|
||||
0xdf, 0x80, 0x3c, 0x0f, 0x89, 0x63, 0xae, 0x2a, 0xf5, 0x67, 0xd6, 0x32, 0xaf, 0x6e, 0xc9, 0x1c,
|
||||
0x5b, 0x21, 0x71, 0xec, 0x4d, 0x1d, 0x23, 0x2f, 0x4f, 0x48, 0x29, 0xc2, 0x37, 0x60, 0x8d, 0x0b,
|
||||
0x2c, 0x06, 0xdc, 0xcc, 0x29, 0xed, 0xd3, 0x0f, 0xd0, 0x56, 0x7c, 0xfb, 0x9e, 0x56, 0x5f, 0x8b,
|
||||
0xce, 0x48, 0xeb, 0xee, 0xff, 0x9e, 0x03, 0x65, 0x09, 0x3b, 0xa3, 0x41, 0xc7, 0x13, 0x1e, 0x0d,
|
||||
0xe0, 0x53, 0x90, 0x17, 0xa3, 0x90, 0xa8, 0xb7, 0x2a, 0xd9, 0x8f, 0xe3, 0xac, 0xae, 0x46, 0x21,
|
||||
0xb9, 0x1d, 0x57, 0xb7, 0x33, 0x60, 0x69, 0x44, 0x0a, 0x0e, 0x3f, 0x4d, 0x52, 0x5d, 0xcd, 0x10,
|
||||
0x75, 0xc0, 0xdb, 0x71, 0xf5, 0x7e, 0x42, 0xcb, 0xe6, 0x00, 0x5d, 0x50, 0xee, 0x63, 0x2e, 0x9a,
|
||||
0x8c, 0xb6, 0xc9, 0x95, 0xe7, 0x13, 0x7d, 0xd9, 0x8f, 0xde, 0xaf, 0x4c, 0x92, 0x61, 0xef, 0xe8,
|
||||
0x68, 0xe5, 0x8b, 0xb4, 0x10, 0xca, 0xea, 0xc2, 0x21, 0x80, 0xd2, 0x70, 0xc5, 0x70, 0xc0, 0xa3,
|
||||
0xfc, 0x65, 0xb4, 0xfc, 0xd2, 0xd1, 0x76, 0x75, 0x34, 0x78, 0x31, 0xa3, 0x86, 0xe6, 0x44, 0x80,
|
||||
0x07, 0x60, 0x8d, 0x11, 0xcc, 0x69, 0x60, 0x16, 0xd4, 0xdb, 0x24, 0xc5, 0x40, 0xca, 0x8a, 0xb4,
|
||||
0x17, 0xfe, 0x1f, 0xac, 0xfb, 0x84, 0x73, 0xec, 0x12, 0x73, 0x4d, 0x01, 0xef, 0x6b, 0xe0, 0xfa,
|
||||
0xcb, 0xc8, 0x8c, 0x62, 0xff, 0xfe, 0x1f, 0x06, 0x28, 0xca, 0x52, 0x5c, 0x78, 0x5c, 0xc0, 0xef,
|
||||
0x66, 0x5a, 0xdc, 0x7a, 0xbf, 0xdb, 0x48, 0xb6, 0x6a, 0xf0, 0x2d, 0x1d, 0xa8, 0x18, 0x5b, 0x52,
|
||||
0xed, 0xfd, 0x1a, 0x14, 0x3c, 0x41, 0x7c, 0x59, 0xd8, 0xdc, 0xe1, 0xc6, 0xc9, 0xc9, 0xf2, 0x3d,
|
||||
0x68, 0x97, 0xb5, 0x7c, 0xa1, 0x21, 0x85, 0x50, 0xa4, 0xb7, 0xff, 0xe7, 0x7a, 0x74, 0x07, 0xd9,
|
||||
0xf0, 0xf0, 0x02, 0x94, 0x99, 0xa4, 0x32, 0xd1, 0xa4, 0x7d, 0xcf, 0x19, 0xa9, 0x26, 0x28, 0xd9,
|
||||
0x07, 0x71, 0x61, 0x51, 0xda, 0x79, 0x7b, 0xd7, 0x80, 0xb2, 0x64, 0xe8, 0x82, 0x47, 0x82, 0x30,
|
||||
0xdf, 0x0b, 0xb0, 0x2c, 0xc2, 0x57, 0x0c, 0x3b, 0xa4, 0x49, 0x98, 0x47, 0x3b, 0x2d, 0xe2, 0xd0,
|
||||
0xa0, 0xc3, 0x55, 0xd1, 0x73, 0xf6, 0xe3, 0xc9, 0xb8, 0xfa, 0xe8, 0x6a, 0x11, 0x10, 0x2d, 0xd6,
|
||||
0x81, 0xaf, 0xc0, 0x0e, 0x76, 0x84, 0x37, 0x24, 0xe7, 0x04, 0x77, 0xfa, 0x5e, 0x40, 0xe2, 0x00,
|
||||
0x05, 0x15, 0xe0, 0x7f, 0x93, 0x71, 0x75, 0xe7, 0xc5, 0x3c, 0x00, 0x9a, 0xcf, 0x83, 0x3f, 0x1b,
|
||||
0x60, 0x33, 0xa0, 0x1d, 0xd2, 0x22, 0x7d, 0xe2, 0x08, 0xca, 0xcc, 0x75, 0xf5, 0xea, 0xf5, 0x0f,
|
||||
0x9b, 0x2a, 0xd6, 0x65, 0x4a, 0xea, 0xcb, 0x40, 0xb0, 0x91, 0xfd, 0x50, 0xbf, 0xe8, 0x66, 0xda,
|
||||
0x85, 0x32, 0x31, 0xe1, 0xd7, 0x00, 0x72, 0xc2, 0x86, 0x9e, 0x43, 0x5e, 0x38, 0x0e, 0x1d, 0x04,
|
||||
0xe2, 0x12, 0xfb, 0xc4, 0x2c, 0xaa, 0x8a, 0x24, 0xcd, 0xdf, 0x9a, 0x41, 0xa0, 0x39, 0x2c, 0x58,
|
||||
0x07, 0xf7, 0xb2, 0x56, 0xb3, 0xa4, 0x74, 0xf6, 0xb4, 0x8e, 0x79, 0x4e, 0x42, 0x46, 0x1c, 0x39,
|
||||
0xba, 0xb3, 0x8a, 0xe8, 0x0e, 0x0f, 0x1e, 0x81, 0xa2, 0xcc, 0x52, 0xe5, 0x02, 0x94, 0x46, 0xd2,
|
||||
0xb6, 0x97, 0xda, 0x8e, 0x12, 0x04, 0x7c, 0x0a, 0x36, 0xba, 0x94, 0x8b, 0x4b, 0x22, 0x7e, 0xa4,
|
||||
0xac, 0x67, 0x6e, 0xec, 0x19, 0x87, 0x45, 0xfb, 0x81, 0x26, 0x6c, 0xd4, 0xa7, 0x2e, 0x94, 0xc6,
|
||||
0xc9, 0xdf, 0xa0, 0x3c, 0x36, 0x1b, 0xe7, 0xe6, 0xa6, 0xa2, 0x24, 0xbf, 0xc1, 0x7a, 0x64, 0x46,
|
||||
0xb1, 0x3f, 0x86, 0x36, 0x9a, 0x67, 0x66, 0x79, 0x16, 0xda, 0x68, 0x9e, 0xa1, 0xd8, 0x2f, 0x53,
|
||||
0x97, 0x9f, 0x81, 0x4c, 0x7d, 0x2b, 0x9b, 0x7a, 0x5d, 0xdb, 0x51, 0x82, 0x80, 0x35, 0x50, 0xe2,
|
||||
0x83, 0x76, 0x87, 0xfa, 0xd8, 0x0b, 0xcc, 0x6d, 0x05, 0xdf, 0xd6, 0xf0, 0x52, 0x2b, 0x76, 0xa0,
|
||||
0x29, 0x06, 0x3e, 0x07, 0x65, 0xb9, 0x06, 0x3b, 0x83, 0x3e, 0x61, 0x2a, 0xc6, 0x03, 0x45, 0x4a,
|
||||
0xa6, 0x62, 0x2b, 0x76, 0xaa, 0x37, 0xca, 0x62, 0x77, 0xbf, 0x00, 0xdb, 0x33, 0x5d, 0x02, 0xb7,
|
||||
0x40, 0xae, 0x47, 0x46, 0xd1, 0x12, 0x40, 0xf2, 0x13, 0x3e, 0x04, 0x85, 0x21, 0xee, 0x0f, 0x48,
|
||||
0x34, 0xdf, 0x51, 0x74, 0xf8, 0x6c, 0xf5, 0xd4, 0xd8, 0xff, 0x2d, 0x07, 0xc0, 0x74, 0xd5, 0xc0,
|
||||
0x27, 0xa0, 0x10, 0x76, 0x31, 0x8f, 0x37, 0x48, 0xdc, 0x2f, 0x85, 0xa6, 0x34, 0xde, 0x8e, 0xab,
|
||||
0x25, 0x89, 0x55, 0x07, 0x14, 0x01, 0x21, 0x05, 0xc0, 0x89, 0x77, 0x43, 0x3c, 0x66, 0x9e, 0x2f,
|
||||
0xdf, 0xf0, 0xc9, 0x7e, 0x99, 0xee, 0xeb, 0xc4, 0xc4, 0x51, 0x2a, 0x44, 0x7a, 0xd0, 0xe6, 0x16,
|
||||
0x0f, 0xda, 0xd4, 0xec, 0xce, 0x2f, 0x9c, 0xdd, 0x07, 0x60, 0x2d, 0x2a, 0xf6, 0xdd, 0x19, 0x1f,
|
||||
0xf5, 0x02, 0xd2, 0x5e, 0x89, 0x73, 0x30, 0x0b, 0x1b, 0x4d, 0x3d, 0xe2, 0x13, 0xdc, 0x99, 0xb2,
|
||||
0x22, 0xed, 0x85, 0xaf, 0x41, 0x49, 0x0d, 0x34, 0xb5, 0xa2, 0xd6, 0x97, 0x5e, 0x51, 0x65, 0xd5,
|
||||
0x2b, 0xb1, 0x00, 0x9a, 0x6a, 0xd9, 0xe8, 0xfa, 0xa6, 0xb2, 0xf2, 0xf6, 0xa6, 0xb2, 0xf2, 0xee,
|
||||
0xa6, 0xb2, 0xf2, 0xd3, 0xa4, 0x62, 0x5c, 0x4f, 0x2a, 0xc6, 0xdb, 0x49, 0xc5, 0x78, 0x37, 0xa9,
|
||||
0x18, 0x7f, 0x4d, 0x2a, 0xc6, 0x2f, 0x7f, 0x57, 0x56, 0xbe, 0x3d, 0x5a, 0xe6, 0x8f, 0xe7, 0x3f,
|
||||
0x01, 0x00, 0x00, 0xff, 0xff, 0x9e, 0xd1, 0x13, 0x90, 0xa7, 0x0a, 0x00, 0x00,
|
||||
// 1115 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x56, 0x41, 0x4f, 0x1b, 0x47,
|
||||
0x14, 0x66, 0xb1, 0x0d, 0xf6, 0x80, 0x1b, 0x18, 0x82, 0xba, 0x45, 0x8a, 0x4d, 0x7c, 0x40, 0xb4,
|
||||
0xa2, 0xeb, 0x80, 0x9a, 0x88, 0x36, 0x95, 0x2a, 0x16, 0xaa, 0x42, 0x45, 0x88, 0x35, 0x46, 0x4a,
|
||||
0xd5, 0xe6, 0x90, 0xf1, 0xee, 0xb0, 0x6c, 0xf1, 0xee, 0xac, 0x66, 0xc6, 0xae, 0x7c, 0xab, 0x7a,
|
||||
0xea, 0xb1, 0x3f, 0xa2, 0x7f, 0xa1, 0x3f, 0xa0, 0x37, 0x8e, 0x39, 0xa6, 0x17, 0xab, 0xb8, 0xff,
|
||||
0x82, 0x53, 0x35, 0xb3, 0xb3, 0xeb, 0x35, 0x06, 0x27, 0xe6, 0xe6, 0x79, 0xef, 0xfb, 0xbe, 0xf7,
|
||||
0xf6, 0xcd, 0x9b, 0xf7, 0x0c, 0xbe, 0xbe, 0xd8, 0xe5, 0x96, 0x4f, 0xeb, 0x38, 0xf2, 0x03, 0xec,
|
||||
0x9c, 0xfb, 0x21, 0x61, 0xbd, 0x7a, 0x74, 0xe1, 0x49, 0x03, 0xaf, 0x0b, 0xc2, 0x05, 0x8e, 0x7c,
|
||||
0x8f, 0xd1, 0x4e, 0x54, 0xef, 0x6e, 0xd7, 0x3d, 0x12, 0x12, 0x86, 0x05, 0x71, 0xad, 0x88, 0x51,
|
||||
0x41, 0xe1, 0x56, 0xcc, 0xb6, 0xb2, 0x6c, 0x2b, 0xba, 0xf0, 0xa4, 0x81, 0x5b, 0x59, 0xb6, 0xd5,
|
||||
0xdd, 0x5e, 0xfb, 0xdc, 0xf3, 0xc5, 0x79, 0xa7, 0x65, 0x39, 0x34, 0xa8, 0x7b, 0xd4, 0xa3, 0x75,
|
||||
0x25, 0xd2, 0xea, 0x9c, 0xa9, 0x93, 0x3a, 0xa8, 0x5f, 0xb1, 0xf8, 0xda, 0x17, 0x13, 0x53, 0x0b,
|
||||
0x88, 0xc0, 0xb7, 0xa4, 0xb4, 0x56, 0xbf, 0x8b, 0xc5, 0x3a, 0xa1, 0xf0, 0x03, 0x32, 0x46, 0x78,
|
||||
0xf6, 0x3e, 0x02, 0x77, 0xce, 0x49, 0x80, 0x6f, 0xf2, 0x6a, 0x7f, 0xce, 0x82, 0xfc, 0x3e, 0x66,
|
||||
0x11, 0x7c, 0x03, 0x8a, 0x32, 0x19, 0x17, 0x0b, 0x6c, 0x1a, 0xeb, 0xc6, 0xe6, 0xc2, 0xce, 0x13,
|
||||
0x6b, 0x62, 0x5d, 0x24, 0xda, 0xea, 0x6e, 0x5b, 0x2f, 0x5b, 0x3f, 0x13, 0x47, 0xbc, 0x20, 0x02,
|
||||
0xdb, 0xf0, 0xb2, 0x5f, 0x9d, 0x19, 0xf4, 0xab, 0x60, 0x68, 0x43, 0xa9, 0x2a, 0xfc, 0x01, 0xe4,
|
||||
0x79, 0x44, 0x1c, 0x73, 0x56, 0xa9, 0x3f, 0xb3, 0xa6, 0xa9, 0xba, 0x25, 0x73, 0x6c, 0x46, 0xc4,
|
||||
0xb1, 0x17, 0x75, 0x8c, 0xbc, 0x3c, 0x21, 0xa5, 0x08, 0xdf, 0x80, 0x39, 0x2e, 0xb0, 0xe8, 0x70,
|
||||
0x33, 0xa7, 0xb4, 0x77, 0xef, 0xa1, 0xad, 0xf8, 0xf6, 0x47, 0x5a, 0x7d, 0x2e, 0x3e, 0x23, 0xad,
|
||||
0x5b, 0xfb, 0x2b, 0x07, 0xca, 0x12, 0xb6, 0x4f, 0x43, 0xd7, 0x17, 0x3e, 0x0d, 0xe1, 0x53, 0x90,
|
||||
0x17, 0xbd, 0x88, 0xa8, 0x5a, 0x95, 0xec, 0xc7, 0x49, 0x56, 0xa7, 0xbd, 0x88, 0x5c, 0xf7, 0xab,
|
||||
0xcb, 0x23, 0x60, 0x69, 0x44, 0x0a, 0x0e, 0xbf, 0x4c, 0x53, 0x9d, 0x1d, 0x21, 0xea, 0x80, 0xd7,
|
||||
0xfd, 0xea, 0x83, 0x94, 0x36, 0x9a, 0x03, 0xf4, 0x40, 0xb9, 0x8d, 0xb9, 0x68, 0x30, 0xda, 0x22,
|
||||
0xa7, 0x7e, 0x40, 0xf4, 0xc7, 0x7e, 0xf6, 0x61, 0xd7, 0x24, 0x19, 0xf6, 0xaa, 0x8e, 0x56, 0x3e,
|
||||
0xce, 0x0a, 0xa1, 0x51, 0x5d, 0xd8, 0x05, 0x50, 0x1a, 0x4e, 0x19, 0x0e, 0x79, 0x9c, 0xbf, 0x8c,
|
||||
0x96, 0x9f, 0x3a, 0xda, 0x9a, 0x8e, 0x06, 0x8f, 0xc7, 0xd4, 0xd0, 0x2d, 0x11, 0xe0, 0x06, 0x98,
|
||||
0x63, 0x04, 0x73, 0x1a, 0x9a, 0x05, 0x55, 0x9b, 0xf4, 0x32, 0x90, 0xb2, 0x22, 0xed, 0x85, 0x9f,
|
||||
0x82, 0xf9, 0x80, 0x70, 0x8e, 0x3d, 0x62, 0xce, 0x29, 0xe0, 0x03, 0x0d, 0x9c, 0x7f, 0x11, 0x9b,
|
||||
0x51, 0xe2, 0xaf, 0x71, 0x50, 0x94, 0x37, 0x71, 0x14, 0x9e, 0x51, 0xf8, 0x31, 0x30, 0xe2, 0xd6,
|
||||
0xce, 0xd9, 0x25, 0x4d, 0x30, 0xf6, 0x90, 0x81, 0xa5, 0xa3, 0xa5, 0xaf, 0x23, 0x75, 0xd8, 0xc8,
|
||||
0x68, 0xc1, 0x15, 0x60, 0x38, 0xea, 0xbb, 0x4b, 0x76, 0x41, 0x1a, 0xf7, 0x91, 0xe1, 0xc0, 0x75,
|
||||
0x90, 0x57, 0x8f, 0x24, 0xa7, 0xec, 0x69, 0x3b, 0x1e, 0x60, 0x81, 0x91, 0xf2, 0xd4, 0xfe, 0x36,
|
||||
0xe2, 0xa8, 0xc7, 0x3e, 0x17, 0xf0, 0xf5, 0xd8, 0xbb, 0xb2, 0x3e, 0xac, 0x84, 0x92, 0xad, 0x5e,
|
||||
0xd5, 0x92, 0x0e, 0x51, 0x4c, 0x2c, 0x99, 0x37, 0xf5, 0x0a, 0x14, 0x7c, 0x41, 0x02, 0xd9, 0x4d,
|
||||
0xb9, 0xcd, 0x85, 0x9d, 0x9d, 0xe9, 0x1b, 0xdf, 0x2e, 0x6b, 0xf9, 0xc2, 0x91, 0x14, 0x42, 0xb1,
|
||||
0x5e, 0xed, 0x9f, 0xf9, 0xf8, 0x1b, 0xe4, 0x2b, 0x83, 0xc7, 0xa0, 0xcc, 0x24, 0x95, 0x89, 0x06,
|
||||
0x6d, 0xfb, 0x4e, 0x4f, 0x7f, 0xfb, 0x46, 0xd2, 0x4d, 0x28, 0xeb, 0xbc, 0xbe, 0x69, 0x40, 0xa3,
|
||||
0x64, 0xe8, 0x81, 0x47, 0x82, 0xb0, 0xc0, 0x0f, 0xb1, 0xbc, 0xf9, 0xef, 0x18, 0x76, 0x48, 0x83,
|
||||
0x30, 0x9f, 0xba, 0x4d, 0xe2, 0xd0, 0xd0, 0xe5, 0xaa, 0xe2, 0x39, 0xfb, 0xf1, 0xa0, 0x5f, 0x7d,
|
||||
0x74, 0x3a, 0x09, 0x88, 0x26, 0xeb, 0xc0, 0x97, 0x60, 0x15, 0x3b, 0xc2, 0xef, 0x92, 0x03, 0x82,
|
||||
0xdd, 0xb6, 0x1f, 0x92, 0x24, 0x40, 0x41, 0x05, 0xf8, 0x64, 0xd0, 0xaf, 0xae, 0xee, 0xdd, 0x06,
|
||||
0x40, 0xb7, 0xf3, 0xe0, 0x6f, 0x06, 0x58, 0x0c, 0xa9, 0x4b, 0x9a, 0xa4, 0x4d, 0x1c, 0x41, 0x99,
|
||||
0x39, 0xaf, 0xaa, 0x7e, 0x78, 0xbf, 0x51, 0x66, 0x9d, 0x64, 0xa4, 0xbe, 0x0d, 0x05, 0xeb, 0xd9,
|
||||
0x0f, 0x75, 0x45, 0x17, 0xb3, 0x2e, 0x34, 0x12, 0x13, 0x7e, 0x0f, 0x20, 0x27, 0xac, 0xeb, 0x3b,
|
||||
0x64, 0xcf, 0x71, 0x68, 0x27, 0x14, 0x27, 0x38, 0x20, 0x66, 0x51, 0xdd, 0x48, 0xfa, 0xe2, 0x9a,
|
||||
0x63, 0x08, 0x74, 0x0b, 0x0b, 0xbe, 0x06, 0xa6, 0x4b, 0x22, 0x46, 0x1c, 0xb9, 0x11, 0x46, 0x39,
|
||||
0x66, 0x49, 0x29, 0xae, 0x6b, 0x45, 0xf3, 0xe0, 0x0e, 0x1c, 0xba, 0x53, 0x01, 0x6e, 0x81, 0xa2,
|
||||
0xcc, 0x5c, 0xe5, 0x07, 0x94, 0x5a, 0xda, 0xca, 0x27, 0xda, 0x8e, 0x52, 0x04, 0x7c, 0x0a, 0x16,
|
||||
0xce, 0x29, 0x17, 0x27, 0x44, 0xfc, 0x42, 0xd9, 0x85, 0xb9, 0xb0, 0x6e, 0x6c, 0x16, 0xed, 0x15,
|
||||
0x4d, 0x58, 0x38, 0x1c, 0xba, 0x50, 0x16, 0x27, 0x87, 0x81, 0x3c, 0x36, 0x8e, 0x0e, 0xcc, 0x45,
|
||||
0x45, 0x49, 0x87, 0xc1, 0x61, 0x6c, 0x46, 0x89, 0x3f, 0x81, 0x1e, 0x35, 0xf6, 0xcd, 0xf2, 0x38,
|
||||
0xf4, 0xa8, 0xb1, 0x8f, 0x12, 0xbf, 0x4c, 0x5d, 0xfe, 0x0c, 0x65, 0xea, 0x4b, 0xa3, 0xa9, 0x1f,
|
||||
0x6a, 0x3b, 0x4a, 0x11, 0xb0, 0x0e, 0x4a, 0xbc, 0xd3, 0x72, 0x69, 0x80, 0xfd, 0xd0, 0x5c, 0x56,
|
||||
0xf0, 0x65, 0x0d, 0x2f, 0x35, 0x13, 0x07, 0x1a, 0x62, 0xe0, 0x73, 0x50, 0x96, 0xfb, 0xd8, 0xed,
|
||||
0xb4, 0x09, 0x53, 0xe5, 0x59, 0x51, 0xa4, 0x74, 0x3c, 0x37, 0xb3, 0x4e, 0x34, 0x8a, 0x5d, 0xfb,
|
||||
0x06, 0x2c, 0x8f, 0x75, 0x0e, 0x5c, 0x02, 0xb9, 0x0b, 0xd2, 0x8b, 0xb7, 0x11, 0x92, 0x3f, 0xe1,
|
||||
0x43, 0x50, 0xe8, 0xe2, 0x76, 0x87, 0xc4, 0x93, 0x0d, 0xc5, 0x87, 0xaf, 0x66, 0x77, 0x8d, 0xda,
|
||||
0xef, 0x79, 0x00, 0x86, 0x3b, 0x0f, 0x3e, 0x01, 0x85, 0xe8, 0x1c, 0xf3, 0x64, 0x95, 0x25, 0x3d,
|
||||
0x54, 0x68, 0x48, 0xe3, 0x75, 0xbf, 0x5a, 0x92, 0x58, 0x75, 0x40, 0x31, 0x10, 0x52, 0x00, 0x9c,
|
||||
0x64, 0x49, 0x25, 0xa3, 0xe7, 0xf9, 0xf4, 0x8f, 0x20, 0x5d, 0x74, 0xc3, 0x3f, 0x0e, 0xa9, 0x89,
|
||||
0xa3, 0x4c, 0x88, 0xec, 0xc4, 0xcf, 0x4d, 0x9e, 0xf8, 0x99, 0x25, 0x92, 0x9f, 0xb8, 0x44, 0x36,
|
||||
0xc0, 0x5c, 0x7c, 0xd9, 0x37, 0x97, 0x4d, 0xdc, 0x0b, 0x48, 0x7b, 0x25, 0xce, 0x91, 0x1b, 0xa4,
|
||||
0xa1, 0x77, 0x4d, 0x8a, 0x53, 0x7b, 0xa5, 0x81, 0xb4, 0x17, 0xbe, 0x02, 0x25, 0x35, 0xe4, 0xd4,
|
||||
0xae, 0x9c, 0x9f, 0x7a, 0x57, 0x96, 0x55, 0xaf, 0x24, 0x02, 0x68, 0xa8, 0x05, 0x7f, 0x02, 0x05,
|
||||
0x3f, 0x3c, 0xa3, 0xdc, 0x2c, 0xaa, 0x3a, 0xdf, 0xe3, 0x7f, 0x93, 0xdc, 0x7e, 0x99, 0x31, 0x2f,
|
||||
0xc5, 0x50, 0xac, 0x69, 0xa3, 0xcb, 0xab, 0xca, 0xcc, 0xdb, 0xab, 0xca, 0xcc, 0xbb, 0xab, 0xca,
|
||||
0xcc, 0xaf, 0x83, 0x8a, 0x71, 0x39, 0xa8, 0x18, 0x6f, 0x07, 0x15, 0xe3, 0xdd, 0xa0, 0x62, 0xfc,
|
||||
0x3b, 0xa8, 0x18, 0x7f, 0xfc, 0x57, 0x99, 0xf9, 0x71, 0x6b, 0x9a, 0xbf, 0xd7, 0xff, 0x07, 0x00,
|
||||
0x00, 0xff, 0xff, 0xa8, 0x1e, 0x19, 0x20, 0x8d, 0x0b, 0x00, 0x00,
|
||||
}
|
||||
|
||||
func (m *Carp) Marshal() (dAtA []byte, err error) {
|
||||
|
@ -383,6 +417,49 @@ func (m *CarpCondition) MarshalToSizedBuffer(dAtA []byte) (int, error) {
|
|||
return len(dAtA) - i, nil
|
||||
}
|
||||
|
||||
func (m *CarpInfo) Marshal() (dAtA []byte, err error) {
|
||||
size := m.Size()
|
||||
dAtA = make([]byte, size)
|
||||
n, err := m.MarshalToSizedBuffer(dAtA[:size])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dAtA[:n], nil
|
||||
}
|
||||
|
||||
func (m *CarpInfo) MarshalTo(dAtA []byte) (int, error) {
|
||||
size := m.Size()
|
||||
return m.MarshalToSizedBuffer(dAtA[:size])
|
||||
}
|
||||
|
||||
func (m *CarpInfo) MarshalToSizedBuffer(dAtA []byte) (int, error) {
|
||||
i := len(dAtA)
|
||||
_ = i
|
||||
var l int
|
||||
_ = l
|
||||
if m.C != nil {
|
||||
i -= len(*m.C)
|
||||
copy(dAtA[i:], *m.C)
|
||||
i = encodeVarintGenerated(dAtA, i, uint64(len(*m.C)))
|
||||
i--
|
||||
dAtA[i] = 0x22
|
||||
}
|
||||
i -= len(m.Data)
|
||||
copy(dAtA[i:], m.Data)
|
||||
i = encodeVarintGenerated(dAtA, i, uint64(len(m.Data)))
|
||||
i--
|
||||
dAtA[i] = 0x1a
|
||||
i -= len(m.B)
|
||||
copy(dAtA[i:], m.B)
|
||||
i = encodeVarintGenerated(dAtA, i, uint64(len(m.B)))
|
||||
i--
|
||||
dAtA[i] = 0x12
|
||||
i = encodeVarintGenerated(dAtA, i, uint64(m.A))
|
||||
i--
|
||||
dAtA[i] = 0x8
|
||||
return len(dAtA) - i, nil
|
||||
}
|
||||
|
||||
func (m *CarpList) Marshal() (dAtA []byte, err error) {
|
||||
size := m.Size()
|
||||
dAtA = make([]byte, size)
|
||||
|
@ -572,6 +649,20 @@ func (m *CarpStatus) MarshalToSizedBuffer(dAtA []byte) (int, error) {
|
|||
_ = i
|
||||
var l int
|
||||
_ = l
|
||||
if len(m.Infos) > 0 {
|
||||
for iNdEx := len(m.Infos) - 1; iNdEx >= 0; iNdEx-- {
|
||||
{
|
||||
size, err := m.Infos[iNdEx].MarshalToSizedBuffer(dAtA[:i])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
i -= size
|
||||
i = encodeVarintGenerated(dAtA, i, uint64(size))
|
||||
}
|
||||
i--
|
||||
dAtA[i] = 0x42
|
||||
}
|
||||
}
|
||||
if m.StartTime != nil {
|
||||
{
|
||||
size, err := m.StartTime.MarshalToSizedBuffer(dAtA[:i])
|
||||
|
@ -673,6 +764,24 @@ func (m *CarpCondition) Size() (n int) {
|
|||
return n
|
||||
}
|
||||
|
||||
func (m *CarpInfo) Size() (n int) {
|
||||
if m == nil {
|
||||
return 0
|
||||
}
|
||||
var l int
|
||||
_ = l
|
||||
n += 1 + sovGenerated(uint64(m.A))
|
||||
l = len(m.B)
|
||||
n += 1 + l + sovGenerated(uint64(l))
|
||||
l = len(m.Data)
|
||||
n += 1 + l + sovGenerated(uint64(l))
|
||||
if m.C != nil {
|
||||
l = len(*m.C)
|
||||
n += 1 + l + sovGenerated(uint64(l))
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (m *CarpList) Size() (n int) {
|
||||
if m == nil {
|
||||
return 0
|
||||
|
@ -756,6 +865,12 @@ func (m *CarpStatus) Size() (n int) {
|
|||
l = m.StartTime.Size()
|
||||
n += 1 + l + sovGenerated(uint64(l))
|
||||
}
|
||||
if len(m.Infos) > 0 {
|
||||
for _, e := range m.Infos {
|
||||
l = e.Size()
|
||||
n += 1 + l + sovGenerated(uint64(l))
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
|
@ -792,6 +907,19 @@ func (this *CarpCondition) String() string {
|
|||
}, "")
|
||||
return s
|
||||
}
|
||||
func (this *CarpInfo) String() string {
|
||||
if this == nil {
|
||||
return "nil"
|
||||
}
|
||||
s := strings.Join([]string{`&CarpInfo{`,
|
||||
`A:` + fmt.Sprintf("%v", this.A) + `,`,
|
||||
`B:` + fmt.Sprintf("%v", this.B) + `,`,
|
||||
`Data:` + fmt.Sprintf("%v", this.Data) + `,`,
|
||||
`C:` + valueToStringGenerated(this.C) + `,`,
|
||||
`}`,
|
||||
}, "")
|
||||
return s
|
||||
}
|
||||
func (this *CarpList) String() string {
|
||||
if this == nil {
|
||||
return "nil"
|
||||
|
@ -849,6 +977,11 @@ func (this *CarpStatus) String() string {
|
|||
repeatedStringForConditions += strings.Replace(strings.Replace(f.String(), "CarpCondition", "CarpCondition", 1), `&`, ``, 1) + ","
|
||||
}
|
||||
repeatedStringForConditions += "}"
|
||||
repeatedStringForInfos := "[]CarpInfo{"
|
||||
for _, f := range this.Infos {
|
||||
repeatedStringForInfos += strings.Replace(strings.Replace(f.String(), "CarpInfo", "CarpInfo", 1), `&`, ``, 1) + ","
|
||||
}
|
||||
repeatedStringForInfos += "}"
|
||||
s := strings.Join([]string{`&CarpStatus{`,
|
||||
`Phase:` + fmt.Sprintf("%v", this.Phase) + `,`,
|
||||
`Conditions:` + repeatedStringForConditions + `,`,
|
||||
|
@ -857,6 +990,7 @@ func (this *CarpStatus) String() string {
|
|||
`HostIP:` + fmt.Sprintf("%v", this.HostIP) + `,`,
|
||||
`CarpIP:` + fmt.Sprintf("%v", this.CarpIP) + `,`,
|
||||
`StartTime:` + strings.Replace(fmt.Sprintf("%v", this.StartTime), "Time", "v1.Time", 1) + `,`,
|
||||
`Infos:` + repeatedStringForInfos + `,`,
|
||||
`}`,
|
||||
}, "")
|
||||
return s
|
||||
|
@ -1262,6 +1396,172 @@ func (m *CarpCondition) Unmarshal(dAtA []byte) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
func (m *CarpInfo) Unmarshal(dAtA []byte) error {
|
||||
l := len(dAtA)
|
||||
iNdEx := 0
|
||||
for iNdEx < l {
|
||||
preIndex := iNdEx
|
||||
var wire uint64
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowGenerated
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
wire |= uint64(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
fieldNum := int32(wire >> 3)
|
||||
wireType := int(wire & 0x7)
|
||||
if wireType == 4 {
|
||||
return fmt.Errorf("proto: CarpInfo: wiretype end group for non-group")
|
||||
}
|
||||
if fieldNum <= 0 {
|
||||
return fmt.Errorf("proto: CarpInfo: illegal tag %d (wire type %d)", fieldNum, wire)
|
||||
}
|
||||
switch fieldNum {
|
||||
case 1:
|
||||
if wireType != 0 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field A", wireType)
|
||||
}
|
||||
m.A = 0
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowGenerated
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
m.A |= int64(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
case 2:
|
||||
if wireType != 2 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field B", wireType)
|
||||
}
|
||||
var stringLen uint64
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowGenerated
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
stringLen |= uint64(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
intStringLen := int(stringLen)
|
||||
if intStringLen < 0 {
|
||||
return ErrInvalidLengthGenerated
|
||||
}
|
||||
postIndex := iNdEx + intStringLen
|
||||
if postIndex < 0 {
|
||||
return ErrInvalidLengthGenerated
|
||||
}
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
m.B = string(dAtA[iNdEx:postIndex])
|
||||
iNdEx = postIndex
|
||||
case 3:
|
||||
if wireType != 2 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field Data", wireType)
|
||||
}
|
||||
var stringLen uint64
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowGenerated
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
stringLen |= uint64(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
intStringLen := int(stringLen)
|
||||
if intStringLen < 0 {
|
||||
return ErrInvalidLengthGenerated
|
||||
}
|
||||
postIndex := iNdEx + intStringLen
|
||||
if postIndex < 0 {
|
||||
return ErrInvalidLengthGenerated
|
||||
}
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
m.Data = string(dAtA[iNdEx:postIndex])
|
||||
iNdEx = postIndex
|
||||
case 4:
|
||||
if wireType != 2 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field C", wireType)
|
||||
}
|
||||
var stringLen uint64
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowGenerated
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
stringLen |= uint64(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
intStringLen := int(stringLen)
|
||||
if intStringLen < 0 {
|
||||
return ErrInvalidLengthGenerated
|
||||
}
|
||||
postIndex := iNdEx + intStringLen
|
||||
if postIndex < 0 {
|
||||
return ErrInvalidLengthGenerated
|
||||
}
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
s := string(dAtA[iNdEx:postIndex])
|
||||
m.C = &s
|
||||
iNdEx = postIndex
|
||||
default:
|
||||
iNdEx = preIndex
|
||||
skippy, err := skipGenerated(dAtA[iNdEx:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if (skippy < 0) || (iNdEx+skippy) < 0 {
|
||||
return ErrInvalidLengthGenerated
|
||||
}
|
||||
if (iNdEx + skippy) > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
iNdEx += skippy
|
||||
}
|
||||
}
|
||||
|
||||
if iNdEx > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (m *CarpList) Unmarshal(dAtA []byte) error {
|
||||
l := len(dAtA)
|
||||
iNdEx := 0
|
||||
|
@ -2139,6 +2439,40 @@ func (m *CarpStatus) Unmarshal(dAtA []byte) error {
|
|||
return err
|
||||
}
|
||||
iNdEx = postIndex
|
||||
case 8:
|
||||
if wireType != 2 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field Infos", wireType)
|
||||
}
|
||||
var msglen int
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowGenerated
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
msglen |= int(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if msglen < 0 {
|
||||
return ErrInvalidLengthGenerated
|
||||
}
|
||||
postIndex := iNdEx + msglen
|
||||
if postIndex < 0 {
|
||||
return ErrInvalidLengthGenerated
|
||||
}
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
m.Infos = append(m.Infos, CarpInfo{})
|
||||
if err := m.Infos[len(m.Infos)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
|
||||
return err
|
||||
}
|
||||
iNdEx = postIndex
|
||||
default:
|
||||
iNdEx = preIndex
|
||||
skippy, err := skipGenerated(dAtA[iNdEx:])
|
||||
|
|
|
@ -77,6 +77,23 @@ message CarpCondition {
|
|||
optional string message = 6;
|
||||
}
|
||||
|
||||
message CarpInfo {
|
||||
// A is the first map key.
|
||||
// +required
|
||||
optional int64 a = 1;
|
||||
|
||||
// B is the second map key.
|
||||
// +required
|
||||
optional string b = 2;
|
||||
|
||||
// C is the third, optional map key
|
||||
// +optional
|
||||
optional string c = 4;
|
||||
|
||||
// Some data for each pair of A and B.
|
||||
optional string data = 3;
|
||||
}
|
||||
|
||||
// CarpList is a list of Carps.
|
||||
message CarpList {
|
||||
// Standard list metadata.
|
||||
|
@ -129,7 +146,7 @@ message CarpSpec {
|
|||
// Deprecated: Use serviceAccountName instead.
|
||||
// +k8s:conversion-gen=false
|
||||
// +optional
|
||||
optional string serviceAccount = 9;
|
||||
optional string deprecatedServiceAccount = 9;
|
||||
|
||||
// NodeName is a request to schedule this carp onto a specific node. If it is non-empty,
|
||||
// the scheduler simply schedules this carp onto that node, assuming that it fits resource
|
||||
|
@ -138,7 +155,6 @@ message CarpSpec {
|
|||
optional string nodeName = 10;
|
||||
|
||||
// Host networking requested for this carp. Use the host's network namespace.
|
||||
// If this option is set, the ports that will be used must be specified.
|
||||
// Default to false.
|
||||
// +k8s:conversion-gen=false
|
||||
// +optional
|
||||
|
@ -169,7 +185,7 @@ message CarpSpec {
|
|||
// If specified, the carp will be dispatched by specified scheduler.
|
||||
// If not specified, the carp will be dispatched by default scheduler.
|
||||
// +optional
|
||||
optional string schedulername = 19;
|
||||
optional string schedulerName = 19;
|
||||
}
|
||||
|
||||
// CarpStatus represents information about the status of a carp. Status may trail the actual
|
||||
|
@ -182,6 +198,10 @@ message CarpStatus {
|
|||
|
||||
// Current service state of carp.
|
||||
// More info: http://kubernetes.io/docs/user-guide/carp-states#carp-conditions
|
||||
// +patchStrategy=merge
|
||||
// +patchMergeKey=type
|
||||
// +listType=map
|
||||
// +listMapKey=type
|
||||
// +optional
|
||||
repeated CarpCondition conditions = 2;
|
||||
|
||||
|
@ -207,5 +227,13 @@ message CarpStatus {
|
|||
// This is before the Kubelet pulled the container image(s) for the carp.
|
||||
// +optional
|
||||
optional .k8s.io.apimachinery.pkg.apis.meta.v1.Time startTime = 7;
|
||||
|
||||
// Carp infos are provided by different clients, hence the map type.
|
||||
//
|
||||
// +listType=map
|
||||
// +listMapKey=a
|
||||
// +listMapKey=b
|
||||
// +listMapKey=c
|
||||
repeated CarpInfo infos = 8;
|
||||
}
|
||||
|
||||
|
|
|
@ -57,6 +57,7 @@ func init() {
|
|||
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
&Carp{},
|
||||
&CarpList{},
|
||||
)
|
||||
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
|
||||
return nil
|
||||
|
|
|
@ -28,6 +28,7 @@ type (
|
|||
)
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
// +k8s:prerelease-lifecycle-gen:introduced=1.1
|
||||
|
||||
// Carp is a collection of containers, used as either input (create, update) or as output (list, get).
|
||||
type Carp struct {
|
||||
|
@ -60,8 +61,12 @@ type CarpStatus struct {
|
|||
Phase CarpPhase `json:"phase,omitempty" protobuf:"bytes,1,opt,name=phase,casttype=CarpPhase"`
|
||||
// Current service state of carp.
|
||||
// More info: http://kubernetes.io/docs/user-guide/carp-states#carp-conditions
|
||||
// +patchStrategy=merge
|
||||
// +patchMergeKey=type
|
||||
// +listType=map
|
||||
// +listMapKey=type
|
||||
// +optional
|
||||
Conditions []CarpCondition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,2,rep,name=conditions"`
|
||||
Conditions []CarpCondition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,2,opt,name=conditions"`
|
||||
// A human readable message indicating details about why the carp is in this condition.
|
||||
// +optional
|
||||
Message string `json:"message,omitempty" protobuf:"bytes,3,opt,name=message"`
|
||||
|
@ -82,6 +87,14 @@ type CarpStatus struct {
|
|||
// This is before the Kubelet pulled the container image(s) for the carp.
|
||||
// +optional
|
||||
StartTime *metav1.Time `json:"startTime,omitempty" protobuf:"bytes,7,opt,name=startTime"`
|
||||
|
||||
// Carp infos are provided by different clients, hence the map type.
|
||||
//
|
||||
// +listType=map
|
||||
// +listMapKey=a
|
||||
// +listMapKey=b
|
||||
// +listMapKey=c
|
||||
Infos []CarpInfo `json:"infos,omitempty" protobuf:"bytes,8,rep,name=infos"`
|
||||
}
|
||||
|
||||
type CarpCondition struct {
|
||||
|
@ -107,6 +120,21 @@ type CarpCondition struct {
|
|||
Message string `json:"message,omitempty" protobuf:"bytes,6,opt,name=message"`
|
||||
}
|
||||
|
||||
type CarpInfo struct {
|
||||
// A is the first map key.
|
||||
// +required
|
||||
A int64 `json:"a" protobuf:"bytes,1,name=a"`
|
||||
// B is the second map key.
|
||||
// +required
|
||||
B string `json:"b" protobuf:"bytes,2,name=b"`
|
||||
// C is the third, optional map key
|
||||
// +optional
|
||||
C *string `json:"c,omitempty" protobuf:"bytes,4,opt,name=c"`
|
||||
|
||||
// Some data for each pair of A and B.
|
||||
Data string `json:"data" protobuf:"bytes,3,name=data"`
|
||||
}
|
||||
|
||||
// CarpSpec is a description of a carp
|
||||
type CarpSpec struct {
|
||||
// Restart policy for all containers within the carp.
|
||||
|
@ -143,7 +171,7 @@ type CarpSpec struct {
|
|||
// Deprecated: Use serviceAccountName instead.
|
||||
// +k8s:conversion-gen=false
|
||||
// +optional
|
||||
DeprecatedServiceAccount string `json:"serviceAccount,omitempty" protobuf:"bytes,9,opt,name=serviceAccount"`
|
||||
DeprecatedServiceAccount string `json:"deprecatedServiceAccount,omitempty" protobuf:"bytes,9,opt,name=deprecatedServiceAccount"`
|
||||
|
||||
// NodeName is a request to schedule this carp onto a specific node. If it is non-empty,
|
||||
// the scheduler simply schedules this carp onto that node, assuming that it fits resource
|
||||
|
@ -151,7 +179,6 @@ type CarpSpec struct {
|
|||
// +optional
|
||||
NodeName string `json:"nodeName,omitempty" protobuf:"bytes,10,opt,name=nodeName"`
|
||||
// Host networking requested for this carp. Use the host's network namespace.
|
||||
// If this option is set, the ports that will be used must be specified.
|
||||
// Default to false.
|
||||
// +k8s:conversion-gen=false
|
||||
// +optional
|
||||
|
@ -177,10 +204,11 @@ type CarpSpec struct {
|
|||
// If specified, the carp will be dispatched by specified scheduler.
|
||||
// If not specified, the carp will be dispatched by default scheduler.
|
||||
// +optional
|
||||
SchedulerName string `json:"schedulername,omitempty" protobuf:"bytes,19,opt,name=schedulername"`
|
||||
SchedulerName string `json:"schedulerName,omitempty" protobuf:"bytes,19,opt,name=schedulerName"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
// +k8s:prerelease-lifecycle-gen:introduced=1.1
|
||||
|
||||
// CarpList is a list of Carps.
|
||||
type CarpList struct {
|
||||
|
|
|
@ -57,6 +57,16 @@ func RegisterConversions(s *runtime.Scheme) error {
|
|||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddGeneratedConversionFunc((*CarpInfo)(nil), (*testapigroup.CarpInfo)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_v1_CarpInfo_To_testapigroup_CarpInfo(a.(*CarpInfo), b.(*testapigroup.CarpInfo), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddGeneratedConversionFunc((*testapigroup.CarpInfo)(nil), (*CarpInfo)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_testapigroup_CarpInfo_To_v1_CarpInfo(a.(*testapigroup.CarpInfo), b.(*CarpInfo), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddGeneratedConversionFunc((*CarpList)(nil), (*testapigroup.CarpList)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_v1_CarpList_To_testapigroup_CarpList(a.(*CarpList), b.(*testapigroup.CarpList), scope)
|
||||
}); err != nil {
|
||||
|
@ -152,6 +162,32 @@ func Convert_testapigroup_CarpCondition_To_v1_CarpCondition(in *testapigroup.Car
|
|||
return autoConvert_testapigroup_CarpCondition_To_v1_CarpCondition(in, out, s)
|
||||
}
|
||||
|
||||
func autoConvert_v1_CarpInfo_To_testapigroup_CarpInfo(in *CarpInfo, out *testapigroup.CarpInfo, s conversion.Scope) error {
|
||||
out.A = in.A
|
||||
out.B = in.B
|
||||
out.C = (*string)(unsafe.Pointer(in.C))
|
||||
out.Data = in.Data
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert_v1_CarpInfo_To_testapigroup_CarpInfo is an autogenerated conversion function.
|
||||
func Convert_v1_CarpInfo_To_testapigroup_CarpInfo(in *CarpInfo, out *testapigroup.CarpInfo, s conversion.Scope) error {
|
||||
return autoConvert_v1_CarpInfo_To_testapigroup_CarpInfo(in, out, s)
|
||||
}
|
||||
|
||||
func autoConvert_testapigroup_CarpInfo_To_v1_CarpInfo(in *testapigroup.CarpInfo, out *CarpInfo, s conversion.Scope) error {
|
||||
out.A = in.A
|
||||
out.B = in.B
|
||||
out.C = (*string)(unsafe.Pointer(in.C))
|
||||
out.Data = in.Data
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert_testapigroup_CarpInfo_To_v1_CarpInfo is an autogenerated conversion function.
|
||||
func Convert_testapigroup_CarpInfo_To_v1_CarpInfo(in *testapigroup.CarpInfo, out *CarpInfo, s conversion.Scope) error {
|
||||
return autoConvert_testapigroup_CarpInfo_To_v1_CarpInfo(in, out, s)
|
||||
}
|
||||
|
||||
func autoConvert_v1_CarpList_To_testapigroup_CarpList(in *CarpList, out *testapigroup.CarpList, s conversion.Scope) error {
|
||||
out.ListMeta = in.ListMeta
|
||||
if in.Items != nil {
|
||||
|
@ -242,6 +278,7 @@ func autoConvert_v1_CarpStatus_To_testapigroup_CarpStatus(in *CarpStatus, out *t
|
|||
out.HostIP = in.HostIP
|
||||
out.CarpIP = in.CarpIP
|
||||
out.StartTime = (*metav1.Time)(unsafe.Pointer(in.StartTime))
|
||||
out.Infos = *(*[]testapigroup.CarpInfo)(unsafe.Pointer(&in.Infos))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -258,6 +295,7 @@ func autoConvert_testapigroup_CarpStatus_To_v1_CarpStatus(in *testapigroup.CarpS
|
|||
out.HostIP = in.HostIP
|
||||
out.CarpIP = in.CarpIP
|
||||
out.StartTime = (*metav1.Time)(unsafe.Pointer(in.StartTime))
|
||||
out.Infos = *(*[]CarpInfo)(unsafe.Pointer(&in.Infos))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -71,6 +71,27 @@ func (in *CarpCondition) DeepCopy() *CarpCondition {
|
|||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *CarpInfo) DeepCopyInto(out *CarpInfo) {
|
||||
*out = *in
|
||||
if in.C != nil {
|
||||
in, out := &in.C, &out.C
|
||||
*out = new(string)
|
||||
**out = **in
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CarpInfo.
|
||||
func (in *CarpInfo) DeepCopy() *CarpInfo {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(CarpInfo)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *CarpList) DeepCopyInto(out *CarpList) {
|
||||
*out = *in
|
||||
|
@ -151,6 +172,13 @@ func (in *CarpStatus) DeepCopyInto(out *CarpStatus) {
|
|||
in, out := &in.StartTime, &out.StartTime
|
||||
*out = (*in).DeepCopy()
|
||||
}
|
||||
if in.Infos != nil {
|
||||
in, out := &in.Infos, &out.Infos
|
||||
*out = make([]CarpInfo, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
//go:build !ignore_autogenerated
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright The Kubernetes 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.
|
||||
*/
|
||||
|
||||
// Code generated by prerelease-lifecycle-gen. DO NOT EDIT.
|
||||
|
||||
package v1
|
||||
|
||||
// APILifecycleIntroduced is an autogenerated function, returning the release in which the API struct was introduced as int versions of major and minor for comparison.
|
||||
// It is controlled by "k8s:prerelease-lifecycle-gen:introduced" tags in types.go.
|
||||
func (in *Carp) APILifecycleIntroduced() (major, minor int) {
|
||||
return 1, 1
|
||||
}
|
||||
|
||||
// APILifecycleIntroduced is an autogenerated function, returning the release in which the API struct was introduced as int versions of major and minor for comparison.
|
||||
// It is controlled by "k8s:prerelease-lifecycle-gen:introduced" tags in types.go.
|
||||
func (in *CarpList) APILifecycleIntroduced() (major, minor int) {
|
||||
return 1, 1
|
||||
}
|
|
@ -71,6 +71,27 @@ func (in *CarpCondition) DeepCopy() *CarpCondition {
|
|||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *CarpInfo) DeepCopyInto(out *CarpInfo) {
|
||||
*out = *in
|
||||
if in.C != nil {
|
||||
in, out := &in.C, &out.C
|
||||
*out = new(string)
|
||||
**out = **in
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CarpInfo.
|
||||
func (in *CarpInfo) DeepCopy() *CarpInfo {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(CarpInfo)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *CarpList) DeepCopyInto(out *CarpList) {
|
||||
*out = *in
|
||||
|
@ -151,6 +172,13 @@ func (in *CarpStatus) DeepCopyInto(out *CarpStatus) {
|
|||
in, out := &in.StartTime, &out.StartTime
|
||||
*out = (*in).DeepCopy()
|
||||
}
|
||||
if in.Infos != nil {
|
||||
in, out := &in.Infos, &out.Infos
|
||||
*out = make([]CarpInfo, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -21,4 +21,4 @@ limitations under the License.
|
|||
// but for the fields which did not change, copying is automated. This makes it
|
||||
// easy to modify the structures you use in memory without affecting the format
|
||||
// you store on disk or respond to in your external API calls.
|
||||
package conversion // import "k8s.io/apimachinery/pkg/conversion"
|
||||
package conversion
|
||||
|
|
|
@ -25,6 +25,7 @@ import (
|
|||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/conversion/queryparams"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
type namedString string
|
||||
|
@ -161,20 +162,20 @@ func TestConvert(t *testing.T) {
|
|||
},
|
||||
{
|
||||
input: &baz{
|
||||
Ptr: intp(5),
|
||||
Bptr: boolp(true),
|
||||
Ptr: ptr.To(5),
|
||||
Bptr: ptr.To(true),
|
||||
},
|
||||
expected: url.Values{"ptr": {"5"}, "bptr": {"true"}},
|
||||
},
|
||||
{
|
||||
input: &baz{
|
||||
Bptr: boolp(true),
|
||||
Bptr: ptr.To(true),
|
||||
},
|
||||
expected: url.Values{"ptr": {""}, "bptr": {"true"}},
|
||||
},
|
||||
{
|
||||
input: &baz{
|
||||
Ptr: intp(5),
|
||||
Ptr: ptr.To(5),
|
||||
},
|
||||
expected: url.Values{"ptr": {"5"}},
|
||||
},
|
||||
|
@ -213,7 +214,3 @@ func TestConvert(t *testing.T) {
|
|||
validateResult(t, test.input, result, test.expected)
|
||||
}
|
||||
}
|
||||
|
||||
func intp(n int) *int { return &n }
|
||||
|
||||
func boolp(b bool) *bool { return &b }
|
||||
|
|
|
@ -16,4 +16,4 @@ limitations under the License.
|
|||
|
||||
// Package queryparams provides conversion from versioned
|
||||
// runtime objects to URL query values
|
||||
package queryparams // import "k8s.io/apimachinery/pkg/conversion/queryparams"
|
||||
package queryparams
|
||||
|
|
|
@ -16,4 +16,4 @@ limitations under the License.
|
|||
|
||||
// Package fields implements a simple field system, parsing and matching
|
||||
// selectors with sets of fields.
|
||||
package fields // import "k8s.io/apimachinery/pkg/fields"
|
||||
package fields
|
||||
|
|
|
@ -16,4 +16,4 @@ limitations under the License.
|
|||
|
||||
// Package labels implements a simple label system, parsing and matching
|
||||
// selectors with sets of labels.
|
||||
package labels // import "k8s.io/apimachinery/pkg/labels"
|
||||
package labels
|
||||
|
|
|
@ -31,6 +31,9 @@ type Labels interface {
|
|||
|
||||
// Get returns the value for the provided label.
|
||||
Get(label string) (value string)
|
||||
|
||||
// Lookup returns the value for the provided label if it exists and whether the provided label exist
|
||||
Lookup(label string) (value string, exists bool)
|
||||
}
|
||||
|
||||
// Set is a map of label:value. It implements Labels.
|
||||
|
@ -59,6 +62,12 @@ func (ls Set) Get(label string) string {
|
|||
return ls[label]
|
||||
}
|
||||
|
||||
// Lookup returns the value for the provided label if it exists and whether the provided label exist
|
||||
func (ls Set) Lookup(label string) (string, bool) {
|
||||
val, exists := ls[label]
|
||||
return val, exists
|
||||
}
|
||||
|
||||
// AsSelector converts labels into a selectors. It does not
|
||||
// perform any validation, which means the server will reject
|
||||
// the request if the Set contains invalid values.
|
||||
|
|
|
@ -23,11 +23,12 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"k8s.io/apimachinery/pkg/selection"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/validation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -115,6 +116,15 @@ func Nothing() Selector {
|
|||
return sharedNothingSelector
|
||||
}
|
||||
|
||||
// MatchesNothing only returns true for selectors which are definitively determined to match no objects.
|
||||
// This currently only detects the `labels.Nothing()` selector, but may change over time to detect more selectors that match no objects.
|
||||
//
|
||||
// Note: The current implementation does not check for selector conflict scenarios (e.g., a=a,a!=a).
|
||||
// Support for detecting such cases can be added in the future.
|
||||
func MatchesNothing(selector Selector) bool {
|
||||
return selector == sharedNothingSelector
|
||||
}
|
||||
|
||||
// NewSelector returns a nil selector
|
||||
func NewSelector() Selector {
|
||||
return internalSelector(nil)
|
||||
|
@ -236,26 +246,29 @@ func (r *Requirement) hasValue(value string) bool {
|
|||
func (r *Requirement) Matches(ls Labels) bool {
|
||||
switch r.operator {
|
||||
case selection.In, selection.Equals, selection.DoubleEquals:
|
||||
if !ls.Has(r.key) {
|
||||
val, exists := ls.Lookup(r.key)
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
return r.hasValue(ls.Get(r.key))
|
||||
return r.hasValue(val)
|
||||
case selection.NotIn, selection.NotEquals:
|
||||
if !ls.Has(r.key) {
|
||||
val, exists := ls.Lookup(r.key)
|
||||
if !exists {
|
||||
return true
|
||||
}
|
||||
return !r.hasValue(ls.Get(r.key))
|
||||
return !r.hasValue(val)
|
||||
case selection.Exists:
|
||||
return ls.Has(r.key)
|
||||
case selection.DoesNotExist:
|
||||
return !ls.Has(r.key)
|
||||
case selection.GreaterThan, selection.LessThan:
|
||||
if !ls.Has(r.key) {
|
||||
val, exists := ls.Lookup(r.key)
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
lsValue, err := strconv.ParseInt(ls.Get(r.key), 10, 64)
|
||||
lsValue, err := strconv.ParseInt(val, 10, 64)
|
||||
if err != nil {
|
||||
klog.V(10).Infof("ParseInt failed for value %+v in label %+v, %+v", ls.Get(r.key), ls, err)
|
||||
klog.V(10).Infof("ParseInt failed for value %+v in label %+v, %+v", val, ls, err)
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -996,7 +1009,8 @@ type ValidatedSetSelector Set
|
|||
|
||||
func (s ValidatedSetSelector) Matches(labels Labels) bool {
|
||||
for k, v := range s {
|
||||
if !labels.Has(k) || v != labels.Get(k) {
|
||||
val, exists := labels.Lookup(k)
|
||||
if !exists || v != val {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -224,14 +224,6 @@ func TestLexer(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func min(l, r int) (m int) {
|
||||
m = r
|
||||
if l < r {
|
||||
m = l
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func TestLexerSequence(t *testing.T) {
|
||||
testcases := []struct {
|
||||
s string
|
||||
|
@ -1016,6 +1008,26 @@ func BenchmarkRequirementString(b *testing.B) {
|
|||
}
|
||||
}
|
||||
|
||||
func BenchmarkRequirementMatches(b *testing.B) {
|
||||
r := Requirement{
|
||||
key: "environment",
|
||||
operator: selection.NotIn,
|
||||
strValues: []string{
|
||||
"dev",
|
||||
},
|
||||
}
|
||||
labels := Set(map[string]string{
|
||||
"key": "value",
|
||||
"environment": "dev",
|
||||
})
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
r.Matches(labels)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequirementEqual(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
@ -1087,3 +1099,114 @@ func TestRequirementEqual(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchesNothing(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
selector string
|
||||
set map[string]string
|
||||
labelSelector Selector
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "MatchNothing should match Nothing()",
|
||||
labelSelector: Nothing(),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "MatchNothing should match sharedNothingSelector",
|
||||
labelSelector: sharedNothingSelector,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "MatchNothing should not match Everything()",
|
||||
labelSelector: Everything(),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "MatchNothing should not match sharedEverythingSelector",
|
||||
labelSelector: sharedEverythingSelector,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "MatchNothing should not match empty set",
|
||||
set: map[string]string{},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "MatchNothing should not match non-empty set",
|
||||
set: map[string]string{"key": "value"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "MatchNothing should not match empty selector",
|
||||
selector: "",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "MatchNothing should not match non-empty selector - exists",
|
||||
selector: "a",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "MatchNothing should not match non-empty selector - not exists",
|
||||
selector: "!a",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "MatchNothing should not match non-empty selector - equals",
|
||||
selector: "a=b",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "MatchNothing should not match non-empty selector - not equals",
|
||||
selector: "a!=b",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "MatchNothing should not match non-empty selector - in",
|
||||
selector: "a in (b)",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "MatchNothing should not match non-empty selector - notin",
|
||||
selector: "a notin (b)",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "MatchNothing should not match non-empty selector - conflict exists and not exists",
|
||||
selector: "a,!a",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "MatchNothing should not match non-empty selector - conflict equals and not equals",
|
||||
selector: "a=b,a!=b",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "MatchNothing should not match non-empty selector - conflict in and notin",
|
||||
selector: "a in (b),a notin (b)",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for i := 0; i < len(tests); i++ {
|
||||
if tests[i].labelSelector != nil {
|
||||
expectMatchNothing(t, tests[i].labelSelector, tests[i].want)
|
||||
} else if tests[i].set != nil {
|
||||
expectMatchNothing(t, SelectorFromSet(tests[i].set), tests[i].want)
|
||||
} else {
|
||||
selector, err := Parse(tests[i].selector)
|
||||
if err != nil {
|
||||
t.Errorf("Unable to parse %v as a selector.\n", selector)
|
||||
}
|
||||
expectMatchNothing(t, selector, tests[i].want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func expectMatchNothing(t *testing.T, selector Selector, want bool) {
|
||||
if MatchesNothing(selector) != want {
|
||||
t.Errorf("Wanted %s to MatchNothing '%t', but it did not.\n", selector, want)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,10 +29,11 @@ import (
|
|||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"sigs.k8s.io/structured-merge-diff/v6/value"
|
||||
|
||||
"k8s.io/apimachinery/pkg/conversion"
|
||||
"k8s.io/apimachinery/pkg/util/json"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"sigs.k8s.io/structured-merge-diff/v4/value"
|
||||
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
@ -53,6 +54,7 @@ type fieldInfo struct {
|
|||
name string
|
||||
nameValue reflect.Value
|
||||
omitempty bool
|
||||
omitzero func(dv reflect.Value) bool
|
||||
}
|
||||
|
||||
type fieldsCacheMap map[structField]*fieldInfo
|
||||
|
@ -376,19 +378,24 @@ func fieldInfoFromField(structType reflect.Type, field int) *fieldInfo {
|
|||
typeField := structType.Field(field)
|
||||
jsonTag := typeField.Tag.Get("json")
|
||||
if len(jsonTag) == 0 {
|
||||
// Make the first character lowercase.
|
||||
if typeField.Name == "" {
|
||||
if !typeField.Anonymous {
|
||||
// match stdlib behavior for naming fields that don't specify a json tag name
|
||||
info.name = typeField.Name
|
||||
} else {
|
||||
info.name = strings.ToLower(typeField.Name[:1]) + typeField.Name[1:]
|
||||
}
|
||||
} else {
|
||||
items := strings.Split(jsonTag, ",")
|
||||
info.name = items[0]
|
||||
if len(info.name) == 0 && !typeField.Anonymous {
|
||||
// match stdlib behavior for naming fields that don't specify a json tag name
|
||||
info.name = typeField.Name
|
||||
}
|
||||
|
||||
for i := range items {
|
||||
if items[i] == "omitempty" {
|
||||
if i > 0 && items[i] == "omitempty" {
|
||||
info.omitempty = true
|
||||
break
|
||||
}
|
||||
if i > 0 && items[i] == "omitzero" {
|
||||
info.omitzero = value.OmitZeroFunc(typeField.Type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -775,7 +782,7 @@ func pointerToUnstructured(sv, dv reflect.Value) error {
|
|||
return toUnstructured(sv.Elem(), dv)
|
||||
}
|
||||
|
||||
func isZero(v reflect.Value) bool {
|
||||
func isEmpty(v reflect.Value) bool {
|
||||
switch v.Kind() {
|
||||
case reflect.Array, reflect.String:
|
||||
return v.Len() == 0
|
||||
|
@ -816,10 +823,14 @@ func structToUnstructured(sv, dv reflect.Value) error {
|
|||
// This field should be skipped.
|
||||
continue
|
||||
}
|
||||
if fieldInfo.omitempty && isZero(fv) {
|
||||
if fieldInfo.omitempty && isEmpty(fv) {
|
||||
// omitempty fields should be ignored.
|
||||
continue
|
||||
}
|
||||
if fieldInfo.omitzero != nil && fieldInfo.omitzero(fv) {
|
||||
// omitzero fields should be ignored
|
||||
continue
|
||||
}
|
||||
if len(fieldInfo.name) == 0 {
|
||||
// This field is inlined.
|
||||
if err := toUnstructured(fv, dv); err != nil {
|
||||
|
|
|
@ -36,9 +36,9 @@ import (
|
|||
"k8s.io/apimachinery/pkg/util/json"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
fuzz "github.com/google/gofuzz"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"sigs.k8s.io/randfill"
|
||||
)
|
||||
|
||||
var simpleEquality = conversion.EqualitiesOrDie(
|
||||
|
@ -640,9 +640,9 @@ func TestUnknownFields(t *testing.T) {
|
|||
// with the various validation directives (Ignore, Warn, Strict)
|
||||
func BenchmarkFromUnstructuredWithValidation(b *testing.B) {
|
||||
re := regexp.MustCompile("^I$")
|
||||
f := fuzz.NewWithSeed(1).NilChance(0.1).SkipFieldsWithPattern(re)
|
||||
f := randfill.NewWithSeed(1).NilChance(0.1).SkipFieldsWithPattern(re)
|
||||
iObj := &I{}
|
||||
f.Fuzz(&iObj)
|
||||
f.Fill(&iObj)
|
||||
|
||||
unstr, err := runtime.DefaultUnstructuredConverter.ToUnstructured(iObj)
|
||||
if err != nil {
|
||||
|
@ -998,3 +998,179 @@ func TestCustomToUnstructuredTopLevel(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
type OmitemptyNameField struct {
|
||||
I int `json:"omitempty"`
|
||||
}
|
||||
|
||||
func TestOmitempty(t *testing.T) {
|
||||
expected := `{"omitempty":0}`
|
||||
|
||||
o := &OmitemptyNameField{}
|
||||
jsonData, err := json.Marshal(o)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if e, a := expected, string(jsonData); e != a {
|
||||
t.Fatalf("expected\n%s\ngot\n%s", e, a)
|
||||
}
|
||||
|
||||
unstr, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&o)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
jsonUnstrData, err := json.Marshal(unstr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if e, a := expected, string(jsonUnstrData); e != a {
|
||||
t.Fatalf("expected\n%s\ngot\n%s", e, a)
|
||||
}
|
||||
}
|
||||
|
||||
type InlineTestPrimitive struct {
|
||||
NoNameTagPrimitive int64 `json:""`
|
||||
NoNameTagInlinePrimitive int64 `json:",inline"`
|
||||
NoNameTagOmitemptyPrimitive int64 `json:",omitempty"`
|
||||
}
|
||||
type InlineTestAnonymous struct {
|
||||
NoTag
|
||||
NoNameTag `json:""`
|
||||
NameTag `json:"nameTagEmbedded"`
|
||||
NoNameTagInline `json:",inline"`
|
||||
NoNameTagOmitempty `json:",omitempty"`
|
||||
}
|
||||
type InlineTestNamed struct {
|
||||
NoTag NoTag
|
||||
NoNameTag NoNameTag `json:""`
|
||||
NameTag NameTag `json:"nameTagEmbedded"`
|
||||
NoNameTagInline NoNameTagInline `json:",inline"`
|
||||
NoNameTagOmitempty NoNameTagOmitempty `json:",omitempty"`
|
||||
}
|
||||
type NoTag struct {
|
||||
Data0 int `json:"data0"`
|
||||
}
|
||||
type NameTag struct {
|
||||
Data1 int `json:"data1"`
|
||||
}
|
||||
type NoNameTag struct {
|
||||
Data2 int `json:"data2"`
|
||||
}
|
||||
type NoNameTagInline struct {
|
||||
Data3 int `json:"data3"`
|
||||
}
|
||||
type NoNameTagOmitempty struct {
|
||||
Data4 int `json:"data4"`
|
||||
}
|
||||
|
||||
func TestInline(t *testing.T) {
|
||||
testcases := []struct {
|
||||
name string
|
||||
obj any
|
||||
expect map[string]any
|
||||
}{
|
||||
{
|
||||
name: "primitive-zero",
|
||||
obj: &InlineTestPrimitive{},
|
||||
expect: map[string]any{
|
||||
"NoNameTagPrimitive": int64(0),
|
||||
"NoNameTagInlinePrimitive": int64(0),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "primitive-set",
|
||||
obj: &InlineTestPrimitive{
|
||||
NoNameTagPrimitive: 1,
|
||||
NoNameTagInlinePrimitive: 2,
|
||||
NoNameTagOmitemptyPrimitive: 3,
|
||||
},
|
||||
expect: map[string]any{
|
||||
"NoNameTagPrimitive": int64(1),
|
||||
"NoNameTagInlinePrimitive": int64(2),
|
||||
"NoNameTagOmitemptyPrimitive": int64(3),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "anonymous-zero",
|
||||
obj: &InlineTestAnonymous{},
|
||||
expect: map[string]any{
|
||||
"data0": int64(0),
|
||||
"data2": int64(0),
|
||||
"data3": int64(0),
|
||||
"data4": int64(0),
|
||||
"nameTagEmbedded": map[string]any{"data1": int64(0)},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "anonymous-set",
|
||||
obj: &InlineTestAnonymous{},
|
||||
expect: map[string]any{
|
||||
"data0": int64(0),
|
||||
"data2": int64(0),
|
||||
"data3": int64(0),
|
||||
"data4": int64(0),
|
||||
"nameTagEmbedded": map[string]any{"data1": int64(0)},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "named-zero",
|
||||
obj: &InlineTestNamed{},
|
||||
expect: map[string]any{
|
||||
"NoTag": map[string]any{"data0": int64(0)},
|
||||
"nameTagEmbedded": map[string]any{"data1": int64(0)},
|
||||
"NoNameTag": map[string]any{"data2": int64(0)},
|
||||
"NoNameTagInline": map[string]any{"data3": int64(0)},
|
||||
"NoNameTagOmitempty": map[string]any{"data4": int64(0)},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "named-set",
|
||||
obj: &InlineTestNamed{
|
||||
NoTag: NoTag{Data0: 10},
|
||||
NameTag: NameTag{Data1: 11},
|
||||
NoNameTag: NoNameTag{Data2: 12},
|
||||
NoNameTagInline: NoNameTagInline{Data3: 13},
|
||||
NoNameTagOmitempty: NoNameTagOmitempty{Data4: 14},
|
||||
},
|
||||
expect: map[string]any{
|
||||
"NoTag": map[string]any{"data0": int64(10)},
|
||||
"nameTagEmbedded": map[string]any{"data1": int64(11)},
|
||||
"NoNameTag": map[string]any{"data2": int64(12)},
|
||||
"NoNameTagInline": map[string]any{"data3": int64(13)},
|
||||
"NoNameTagOmitempty": map[string]any{"data4": int64(14)},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
defer func() {
|
||||
// handle panics
|
||||
if err := recover(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Check the expectation against stdlib
|
||||
jsonData, err := json.Marshal(tc.obj)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
jsonUnstr := map[string]any{}
|
||||
if err := json.Unmarshal(jsonData, &jsonUnstr); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(tc.expect, jsonUnstr) {
|
||||
t.Fatal(cmp.Diff(tc.expect, jsonUnstr))
|
||||
}
|
||||
|
||||
// Check the expectation against DefaultUnstructuredConverter.ToUnstructured
|
||||
unstr, err := runtime.DefaultUnstructuredConverter.ToUnstructured(tc.obj)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(tc.expect, unstr) {
|
||||
t.Fatal(cmp.Diff(tc.expect, unstr))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,359 @@
|
|||
/*
|
||||
Copyright 2025 The Kubernetes 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.
|
||||
*/
|
||||
|
||||
// These tests are in a separate package to break cyclic dependency in tests.
|
||||
// Unstructured type depends on unstructured converter package but we want to test how the converter handles
|
||||
// the Unstructured type so we need to import both.
|
||||
|
||||
package runtime
|
||||
|
||||
import (
|
||||
encodingjson "encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/json"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
type ZeroParent struct {
|
||||
Int int `json:"int,omitzero"`
|
||||
IntP *int `json:"intP,omitzero"`
|
||||
String string `json:"string,omitzero"`
|
||||
StringP *string `json:"stringP,omitzero"`
|
||||
Bool bool `json:"bool,omitzero"`
|
||||
BoolP bool `json:"boolP,omitzero"`
|
||||
Slice []int `json:"slice,omitzero"`
|
||||
SliceP *[]int `json:"sliceP,omitzero"`
|
||||
Map map[string]int `json:"map,omitzero"`
|
||||
MapP *map[string]int `json:"mapP,omitzero"`
|
||||
Struct ZeroChild `json:"struct,omitzero"`
|
||||
StructP *ZeroChild `json:"structP,omitzero"`
|
||||
CustomPrimitive ZeroCustomPrimitive `json:"customPrimitive,omitzero"`
|
||||
CustomPrimitiveP *ZeroCustomPrimitiveP `json:"customPrimitiveP,omitzero"`
|
||||
CustomStruct ZeroCustomStruct `json:"customStruct,omitzero"`
|
||||
CustomStructP *ZeroCustomStructP `json:"customStructP,omitzero"`
|
||||
CustomPPrimitive ZeroCustomPPrimitive `json:"customPPrimitive,omitzero"`
|
||||
CustomPPrimitiveP *ZeroCustomPPrimitiveP `json:"customPPrimitiveP,omitzero"`
|
||||
CustomPStruct ZeroCustomPStruct `json:"customPStruct,omitzero"`
|
||||
CustomPStructP *ZeroCustomPStructP `json:"customPStructP,omitzero"`
|
||||
}
|
||||
type ZeroChild struct {
|
||||
Data int `json:"data"`
|
||||
}
|
||||
|
||||
type ZeroCustomPrimitive int
|
||||
|
||||
func (z ZeroCustomPrimitive) IsZero() bool {
|
||||
return z == 42
|
||||
}
|
||||
|
||||
type ZeroCustomPrimitiveP int
|
||||
|
||||
func (z ZeroCustomPrimitiveP) IsZero() bool {
|
||||
return z == 42
|
||||
}
|
||||
|
||||
type ZeroCustomStruct struct {
|
||||
Data int `json:"data"`
|
||||
}
|
||||
|
||||
func (z ZeroCustomStruct) IsZero() bool {
|
||||
return z.Data == 42
|
||||
}
|
||||
|
||||
type ZeroCustomStructP struct {
|
||||
Data int `json:"data"`
|
||||
}
|
||||
|
||||
func (z ZeroCustomStructP) IsZero() bool {
|
||||
return z.Data == 42
|
||||
}
|
||||
|
||||
type ZeroCustomPPrimitive int
|
||||
|
||||
func (z *ZeroCustomPPrimitive) IsZero() bool {
|
||||
return *z == 42
|
||||
}
|
||||
|
||||
type ZeroCustomPPrimitiveP int
|
||||
|
||||
func (z *ZeroCustomPPrimitiveP) IsZero() bool {
|
||||
return *z == 42
|
||||
}
|
||||
|
||||
type ZeroCustomPStruct struct {
|
||||
Data int `json:"data"`
|
||||
}
|
||||
|
||||
func (z *ZeroCustomPStruct) IsZero() bool {
|
||||
return z.Data == 42
|
||||
}
|
||||
|
||||
type ZeroCustomPStructP struct {
|
||||
Data int `json:"data"`
|
||||
}
|
||||
|
||||
func (z *ZeroCustomPStructP) IsZero() bool {
|
||||
return z.Data == 42
|
||||
}
|
||||
|
||||
func TestOmitZero2(t *testing.T) {
|
||||
testcases := []struct {
|
||||
name string
|
||||
obj any
|
||||
expect map[string]any
|
||||
}{
|
||||
{
|
||||
name: "emptyzero",
|
||||
obj: &ZeroParent{},
|
||||
expect: map[string]any{
|
||||
"customPPrimitive": int64(0),
|
||||
"customPStruct": map[string]any{"data": int64(0)},
|
||||
"customPrimitive": int64(0),
|
||||
"customStruct": map[string]any{"data": int64(0)},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
jsonData, err := json.Marshal(tc.obj)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
jsonUnstructured := map[string]any{}
|
||||
if err := json.Unmarshal(jsonData, &jsonUnstructured); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(jsonUnstructured, tc.expect) {
|
||||
t.Fatal(cmp.Diff(tc.expect, jsonUnstructured))
|
||||
}
|
||||
|
||||
unstr, err := DefaultUnstructuredConverter.ToUnstructured(tc.obj)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(unstr, tc.expect) {
|
||||
t.Fatal(cmp.Diff(tc.expect, unstr))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type NonZeroStruct struct{}
|
||||
|
||||
func (nzs NonZeroStruct) IsZero() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type NoPanicStruct struct {
|
||||
Int int `json:"int,omitzero"`
|
||||
}
|
||||
|
||||
func (nps *NoPanicStruct) IsZero() bool {
|
||||
return nps.Int != 0
|
||||
}
|
||||
|
||||
type isZeroer interface {
|
||||
IsZero() bool
|
||||
}
|
||||
|
||||
type OptionalsZero struct {
|
||||
Sr string `json:"sr"`
|
||||
So string `json:"so,omitzero"`
|
||||
Sw string `json:"-"`
|
||||
|
||||
Ir int `json:"omitzero"` // actually named omitzero, not an option
|
||||
Io int `json:"io,omitzero"`
|
||||
|
||||
Slr []string `json:"slr,random"` //nolint:staticcheck // SA5008
|
||||
Slo []string `json:"slo,omitzero"`
|
||||
SloNonNil []string `json:"slononnil,omitzero"`
|
||||
|
||||
Mr map[string]any `json:"mr"`
|
||||
Mo map[string]any `json:",omitzero"`
|
||||
Moo map[string]any `json:"moo,omitzero"`
|
||||
|
||||
Fr float64 `json:"fr"`
|
||||
Fo float64 `json:"fo,omitzero"`
|
||||
Foo float64 `json:"foo,omitzero"`
|
||||
Foo2 [2]float64 `json:"foo2,omitzero"`
|
||||
|
||||
Br bool `json:"br"`
|
||||
Bo bool `json:"bo,omitzero"`
|
||||
|
||||
Ur uint `json:"ur"`
|
||||
Uo uint `json:"uo,omitzero"`
|
||||
|
||||
Str struct{} `json:"str"`
|
||||
Sto struct{} `json:"sto,omitzero"`
|
||||
|
||||
Time time.Time `json:"time,omitzero"`
|
||||
TimeLocal time.Time `json:"timelocal,omitzero"`
|
||||
Nzs NonZeroStruct `json:"nzs,omitzero"`
|
||||
|
||||
NilIsZeroer isZeroer `json:"niliszeroer,omitzero"` // nil interface
|
||||
NonNilIsZeroer isZeroer `json:"nonniliszeroer,omitzero"` // non-nil interface
|
||||
NoPanicStruct0 isZeroer `json:"nps0,omitzero"` // non-nil interface with nil pointer
|
||||
NoPanicStruct1 isZeroer `json:"nps1,omitzero"` // non-nil interface with non-nil pointer
|
||||
NoPanicStruct2 *NoPanicStruct `json:"nps2,omitzero"` // nil pointer
|
||||
NoPanicStruct3 *NoPanicStruct `json:"nps3,omitzero"` // non-nil pointer
|
||||
NoPanicStruct4 NoPanicStruct `json:"nps4,omitzero"` // concrete type
|
||||
}
|
||||
|
||||
func TestOmitZero(t *testing.T) {
|
||||
const want = `{
|
||||
"Mo": {},
|
||||
"br": false,
|
||||
"fr": 0,
|
||||
"mr": {},
|
||||
"nps1": {},
|
||||
"nps3": {},
|
||||
"nps4": {},
|
||||
"nzs": {},
|
||||
"omitzero": 0,
|
||||
"slononnil": [],
|
||||
"slr": null,
|
||||
"sr": "",
|
||||
"str": {},
|
||||
"ur": 0
|
||||
}`
|
||||
var o OptionalsZero
|
||||
o.Sw = "something"
|
||||
o.SloNonNil = make([]string, 0)
|
||||
o.Mr = map[string]any{}
|
||||
o.Mo = map[string]any{}
|
||||
|
||||
o.Foo = -0
|
||||
o.Foo2 = [2]float64{+0, -0}
|
||||
|
||||
o.TimeLocal = time.Time{}.Local()
|
||||
|
||||
o.NonNilIsZeroer = time.Time{}
|
||||
o.NoPanicStruct0 = (*NoPanicStruct)(nil)
|
||||
o.NoPanicStruct1 = &NoPanicStruct{}
|
||||
o.NoPanicStruct3 = &NoPanicStruct{}
|
||||
|
||||
unstr, err := DefaultUnstructuredConverter.ToUnstructured(&o)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := encodingjson.MarshalIndent(unstr, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("MarshalIndent error: %v", err)
|
||||
}
|
||||
if got := string(got); got != want {
|
||||
t.Errorf("MarshalIndent:\n\tgot: %s\n\twant: %s\n", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOmitZeroMap(t *testing.T) {
|
||||
const want = `{
|
||||
"foo": {
|
||||
"br": false,
|
||||
"fr": 0,
|
||||
"mr": null,
|
||||
"nps4": {},
|
||||
"nzs": {},
|
||||
"omitzero": 0,
|
||||
"slr": null,
|
||||
"sr": "",
|
||||
"str": {},
|
||||
"ur": 0
|
||||
}
|
||||
}`
|
||||
|
||||
m := map[string]OptionalsZero{"foo": {}}
|
||||
|
||||
unstr, err := DefaultUnstructuredConverter.ToUnstructured(&m)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := encodingjson.MarshalIndent(unstr, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("MarshalIndent error: %v", err)
|
||||
}
|
||||
if got := string(got); got != want {
|
||||
fmt.Println(got)
|
||||
t.Errorf("MarshalIndent:\n\tgot: %s\n\twant: %s\n", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
type OptionalsEmptyZero struct {
|
||||
Sr string `json:"sr"`
|
||||
So string `json:"so,omitempty,omitzero"`
|
||||
Sw string `json:"-"`
|
||||
|
||||
Io int `json:"io,omitempty,omitzero"`
|
||||
|
||||
Slr []string `json:"slr,random"` //nolint:staticcheck // SA5008
|
||||
Slo []string `json:"slo,omitempty,omitzero"`
|
||||
SloNonNil []string `json:"slononnil,omitempty,omitzero"`
|
||||
|
||||
Mr map[string]any `json:"mr"`
|
||||
Mo map[string]any `json:",omitempty,omitzero"`
|
||||
|
||||
Fr float64 `json:"fr"`
|
||||
Fo float64 `json:"fo,omitempty,omitzero"`
|
||||
|
||||
Br bool `json:"br"`
|
||||
Bo bool `json:"bo,omitempty,omitzero"`
|
||||
|
||||
Ur uint `json:"ur"`
|
||||
Uo uint `json:"uo,omitempty,omitzero"`
|
||||
|
||||
Str struct{} `json:"str"`
|
||||
Sto struct{} `json:"sto,omitempty,omitzero"`
|
||||
|
||||
Time time.Time `json:"time,omitempty,omitzero"`
|
||||
Nzs NonZeroStruct `json:"nzs,omitempty,omitzero"`
|
||||
}
|
||||
|
||||
func TestOmitEmptyZero(t *testing.T) {
|
||||
const want = `{
|
||||
"br": false,
|
||||
"fr": 0,
|
||||
"mr": {},
|
||||
"nzs": {},
|
||||
"slr": null,
|
||||
"sr": "",
|
||||
"str": {},
|
||||
"ur": 0
|
||||
}`
|
||||
var o OptionalsEmptyZero
|
||||
o.Sw = "something"
|
||||
o.SloNonNil = make([]string, 0)
|
||||
o.Mr = map[string]any{}
|
||||
o.Mo = map[string]any{}
|
||||
|
||||
unstr, err := DefaultUnstructuredConverter.ToUnstructured(&o)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := encodingjson.MarshalIndent(unstr, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("MarshalIndent error: %v", err)
|
||||
}
|
||||
if got := string(got); got != want {
|
||||
t.Errorf("MarshalIndent:\n\tgot: %s\n\twant: %s\n", got, want)
|
||||
}
|
||||
}
|
|
@ -48,4 +48,4 @@ limitations under the License.
|
|||
//
|
||||
// As a bonus, a few common types useful from all api objects and versions
|
||||
// are provided in types.go.
|
||||
package runtime // import "k8s.io/apimachinery/pkg/runtime"
|
||||
package runtime
|
||||
|
|
|
@ -259,6 +259,7 @@ type ObjectDefaulter interface {
|
|||
|
||||
type ObjectVersioner interface {
|
||||
ConvertToVersion(in Object, gv GroupVersioner) (out Object, err error)
|
||||
PrioritizedVersionsForGroup(group string) []schema.GroupVersion
|
||||
}
|
||||
|
||||
// ObjectConvertor converts an object to a different version.
|
||||
|
@ -384,3 +385,9 @@ type Unstructured interface {
|
|||
// If the items passed to fn are not retained, or are retained for the same duration, use EachListItem instead for memory efficiency.
|
||||
EachListItemWithAlloc(func(Object) error) error
|
||||
}
|
||||
|
||||
// ApplyConfiguration is an interface that root apply configuration types implement.
|
||||
type ApplyConfiguration interface {
|
||||
// IsApplyConfiguration is implemented if the object is the root of an apply configuration.
|
||||
IsApplyConfiguration()
|
||||
}
|
||||
|
|
|
@ -17,15 +17,18 @@ limitations under the License.
|
|||
package runtime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/operation"
|
||||
"k8s.io/apimachinery/pkg/conversion"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/naming"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
// Scheme defines methods for serializing and deserializing API objects, a type
|
||||
|
@ -68,6 +71,12 @@ type Scheme struct {
|
|||
// the provided object must be a pointer.
|
||||
defaulterFuncs map[reflect.Type]func(interface{})
|
||||
|
||||
// validationFuncs is a map to funcs to be called with an object to perform validation.
|
||||
// The provided object must be a pointer.
|
||||
// If oldObject is non-nil, update validation is performed and may perform additional
|
||||
// validation such as transition rules and immutability checks.
|
||||
validationFuncs map[reflect.Type]func(ctx context.Context, op operation.Operation, object, oldObject interface{}) field.ErrorList
|
||||
|
||||
// converter stores all registered conversion functions. It also has
|
||||
// default converting behavior.
|
||||
converter *conversion.Converter
|
||||
|
@ -96,6 +105,7 @@ func NewScheme() *Scheme {
|
|||
unversionedKinds: map[string]reflect.Type{},
|
||||
fieldLabelConversionFuncs: map[schema.GroupVersionKind]FieldLabelConversionFunc{},
|
||||
defaulterFuncs: map[reflect.Type]func(interface{}){},
|
||||
validationFuncs: map[reflect.Type]func(ctx context.Context, op operation.Operation, object, oldObject interface{}) field.ErrorList{},
|
||||
versionPriority: map[string][]string{},
|
||||
schemeName: naming.GetNameFromCallsite(internalPackages...),
|
||||
}
|
||||
|
@ -347,6 +357,35 @@ func (s *Scheme) Default(src Object) {
|
|||
}
|
||||
}
|
||||
|
||||
// AddValidationFunc registered a function that can validate the object, and
|
||||
// oldObject. These functions will be invoked when Validate() or ValidateUpdate()
|
||||
// is called. The function will never be called unless the validated object
|
||||
// matches srcType. If this function is invoked twice with the same srcType, the
|
||||
// fn passed to the later call will be used instead.
|
||||
func (s *Scheme) AddValidationFunc(srcType Object, fn func(ctx context.Context, op operation.Operation, object, oldObject interface{}) field.ErrorList) {
|
||||
s.validationFuncs[reflect.TypeOf(srcType)] = fn
|
||||
}
|
||||
|
||||
// Validate validates the provided Object according to the generated declarative validation code.
|
||||
// WARNING: This does not validate all objects! The handwritten validation code in validation.go
|
||||
// is not run when this is called. Only the generated zz_generated.validations.go validation code is run.
|
||||
func (s *Scheme) Validate(ctx context.Context, options []string, object Object, subresources ...string) field.ErrorList {
|
||||
if fn, ok := s.validationFuncs[reflect.TypeOf(object)]; ok {
|
||||
return fn(ctx, operation.Operation{Type: operation.Create, Request: operation.Request{Subresources: subresources}, Options: options}, object, nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateUpdate validates the provided object and oldObject according to the generated declarative validation code.
|
||||
// WARNING: This does not validate all objects! The handwritten validation code in validation.go
|
||||
// is not run when this is called. Only the generated zz_generated.validations.go validation code is run.
|
||||
func (s *Scheme) ValidateUpdate(ctx context.Context, options []string, object, oldObject Object, subresources ...string) field.ErrorList {
|
||||
if fn, ok := s.validationFuncs[reflect.TypeOf(object)]; ok {
|
||||
return fn(ctx, operation.Operation{Type: operation.Update, Request: operation.Request{Subresources: subresources}, Options: options}, object, oldObject)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert will attempt to convert in into out. Both must be pointers. For easy
|
||||
// testing of conversion functions. Returns an error if the conversion isn't
|
||||
// possible. You can call this with types that haven't been registered (for example,
|
||||
|
@ -704,3 +743,67 @@ func (s *Scheme) Name() string {
|
|||
// internalPackages are packages that ignored when creating a default reflector name. These packages are in the common
|
||||
// call chains to NewReflector, so they'd be low entropy names for reflectors
|
||||
var internalPackages = []string{"k8s.io/apimachinery/pkg/runtime/scheme.go"}
|
||||
|
||||
// ToOpenAPIDefinitionName returns the REST-friendly OpenAPI definition name known type identified by groupVersionKind.
|
||||
// If the groupVersionKind does not identify a known type, an error is returned.
|
||||
// The Version field of groupVersionKind is required, and the Group and Kind fields are required for unstructured.Unstructured
|
||||
// types. If a required field is empty, an error is returned.
|
||||
//
|
||||
// The OpenAPI definition name is the canonical name of the type, with the group and version removed.
|
||||
// For example, the OpenAPI definition name of Pod is `io.k8s.api.core.v1.Pod`.
|
||||
//
|
||||
// A known type that is registered as an unstructured.Unstructured type is treated as a custom resource and
|
||||
// which has an OpenAPI definition name of the form `<reversed-group>.<version.<kind>`.
|
||||
// For example, the OpenAPI definition name of `group: stable.example.com, version: v1, kind: Pod` is
|
||||
// `com.example.stable.v1.Pod`.
|
||||
func (s *Scheme) ToOpenAPIDefinitionName(groupVersionKind schema.GroupVersionKind) (string, error) {
|
||||
if groupVersionKind.Version == "" { // Empty version is not allowed by New() so check it first to avoid a panic.
|
||||
return "", fmt.Errorf("version is required on all types: %v", groupVersionKind)
|
||||
}
|
||||
example, err := s.New(groupVersionKind)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, ok := example.(Unstructured); ok {
|
||||
if groupVersionKind.Group == "" || groupVersionKind.Kind == "" {
|
||||
return "", fmt.Errorf("unable to convert GroupVersionKind with empty fields to unstructured type to an OpenAPI definition name: %v", groupVersionKind)
|
||||
}
|
||||
return reverseParts(groupVersionKind.Group) + "." + groupVersionKind.Version + "." + groupVersionKind.Kind, nil
|
||||
}
|
||||
rtype := reflect.TypeOf(example).Elem()
|
||||
name := toOpenAPIDefinitionName(rtype.PkgPath() + "." + rtype.Name())
|
||||
return name, nil
|
||||
}
|
||||
|
||||
// toOpenAPIDefinitionName converts Golang package/type canonical name into REST friendly OpenAPI name.
|
||||
// Input is expected to be `PkgPath + "." TypeName.
|
||||
//
|
||||
// Examples of REST friendly OpenAPI name:
|
||||
//
|
||||
// Input: k8s.io/api/core/v1.Pod
|
||||
// Output: io.k8s.api.core.v1.Pod
|
||||
//
|
||||
// Input: k8s.io/api/core/v1
|
||||
// Output: io.k8s.api.core.v1
|
||||
//
|
||||
// Input: csi.storage.k8s.io/v1alpha1.CSINodeInfo
|
||||
// Output: io.k8s.storage.csi.v1alpha1.CSINodeInfo
|
||||
//
|
||||
// Note that this is a copy of ToRESTFriendlyName from k8s.io/kube-openapi/pkg/util. It is duplicated here to avoid
|
||||
// a dependency on kube-openapi.
|
||||
func toOpenAPIDefinitionName(name string) string {
|
||||
nameParts := strings.Split(name, "/")
|
||||
// Reverse first part. e.g., io.k8s... instead of k8s.io...
|
||||
if len(nameParts) > 0 && strings.Contains(nameParts[0], ".") {
|
||||
nameParts[0] = reverseParts(nameParts[0])
|
||||
}
|
||||
return strings.Join(nameParts, ".")
|
||||
}
|
||||
|
||||
func reverseParts(dotSeparatedName string) string {
|
||||
parts := strings.Split(dotSeparatedName, ".")
|
||||
for i, j := 0, len(parts)-1; i < j; i, j = i+1, j-1 {
|
||||
parts[i], parts[j] = parts[j], parts[i]
|
||||
}
|
||||
return strings.Join(parts, ".")
|
||||
}
|
||||
|
|
|
@ -17,12 +17,17 @@ limitations under the License.
|
|||
package runtime_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/operation"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/conversion"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
@ -30,6 +35,7 @@ import (
|
|||
runtimetesting "k8s.io/apimachinery/pkg/runtime/testing"
|
||||
"k8s.io/apimachinery/pkg/util/diff"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
type testConversions struct {
|
||||
|
@ -1009,3 +1015,165 @@ func TestMetaValuesUnregisteredConvert(t *testing.T) {
|
|||
t.Errorf("Expected %v, got %v", e, a)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterValidate(t *testing.T) {
|
||||
invalidValue := field.Invalid(field.NewPath("testString"), "", "Invalid value").WithOrigin("invalid-value")
|
||||
invalidLength := field.Invalid(field.NewPath("testString"), "", "Invalid length").WithOrigin("invalid-length")
|
||||
invalidStatusErr := field.Invalid(field.NewPath("testString"), "", "Invalid condition").WithOrigin("invalid-condition")
|
||||
invalidIfOptionErr := field.Invalid(field.NewPath("testString"), "", "Invalid when option is set").WithOrigin("invalid-when-option-set")
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
object runtime.Object
|
||||
oldObject runtime.Object
|
||||
subresource []string
|
||||
options []string
|
||||
expected field.ErrorList
|
||||
}{
|
||||
{
|
||||
name: "single error",
|
||||
object: &TestType1{},
|
||||
expected: field.ErrorList{invalidValue},
|
||||
},
|
||||
{
|
||||
name: "multiple errors",
|
||||
object: &TestType2{},
|
||||
expected: field.ErrorList{invalidValue, invalidLength},
|
||||
},
|
||||
{
|
||||
name: "update error",
|
||||
object: &TestType2{},
|
||||
oldObject: &TestType2{},
|
||||
expected: field.ErrorList{invalidLength},
|
||||
},
|
||||
{
|
||||
name: "options error",
|
||||
object: &TestType1{},
|
||||
options: []string{"option1"},
|
||||
expected: field.ErrorList{invalidIfOptionErr},
|
||||
},
|
||||
{
|
||||
name: "subresource error",
|
||||
object: &TestType1{},
|
||||
subresource: []string{"status"},
|
||||
expected: field.ErrorList{invalidStatusErr},
|
||||
},
|
||||
}
|
||||
|
||||
s := runtime.NewScheme()
|
||||
ctx := context.Background()
|
||||
|
||||
// register multiple types for testing to ensure registration is working as expected
|
||||
s.AddValidationFunc(&TestType1{}, func(ctx context.Context, op operation.Operation, object, oldObject interface{}) field.ErrorList {
|
||||
if op.HasOption("option1") {
|
||||
return field.ErrorList{invalidIfOptionErr}
|
||||
}
|
||||
if slices.Equal(op.Request.Subresources, []string{"status"}) {
|
||||
return field.ErrorList{invalidStatusErr}
|
||||
}
|
||||
return field.ErrorList{invalidValue}
|
||||
})
|
||||
|
||||
s.AddValidationFunc(&TestType2{}, func(ctx context.Context, op operation.Operation, object, oldObject interface{}) field.ErrorList {
|
||||
if oldObject != nil {
|
||||
return field.ErrorList{invalidLength}
|
||||
}
|
||||
return field.ErrorList{invalidValue, invalidLength}
|
||||
})
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var results field.ErrorList
|
||||
if tc.oldObject == nil {
|
||||
results = s.Validate(ctx, tc.options, tc.object, tc.subresource...)
|
||||
} else {
|
||||
results = s.ValidateUpdate(ctx, tc.options, tc.object, tc.oldObject, tc.subresource...)
|
||||
}
|
||||
matcher := field.ErrorMatcher{}.ByType().ByField().ByOrigin()
|
||||
matcher.Test(t, tc.expected, results)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type TestType1 struct {
|
||||
Version string `json:"apiVersion,omitempty"`
|
||||
Kind string `json:"kind,omitempty"`
|
||||
TestString string `json:"testString"`
|
||||
}
|
||||
|
||||
func (TestType1) GetObjectKind() schema.ObjectKind { return schema.EmptyObjectKind }
|
||||
|
||||
func (TestType1) DeepCopyObject() runtime.Object { return nil }
|
||||
|
||||
type TestType2 struct {
|
||||
Version string `json:"apiVersion,omitempty"`
|
||||
Kind string `json:"kind,omitempty"`
|
||||
TestString string `json:"testString"`
|
||||
}
|
||||
|
||||
func (TestType2) GetObjectKind() schema.ObjectKind { return schema.EmptyObjectKind }
|
||||
|
||||
func (TestType2) DeepCopyObject() runtime.Object { return nil }
|
||||
|
||||
func TestToOpenAPIDefinitionName(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
registerGvk *schema.GroupVersionKind // defaults to gvk unless set
|
||||
registerObj runtime.Object
|
||||
gvk schema.GroupVersionKind
|
||||
out string
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "unstructured type",
|
||||
registerObj: &unstructured.Unstructured{},
|
||||
gvk: schema.GroupVersionKind{Group: "stable.example.com", Version: "v1", Kind: "CronTab"},
|
||||
out: "com.example.stable.v1.CronTab",
|
||||
},
|
||||
{
|
||||
name: "unregistered type: empty group",
|
||||
registerObj: &unstructured.Unstructured{},
|
||||
gvk: schema.GroupVersionKind{Version: "v1", Kind: "CronTab"},
|
||||
wantErr: fmt.Errorf("unable to convert GroupVersionKind with empty fields to unstructured type to an OpenAPI definition name: %v", schema.GroupVersionKind{Version: "v1", Kind: "CronTab"}),
|
||||
},
|
||||
{
|
||||
name: "unregistered type: empty version",
|
||||
registerObj: &unstructured.Unstructured{},
|
||||
registerGvk: &schema.GroupVersionKind{Group: "stable.example.com", Version: "v1", Kind: "CronTab"},
|
||||
gvk: schema.GroupVersionKind{Group: "stable.example.com", Kind: "CronTab"},
|
||||
wantErr: fmt.Errorf("version is required on all types: %v", schema.GroupVersionKind{Group: "stable.example.com", Kind: "CronTab"}),
|
||||
},
|
||||
{
|
||||
name: "unregistered type: empty kind",
|
||||
registerObj: &unstructured.Unstructured{},
|
||||
gvk: schema.GroupVersionKind{Group: "stable.example.com", Version: "v1"},
|
||||
wantErr: fmt.Errorf("unable to convert GroupVersionKind with empty fields to unstructured type to an OpenAPI definition name: %v", schema.GroupVersionKind{Group: "stable.example.com", Version: "v1"}),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if test.registerGvk == nil {
|
||||
test.registerGvk = &test.gvk
|
||||
}
|
||||
|
||||
scheme := runtime.NewScheme()
|
||||
scheme.AddKnownTypeWithName(*test.registerGvk, test.registerObj)
|
||||
utilruntime.Must(runtimetesting.RegisterConversions(scheme))
|
||||
|
||||
out, err := scheme.ToOpenAPIDefinitionName(test.gvk)
|
||||
if test.wantErr != nil {
|
||||
if err == nil || err.Error() != test.wantErr.Error() {
|
||||
t.Errorf("expected error: %v but got %v", test.wantErr, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if out != test.out {
|
||||
t.Errorf("expected %s, got %s", test.out, out)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -152,10 +152,6 @@ func (s *serializer) encode(mode modes.EncMode, obj runtime.Object, w io.Writer)
|
|||
v = u.UnstructuredContent()
|
||||
}
|
||||
|
||||
if err := modes.RejectCustomMarshalers(v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := w.Write(selfDescribedCBOR); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -270,8 +266,6 @@ func (s *serializer) unmarshal(data []byte, into interface{}) (strict, lax error
|
|||
}
|
||||
}()
|
||||
into = &content
|
||||
} else if err := modes.RejectCustomMarshalers(into); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !s.options.strict {
|
||||
|
|
|
@ -209,38 +209,38 @@ func TestEncode(t *testing.T) {
|
|||
},
|
||||
},
|
||||
{
|
||||
name: "unsupported marshaler",
|
||||
name: "text marshaler",
|
||||
in: &textMarshalerObject{},
|
||||
assertOnWriter: func() (io.Writer, func(*testing.T)) {
|
||||
var b bytes.Buffer
|
||||
return &b, func(t *testing.T) {
|
||||
if b.Len() != 0 {
|
||||
t.Errorf("expected no bytes to be written, got %d", b.Len())
|
||||
if diff := cmp.Diff(b.Bytes(), []byte{0xd9, 0xd9, 0xf7, 0x64, 't', 'e', 's', 't'}); diff != "" {
|
||||
t.Errorf("unexpected diff:\n%s", diff)
|
||||
}
|
||||
}
|
||||
},
|
||||
assertOnError: func(t *testing.T, err error) {
|
||||
if want := "unable to serialize *cbor.textMarshalerObject: *cbor.textMarshalerObject implements encoding.TextMarshaler without corresponding cbor interface"; err == nil || err.Error() != want {
|
||||
t.Errorf("expected error %q, got: %v", want, err)
|
||||
if err != nil {
|
||||
t.Errorf("expected nil error, got: %v", err)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unsupported marshaler within unstructured content",
|
||||
name: "text marshaler within unstructured content",
|
||||
in: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{"": textMarshalerObject{}},
|
||||
},
|
||||
assertOnWriter: func() (io.Writer, func(*testing.T)) {
|
||||
var b bytes.Buffer
|
||||
return &b, func(t *testing.T) {
|
||||
if b.Len() != 0 {
|
||||
t.Errorf("expected no bytes to be written, got %d", b.Len())
|
||||
if diff := cmp.Diff(b.Bytes(), []byte{0xd9, 0xd9, 0xf7, 0xa1, 0x40, 0x64, 't', 'e', 's', 't'}); diff != "" {
|
||||
t.Errorf("unexpected diff:\n%s", diff)
|
||||
}
|
||||
}
|
||||
},
|
||||
assertOnError: func(t *testing.T, err error) {
|
||||
if want := "unable to serialize map[string]interface {}: cbor.textMarshalerObject implements encoding.TextMarshaler without corresponding cbor interface"; err == nil || err.Error() != want {
|
||||
t.Errorf("expected error %q, got: %v", want, err)
|
||||
if err != nil {
|
||||
t.Errorf("expected nil error, got: %v", err)
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@ -689,19 +689,6 @@ func TestDecode(t *testing.T) {
|
|||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "into unsupported marshaler",
|
||||
data: []byte("\xa0"),
|
||||
into: &textMarshalerObject{},
|
||||
metaFactory: stubMetaFactory{gvk: &schema.GroupVersionKind{}},
|
||||
typer: stubTyper{gvks: []schema.GroupVersionKind{{Version: "v", Kind: "k"}}},
|
||||
expectedGVK: &schema.GroupVersionKind{Version: "v", Kind: "k"},
|
||||
assertOnError: func(t *testing.T, err error) {
|
||||
if want := "unable to serialize *cbor.textMarshalerObject: *cbor.textMarshalerObject implements encoding.TextMarshaler without corresponding cbor interface"; err == nil || err.Error() != want {
|
||||
t.Errorf("expected error %q, got: %v", want, err)
|
||||
}
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := newSerializer(tc.metaFactory, tc.creater, tc.typer, tc.options...)
|
||||
|
@ -731,7 +718,7 @@ func (textMarshalerObject) DeepCopyObject() runtime.Object {
|
|||
}
|
||||
|
||||
func (textMarshalerObject) MarshalText() ([]byte, error) {
|
||||
return nil, nil
|
||||
return []byte("test"), nil
|
||||
}
|
||||
|
||||
func TestMetaFactoryInterpret(t *testing.T) {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue