Compare commits
132 Commits
kubernetes
...
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 |
27
go.mod
27
go.mod
|
@ -9,26 +9,27 @@ godebug default=go1.24
|
|||
require (
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/fxamacker/cbor/v2 v2.7.0
|
||||
github.com/fxamacker/cbor/v2 v2.9.0
|
||||
github.com/gogo/protobuf v1.3.2
|
||||
github.com/google/gnostic-models v0.6.9
|
||||
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/spf13/pflag v1.0.5
|
||||
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.33.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-20250318190949-c8a335a9a2ff
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3
|
||||
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/v4 v4.6.0
|
||||
sigs.k8s.io/yaml v1.4.0
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0
|
||||
sigs.k8s.io/yaml v1.6.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
@ -40,15 +41,15 @@ require (
|
|||
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.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // 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
|
||||
)
|
||||
|
|
53
go.sum
53
go.sum
|
@ -4,8 +4,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
|
|||
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/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=
|
||||
|
@ -20,9 +20,8 @@ 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/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw=
|
||||
github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw=
|
||||
github.com/google/go-cmp v0.5.9/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=
|
||||
|
@ -50,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=
|
||||
|
@ -64,8 +64,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
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.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
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,6 +79,10 @@ 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=
|
||||
|
@ -88,20 +92,20 @@ 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.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
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.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.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.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
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=
|
||||
|
@ -128,16 +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-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
|
||||
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=
|
||||
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/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
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/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
|
||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -20,6 +20,7 @@ import (
|
|||
"bytes"
|
||||
gojson "encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
@ -28,7 +29,7 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp" //nolint:depguard
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
@ -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,7 +24,7 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp" //nolint:depguard
|
||||
"github.com/google/go-cmp/cmp"
|
||||
flag "github.com/spf13/pflag"
|
||||
"sigs.k8s.io/randfill"
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ import (
|
|||
jsonserializer "k8s.io/apimachinery/pkg/runtime/serializer/json"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
|
||||
"github.com/google/go-cmp/cmp" //nolint:depguard
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
// RoundtripToUnstructured verifies the roundtrip faithfulness of all external types in a scheme
|
||||
|
|
|
@ -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,7 +16,10 @@ limitations under the License.
|
|||
|
||||
package operation
|
||||
|
||||
import "k8s.io/apimachinery/pkg/util/sets"
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Operation provides contextual information about a validation request and the API
|
||||
// operation being validated.
|
||||
|
@ -41,7 +44,50 @@ type Operation struct {
|
|||
// resource first began using the feature.
|
||||
//
|
||||
// Unset options are disabled/false.
|
||||
Options sets.Set[string]
|
||||
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.
|
||||
|
|
|
@ -16,10 +16,16 @@ 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.
|
||||
// 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
|
||||
|
@ -35,3 +41,19 @@ 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()
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ package content
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/validate/constraints"
|
||||
)
|
||||
|
@ -27,3 +28,12 @@ import (
|
|||
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)
|
||||
}
|
||||
|
|
|
@ -18,25 +18,45 @@ package validate
|
|||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/equality"
|
||||
"k8s.io/apimachinery/pkg/api/operation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
// CompareFunc is a function that compares two values of the same type.
|
||||
type CompareFunc[T any] func(T, T) bool
|
||||
// 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 validates each element of newSlice with the specified
|
||||
// validation function. The comparison function is used to find the
|
||||
// corresponding value in oldSlice. The value-type of the slices is assumed to
|
||||
// not be nilable.
|
||||
// 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,
|
||||
cmp CompareFunc[T], validator ValidateFunc[*T]) field.ErrorList {
|
||||
match, equiv MatchFunc[T], validator ValidateFunc[*T]) field.ErrorList {
|
||||
var errs field.ErrorList
|
||||
for i, val := range newSlice {
|
||||
var old *T
|
||||
if cmp != nil && len(oldSlice) > 0 {
|
||||
old = lookup(oldSlice, val, cmp)
|
||||
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)...)
|
||||
}
|
||||
|
@ -45,39 +65,107 @@ func EachSliceVal[T any](ctx context.Context, op operation.Operation, fldPath *f
|
|||
|
||||
// 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, cmp func(T, T) bool) *T {
|
||||
func lookup[T any](list []T, target T, match MatchFunc[T]) *T {
|
||||
for i := range list {
|
||||
if cmp(list[i], target) {
|
||||
if match(list[i], target) {
|
||||
return &list[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EachMapVal validates each element of newMap with the specified validation
|
||||
// function and, if the corresponding key is found in oldMap, the old value.
|
||||
// The value-type of the slices is assumed to not be nilable.
|
||||
// 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,
|
||||
validator ValidateFunc[*V]) field.ErrorList {
|
||||
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. The oldMap argument is not used.
|
||||
// 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
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import (
|
|||
|
||||
"k8s.io/apimachinery/pkg/api/operation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
type TestStruct struct {
|
||||
|
@ -32,6 +33,32 @@ type TestStruct struct {
|
|||
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"})
|
||||
|
@ -70,7 +97,7 @@ func testEachSliceVal[T any](t *testing.T, name string, input []T) {
|
|||
calls++
|
||||
return nil
|
||||
}
|
||||
_ = EachSliceVal(context.Background(), operation.Operation{}, field.NewPath("test"), input, nil, nil, vfn)
|
||||
_ = 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)
|
||||
}
|
||||
|
@ -95,14 +122,98 @@ func testEachSliceValUpdate[T any](t *testing.T, name string, input []T) {
|
|||
old := make([]T, len(input))
|
||||
copy(old, input)
|
||||
slices.Reverse(old)
|
||||
cmp := func(a, b T) bool { return reflect.DeepEqual(a, b) }
|
||||
_ = EachSliceVal(context.Background(), operation.Operation{}, field.NewPath("test"), input, old, cmp, vfn)
|
||||
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"})
|
||||
|
@ -129,13 +240,131 @@ func testEachMapVal[T any](t *testing.T, name string, input map[string]T) {
|
|||
calls++
|
||||
return nil
|
||||
}
|
||||
_ = EachMapVal(context.Background(), operation.Operation{}, field.NewPath("test"), input, nil, vfn)
|
||||
_ = 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) {
|
||||
|
@ -161,3 +390,103 @@ func testEachMapKey[K ~string, V any](t *testing.T, name string, input map[K]V)
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
|
@ -24,11 +24,15 @@ import (
|
|||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
// Immutable 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 ImmutableNonComparable instead.
|
||||
func Immutable[T comparable](_ context.Context, op operation.Operation, fldPath *field.Path, value, oldValue *T) field.ErrorList {
|
||||
// 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
|
||||
}
|
||||
|
@ -43,11 +47,11 @@ func Immutable[T comparable](_ context.Context, op operation.Operation, fldPath
|
|||
return nil
|
||||
}
|
||||
|
||||
// ImmutableNonComparable verifies that the specified value has not changed in
|
||||
// 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 Immutable, this function can be used with types that are
|
||||
// provided. Unlike ImmutableByCompare, this function can be used with types that are
|
||||
// not directly comparable, at the cost of performance.
|
||||
func ImmutableNonComparable[T any](_ context.Context, op operation.Operation, fldPath *field.Path, value, oldValue T) field.ErrorList {
|
||||
func ImmutableByReflect[T any](_ context.Context, op operation.Operation, fldPath *field.Path, value, oldValue T) field.ErrorList {
|
||||
if op.Type != operation.Update {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -25,15 +25,16 @@ import (
|
|||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
type Struct struct {
|
||||
type StructComparable struct {
|
||||
S string
|
||||
I int
|
||||
B bool
|
||||
}
|
||||
|
||||
func TestImmutable(t *testing.T) {
|
||||
structA := Struct{"abc", 123, true}
|
||||
structB := Struct{"xyz", 456, false}
|
||||
func TestImmutableByCompare(t *testing.T) {
|
||||
structA := StructComparable{"abc", 123, true}
|
||||
structA2 := structA
|
||||
structB := StructComparable{"xyz", 456, false}
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
|
@ -42,62 +43,194 @@ func TestImmutable(t *testing.T) {
|
|||
}{{
|
||||
name: "nil both values",
|
||||
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
|
||||
return Immutable[int](context.Background(), op, fld, nil, nil)
|
||||
return ImmutableByCompare[int](context.Background(), op, fld, nil, nil)
|
||||
},
|
||||
}, {
|
||||
name: "nil value",
|
||||
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
|
||||
return Immutable(context.Background(), op, fld, nil, ptr.To(123))
|
||||
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 Immutable(context.Background(), op, fld, ptr.To(123), nil)
|
||||
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 Immutable(context.Background(), op, fld, ptr.To(123), ptr.To(123))
|
||||
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 Immutable(context.Background(), op, fld, ptr.To(123), ptr.To(456))
|
||||
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 Immutable(context.Background(), op, fld, ptr.To("abc"), ptr.To("abc"))
|
||||
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 Immutable(context.Background(), op, fld, ptr.To("abc"), ptr.To("xyz"))
|
||||
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 Immutable(context.Background(), op, fld, ptr.To(true), ptr.To(true))
|
||||
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 Immutable(context.Background(), op, fld, ptr.To(true), ptr.To(false))
|
||||
return ImmutableByCompare(context.Background(), op, fld, ptr.To(true), ptr.To(false))
|
||||
},
|
||||
fail: true,
|
||||
}, {
|
||||
name: "struct",
|
||||
name: "same struct",
|
||||
fn: func(op operation.Operation, fld *field.Path) field.ErrorList {
|
||||
return Immutable(context.Background(), op, fld, ptr.To(structA), ptr.To(structA))
|
||||
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 Immutable(context.Background(), op, fld, ptr.To(structA), ptr.To(structB))
|
||||
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,
|
||||
}} {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -83,7 +83,7 @@ func ForbiddenSlice[T any](_ context.Context, _ operation.Operation, fldPath *fi
|
|||
return field.ErrorList{field.Forbidden(fldPath, "")}
|
||||
}
|
||||
|
||||
// RequiredMap verifies that the specified map is empty.
|
||||
// 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
|
||||
|
|
|
@ -36,6 +36,11 @@ func Subfield[Tstruct any, Tfield any](ctx context.Context, op operation.Operati
|
|||
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,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,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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -55,11 +55,9 @@ func TestObjectToUnstructuredConversion(t *testing.T) {
|
|||
Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Carp",
|
||||
"metadata": map[string]interface{}{
|
||||
"creationTimestamp": nil,
|
||||
},
|
||||
"spec": map[string]interface{}{},
|
||||
"status": map[string]interface{}{},
|
||||
"metadata": map[string]interface{}{},
|
||||
"spec": map[string]interface{}{},
|
||||
"status": map[string]interface{}{},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -78,8 +76,7 @@ func TestObjectToUnstructuredConversion(t *testing.T) {
|
|||
"apiVersion": "v1",
|
||||
"kind": "Carp",
|
||||
"metadata": map[string]interface{}{
|
||||
"creationTimestamp": nil,
|
||||
"name": "noxu",
|
||||
"name": "noxu",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"hostname": "example.com",
|
||||
|
@ -155,8 +152,7 @@ func TestUnstructuredToObjectConversion(t *testing.T) {
|
|||
"apiVersion": "v1",
|
||||
"kind": "Carp",
|
||||
"metadata": map[string]interface{}{
|
||||
"creationTimestamp": nil,
|
||||
"name": "noxu",
|
||||
"name": "noxu",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"hostname": "example.com",
|
||||
|
@ -181,8 +177,7 @@ func TestUnstructuredToObjectConversion(t *testing.T) {
|
|||
"apiVersion": "v1",
|
||||
"kind": "Carp",
|
||||
"metadata": map[string]interface{}{
|
||||
"creationTimestamp": nil,
|
||||
"name": "noxu",
|
||||
"name": "noxu",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"hostname": "example.com",
|
||||
|
@ -207,8 +202,7 @@ func TestUnstructuredToObjectConversion(t *testing.T) {
|
|||
"apiVersion": "v9",
|
||||
"kind": "Carp",
|
||||
"metadata": map[string]interface{}{
|
||||
"creationTimestamp": nil,
|
||||
"name": "noxu",
|
||||
"name": "noxu",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"hostname": "example.com",
|
||||
|
@ -232,8 +226,7 @@ func TestUnstructuredToObjectConversion(t *testing.T) {
|
|||
"apiVersion": "v1",
|
||||
"kind": "Carp",
|
||||
"metadata": map[string]interface{}{
|
||||
"creationTimestamp": nil,
|
||||
"name": "noxu",
|
||||
"name": "noxu",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"hostname": "example.com",
|
||||
|
@ -358,8 +351,7 @@ func TestUnstructuredToGVConversion(t *testing.T) {
|
|||
"apiVersion": "v1",
|
||||
"kind": "Carp",
|
||||
"metadata": map[string]interface{}{
|
||||
"creationTimestamp": nil,
|
||||
"name": "noxu",
|
||||
"name": "noxu",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"hostname": "example.com",
|
||||
|
@ -388,8 +380,7 @@ func TestUnstructuredToGVConversion(t *testing.T) {
|
|||
"apiVersion": "v1",
|
||||
"kind": "Carp",
|
||||
"metadata": map[string]interface{}{
|
||||
"creationTimestamp": nil,
|
||||
"name": "noxu",
|
||||
"name": "noxu",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"hostname": "example.com",
|
||||
|
@ -414,8 +405,7 @@ func TestUnstructuredToGVConversion(t *testing.T) {
|
|||
"apiVersion": "v1",
|
||||
"kind": "Carp",
|
||||
"metadata": map[string]interface{}{
|
||||
"creationTimestamp": nil,
|
||||
"name": "noxu",
|
||||
"name": "noxu",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"hostname": "example.com",
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1008,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
|
||||
|
@ -1079,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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -385,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()
|
||||
}
|
||||
|
|
|
@ -75,7 +75,7 @@ type Scheme struct {
|
|||
// 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{}, subresources ...string) field.ErrorList
|
||||
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.
|
||||
|
@ -105,7 +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{}, subresource ...string) field.ErrorList{},
|
||||
validationFuncs: map[reflect.Type]func(ctx context.Context, op operation.Operation, object, oldObject interface{}) field.ErrorList{},
|
||||
versionPriority: map[string][]string{},
|
||||
schemeName: naming.GetNameFromCallsite(internalPackages...),
|
||||
}
|
||||
|
@ -362,16 +362,16 @@ func (s *Scheme) Default(src Object) {
|
|||
// 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{}, subresources ...string) field.ErrorList) {
|
||||
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 sets.Set[string], object Object, subresources ...string) field.ErrorList {
|
||||
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, Options: options}, object, nil, subresources...)
|
||||
return fn(ctx, operation.Operation{Type: operation.Create, Request: operation.Request{Subresources: subresources}, Options: options}, object, nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -379,9 +379,9 @@ func (s *Scheme) Validate(ctx context.Context, options sets.Set[string], object
|
|||
// 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 sets.Set[string], object, oldObject Object, subresources ...string) field.ErrorList {
|
||||
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, Options: options}, object, oldObject, subresources...)
|
||||
return fn(ctx, operation.Operation{Type: operation.Update, Request: operation.Request{Subresources: subresources}, Options: options}, object, oldObject)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -743,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, ".")
|
||||
}
|
||||
|
|
|
@ -20,12 +20,14 @@ 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"
|
||||
|
@ -33,7 +35,6 @@ 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/sets"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
|
@ -1026,7 +1027,7 @@ func TestRegisterValidate(t *testing.T) {
|
|||
object runtime.Object
|
||||
oldObject runtime.Object
|
||||
subresource []string
|
||||
options sets.Set[string]
|
||||
options []string
|
||||
expected field.ErrorList
|
||||
}{
|
||||
{
|
||||
|
@ -1048,7 +1049,7 @@ func TestRegisterValidate(t *testing.T) {
|
|||
{
|
||||
name: "options error",
|
||||
object: &TestType1{},
|
||||
options: sets.New("option1"),
|
||||
options: []string{"option1"},
|
||||
expected: field.ErrorList{invalidIfOptionErr},
|
||||
},
|
||||
{
|
||||
|
@ -1063,17 +1064,17 @@ func TestRegisterValidate(t *testing.T) {
|
|||
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{}, subresources ...string) field.ErrorList {
|
||||
if op.Options.Has("option1") {
|
||||
s.AddValidationFunc(&TestType1{}, func(ctx context.Context, op operation.Operation, object, oldObject interface{}) field.ErrorList {
|
||||
if op.HasOption("option1") {
|
||||
return field.ErrorList{invalidIfOptionErr}
|
||||
}
|
||||
if len(subresources) == 1 && subresources[0] == "status" {
|
||||
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{}, subresources ...string) field.ErrorList {
|
||||
s.AddValidationFunc(&TestType2{}, func(ctx context.Context, op operation.Operation, object, oldObject interface{}) field.ErrorList {
|
||||
if oldObject != nil {
|
||||
return field.ErrorList{invalidLength}
|
||||
}
|
||||
|
@ -1113,3 +1114,66 @@ type TestType2 struct {
|
|||
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) {
|
||||
|
|
|
@ -25,31 +25,13 @@ import (
|
|||
|
||||
// Marshal serializes a value to CBOR. If there is more than one way to encode the value, it will
|
||||
// make the same choice as the CBOR implementation of runtime.Serializer.
|
||||
//
|
||||
// Note: Support for CBOR is at an alpha stage. If the value (or, for composite types, any of its
|
||||
// nested values) implement any of the interfaces encoding.TextMarshaler, encoding.TextUnmarshaler,
|
||||
// encoding/json.Marshaler, or encoding/json.Unmarshaler, a non-nil error will be returned unless
|
||||
// the value also implements the corresponding CBOR interfaces. This limitation will ultimately be
|
||||
// removed in favor of automatic transcoding to CBOR.
|
||||
func Marshal(src interface{}) ([]byte, error) {
|
||||
if err := modes.RejectCustomMarshalers(src); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func Marshal(src any) ([]byte, error) {
|
||||
return modes.Encode.Marshal(src)
|
||||
}
|
||||
|
||||
// Unmarshal deserializes from CBOR into an addressable value. If there is more than one way to
|
||||
// unmarshal a value, it will make the same choice as the CBOR implementation of runtime.Serializer.
|
||||
//
|
||||
// Note: Support for CBOR is at an alpha stage. If the value (or, for composite types, any of its
|
||||
// nested values) implement any of the interfaces encoding.TextMarshaler, encoding.TextUnmarshaler,
|
||||
// encoding/json.Marshaler, or encoding/json.Unmarshaler, a non-nil error will be returned unless
|
||||
// the value also implements the corresponding CBOR interfaces. This limitation will ultimately be
|
||||
// removed in favor of automatic transcoding to CBOR.
|
||||
func Unmarshal(src []byte, dst interface{}) error {
|
||||
if err := modes.RejectCustomMarshalers(dst); err != nil {
|
||||
return err
|
||||
}
|
||||
func Unmarshal(src []byte, dst any) error {
|
||||
return modes.Decode.Unmarshal(src, dst)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,81 +0,0 @@
|
|||
/*
|
||||
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 direct_test
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer/cbor/direct"
|
||||
)
|
||||
|
||||
var _ json.Marshaler = CustomJSONMarshaler{}
|
||||
|
||||
type CustomJSONMarshaler struct{}
|
||||
|
||||
func (CustomJSONMarshaler) MarshalJSON() ([]byte, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
var _ json.Unmarshaler = CustomJSONUnmarshaler{}
|
||||
|
||||
type CustomJSONUnmarshaler struct{}
|
||||
|
||||
func (CustomJSONUnmarshaler) UnmarshalJSON([]byte) error {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
var _ encoding.TextMarshaler = CustomTextMarshaler{}
|
||||
|
||||
type CustomTextMarshaler struct{}
|
||||
|
||||
func (CustomTextMarshaler) MarshalText() ([]byte, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
var _ encoding.TextUnmarshaler = CustomTextUnmarshaler{}
|
||||
|
||||
type CustomTextUnmarshaler struct{}
|
||||
|
||||
func (CustomTextUnmarshaler) UnmarshalText([]byte) error {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
func TestRejectsCustom(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
value interface{}
|
||||
iface reflect.Type
|
||||
}{
|
||||
{value: CustomJSONMarshaler{}, iface: reflect.TypeFor[json.Marshaler]()},
|
||||
{value: CustomJSONUnmarshaler{}, iface: reflect.TypeFor[json.Unmarshaler]()},
|
||||
{value: CustomTextMarshaler{}, iface: reflect.TypeFor[encoding.TextMarshaler]()},
|
||||
{value: CustomTextUnmarshaler{}, iface: reflect.TypeFor[encoding.TextUnmarshaler]()},
|
||||
} {
|
||||
t.Run(fmt.Sprintf("%T", tc.value), func(t *testing.T) {
|
||||
want := fmt.Sprintf("unable to serialize %T: %T implements %s without corresponding cbor interface", tc.value, tc.value, tc.iface.String())
|
||||
if _, err := direct.Marshal(tc.value); err == nil || err.Error() != want {
|
||||
t.Errorf("want error: %q, got: %v", want, err)
|
||||
}
|
||||
if err := direct.Unmarshal(nil, tc.value); err == nil || err.Error() != want {
|
||||
t.Errorf("want error: %q, got: %v", want, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,422 +0,0 @@
|
|||
/*
|
||||
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 modes
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
)
|
||||
|
||||
// Returns a non-nil error if and only if the argument's type (or one of its component types, for
|
||||
// composite types) implements json.Marshaler or encoding.TextMarshaler without also implementing
|
||||
// cbor.Marshaler and likewise for the respective Unmarshaler interfaces.
|
||||
//
|
||||
// This is a temporary, graduation-blocking restriction and will be removed in favor of automatic
|
||||
// transcoding between CBOR and JSON/text for these types. This restriction allows CBOR to be
|
||||
// exercised for in-tree and unstructured types while mitigating the risk of mangling out-of-tree
|
||||
// types in client programs.
|
||||
func RejectCustomMarshalers(v interface{}) error {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
rv := reflect.ValueOf(v)
|
||||
if err := marshalerCache.getChecker(rv.Type()).check(rv, maxDepth); err != nil {
|
||||
return fmt.Errorf("unable to serialize %T: %w", v, err)
|
||||
}
|
||||
if err := unmarshalerCache.getChecker(rv.Type()).check(rv, maxDepth); err != nil {
|
||||
return fmt.Errorf("unable to serialize %T: %w", v, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Recursion depth is limited as a basic mitigation against cyclic objects. Objects created by the
|
||||
// decoder shouldn't be able to contain cycles, but practically any object can be passed to the
|
||||
// encoder.
|
||||
var errMaxDepthExceeded = errors.New("object depth exceeds limit (possible cycle?)")
|
||||
|
||||
// The JSON encoder begins detecting cycles after depth 1000. Use a generous limit here, knowing
|
||||
// that it can might deeply nested acyclic objects. The limit will be removed along with the rest of
|
||||
// this mechanism.
|
||||
const maxDepth = 2048
|
||||
|
||||
var marshalerCache = checkers{
|
||||
cborInterface: reflect.TypeFor[cbor.Marshaler](),
|
||||
nonCBORInterfaces: []reflect.Type{
|
||||
reflect.TypeFor[json.Marshaler](),
|
||||
reflect.TypeFor[encoding.TextMarshaler](),
|
||||
},
|
||||
}
|
||||
|
||||
var unmarshalerCache = checkers{
|
||||
cborInterface: reflect.TypeFor[cbor.Unmarshaler](),
|
||||
nonCBORInterfaces: []reflect.Type{
|
||||
reflect.TypeFor[json.Unmarshaler](),
|
||||
reflect.TypeFor[encoding.TextUnmarshaler](),
|
||||
},
|
||||
assumeAddressableValues: true,
|
||||
}
|
||||
|
||||
// checker wraps a function for dynamically checking a value of a specific type for custom JSON
|
||||
// behaviors not matched by a custom CBOR behavior.
|
||||
type checker struct {
|
||||
// check returns a non-nil error if the given value might be marshalled to or from CBOR
|
||||
// using the default behavior for its kind, but marshalled to or from JSON using custom
|
||||
// behavior.
|
||||
check func(rv reflect.Value, depth int) error
|
||||
|
||||
// safe returns true if all values of this type are safe from mismatched custom marshalers.
|
||||
safe func() bool
|
||||
}
|
||||
|
||||
// TODO: stale
|
||||
// Having a single addressable checker for comparisons lets us prune and collapse parts of the
|
||||
// object traversal that are statically known to be safe. Depending on the type, it may be
|
||||
// unnecessary to inspect each value of that type. For example, no value of the built-in type bool
|
||||
// can implement json.Marshaler (a named type whose underlying type is bool could, but it is a
|
||||
// distinct type from bool).
|
||||
var noop = checker{
|
||||
safe: func() bool {
|
||||
return true
|
||||
},
|
||||
check: func(rv reflect.Value, depth int) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
type checkers struct {
|
||||
m sync.Map // reflect.Type => *checker
|
||||
|
||||
cborInterface reflect.Type
|
||||
nonCBORInterfaces []reflect.Type
|
||||
|
||||
assumeAddressableValues bool
|
||||
}
|
||||
|
||||
func (cache *checkers) getChecker(rt reflect.Type) checker {
|
||||
if ptr, ok := cache.m.Load(rt); ok {
|
||||
return *ptr.(*checker)
|
||||
}
|
||||
|
||||
return cache.getCheckerInternal(rt, nil)
|
||||
}
|
||||
|
||||
// linked list node representing the path from a composite type to an element type
|
||||
type path struct {
|
||||
Type reflect.Type
|
||||
Parent *path
|
||||
}
|
||||
|
||||
func (p path) cyclic(rt reflect.Type) bool {
|
||||
for ancestor := &p; ancestor != nil; ancestor = ancestor.Parent {
|
||||
if ancestor.Type == rt {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (cache *checkers) getCheckerInternal(rt reflect.Type, parent *path) (c checker) {
|
||||
// Store a placeholder cache entry first to handle cyclic types.
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
defer wg.Done()
|
||||
placeholder := checker{
|
||||
safe: func() bool {
|
||||
wg.Wait()
|
||||
return c.safe()
|
||||
},
|
||||
check: func(rv reflect.Value, depth int) error {
|
||||
wg.Wait()
|
||||
return c.check(rv, depth)
|
||||
},
|
||||
}
|
||||
if actual, loaded := cache.m.LoadOrStore(rt, &placeholder); loaded {
|
||||
// Someone else stored an entry for this type, use it.
|
||||
return *actual.(*checker)
|
||||
}
|
||||
|
||||
// Take a nonreflective path for the unstructured container types. They're common and
|
||||
// usually nested inside one another.
|
||||
switch rt {
|
||||
case reflect.TypeFor[map[string]interface{}](), reflect.TypeFor[[]interface{}]():
|
||||
return checker{
|
||||
safe: func() bool {
|
||||
return false
|
||||
},
|
||||
check: func(rv reflect.Value, depth int) error {
|
||||
return checkUnstructuredValue(cache, rv.Interface(), depth)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// It's possible that one of the relevant interfaces is implemented on a type with a pointer
|
||||
// receiver, but that a particular value of that type is not addressable. For example:
|
||||
//
|
||||
// func (Foo) MarshalText() ([]byte, error) { ... }
|
||||
// func (*Foo) MarshalCBOR() ([]byte, error) { ... }
|
||||
//
|
||||
// Both methods are in the method set of *Foo, but the method set of Foo contains only
|
||||
// MarshalText.
|
||||
//
|
||||
// Both the unmarshaler and marshaler checks assume that methods implementing a JSON or text
|
||||
// interface with a pointer receiver are always accessible. Only the unmarshaler check
|
||||
// assumes that CBOR methods with pointer receivers are accessible.
|
||||
|
||||
if rt.Implements(cache.cborInterface) {
|
||||
return noop
|
||||
}
|
||||
for _, unsafe := range cache.nonCBORInterfaces {
|
||||
if rt.Implements(unsafe) {
|
||||
err := fmt.Errorf("%v implements %v without corresponding cbor interface", rt, unsafe)
|
||||
return checker{
|
||||
safe: func() bool {
|
||||
return false
|
||||
},
|
||||
check: func(reflect.Value, int) error {
|
||||
return err
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cache.assumeAddressableValues && reflect.PointerTo(rt).Implements(cache.cborInterface) {
|
||||
return noop
|
||||
}
|
||||
for _, unsafe := range cache.nonCBORInterfaces {
|
||||
if reflect.PointerTo(rt).Implements(unsafe) {
|
||||
err := fmt.Errorf("%v implements %v without corresponding cbor interface", reflect.PointerTo(rt), unsafe)
|
||||
return checker{
|
||||
safe: func() bool {
|
||||
return false
|
||||
},
|
||||
check: func(reflect.Value, int) error {
|
||||
return err
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self := &path{Type: rt, Parent: parent}
|
||||
|
||||
switch rt.Kind() {
|
||||
case reflect.Array:
|
||||
ce := cache.getCheckerInternal(rt.Elem(), self)
|
||||
rtlen := rt.Len()
|
||||
if rtlen == 0 || (!self.cyclic(rt.Elem()) && ce.safe()) {
|
||||
return noop
|
||||
}
|
||||
return checker{
|
||||
safe: func() bool {
|
||||
return false
|
||||
},
|
||||
check: func(rv reflect.Value, depth int) error {
|
||||
if depth <= 0 {
|
||||
return errMaxDepthExceeded
|
||||
}
|
||||
for i := 0; i < rtlen; i++ {
|
||||
if err := ce.check(rv.Index(i), depth-1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
case reflect.Interface:
|
||||
// All interface values have to be checked because their dynamic type might
|
||||
// implement one of the interesting interfaces or be composed of another type that
|
||||
// does.
|
||||
return checker{
|
||||
safe: func() bool {
|
||||
return false
|
||||
},
|
||||
check: func(rv reflect.Value, depth int) error {
|
||||
if rv.IsNil() {
|
||||
return nil
|
||||
}
|
||||
// Unpacking interfaces must count against recursion depth,
|
||||
// consider this cycle:
|
||||
// > var i interface{}
|
||||
// > var p *interface{} = &i
|
||||
// > i = p
|
||||
// > rv := reflect.ValueOf(i)
|
||||
// > for {
|
||||
// > rv = rv.Elem()
|
||||
// > }
|
||||
if depth <= 0 {
|
||||
return errMaxDepthExceeded
|
||||
}
|
||||
rv = rv.Elem()
|
||||
return cache.getChecker(rv.Type()).check(rv, depth-1)
|
||||
},
|
||||
}
|
||||
|
||||
case reflect.Map:
|
||||
rtk := rt.Key()
|
||||
ck := cache.getCheckerInternal(rtk, self)
|
||||
rte := rt.Elem()
|
||||
ce := cache.getCheckerInternal(rte, self)
|
||||
if !self.cyclic(rtk) && !self.cyclic(rte) && ck.safe() && ce.safe() {
|
||||
return noop
|
||||
}
|
||||
return checker{
|
||||
safe: func() bool {
|
||||
return false
|
||||
},
|
||||
check: func(rv reflect.Value, depth int) error {
|
||||
if depth <= 0 {
|
||||
return errMaxDepthExceeded
|
||||
}
|
||||
iter := rv.MapRange()
|
||||
rvk := reflect.New(rtk).Elem()
|
||||
rve := reflect.New(rte).Elem()
|
||||
for iter.Next() {
|
||||
rvk.SetIterKey(iter)
|
||||
if err := ck.check(rvk, depth-1); err != nil {
|
||||
return err
|
||||
}
|
||||
rve.SetIterValue(iter)
|
||||
if err := ce.check(rve, depth-1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
case reflect.Pointer:
|
||||
ce := cache.getCheckerInternal(rt.Elem(), self)
|
||||
if !self.cyclic(rt.Elem()) && ce.safe() {
|
||||
return noop
|
||||
}
|
||||
return checker{
|
||||
safe: func() bool {
|
||||
return false
|
||||
},
|
||||
check: func(rv reflect.Value, depth int) error {
|
||||
if rv.IsNil() {
|
||||
return nil
|
||||
}
|
||||
if depth <= 0 {
|
||||
return errMaxDepthExceeded
|
||||
}
|
||||
return ce.check(rv.Elem(), depth-1)
|
||||
},
|
||||
}
|
||||
|
||||
case reflect.Slice:
|
||||
ce := cache.getCheckerInternal(rt.Elem(), self)
|
||||
if !self.cyclic(rt.Elem()) && ce.safe() {
|
||||
return noop
|
||||
}
|
||||
return checker{
|
||||
safe: func() bool {
|
||||
return false
|
||||
},
|
||||
check: func(rv reflect.Value, depth int) error {
|
||||
if depth <= 0 {
|
||||
return errMaxDepthExceeded
|
||||
}
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
if err := ce.check(rv.Index(i), depth-1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
case reflect.Struct:
|
||||
type field struct {
|
||||
Index int
|
||||
Checker checker
|
||||
}
|
||||
var fields []field
|
||||
for i := 0; i < rt.NumField(); i++ {
|
||||
f := rt.Field(i)
|
||||
cf := cache.getCheckerInternal(f.Type, self)
|
||||
if !self.cyclic(f.Type) && cf.safe() {
|
||||
continue
|
||||
}
|
||||
fields = append(fields, field{Index: i, Checker: cf})
|
||||
}
|
||||
if len(fields) == 0 {
|
||||
return noop
|
||||
}
|
||||
return checker{
|
||||
safe: func() bool {
|
||||
return false
|
||||
},
|
||||
check: func(rv reflect.Value, depth int) error {
|
||||
if depth <= 0 {
|
||||
return errMaxDepthExceeded
|
||||
}
|
||||
for _, fi := range fields {
|
||||
if err := fi.Checker.check(rv.Field(fi.Index), depth-1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
default:
|
||||
// Not a serializable composite type (funcs and channels are composite types but are
|
||||
// rejected by JSON and CBOR serialization).
|
||||
return noop
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func checkUnstructuredValue(cache *checkers, v interface{}, depth int) error {
|
||||
switch v := v.(type) {
|
||||
case nil, bool, int64, float64, string:
|
||||
return nil
|
||||
case []interface{}:
|
||||
if depth <= 0 {
|
||||
return errMaxDepthExceeded
|
||||
}
|
||||
for _, element := range v {
|
||||
if err := checkUnstructuredValue(cache, element, depth-1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
case map[string]interface{}:
|
||||
if depth <= 0 {
|
||||
return errMaxDepthExceeded
|
||||
}
|
||||
for _, element := range v {
|
||||
if err := checkUnstructuredValue(cache, element, depth-1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
// Unmarshaling an unstructured doesn't use other dynamic types, but nothing
|
||||
// prevents inserting values with arbitrary dynamic types into unstructured content,
|
||||
// as long as they can be marshalled.
|
||||
rv := reflect.ValueOf(v)
|
||||
return cache.getChecker(rv.Type()).check(rv, depth)
|
||||
}
|
||||
}
|
|
@ -1,198 +0,0 @@
|
|||
/*
|
||||
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 modes
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
)
|
||||
|
||||
type TextUnmarshalerPointer struct{}
|
||||
|
||||
func (*TextUnmarshalerPointer) UnmarshalText([]byte) error { return nil }
|
||||
|
||||
type Struct[F any] struct {
|
||||
Field F
|
||||
}
|
||||
|
||||
type StructCBORMarshalerValue[F any] struct {
|
||||
Field F
|
||||
}
|
||||
|
||||
func (StructCBORMarshalerValue[F]) MarshalCBOR() ([]byte, error) { return nil, nil }
|
||||
|
||||
type StructCBORMarshalerPointer[F any] struct {
|
||||
Field F
|
||||
}
|
||||
|
||||
func (*StructCBORMarshalerPointer[F]) MarshalCBOR() ([]byte, error) { return nil, nil }
|
||||
|
||||
type StructCBORUnmarshalerPointer[F any] struct {
|
||||
Field F
|
||||
}
|
||||
|
||||
func (*StructCBORUnmarshalerPointer[F]) UnmarshalCBOR([]byte) error { return nil }
|
||||
|
||||
type StructCBORUnmarshalerValueWithEmbeddedJSONMarshaler struct {
|
||||
json.Marshaler
|
||||
}
|
||||
|
||||
func (StructCBORUnmarshalerValueWithEmbeddedJSONMarshaler) MarshalCBOR() ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type SafeCyclicTypeA struct {
|
||||
Bs []SafeCyclicTypeB
|
||||
}
|
||||
|
||||
type SafeCyclicTypeB struct {
|
||||
As []SafeCyclicTypeA
|
||||
}
|
||||
|
||||
type UnsafeCyclicTypeA struct {
|
||||
Bs []UnsafeCyclicTypeB
|
||||
}
|
||||
|
||||
type UnsafeCyclicTypeB struct {
|
||||
As []UnsafeCyclicTypeA
|
||||
|
||||
json.Marshaler
|
||||
}
|
||||
|
||||
func TestCheckUnsupportedMarshalers(t *testing.T) {
|
||||
t.Run("accepted", func(t *testing.T) {
|
||||
for _, v := range []interface{}{
|
||||
// Unstructured types.
|
||||
nil,
|
||||
false,
|
||||
0,
|
||||
0.0,
|
||||
"",
|
||||
[]interface{}{nil, false, 0, 0.0, "", []interface{}{}, map[string]interface{}{}},
|
||||
map[string]interface{}{
|
||||
"nil": nil,
|
||||
"false": false,
|
||||
"0": 0,
|
||||
"0.0": 0.0,
|
||||
`""`: "",
|
||||
"[]interface{}{}": []interface{}{},
|
||||
"map[string]interface{}{}": map[string]interface{}{},
|
||||
},
|
||||
// Zero-length array is statically OK, even though its element type is
|
||||
// json.Marshaler.
|
||||
[0]json.Marshaler{},
|
||||
[3]bool{},
|
||||
// Has to be dynamically checked.
|
||||
[1]interface{}{},
|
||||
map[int]interface{}{0: nil},
|
||||
Struct[string]{},
|
||||
StructCBORMarshalerValue[json.Marshaler]{},
|
||||
&StructCBORMarshalerValue[json.Marshaler]{},
|
||||
&StructCBORMarshalerPointer[json.Marshaler]{},
|
||||
new(string),
|
||||
map[cbor.Marshaler]cbor.Marshaler{},
|
||||
make([]string, 10),
|
||||
[]Struct[interface{}]{{Field: true}},
|
||||
&Struct[StructCBORUnmarshalerPointer[json.Unmarshaler]]{},
|
||||
// Checked dynamically because the dynamic type of an interface, even
|
||||
// encoding.TextMarshaler or json.Marshaler, might also implement
|
||||
// cbor.Marshaler. Accepted because there are no map entries.
|
||||
map[encoding.TextMarshaler]struct{}{},
|
||||
map[string]json.Marshaler{},
|
||||
// Methods of json.Marshaler and cbor.Marshaler are both promoted to the
|
||||
// struct's method set.
|
||||
struct {
|
||||
json.Marshaler
|
||||
StructCBORMarshalerValue[int]
|
||||
}{},
|
||||
|
||||
// Embedded field's json.Marshaler implementation is promoted, but the
|
||||
// containing struct implements cbor.Marshaler.
|
||||
StructCBORUnmarshalerValueWithEmbeddedJSONMarshaler{},
|
||||
|
||||
SafeCyclicTypeA{},
|
||||
SafeCyclicTypeB{},
|
||||
} {
|
||||
if err := RejectCustomMarshalers(v); err != nil {
|
||||
t.Errorf("%#v: unexpected non-nil error: %v", v, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("rejected", func(t *testing.T) {
|
||||
for _, v := range []interface{}{
|
||||
Struct[json.Marshaler]{},
|
||||
StructCBORMarshalerValue[json.Unmarshaler]{},
|
||||
Struct[interface{}]{Field: [1]json.Unmarshaler{}},
|
||||
// cbor.Marshaler implemented with pointer receiver on non-pointer type.
|
||||
StructCBORMarshalerPointer[json.Marshaler]{},
|
||||
[1]json.Marshaler{},
|
||||
[1]interface{}{[1]json.Marshaler{}},
|
||||
map[int]interface{}{0: [1]json.Marshaler{}},
|
||||
[]interface{}{[1]json.Marshaler{}},
|
||||
map[string]interface{}{"": [1]json.Marshaler{}},
|
||||
[]Struct[interface{}]{{Field: [1]json.Marshaler{}}},
|
||||
map[encoding.TextMarshaler]struct{}{[1]encoding.TextMarshaler{}[0]: {}},
|
||||
map[string]json.Marshaler{"": [1]json.Marshaler{}[0]},
|
||||
&Struct[StructCBORMarshalerPointer[json.Marshaler]]{},
|
||||
Struct[TextUnmarshalerPointer]{},
|
||||
struct {
|
||||
json.Marshaler
|
||||
}{},
|
||||
UnsafeCyclicTypeA{Bs: []UnsafeCyclicTypeB{{}}},
|
||||
UnsafeCyclicTypeB{},
|
||||
} {
|
||||
if err := RejectCustomMarshalers(v); err == nil {
|
||||
t.Errorf("%#v: unexpected nil error", v)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// With -test.race, retrieves a custom marshaler checker for a cyclic type concurrently from many
|
||||
// goroutines to root out data races in checker lazy initialization.
|
||||
func TestLazyCheckerInitializationDataRace(t *testing.T) {
|
||||
cache := checkers{
|
||||
cborInterface: reflect.TypeFor[cbor.Marshaler](),
|
||||
nonCBORInterfaces: []reflect.Type{
|
||||
reflect.TypeFor[json.Marshaler](),
|
||||
reflect.TypeFor[encoding.TextMarshaler](),
|
||||
},
|
||||
}
|
||||
|
||||
rt := reflect.TypeFor[SafeCyclicTypeA]()
|
||||
|
||||
begin := make(chan struct{})
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for range 32 {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
<-begin
|
||||
cache.getChecker(rt)
|
||||
}()
|
||||
}
|
||||
|
||||
close(begin)
|
||||
wg.Wait()
|
||||
}
|
|
@ -44,7 +44,10 @@ var simpleValues *cbor.SimpleValueRegistry = func() *cbor.SimpleValueRegistry {
|
|||
return simpleValues
|
||||
}()
|
||||
|
||||
var Decode cbor.DecMode = func() cbor.DecMode {
|
||||
// decode is the basis for the Decode mode, with no JSONUnmarshalerTranscoder
|
||||
// configured. TranscodeToJSON uses this directly rather than Decode to avoid an initialization
|
||||
// cycle between the two. Everything else should use one of the exported DecModes.
|
||||
var decode cbor.DecMode = func() cbor.DecMode {
|
||||
decode, err := cbor.DecOptions{
|
||||
// Maps with duplicate keys are well-formed but invalid according to the CBOR spec
|
||||
// and never acceptable. Unlike the JSON serializer, inputs containing duplicate map
|
||||
|
@ -139,6 +142,10 @@ var Decode cbor.DecMode = func() cbor.DecMode {
|
|||
// Disable default recognition of types implementing encoding.BinaryUnmarshaler,
|
||||
// which is not recognized for JSON decoding.
|
||||
BinaryUnmarshaler: cbor.BinaryUnmarshalerNone,
|
||||
|
||||
// Marshal types that implement encoding.TextMarshaler by calling their MarshalText
|
||||
// method and encoding the result to a CBOR text string.
|
||||
TextUnmarshaler: cbor.TextUnmarshalerTextString,
|
||||
}.DecMode()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
@ -146,6 +153,19 @@ var Decode cbor.DecMode = func() cbor.DecMode {
|
|||
return decode
|
||||
}()
|
||||
|
||||
var Decode cbor.DecMode = func() cbor.DecMode {
|
||||
opts := decode.DecOptions()
|
||||
// When decoding into a value of a type that implements json.Unmarshaler (and does not
|
||||
// implement cbor.Unmarshaler), transcode the input to JSON and pass it to the value's
|
||||
// UnmarshalJSON method.
|
||||
opts.JSONUnmarshalerTranscoder = TranscodeFunc(TranscodeToJSON)
|
||||
dm, err := opts.DecMode()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return dm
|
||||
}()
|
||||
|
||||
// DecodeLax is derived from Decode, but does not complain about unknown fields in the input.
|
||||
var DecodeLax cbor.DecMode = func() cbor.DecMode {
|
||||
opts := Decode.DecOptions()
|
||||
|
|
|
@ -211,6 +211,20 @@ func TestDecode(t *testing.T) {
|
|||
}
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "text unmarshaler",
|
||||
in: hex("4161"),
|
||||
into: &RoundtrippableText{},
|
||||
want: &RoundtrippableText{Text: "a"},
|
||||
assertOnError: assertNilError,
|
||||
},
|
||||
{
|
||||
name: "json unmarshaler",
|
||||
in: hex("4161"),
|
||||
into: &RoundtrippableJSON{},
|
||||
want: &RoundtrippableJSON{Raw: `"a"`},
|
||||
assertOnError: assertNilError,
|
||||
},
|
||||
})
|
||||
|
||||
group(t, "text string", []test{
|
||||
|
@ -236,6 +250,20 @@ func TestDecode(t *testing.T) {
|
|||
want: "",
|
||||
assertOnError: assertNilError,
|
||||
},
|
||||
{
|
||||
name: "text unmarshaler",
|
||||
in: hex("6161"),
|
||||
into: &RoundtrippableText{},
|
||||
want: &RoundtrippableText{Text: "a"},
|
||||
assertOnError: assertNilError,
|
||||
},
|
||||
{
|
||||
name: "json unmarshaler",
|
||||
in: hex("6161"),
|
||||
into: &RoundtrippableJSON{},
|
||||
want: &RoundtrippableJSON{Raw: `"a"`},
|
||||
assertOnError: assertNilError,
|
||||
},
|
||||
})
|
||||
|
||||
group(t, "array", []test{
|
||||
|
|
|
@ -22,7 +22,10 @@ import (
|
|||
"github.com/fxamacker/cbor/v2"
|
||||
)
|
||||
|
||||
var Encode = EncMode{
|
||||
// encode is the basis for the Encode mode, with no JSONMarshalerTranscoder
|
||||
// configured. TranscodeFromJSON uses this directly rather than Encode to avoid an initialization
|
||||
// cycle between the two. Everything else should use one of the exported EncModes.
|
||||
var encode = EncMode{
|
||||
delegate: func() cbor.UserBufferEncMode {
|
||||
encode, err := cbor.EncOptions{
|
||||
// Map keys need to be sorted to have deterministic output, and this is the order
|
||||
|
@ -94,6 +97,10 @@ var Encode = EncMode{
|
|||
// Disable default recognition of types implementing encoding.BinaryMarshaler, which
|
||||
// is not recognized for JSON encoding.
|
||||
BinaryMarshaler: cbor.BinaryMarshalerNone,
|
||||
|
||||
// Unmarshal into types that implement encoding.TextUnmarshaler by passing
|
||||
// the contents of a CBOR string to their UnmarshalText method.
|
||||
TextMarshaler: cbor.TextMarshalerTextString,
|
||||
}.UserBufferEncMode()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
@ -102,6 +109,21 @@ var Encode = EncMode{
|
|||
}(),
|
||||
}
|
||||
|
||||
var Encode = EncMode{
|
||||
delegate: func() cbor.UserBufferEncMode {
|
||||
opts := encode.options()
|
||||
// To encode a value of a type that implements json.Marshaler (and does not
|
||||
// implement cbor.Marshaler), transcode the result of calling its MarshalJSON method
|
||||
// directly to CBOR.
|
||||
opts.JSONMarshalerTranscoder = TranscodeFunc(TranscodeFromJSON)
|
||||
em, err := opts.UserBufferEncMode()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return em
|
||||
}(),
|
||||
}
|
||||
|
||||
var EncodeNondeterministic = EncMode{
|
||||
delegate: func() cbor.UserBufferEncMode {
|
||||
opts := Encode.options()
|
||||
|
|
|
@ -96,6 +96,18 @@ func TestEncode(t *testing.T) {
|
|||
want: []byte{0xd6, 0x45, 'h', 'e', 'l', 'l', 'o'},
|
||||
assertOnError: assertNilError,
|
||||
},
|
||||
{
|
||||
name: "text marshaler",
|
||||
in: &RoundtrippableText{Text: "a"},
|
||||
want: []byte{0x61, 0x61},
|
||||
assertOnError: assertNilError,
|
||||
},
|
||||
{
|
||||
name: "json marshaler",
|
||||
in: &RoundtrippableJSON{Raw: `"a"`},
|
||||
want: []byte{0x41, 0x61},
|
||||
assertOnError: assertNilError,
|
||||
},
|
||||
} {
|
||||
encModes := tc.modes
|
||||
if len(encModes) == 0 {
|
||||
|
|
|
@ -34,6 +34,28 @@ func nilPointerFor[T interface{}]() *T {
|
|||
return nil
|
||||
}
|
||||
|
||||
type RoundtrippableText struct{ Text string }
|
||||
|
||||
func (rt RoundtrippableText) MarshalText() ([]byte, error) {
|
||||
return []byte(rt.Text), nil
|
||||
}
|
||||
|
||||
func (rt *RoundtrippableText) UnmarshalText(text []byte) error {
|
||||
rt.Text = string(text)
|
||||
return nil
|
||||
}
|
||||
|
||||
type RoundtrippableJSON struct{ Raw string }
|
||||
|
||||
func (rj RoundtrippableJSON) MarshalJSON() ([]byte, error) {
|
||||
return []byte(rj.Raw), nil
|
||||
}
|
||||
|
||||
func (rj *RoundtrippableJSON) UnmarshalJSON(raw []byte) error {
|
||||
rj.Raw = string(raw)
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestRoundtrip roundtrips object serialization to interface{} and back via CBOR.
|
||||
func TestRoundtrip(t *testing.T) {
|
||||
type modePair struct {
|
||||
|
@ -264,6 +286,14 @@ func TestRoundtrip(t *testing.T) {
|
|||
V *map[string]interface{} `json:"v,omitempty"`
|
||||
}{},
|
||||
},
|
||||
{
|
||||
name: "textmarshaler and textunmarshaler",
|
||||
obj: RoundtrippableText{Text: "foo"},
|
||||
},
|
||||
{
|
||||
name: "json marshaler and unmarshaler",
|
||||
obj: RoundtrippableJSON{Raw: `{"foo":[42,3.1,true,false,null]}`},
|
||||
},
|
||||
} {
|
||||
modePairs := tc.modePairs
|
||||
if len(modePairs) == 0 {
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
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 modes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
kjson "sigs.k8s.io/json"
|
||||
)
|
||||
|
||||
type TranscodeFunc func(dst io.Writer, src io.Reader) error
|
||||
|
||||
func (f TranscodeFunc) Transcode(dst io.Writer, src io.Reader) error {
|
||||
return f(dst, src)
|
||||
}
|
||||
|
||||
func TranscodeFromJSON(dst io.Writer, src io.Reader) error {
|
||||
var tmp any
|
||||
dec := kjson.NewDecoderCaseSensitivePreserveInts(src)
|
||||
if err := dec.Decode(&tmp); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := dec.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
|
||||
return errors.New("extraneous data")
|
||||
}
|
||||
|
||||
return encode.MarshalTo(tmp, dst)
|
||||
}
|
||||
|
||||
func TranscodeToJSON(dst io.Writer, src io.Reader) error {
|
||||
var tmp any
|
||||
dec := decode.NewDecoder(src)
|
||||
if err := dec.Decode(&tmp); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := dec.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
|
||||
return errors.New("extraneous data")
|
||||
}
|
||||
|
||||
// Use an Encoder to avoid the extra []byte allocated by Marshal. Encode, unlike Marshal,
|
||||
// appends a trailing newline to separate consecutive encodings of JSON values that aren't
|
||||
// self-delimiting, like numbers. Strip the newline to avoid the assumption that every
|
||||
// json.Unmarshaler implementation will accept trailing whitespace.
|
||||
enc := json.NewEncoder(&trailingLinefeedSuppressor{delegate: dst})
|
||||
enc.SetIndent("", "")
|
||||
return enc.Encode(tmp)
|
||||
}
|
||||
|
||||
// trailingLinefeedSuppressor is an io.Writer that wraps another io.Writer, suppressing a single
|
||||
// trailing linefeed if it is the last byte written by the latest call to Write.
|
||||
type trailingLinefeedSuppressor struct {
|
||||
lf bool
|
||||
delegate io.Writer
|
||||
}
|
||||
|
||||
func (w *trailingLinefeedSuppressor) Write(p []byte) (int, error) {
|
||||
if len(p) == 0 {
|
||||
// Avoid flushing a buffered linefeeds on an empty write.
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
if w.lf {
|
||||
// The previous write had a trailing linefeed that was buffered. That wasn't the
|
||||
// last Write call, so flush the buffered linefeed before continuing.
|
||||
n, err := w.delegate.Write([]byte{'\n'})
|
||||
if n > 0 {
|
||||
w.lf = false
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
if p[len(p)-1] != '\n' {
|
||||
return w.delegate.Write(p)
|
||||
}
|
||||
|
||||
p = p[:len(p)-1]
|
||||
|
||||
if len(p) == 0 { // []byte{'\n'}
|
||||
w.lf = true
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
n, err := w.delegate.Write(p)
|
||||
if n == len(p) {
|
||||
// Everything up to the trailing linefeed has been flushed. Eat the linefeed.
|
||||
w.lf = true
|
||||
n++
|
||||
}
|
||||
return n, err
|
||||
}
|
|
@ -0,0 +1,291 @@
|
|||
/*
|
||||
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 modes
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTranscodeFromJSON(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
json string
|
||||
cbor string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "whole number",
|
||||
json: "42",
|
||||
cbor: "\x18\x2a",
|
||||
},
|
||||
{
|
||||
name: "decimal number",
|
||||
json: "1.5",
|
||||
cbor: "\xf9\x3e\x00",
|
||||
},
|
||||
{
|
||||
name: "false",
|
||||
json: "false",
|
||||
cbor: "\xf4",
|
||||
},
|
||||
{
|
||||
name: "true",
|
||||
json: "true",
|
||||
cbor: "\xf5",
|
||||
},
|
||||
{
|
||||
name: "null",
|
||||
json: "null",
|
||||
cbor: "\xf6",
|
||||
},
|
||||
{
|
||||
name: "string",
|
||||
json: `"foo"`,
|
||||
cbor: "\x43foo",
|
||||
},
|
||||
{
|
||||
name: "array",
|
||||
json: `[]`,
|
||||
cbor: "\x80",
|
||||
},
|
||||
{
|
||||
name: "object",
|
||||
json: `{"foo":"bar"}`,
|
||||
cbor: "\xa1\x43foo\x43bar",
|
||||
},
|
||||
{
|
||||
name: "extraneous data",
|
||||
json: "{}{}",
|
||||
err: errors.New("extraneous data"),
|
||||
},
|
||||
{
|
||||
name: "eof",
|
||||
json: "",
|
||||
err: errors.New("EOF"),
|
||||
},
|
||||
{
|
||||
name: "unexpected eof",
|
||||
json: "{",
|
||||
err: errors.New("unexpected EOF"),
|
||||
},
|
||||
{
|
||||
name: "malformed json",
|
||||
json: "}",
|
||||
err: errors.New("invalid character '}' looking for beginning of value"),
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var out strings.Builder
|
||||
err := TranscodeFromJSON(&out, strings.NewReader(tc.json))
|
||||
if (err == nil) != (tc.err == nil) || tc.err != nil && tc.err.Error() != err.Error() {
|
||||
t.Fatalf("unexpected error: want %v got %v", tc.err, err)
|
||||
}
|
||||
if got, want := out.String(), tc.cbor; got != want {
|
||||
t.Errorf("unexpected transcoding: want 0x%x got 0x%x", want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTranscodeToJSON(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
cbor string
|
||||
json string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "whole number",
|
||||
cbor: "\x18\x2a",
|
||||
json: "42",
|
||||
},
|
||||
{
|
||||
name: "decimal number",
|
||||
cbor: "\xf9\x3e\x00",
|
||||
json: "1.5",
|
||||
},
|
||||
{
|
||||
name: "false",
|
||||
cbor: "\xf4",
|
||||
json: "false",
|
||||
},
|
||||
{
|
||||
name: "true",
|
||||
cbor: "\xf5",
|
||||
json: "true",
|
||||
},
|
||||
{
|
||||
name: "null",
|
||||
cbor: "\xf6",
|
||||
json: "null",
|
||||
},
|
||||
{
|
||||
name: "string",
|
||||
cbor: "\x43foo",
|
||||
json: `"foo"`,
|
||||
},
|
||||
{
|
||||
name: "array",
|
||||
cbor: "\x80",
|
||||
json: `[]`,
|
||||
},
|
||||
{
|
||||
name: "object",
|
||||
cbor: "\xa1\x43foo\x43bar",
|
||||
json: `{"foo":"bar"}`,
|
||||
},
|
||||
{
|
||||
name: "extraneous data",
|
||||
cbor: "\xa0\xa0",
|
||||
err: errors.New("extraneous data"),
|
||||
},
|
||||
{
|
||||
name: "unexpected eof",
|
||||
cbor: "\xa1",
|
||||
err: errors.New("unexpected EOF"),
|
||||
},
|
||||
{
|
||||
name: "malformed cbor",
|
||||
cbor: "\xff",
|
||||
err: errors.New(`cbor: unexpected "break" code`),
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var out strings.Builder
|
||||
err := TranscodeToJSON(&out, strings.NewReader(tc.cbor))
|
||||
if (err == nil) != (tc.err == nil) || tc.err != nil && tc.err.Error() != err.Error() {
|
||||
t.Fatalf("unexpected error: want %v got %v", tc.err, err)
|
||||
}
|
||||
if got, want := out.String(), tc.json; got != want {
|
||||
t.Errorf("unexpected transcoding: want %q got %q", want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type write struct {
|
||||
p string
|
||||
n int
|
||||
err error
|
||||
}
|
||||
|
||||
type mockWriter struct {
|
||||
t testing.TB
|
||||
calls []write
|
||||
}
|
||||
|
||||
func (m *mockWriter) Write(p []byte) (int, error) {
|
||||
if len(m.calls) == 0 {
|
||||
m.t.Fatalf("unexpected call (p=%q)", string(p))
|
||||
}
|
||||
if got, want := string(p), m.calls[0].p; got != want {
|
||||
m.t.Errorf("unexpected argument: want %q, got %q", want, got)
|
||||
}
|
||||
n, err := m.calls[0].n, m.calls[0].err
|
||||
m.calls = m.calls[1:]
|
||||
return n, err
|
||||
}
|
||||
|
||||
func TestTrailingLinefeedSuppressor(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
calls []write
|
||||
delegated []write
|
||||
}{
|
||||
{
|
||||
name: "one write without newline",
|
||||
calls: []write{{"foo", 3, nil}},
|
||||
delegated: []write{{"foo", 3, nil}},
|
||||
},
|
||||
{
|
||||
name: "one write with newline",
|
||||
calls: []write{{"foo\n", 4, nil}},
|
||||
delegated: []write{{"foo", 3, nil}},
|
||||
},
|
||||
{
|
||||
name: "one write with only newline",
|
||||
calls: []write{{"\n", 1, nil}},
|
||||
delegated: nil,
|
||||
},
|
||||
{
|
||||
name: "one empty write",
|
||||
calls: []write{{"", 0, nil}},
|
||||
delegated: nil,
|
||||
},
|
||||
{
|
||||
name: "three writes, all with only newline",
|
||||
calls: []write{{"\n", 1, nil}, {"\n", 1, nil}, {"\n", 1, nil}},
|
||||
delegated: []write{{"\n", 1, nil}, {"\n", 1, nil}},
|
||||
},
|
||||
{
|
||||
name: "buffered linefeed not flushed on empty write",
|
||||
calls: []write{{"\n", 1, nil}, {"", 0, nil}},
|
||||
delegated: nil,
|
||||
},
|
||||
|
||||
{
|
||||
name: "two writes, last with trailing newline",
|
||||
calls: []write{{"foo", 3, nil}, {"bar\n", 4, nil}},
|
||||
delegated: []write{{"foo", 3, nil}, {"bar", 3, nil}},
|
||||
},
|
||||
{
|
||||
name: "two writes, first with trailing newline",
|
||||
calls: []write{{"foo\n", 4, nil}, {"bar", 3, nil}},
|
||||
delegated: []write{{"foo", 3, nil}, {"\n", 1, nil}, {"bar", 3, nil}},
|
||||
},
|
||||
{
|
||||
name: "two writes, both with trailing newlines",
|
||||
calls: []write{{"foo\n", 4, nil}, {"bar\n", 4, nil}},
|
||||
delegated: []write{{"foo", 3, nil}, {"\n", 1, nil}, {"bar", 3, nil}},
|
||||
},
|
||||
{
|
||||
name: "two writes, neither with newlines",
|
||||
calls: []write{{"foo", 3, nil}, {"bar", 3, nil}},
|
||||
delegated: []write{{"foo", 3, nil}, {"bar", 3, nil}},
|
||||
},
|
||||
{
|
||||
name: "delegate error before reaching newline",
|
||||
calls: []write{{"foo\n", 1, errors.New("test")}, {"oo\n", 3, nil}},
|
||||
delegated: []write{{"foo", 1, errors.New("test")}, {"oo", 2, nil}},
|
||||
},
|
||||
{
|
||||
name: "delegate error after reaching newline",
|
||||
calls: []write{{"foo\n", 4, errors.New("test")}},
|
||||
delegated: []write{{"foo", 3, errors.New("test")}},
|
||||
},
|
||||
{
|
||||
name: "delegate error flushing newline",
|
||||
calls: []write{{"foo\n", 4, nil}, {"\n", 0, errors.New("test")}, {"\n", 1, nil}},
|
||||
delegated: []write{{"foo", 3, nil}, {"\n", 0, errors.New("test")}, {"\n", 1, nil}},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
w := &trailingLinefeedSuppressor{delegate: &mockWriter{t: t, calls: tc.delegated}}
|
||||
for _, call := range tc.calls {
|
||||
n, err := w.Write([]byte(call.p))
|
||||
if n != call.n {
|
||||
t.Errorf("unexpected n: want %d got %d", call.n, n)
|
||||
}
|
||||
if (err == nil) != (call.err == nil) || call.err != nil && call.err.Error() != err.Error() {
|
||||
t.Errorf("unexpected error: want %v got %v", call.err, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -23,11 +23,12 @@ import (
|
|||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"sigs.k8s.io/randfill"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
testapigroupv1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/randfill"
|
||||
)
|
||||
|
||||
func TestCollectionsEncoding(t *testing.T) {
|
||||
|
@ -63,7 +64,7 @@ func testCollectionsEncoding(t *testing.T, s *Serializer, streamingEnabled bool)
|
|||
},
|
||||
},
|
||||
},
|
||||
expect: "{\"metadata\":{},\"items\":[{\"metadata\":{\"creationTimestamp\":null},\"Int\":1,\"Float32\":1,\"Float64\":1.1}]}\n",
|
||||
expect: "{\"metadata\":{},\"items\":[{\"metadata\":{},\"Int\":1,\"Float32\":1,\"Float64\":1.1}]}\n",
|
||||
},
|
||||
{
|
||||
name: "Unstructured object float",
|
||||
|
@ -102,7 +103,7 @@ func testCollectionsEncoding(t *testing.T, s *Serializer, streamingEnabled bool)
|
|||
},
|
||||
},
|
||||
},
|
||||
expect: "{\"metadata\":{},\"items\":[{\"metadata\":{\"creationTimestamp\":null}}]}\n",
|
||||
expect: "{\"metadata\":{},\"items\":[{\"metadata\":{}}]}\n",
|
||||
},
|
||||
// Encoding Go strings containing invalid UTF-8 sequences without error
|
||||
{
|
||||
|
@ -146,7 +147,7 @@ func testCollectionsEncoding(t *testing.T, s *Serializer, streamingEnabled bool)
|
|||
},
|
||||
},
|
||||
},
|
||||
expect: "{\"metadata\":{},\"items\":[{\"metadata\":{\"creationTimestamp\":null},\"spec\":{},\"status\":{}}]}\n",
|
||||
expect: "{\"metadata\":{},\"items\":[{\"metadata\":{},\"spec\":{},\"status\":{}}]}\n",
|
||||
},
|
||||
{
|
||||
name: "CarpList map nil",
|
||||
|
@ -159,7 +160,7 @@ func testCollectionsEncoding(t *testing.T, s *Serializer, streamingEnabled bool)
|
|||
},
|
||||
},
|
||||
},
|
||||
expect: "{\"metadata\":{},\"items\":[{\"metadata\":{\"creationTimestamp\":null},\"spec\":{},\"status\":{}}]}\n",
|
||||
expect: "{\"metadata\":{},\"items\":[{\"metadata\":{},\"spec\":{},\"status\":{}}]}\n",
|
||||
},
|
||||
{
|
||||
name: "UnstructuredList items nil",
|
||||
|
@ -237,7 +238,7 @@ func testCollectionsEncoding(t *testing.T, s *Serializer, streamingEnabled bool)
|
|||
},
|
||||
},
|
||||
},
|
||||
expect: "{\"metadata\":{},\"items\":[{\"metadata\":{\"creationTimestamp\":null},\"spec\":{},\"status\":{}}]}\n",
|
||||
expect: "{\"metadata\":{},\"items\":[{\"metadata\":{},\"spec\":{},\"status\":{}}]}\n",
|
||||
},
|
||||
{
|
||||
name: "CarpList map empty",
|
||||
|
@ -250,7 +251,7 @@ func testCollectionsEncoding(t *testing.T, s *Serializer, streamingEnabled bool)
|
|||
},
|
||||
},
|
||||
},
|
||||
expect: "{\"metadata\":{},\"items\":[{\"metadata\":{\"creationTimestamp\":null},\"spec\":{},\"status\":{}}]}\n",
|
||||
expect: "{\"metadata\":{},\"items\":[{\"metadata\":{},\"spec\":{},\"status\":{}}]}\n",
|
||||
},
|
||||
{
|
||||
name: "UnstructuredList items empty",
|
||||
|
@ -337,7 +338,7 @@ func testCollectionsEncoding(t *testing.T, s *Serializer, streamingEnabled bool)
|
|||
},
|
||||
},
|
||||
},
|
||||
expect: "{\"metadata\":{},\"items\":[{\"metadata\":{\"creationTimestamp\":null},\"Slice\":\"AQID\",\"Array\":[1,2,3]}]}\n",
|
||||
expect: "{\"metadata\":{},\"items\":[{\"metadata\":{},\"Slice\":\"AQID\",\"Array\":[1,2,3]}]}\n",
|
||||
},
|
||||
{
|
||||
name: "UnstructuredList object raw bytes",
|
||||
|
@ -415,7 +416,7 @@ func testCollectionsEncoding(t *testing.T, s *Serializer, streamingEnabled bool)
|
|||
}},
|
||||
},
|
||||
},
|
||||
expect: "{\"kind\":\"List\",\"apiVersion\":\"v1\",\"metadata\":{\"resourceVersion\":\"2345\",\"continue\":\"abc\",\"remainingItemCount\":1},\"items\":[{\"kind\":\"Carp\",\"apiVersion\":\"v1\",\"metadata\":{\"name\":\"pod\",\"namespace\":\"default\",\"creationTimestamp\":null},\"spec\":{},\"status\":{}}]}\n",
|
||||
expect: "{\"kind\":\"List\",\"apiVersion\":\"v1\",\"metadata\":{\"resourceVersion\":\"2345\",\"continue\":\"abc\",\"remainingItemCount\":1},\"items\":[{\"kind\":\"Carp\",\"apiVersion\":\"v1\",\"metadata\":{\"name\":\"pod\",\"namespace\":\"default\"},\"spec\":{},\"status\":{}}]}\n",
|
||||
},
|
||||
{
|
||||
name: "List two elements",
|
||||
|
@ -438,7 +439,7 @@ func testCollectionsEncoding(t *testing.T, s *Serializer, streamingEnabled bool)
|
|||
}},
|
||||
},
|
||||
},
|
||||
expect: `{"kind":"List","apiVersion":"v1","metadata":{"resourceVersion":"2345"},"items":[{"kind":"Carp","apiVersion":"v1","metadata":{"name":"pod","namespace":"default","creationTimestamp":null},"spec":{},"status":{}},{"kind":"Carp","apiVersion":"v1","metadata":{"name":"pod2","namespace":"default2","creationTimestamp":null},"spec":{},"status":{}}]}
|
||||
expect: `{"kind":"List","apiVersion":"v1","metadata":{"resourceVersion":"2345"},"items":[{"kind":"Carp","apiVersion":"v1","metadata":{"name":"pod","namespace":"default"},"spec":{},"status":{}},{"kind":"Carp","apiVersion":"v1","metadata":{"name":"pod2","namespace":"default2"},"spec":{},"status":{}}]}
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
@ -465,7 +466,7 @@ func testCollectionsEncoding(t *testing.T, s *Serializer, streamingEnabled bool)
|
|||
},
|
||||
},
|
||||
cannotStream: true,
|
||||
expect: "{\"kind\":\"List\",\"apiVersion\":\"v1\",\"metadata\":{\"creationTimestamp\":null},\"spec\":{},\"status\":{}}\n",
|
||||
expect: "{\"kind\":\"List\",\"apiVersion\":\"v1\",\"metadata\":{},\"spec\":{},\"status\":{}}\n",
|
||||
},
|
||||
{
|
||||
name: "UnstructuredList empty",
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// +k8s:deepcopy-gen=package
|
||||
|
||||
package v1
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
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 v1
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type ExternalSimple struct {
|
||||
runtime.TypeMeta `json:",inline"`
|
||||
TestString string `json:"testString"`
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
//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 deepcopy-gen. DO NOT EDIT.
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ExternalSimple) DeepCopyInto(out *ExternalSimple) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalSimple.
|
||||
func (in *ExternalSimple) DeepCopy() *ExternalSimple {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ExternalSimple)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *ExternalSimple) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -21,7 +21,6 @@ import (
|
|||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
|
@ -38,26 +37,25 @@ type VersionValidationRunner func(t *testing.T, gv string, versionValidationErro
|
|||
// errs = append(errs, versionValidationErrors...) // generated declarative validation
|
||||
// // Validate that the errors are what was expected for this test case.
|
||||
// })
|
||||
func RunValidationForEachVersion(t *testing.T, scheme *runtime.Scheme, options sets.Set[string], unversioned runtime.Object, fn VersionValidationRunner) {
|
||||
runValidation(t, scheme, options, unversioned, fn)
|
||||
func RunValidationForEachVersion(t *testing.T, scheme *runtime.Scheme, options []string, unversioned runtime.Object, fn VersionValidationRunner, subresources ...string) {
|
||||
runValidation(t, scheme, options, unversioned, fn, subresources...)
|
||||
}
|
||||
|
||||
// RunUpdateValidationForEachVersion is like RunValidationForEachVersion but for update validation.
|
||||
func RunUpdateValidationForEachVersion(t *testing.T, scheme *runtime.Scheme, options sets.Set[string], unversioned, unversionedOld runtime.Object, fn VersionValidationRunner) {
|
||||
runUpdateValidation(t, scheme, options, unversioned, unversionedOld, fn)
|
||||
func RunUpdateValidationForEachVersion(t *testing.T, scheme *runtime.Scheme, options []string, unversioned, unversionedOld runtime.Object, fn VersionValidationRunner, subresources ...string) {
|
||||
runUpdateValidation(t, scheme, options, unversioned, unversionedOld, fn, subresources...)
|
||||
}
|
||||
|
||||
// RunStatusValidationForEachVersion is like RunUpdateValidationForEachVersion but for status validation.
|
||||
func RunStatusValidationForEachVersion(t *testing.T, scheme *runtime.Scheme, options sets.Set[string], unversioned, unversionedOld runtime.Object, fn VersionValidationRunner) {
|
||||
runUpdateValidation(t, scheme, options, unversioned, unversionedOld, fn, "status")
|
||||
}
|
||||
|
||||
func runValidation(t *testing.T, scheme *runtime.Scheme, options sets.Set[string], unversioned runtime.Object, fn VersionValidationRunner, subresources ...string) {
|
||||
func runValidation(t *testing.T, scheme *runtime.Scheme, options []string, unversioned runtime.Object, fn VersionValidationRunner, subresources ...string) {
|
||||
unversionedGVKs, _, err := scheme.ObjectKinds(unversioned)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, unversionedGVK := range unversionedGVKs {
|
||||
// skip if passed in unversioned object is not internal.
|
||||
if unversionedGVK.Version != runtime.APIVersionInternal {
|
||||
continue
|
||||
}
|
||||
gvs := scheme.VersionsForGroupKind(unversionedGVK.GroupKind())
|
||||
for _, gv := range gvs {
|
||||
gvk := gv.WithKind(unversionedGVK.Kind)
|
||||
|
@ -78,12 +76,16 @@ func runValidation(t *testing.T, scheme *runtime.Scheme, options sets.Set[string
|
|||
}
|
||||
}
|
||||
|
||||
func runUpdateValidation(t *testing.T, scheme *runtime.Scheme, options sets.Set[string], unversionedNew, unversionedOld runtime.Object, fn VersionValidationRunner, subresources ...string) {
|
||||
func runUpdateValidation(t *testing.T, scheme *runtime.Scheme, options []string, unversionedNew, unversionedOld runtime.Object, fn VersionValidationRunner, subresources ...string) {
|
||||
unversionedGVKs, _, err := scheme.ObjectKinds(unversionedNew)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, unversionedGVK := range unversionedGVKs {
|
||||
// skip if passed in unversioned object is not internal.
|
||||
if unversionedGVK.Version != runtime.APIVersionInternal {
|
||||
continue
|
||||
}
|
||||
gvs := scheme.VersionsForGroupKind(unversionedGVK.GroupKind())
|
||||
for _, gv := range gvs {
|
||||
gvk := gv.WithKind(unversionedGVK.Kind)
|
||||
|
|
|
@ -147,7 +147,7 @@ func TestGenericTypeMeta(t *testing.T) {
|
|||
Name string `json:"name,omitempty"`
|
||||
GenerateName string `json:"generateName,omitempty"`
|
||||
UID string `json:"uid,omitempty"`
|
||||
CreationTimestamp metav1.Time `json:"creationTimestamp,omitempty"`
|
||||
CreationTimestamp metav1.Time `json:"creationTimestamp,omitempty,omitzero"`
|
||||
SelfLink string `json:"selfLink,omitempty"`
|
||||
ResourceVersion string `json:"resourceVersion,omitempty"`
|
||||
APIVersion string `json:"apiVersion,omitempty"`
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
//go:build usegocmp
|
||||
// +build usegocmp
|
||||
|
||||
/*
|
||||
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 diff
|
||||
|
||||
import (
|
||||
"github.com/google/go-cmp/cmp" //nolint:depguard
|
||||
)
|
||||
|
||||
// Diff returns a string representation of the difference between two objects.
|
||||
// When built with the usegocmp tag, it uses go-cmp/cmp to generate a diff
|
||||
// between the objects.
|
||||
func Diff(a, b any) string {
|
||||
return cmp.Diff(a, b)
|
||||
}
|
|
@ -0,0 +1,356 @@
|
|||
//go:build !usegocmp
|
||||
// +build !usegocmp
|
||||
|
||||
/*
|
||||
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 diff
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
)
|
||||
|
||||
// TestDiffWithRealGoCmp is a comprehensive test that compares our Diff with the actual go-cmp Diff
|
||||
// across a variety of data structures and types to ensure compatibility.
|
||||
func TestDiffWithRealGoCmp(t *testing.T) {
|
||||
// Test with simple types
|
||||
t.Run("SimpleTypes", func(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
a interface{}
|
||||
b interface{}
|
||||
}{
|
||||
{name: "Integers", a: 42, b: 43},
|
||||
{name: "Strings", a: "hello", b: "world"},
|
||||
{name: "Booleans", a: true, b: false},
|
||||
{name: "Floats", a: 3.14, b: 2.71},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ourDiff := Diff(tc.a, tc.b)
|
||||
goCmpDiff := cmp.Diff(tc.a, tc.b)
|
||||
|
||||
t.Logf("Our diff:\n%s\n\nGo-cmp diff:\n%s", ourDiff, goCmpDiff)
|
||||
|
||||
// Verify both diffs are non-empty
|
||||
if ourDiff == "" || goCmpDiff == "" {
|
||||
t.Errorf("Expected non-empty diffs, got ourDiff: %v, goCmpDiff: %v",
|
||||
ourDiff == "", goCmpDiff == "")
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Test with a simple struct
|
||||
t.Run("SimpleStruct", func(t *testing.T) {
|
||||
type TestStruct struct {
|
||||
Name string
|
||||
Age int
|
||||
Tags []string
|
||||
Map map[string]int
|
||||
}
|
||||
|
||||
a := TestStruct{
|
||||
Name: "Alice",
|
||||
Age: 30,
|
||||
Tags: []string{"tag1", "tag2"},
|
||||
Map: map[string]int{"a": 1, "b": 2},
|
||||
}
|
||||
|
||||
b := TestStruct{
|
||||
Name: "Bob",
|
||||
Age: 25,
|
||||
Tags: []string{"tag1", "tag3"},
|
||||
Map: map[string]int{"a": 1, "c": 3},
|
||||
}
|
||||
|
||||
ourDiff := Diff(a, b)
|
||||
goCmpDiff := cmp.Diff(a, b)
|
||||
|
||||
t.Logf("Our diff:\n%s\n\nGo-cmp diff:\n%s", ourDiff, goCmpDiff)
|
||||
|
||||
// Check that our diff contains key differences
|
||||
keyDifferences := []string{
|
||||
"Name", "Alice", "Bob",
|
||||
"Age", "30", "25",
|
||||
"Tags", "tag2", "tag3",
|
||||
"Map", "b", "c",
|
||||
}
|
||||
|
||||
for _, key := range keyDifferences {
|
||||
if !strings.Contains(ourDiff, key) {
|
||||
t.Errorf("Our diff doesn't contain expected key difference: %q", key)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Test with a complex nested struct
|
||||
t.Run("ComplexNestedStruct", func(t *testing.T) {
|
||||
type Address struct {
|
||||
Street string
|
||||
City string
|
||||
State string
|
||||
PostalCode string
|
||||
Country string
|
||||
}
|
||||
|
||||
type Contact struct {
|
||||
Type string
|
||||
Value string
|
||||
}
|
||||
|
||||
type Person struct {
|
||||
ID int
|
||||
FirstName string
|
||||
LastName string
|
||||
Age int
|
||||
Addresses map[string]Address
|
||||
Contacts []Contact
|
||||
Metadata map[string]interface{}
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
later := now.Add(24 * time.Hour)
|
||||
|
||||
person1 := Person{
|
||||
ID: 1,
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Age: 30,
|
||||
Addresses: map[string]Address{
|
||||
"home": {
|
||||
Street: "123 Main St",
|
||||
City: "Anytown",
|
||||
State: "CA",
|
||||
PostalCode: "12345",
|
||||
Country: "USA",
|
||||
},
|
||||
"work": {
|
||||
Street: "456 Market St",
|
||||
City: "Worktown",
|
||||
State: "CA",
|
||||
PostalCode: "54321",
|
||||
Country: "USA",
|
||||
},
|
||||
},
|
||||
Contacts: []Contact{
|
||||
{Type: "email", Value: "john.doe@example.com"},
|
||||
{Type: "phone", Value: "555-1234"},
|
||||
},
|
||||
Metadata: map[string]interface{}{
|
||||
"created": "2023-01-01",
|
||||
"loginCount": 42,
|
||||
"settings": map[string]bool{
|
||||
"notifications": true,
|
||||
"darkMode": false,
|
||||
},
|
||||
},
|
||||
CreatedAt: now,
|
||||
}
|
||||
|
||||
person2 := Person{
|
||||
ID: 1,
|
||||
FirstName: "John",
|
||||
LastName: "Smith", // Different
|
||||
Age: 31, // Different
|
||||
Addresses: map[string]Address{
|
||||
"home": {
|
||||
Street: "123 Main St",
|
||||
City: "Anytown",
|
||||
State: "CA",
|
||||
PostalCode: "12345",
|
||||
Country: "USA",
|
||||
},
|
||||
// "work" address is missing
|
||||
},
|
||||
Contacts: []Contact{
|
||||
{Type: "email", Value: "john.smith@example.com"}, // Different
|
||||
{Type: "phone", Value: "555-1234"},
|
||||
{Type: "fax", Value: "555-5678"}, // Additional
|
||||
},
|
||||
Metadata: map[string]interface{}{
|
||||
"created": "2023-01-01",
|
||||
"loginCount": 43, // Different
|
||||
"settings": map[string]bool{
|
||||
"notifications": false, // Different
|
||||
"darkMode": true, // Different
|
||||
},
|
||||
"newField": "new value", // New field
|
||||
},
|
||||
CreatedAt: later, // Different
|
||||
}
|
||||
|
||||
ourDiff := Diff(person1, person2)
|
||||
goCmpDiff := cmp.Diff(person1, person2)
|
||||
|
||||
t.Logf("Our diff:\n%s\n\nGo-cmp diff:\n%s", ourDiff, goCmpDiff)
|
||||
|
||||
// Check that our diff contains key differences
|
||||
keyDifferences := []string{
|
||||
"LastName", "Smith",
|
||||
"Age",
|
||||
"Addresses", "work",
|
||||
"Contacts", "john.smith@example.com", "fax",
|
||||
"Metadata", "loginCount", "settings", "notifications", "darkMode", "newField",
|
||||
}
|
||||
|
||||
for _, key := range keyDifferences {
|
||||
if !strings.Contains(ourDiff, key) {
|
||||
t.Errorf("Our diff doesn't contain expected key difference: %q", key)
|
||||
}
|
||||
}
|
||||
|
||||
// Check that both diffs are non-empty
|
||||
if ourDiff == "" || goCmpDiff == "" {
|
||||
t.Errorf("Expected non-empty diffs, got ourDiff: %v, goCmpDiff: %v",
|
||||
ourDiff == "", goCmpDiff == "")
|
||||
}
|
||||
})
|
||||
|
||||
// Test with slices and maps
|
||||
t.Run("SlicesAndMaps", func(t *testing.T) {
|
||||
// Test with slices
|
||||
a1 := []int{1, 2, 3, 4, 5}
|
||||
b1 := []int{1, 2, 6, 4, 7}
|
||||
|
||||
ourDiff1 := Diff(a1, b1)
|
||||
goCmpDiff1 := cmp.Diff(a1, b1)
|
||||
|
||||
t.Logf("Our diff (slices):\n%s\n\nGo-cmp diff (slices):\n%s", ourDiff1, goCmpDiff1)
|
||||
|
||||
// Check that our diff contains the differences
|
||||
if !strings.Contains(ourDiff1, "3") || !strings.Contains(ourDiff1, "6") ||
|
||||
!strings.Contains(ourDiff1, "5") || !strings.Contains(ourDiff1, "7") {
|
||||
t.Errorf("Our diff doesn't contain all expected differences for slices")
|
||||
}
|
||||
|
||||
// Test with maps
|
||||
a2 := map[string]int{"a": 1, "b": 2, "c": 3}
|
||||
b2 := map[string]int{"a": 1, "b": 5, "d": 4}
|
||||
|
||||
ourDiff2 := Diff(a2, b2)
|
||||
goCmpDiff2 := cmp.Diff(a2, b2)
|
||||
|
||||
t.Logf("Our diff (maps):\n%s\n\nGo-cmp diff (maps):\n%s", ourDiff2, goCmpDiff2)
|
||||
|
||||
// Check that our diff contains the differences
|
||||
if !strings.Contains(ourDiff2, "b") || !strings.Contains(ourDiff2, "5") ||
|
||||
!strings.Contains(ourDiff2, "c") || !strings.Contains(ourDiff2, "d") {
|
||||
t.Errorf("Our diff doesn't contain all expected differences for maps")
|
||||
}
|
||||
})
|
||||
|
||||
// Test with unexported fields
|
||||
t.Run("UnexportedFields", func(t *testing.T) {
|
||||
type WithUnexported struct {
|
||||
Exported int
|
||||
unexported int
|
||||
}
|
||||
|
||||
a := WithUnexported{Exported: 1, unexported: 2}
|
||||
b := WithUnexported{Exported: 3, unexported: 4}
|
||||
|
||||
ourDiff := Diff(a, b)
|
||||
// Use cmpopts.IgnoreUnexported to ignore unexported fields in go-cmp
|
||||
goCmpDiff := cmp.Diff(a, b, cmpopts.IgnoreUnexported(WithUnexported{}))
|
||||
|
||||
t.Logf("Our diff (unexported):\n%s\n\nGo-cmp diff (unexported):\n%s", ourDiff, goCmpDiff)
|
||||
|
||||
// Check that our diff contains only the exported field difference
|
||||
if !strings.Contains(ourDiff, "Exported") || !strings.Contains(ourDiff, "1") || !strings.Contains(ourDiff, "3") {
|
||||
t.Errorf("Our diff doesn't contain the exported field difference")
|
||||
}
|
||||
})
|
||||
|
||||
// Test with embedded structs
|
||||
t.Run("EmbeddedStructs", func(t *testing.T) {
|
||||
type Embedded struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
type Container struct {
|
||||
Embedded
|
||||
Extra string
|
||||
}
|
||||
|
||||
a := Container{Embedded: Embedded{Value: 1}, Extra: "a"}
|
||||
b := Container{Embedded: Embedded{Value: 2}, Extra: "b"}
|
||||
|
||||
ourDiff := Diff(a, b)
|
||||
goCmpDiff := cmp.Diff(a, b)
|
||||
|
||||
t.Logf("Our diff (embedded):\n%s\n\nGo-cmp diff (embedded):\n%s", ourDiff, goCmpDiff)
|
||||
|
||||
// Check that our diff contains the container field difference
|
||||
if !strings.Contains(ourDiff, "Extra") || !strings.Contains(ourDiff, "a") || !strings.Contains(ourDiff, "b") {
|
||||
t.Errorf("Our diff doesn't contain the container field difference")
|
||||
}
|
||||
})
|
||||
|
||||
// Test with interface values of same type
|
||||
t.Run("InterfaceValues", func(t *testing.T) {
|
||||
type Container struct {
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
// Test with same type in interface
|
||||
c := Container{Value: 42}
|
||||
d := Container{Value: 43}
|
||||
|
||||
ourDiff := Diff(c, d)
|
||||
goCmpDiff := cmp.Diff(c, d)
|
||||
|
||||
t.Logf("Our diff (interface same type):\n%s\n\nGo-cmp diff (interface same type):\n%s", ourDiff, goCmpDiff)
|
||||
|
||||
// Check that our diff contains the value difference
|
||||
if !strings.Contains(ourDiff, "42") || !strings.Contains(ourDiff, "43") {
|
||||
t.Errorf("Our diff doesn't contain the value difference for interface values of same type")
|
||||
}
|
||||
})
|
||||
|
||||
// Test with objects that cannot be marshaled to JSON
|
||||
t.Run("UnmarshalableObjects", func(t *testing.T) {
|
||||
// Test with a circular reference, which cannot be marshaled to JSON
|
||||
type Node struct {
|
||||
Value int
|
||||
Next *Node
|
||||
}
|
||||
|
||||
// Create a circular reference
|
||||
nodeA := &Node{Value: 1}
|
||||
nodeA.Next = nodeA // Points to itself
|
||||
|
||||
nodeB := &Node{Value: 2}
|
||||
nodeB.Next = nodeB // Points to itself
|
||||
|
||||
// This should fall back to using dump.Pretty
|
||||
circularDiff := Diff(nodeA, nodeB)
|
||||
|
||||
t.Logf("Diff for circular references:\n%s", circularDiff)
|
||||
|
||||
// Verify the diff contains the values
|
||||
if !strings.Contains(circularDiff, "1") || !strings.Contains(circularDiff, "2") {
|
||||
t.Errorf("Diff doesn't contain expected value differences for circular references")
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1,5 +1,8 @@
|
|||
//go:build !usegocmp
|
||||
// +build !usegocmp
|
||||
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors.
|
||||
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.
|
||||
|
@ -17,122 +20,43 @@ limitations under the License.
|
|||
package diff
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/google/go-cmp/cmp" //nolint:depguard
|
||||
"github.com/pmezard/go-difflib/difflib"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/dump"
|
||||
)
|
||||
|
||||
func legacyDiff(a, b interface{}) string {
|
||||
return cmp.Diff(a, b)
|
||||
}
|
||||
// Diff returns a string representation of the difference between two objects.
|
||||
// When built without the usegocmp tag, it uses go-difflib/difflib to generate a
|
||||
// unified diff of the objects. It attempts to use JSON serialization first,
|
||||
// falling back to an object dump via the dump package if JSON marshaling fails.
|
||||
func Diff(a, b any) string {
|
||||
|
||||
// StringDiff diffs a and b and returns a human readable diff.
|
||||
// DEPRECATED: use github.com/google/go-cmp/cmp.Diff
|
||||
func StringDiff(a, b string) string {
|
||||
return legacyDiff(a, b)
|
||||
}
|
||||
|
||||
// ObjectDiff prints the diff of two go objects and fails if the objects
|
||||
// contain unhandled unexported fields.
|
||||
// DEPRECATED: use github.com/google/go-cmp/cmp.Diff
|
||||
func ObjectDiff(a, b interface{}) string {
|
||||
return legacyDiff(a, b)
|
||||
}
|
||||
|
||||
// ObjectGoPrintDiff prints the diff of two go objects and fails if the objects
|
||||
// contain unhandled unexported fields.
|
||||
// DEPRECATED: use github.com/google/go-cmp/cmp.Diff
|
||||
func ObjectGoPrintDiff(a, b interface{}) string {
|
||||
return legacyDiff(a, b)
|
||||
}
|
||||
|
||||
// ObjectReflectDiff prints the diff of two go objects and fails if the objects
|
||||
// contain unhandled unexported fields.
|
||||
// DEPRECATED: use github.com/google/go-cmp/cmp.Diff
|
||||
func ObjectReflectDiff(a, b interface{}) string {
|
||||
return legacyDiff(a, b)
|
||||
}
|
||||
|
||||
// ObjectGoPrintSideBySide prints a and b as textual dumps side by side,
|
||||
// enabling easy visual scanning for mismatches.
|
||||
func ObjectGoPrintSideBySide(a, b interface{}) string {
|
||||
sA := dump.Pretty(a)
|
||||
sB := dump.Pretty(b)
|
||||
|
||||
linesA := strings.Split(sA, "\n")
|
||||
linesB := strings.Split(sB, "\n")
|
||||
width := 0
|
||||
for _, s := range linesA {
|
||||
l := len(s)
|
||||
if l > width {
|
||||
width = l
|
||||
}
|
||||
aStr, aErr := toPrettyJSON(a)
|
||||
bStr, bErr := toPrettyJSON(b)
|
||||
if aErr != nil || bErr != nil {
|
||||
aStr = dump.Pretty(a)
|
||||
bStr = dump.Pretty(b)
|
||||
}
|
||||
for _, s := range linesB {
|
||||
l := len(s)
|
||||
if l > width {
|
||||
width = l
|
||||
}
|
||||
|
||||
diff := difflib.UnifiedDiff{
|
||||
A: difflib.SplitLines(aStr),
|
||||
B: difflib.SplitLines(bStr),
|
||||
Context: 3,
|
||||
}
|
||||
buf := &bytes.Buffer{}
|
||||
w := tabwriter.NewWriter(buf, width, 0, 1, ' ', 0)
|
||||
max := len(linesA)
|
||||
if len(linesB) > max {
|
||||
max = len(linesB)
|
||||
|
||||
diffstr, err := difflib.GetUnifiedDiffString(diff)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("error generating diff: %v", err)
|
||||
}
|
||||
for i := 0; i < max; i++ {
|
||||
var a, b string
|
||||
if i < len(linesA) {
|
||||
a = linesA[i]
|
||||
}
|
||||
if i < len(linesB) {
|
||||
b = linesB[i]
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%s\n", a, b)
|
||||
}
|
||||
w.Flush()
|
||||
return buf.String()
|
||||
|
||||
return diffstr
|
||||
}
|
||||
|
||||
// IgnoreUnset is an option that ignores fields that are unset on the right
|
||||
// hand side of a comparison. This is useful in testing to assert that an
|
||||
// object is a derivative.
|
||||
func IgnoreUnset() cmp.Option {
|
||||
return cmp.Options{
|
||||
// ignore unset fields in v2
|
||||
cmp.FilterPath(func(path cmp.Path) bool {
|
||||
_, v2 := path.Last().Values()
|
||||
switch v2.Kind() {
|
||||
case reflect.Slice, reflect.Map:
|
||||
if v2.IsNil() || v2.Len() == 0 {
|
||||
return true
|
||||
}
|
||||
case reflect.String:
|
||||
if v2.Len() == 0 {
|
||||
return true
|
||||
}
|
||||
case reflect.Interface, reflect.Pointer:
|
||||
if v2.IsNil() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}, cmp.Ignore()),
|
||||
// ignore map entries that aren't set in v2
|
||||
cmp.FilterPath(func(path cmp.Path) bool {
|
||||
switch i := path.Last().(type) {
|
||||
case cmp.MapIndex:
|
||||
if _, v2 := i.Values(); !v2.IsValid() {
|
||||
fmt.Println("E")
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}, cmp.Ignore()),
|
||||
}
|
||||
// toPrettyJSON converts an object to a pretty-printed JSON string.
|
||||
func toPrettyJSON(data any) (string, error) {
|
||||
jsonData, err := json.MarshalIndent(data, "", " ")
|
||||
return string(jsonData), err
|
||||
}
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
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 diff
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/dump"
|
||||
)
|
||||
|
||||
// ObjectGoPrintSideBySide prints a and b as textual dumps side by side,
|
||||
// enabling easy visual scanning for mismatches.
|
||||
func ObjectGoPrintSideBySide(a, b interface{}) string {
|
||||
sA := dump.Pretty(a)
|
||||
sB := dump.Pretty(b)
|
||||
|
||||
linesA := strings.Split(sA, "\n")
|
||||
linesB := strings.Split(sB, "\n")
|
||||
width := 0
|
||||
for _, s := range linesA {
|
||||
l := len(s)
|
||||
if l > width {
|
||||
width = l
|
||||
}
|
||||
}
|
||||
for _, s := range linesB {
|
||||
l := len(s)
|
||||
if l > width {
|
||||
width = l
|
||||
}
|
||||
}
|
||||
buf := &bytes.Buffer{}
|
||||
w := tabwriter.NewWriter(buf, width, 0, 1, ' ', 0)
|
||||
max := len(linesA)
|
||||
if len(linesB) > max {
|
||||
max = len(linesB)
|
||||
}
|
||||
for i := 0; i < max; i++ {
|
||||
var a, b string
|
||||
if i < len(linesA) {
|
||||
a = linesA[i]
|
||||
}
|
||||
if i < len(linesB) {
|
||||
b = linesB[i]
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", a, b)
|
||||
}
|
||||
_ = w.Flush()
|
||||
return buf.String()
|
||||
}
|
|
@ -19,16 +19,10 @@ package dump
|
|||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
func ptrint(i int) *int {
|
||||
return &i
|
||||
}
|
||||
|
||||
func ptrstr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
// custom type to test Stringer interface on non-pointer receiver.
|
||||
type customString string
|
||||
|
||||
|
@ -101,7 +95,7 @@ func TestPretty(t *testing.T) {
|
|||
{uint32(93), "(uint32) 93\n"},
|
||||
{uint64(93), "(uint64) 93\n"},
|
||||
{uintptr(93), "(uintptr) 0x5d\n"},
|
||||
{ptrint(93), "(*int)(93)\n"},
|
||||
{ptr.To(93), "(*int)(93)\n"},
|
||||
{float32(93.76), "(float32) 93.76\n"},
|
||||
{float64(93.76), "(float64) 93.76\n"},
|
||||
{complex64(93i), "(complex64) (0+93i)\n"},
|
||||
|
@ -109,7 +103,7 @@ func TestPretty(t *testing.T) {
|
|||
{bool(true), "(bool) true\n"},
|
||||
{bool(false), "(bool) false\n"},
|
||||
{string("test"), "(string) (len=4) \"test\"\n"},
|
||||
{ptrstr("test"), "(*string)((len=4) \"test\")\n"},
|
||||
{ptr.To("test"), "(*string)((len=4) \"test\")\n"},
|
||||
{[1]string{"arr"}, "([1]string) (len=1) {\n (string) (len=3) \"arr\"\n}\n"},
|
||||
{[]string{"slice"}, "([]string) (len=1) {\n (string) (len=5) \"slice\"\n}\n"},
|
||||
{tcs, "(dump.customString) (len=4) \"test\"\n"},
|
||||
|
@ -180,7 +174,7 @@ func TestForHash(t *testing.T) {
|
|||
{uint32(93), "(uint32)93"},
|
||||
{uint64(93), "(uint64)93"},
|
||||
{uintptr(93), "(uintptr)0x5d"},
|
||||
{ptrint(93), "(*int)93"},
|
||||
{ptr.To(93), "(*int)93"},
|
||||
{float32(93.76), "(float32)93.76"},
|
||||
{float64(93.76), "(float64)93.76"},
|
||||
{complex64(93i), "(complex64)(0+93i)"},
|
||||
|
@ -188,7 +182,7 @@ func TestForHash(t *testing.T) {
|
|||
{bool(true), "(bool)true"},
|
||||
{bool(false), "(bool)false"},
|
||||
{string("test"), "(string)test"},
|
||||
{ptrstr("test"), "(*string)test"},
|
||||
{ptr.To("test"), "(*string)test"},
|
||||
{[1]string{"arr"}, "([1]string)[arr]"},
|
||||
{[]string{"slice"}, "([]string)[slice]"},
|
||||
{tcs, "(dump.customString)test"},
|
||||
|
@ -259,7 +253,7 @@ func TestOneLine(t *testing.T) {
|
|||
{uint32(93), "(uint32)93"},
|
||||
{uint64(93), "(uint64)93"},
|
||||
{uintptr(93), "(uintptr)0x5d"},
|
||||
{ptrint(93), "(*int)93"},
|
||||
{ptr.To(93), "(*int)93"},
|
||||
{float32(93.76), "(float32)93.76"},
|
||||
{float64(93.76), "(float64)93.76"},
|
||||
{complex64(93i), "(complex64)(0+93i)"},
|
||||
|
@ -267,7 +261,7 @@ func TestOneLine(t *testing.T) {
|
|||
{bool(true), "(bool)true"},
|
||||
{bool(false), "(bool)false"},
|
||||
{string("test"), "(string)test"},
|
||||
{ptrstr("test"), "(*string)test"},
|
||||
{ptr.To("test"), "(*string)test"},
|
||||
{[1]string{"arr"}, "([1]string)[arr]"},
|
||||
{[]string{"slice"}, "([]string)[slice]"},
|
||||
{tcs, "(dump.customString)test"},
|
||||
|
|
|
@ -24,6 +24,7 @@ import (
|
|||
)
|
||||
|
||||
// MessageCountMap contains occurrence for each error message.
|
||||
// Deprecated: Not used anymore in the k8s.io codebase, use `errors.Join` instead.
|
||||
type MessageCountMap map[string]int
|
||||
|
||||
// Aggregate represents an object that contains multiple errors, but does not
|
||||
|
@ -199,6 +200,7 @@ func Flatten(agg Aggregate) Aggregate {
|
|||
}
|
||||
|
||||
// CreateAggregateFromMessageCountMap converts MessageCountMap Aggregate
|
||||
// Deprecated: Not used anymore in the k8s.io codebase, use `errors.Join` instead.
|
||||
func CreateAggregateFromMessageCountMap(m MessageCountMap) Aggregate {
|
||||
if m == nil {
|
||||
return nil
|
||||
|
|
|
@ -79,7 +79,7 @@ func TestHandshake(t *testing.T) {
|
|||
}
|
||||
|
||||
for name, test := range tests {
|
||||
req, err := http.NewRequest("GET", "http://www.example.com/", nil)
|
||||
req, err := http.NewRequest(http.MethodGet, "http://www.example.com/", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: error creating request: %v", name, err)
|
||||
}
|
||||
|
|
|
@ -194,7 +194,7 @@ func (s *SpdyRoundTripper) dialWithHttpProxy(req *http.Request, proxyURL *url.UR
|
|||
|
||||
// proxying logic adapted from http://blog.h6t.eu/post/74098062923/golang-websocket-with-http-proxy-support
|
||||
proxyReq := http.Request{
|
||||
Method: "CONNECT",
|
||||
Method: http.MethodConnect,
|
||||
URL: &url.URL{},
|
||||
Host: targetHost,
|
||||
}
|
||||
|
|
|
@ -321,7 +321,7 @@ func TestRoundTripAndNewConnection(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("error creating request: %s", err)
|
||||
}
|
||||
req, err := http.NewRequest("GET", server.URL, nil)
|
||||
req, err := http.NewRequest(http.MethodGet, server.URL, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("error creating request: %s", err)
|
||||
}
|
||||
|
@ -612,7 +612,7 @@ func TestRoundTripSocks5AndNewConnection(t *testing.T) {
|
|||
))
|
||||
defer server.Close()
|
||||
|
||||
req, err := http.NewRequest("GET", server.URL, nil)
|
||||
req, err := http.NewRequest(http.MethodGet, server.URL, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("error creating request: %s", err)
|
||||
}
|
||||
|
@ -778,7 +778,7 @@ func TestRoundTripPassesContextToDialer(t *testing.T) {
|
|||
t.Run(u, func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", u, nil)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
|
||||
require.NoError(t, err)
|
||||
spdyTransport, err := NewRoundTripper(&tls.Config{})
|
||||
if err != nil {
|
||||
|
|
|
@ -68,7 +68,7 @@ func TestUpgradeResponse(t *testing.T) {
|
|||
}))
|
||||
defer server.Close()
|
||||
|
||||
req, err := http.NewRequest("GET", server.URL, nil)
|
||||
req, err := http.NewRequest(http.MethodGet, server.URL, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("%d: error creating request: %s", i, err)
|
||||
}
|
||||
|
|
|
@ -369,7 +369,7 @@ func TestIsWebSocketRequestWithStreamCloseProtocol(t *testing.T) {
|
|||
}
|
||||
|
||||
for name, test := range tests {
|
||||
req, err := http.NewRequest("GET", "http://www.example.com/", nil)
|
||||
req, err := http.NewRequest(http.MethodGet, "http://www.example.com/", nil)
|
||||
require.NoError(t, err)
|
||||
for key, value := range test.headers {
|
||||
req.Header.Add(key, value)
|
||||
|
|
|
@ -20,8 +20,8 @@ import (
|
|||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
||||
"sigs.k8s.io/structured-merge-diff/v4/typed"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/typed"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
|
|
@ -20,7 +20,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"sigs.k8s.io/structured-merge-diff/v4/typed"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/typed"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/equality"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
|
|
|
@ -19,7 +19,7 @@ package managedfields
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
|
|
@ -22,8 +22,8 @@ import (
|
|||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/kube-openapi/pkg/schemaconv"
|
||||
"k8s.io/kube-openapi/pkg/util/proto"
|
||||
smdschema "sigs.k8s.io/structured-merge-diff/v4/schema"
|
||||
"sigs.k8s.io/structured-merge-diff/v4/typed"
|
||||
smdschema "sigs.k8s.io/structured-merge-diff/v6/schema"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/typed"
|
||||
)
|
||||
|
||||
// groupVersionKindExtensionKey is the key used to lookup the
|
||||
|
|
|
@ -22,7 +22,7 @@ import (
|
|||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
|
||||
)
|
||||
|
||||
type capManagersManager struct {
|
||||
|
|
|
@ -32,7 +32,7 @@ import (
|
|||
"k8s.io/apimachinery/pkg/util/managedfields/internal"
|
||||
internaltesting "k8s.io/apimachinery/pkg/util/managedfields/internal/testing"
|
||||
"k8s.io/apimachinery/pkg/util/managedfields/managedfieldstest"
|
||||
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
|
||||
)
|
||||
|
||||
type fakeManager struct{}
|
||||
|
|
|
@ -25,8 +25,8 @@ import (
|
|||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
||||
"sigs.k8s.io/structured-merge-diff/v4/merge"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/merge"
|
||||
)
|
||||
|
||||
// NewConflictError returns an error including details on the requests apply conflicts
|
||||
|
|
|
@ -24,8 +24,8 @@ import (
|
|||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/managedfields/internal"
|
||||
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
||||
"sigs.k8s.io/structured-merge-diff/v4/merge"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/merge"
|
||||
)
|
||||
|
||||
// TestNewConflictError tests that NewConflictError creates the correct StatusError for a given smd Conflicts
|
||||
|
|
|
@ -26,7 +26,7 @@ import (
|
|||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/klog/v2"
|
||||
"sigs.k8s.io/structured-merge-diff/v4/merge"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/merge"
|
||||
)
|
||||
|
||||
// DefaultMaxUpdateManagers defines the default maximum retained number of managedFields entries from updates
|
||||
|
|
|
@ -21,7 +21,7 @@ import (
|
|||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
|
||||
)
|
||||
|
||||
// EmptyFields represents a set with no paths
|
||||
|
|
|
@ -23,7 +23,7 @@ import (
|
|||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
|
||||
)
|
||||
|
||||
// TestFieldsRoundTrip tests that a fields trie can be round tripped as a path set
|
||||
|
|
|
@ -24,8 +24,8 @@ import (
|
|||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
||||
"sigs.k8s.io/structured-merge-diff/v4/merge"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/merge"
|
||||
)
|
||||
|
||||
type lastAppliedManager struct {
|
||||
|
|
|
@ -29,8 +29,8 @@ import (
|
|||
"k8s.io/apimachinery/pkg/util/managedfields/internal"
|
||||
"k8s.io/apimachinery/pkg/util/managedfields/managedfieldstest"
|
||||
yamlutil "k8s.io/apimachinery/pkg/util/yaml"
|
||||
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
||||
"sigs.k8s.io/structured-merge-diff/v4/merge"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/merge"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ import (
|
|||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
|
||||
)
|
||||
|
||||
// ManagedInterface groups a fieldpath.ManagedFields together with the timestamps associated with each operation.
|
||||
|
|
|
@ -21,7 +21,7 @@ import (
|
|||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
|
||||
)
|
||||
|
||||
type managedFieldsUpdater struct {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue