diff --git a/OWNERS b/OWNERS index 696f62315..804e4b6d1 100644 --- a/OWNERS +++ b/OWNERS @@ -1,25 +1,25 @@ # See the OWNERS docs at https://go.k8s.io/owners approvers: -- lavalamp -- smarterclayton -- deads2k -- sttts -- liggitt -- caesarxuchao + - lavalamp + - smarterclayton + - deads2k + - sttts + - liggitt + - caesarxuchao reviewers: -- thockin -- lavalamp -- smarterclayton -- wojtek-t -- deads2k -- derekwaynecarr -- caesarxuchao -- cheftako -- mikedanese -- liggitt -- sttts -- ncdc -- logicalhan + - thockin + - lavalamp + - smarterclayton + - wojtek-t + - deads2k + - derekwaynecarr + - caesarxuchao + - cheftako + - mikedanese + - liggitt + - sttts + - ncdc + - logicalhan labels: -- sig/api-machinery + - sig/api-machinery diff --git a/README.md b/README.md index 77800f292..06d0e3b64 100644 --- a/README.md +++ b/README.md @@ -27,3 +27,4 @@ Code changes are made in that location, merged into `k8s.io/kubernetes` and late 1. Add API types to this repo. This is for the machinery, not for the types. 2. Directly modify any files under `pkg` in this repo. Those are driven from `k8s.io/kubernetes/staging/src/k8s.io/apimachinery`. 3. Expect compatibility. This repo is direct support of Kubernetes and the API isn't yet stable enough for API guarantees. + diff --git a/doc.go b/doc.go new file mode 100644 index 000000000..23f567699 --- /dev/null +++ b/doc.go @@ -0,0 +1,17 @@ +/* +Copyright 2021 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 apimachinery // import "k8s.io/apimachinery" diff --git a/go.mod b/go.mod index 265fb76df..3133c8e60 100644 --- a/go.mod +++ b/go.mod @@ -2,36 +2,47 @@ module k8s.io/apimachinery -go 1.16 +go 1.19 require ( + github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 github.com/davecgh/go-spew v1.1.1 github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 - github.com/evanphx/json-patch v4.11.0+incompatible + github.com/evanphx/json-patch v4.12.0+incompatible github.com/gogo/protobuf v1.3.2 github.com/golang/protobuf v1.5.2 - github.com/google/go-cmp v0.5.5 + github.com/google/gnostic v0.5.7-v3refs + github.com/google/go-cmp v0.5.9 github.com/google/gofuzz v1.1.0 github.com/google/uuid v1.1.2 - github.com/googleapis/gnostic v0.5.5 - github.com/json-iterator/go v1.1.11 - github.com/kr/text v0.2.0 // indirect github.com/moby/spdystream v0.2.0 - github.com/modern-go/reflect2 v1.0.1 github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f + github.com/spf13/pflag v1.0.5 + github.com/stretchr/testify v1.8.0 + golang.org/x/net v0.3.1-0.20221206200815-1e63c2f08a10 + gopkg.in/inf.v0 v0.9.1 + k8s.io/klog/v2 v2.80.1 + k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 + k8s.io/utils v0.0.0-20221107191617-1a15be271d1d + sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 + sigs.k8s.io/structured-merge-diff/v4 v4.2.3 + sigs.k8s.io/yaml v1.3.0 +) + +require ( + github.com/go-logr/logr v1.2.3 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect - github.com/onsi/ginkgo v1.14.0 // indirect + github.com/onsi/ginkgo/v2 v2.4.0 // indirect + github.com/onsi/gomega v1.23.0 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/spf13/pflag v1.0.5 - github.com/stretchr/testify v1.7.0 - golang.org/x/net v0.0.0-20210520170846-37e1c6afe023 - golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/text v0.5.0 // indirect + google.golang.org/protobuf v1.28.1 // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect - gopkg.in/inf.v0 v0.9.1 gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect - k8s.io/klog/v2 v2.9.0 - k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e - sigs.k8s.io/structured-merge-diff/v4 v4.1.2 - sigs.k8s.io/yaml v1.2.0 + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 29d9d0c8a..638a4ddc4 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,7 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= -github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -13,20 +11,13 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= -github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanphx/json-patch v4.11.0+incompatible h1:glyUF9yIYtMHzn8xaKw5rMhdWcwsYV8dZHIq5567/xs= -github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= -github.com/go-logr/logr v0.4.0 h1:K7/B1jt6fIBQVd4Owv2MqGQClcgf0R266+7C/QjRcLc= -github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= +github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -39,79 +30,63 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= +github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= -github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw= -github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= 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 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +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/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/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= -github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/ginkgo/v2 v2.4.0 h1:+Ig9nvqgS5OBSACXNk15PLdp0U9XPYROt9CFzVdFGIs= +github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkAt4OrFo= +github.com/onsi/gomega v1.23.0 h1:/oxKu9c2HVap+F3PfKort2Hw5DEU+HGlW8n+tguWsys= +github.com/onsi/gomega v1.23.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -125,18 +100,14 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/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-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210520170846-37e1c6afe023 h1:ADo5wSpq2gqaCGQWzk7S5vd//0iyyLeAratkEoG5dLE= -golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.3.1-0.20221206200815-1e63c2f08a10 h1:Frnccbp+ok2GkUS2tC84yAq/U9Vg+0sIO7aRL3T4Xnc= +golang.org/x/net v0.3.1-0.20221206200815-1e63c2f08a10/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -144,25 +115,14 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ 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-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -174,7 +134,6 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -191,43 +150,37 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= -k8s.io/klog/v2 v2.9.0 h1:D7HV+n1V57XeZ0m6tdRkfknthUaM06VFbWldOFh8kzM= -k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= -k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e h1:KLHHjkdQFomZy8+06csTWZ0m1343QqxZhR2LJ1OxCYM= -k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= -sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/structured-merge-diff/v4 v4.1.2 h1:Hr/htKFmJEbtMgS/UD0N+gtgctAqz81t3nu+sPzynno= -sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= -sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +k8s.io/klog/v2 v2.80.1 h1:atnLQ121W371wYYFawwYx1aEY2eUfs4l3J72wtgAwV4= +k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 h1:+70TFaan3hfJzs+7VK2o+OGxg8HsuBr/5f6tVAjDu6E= +k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280/go.mod h1:+Axhij7bCpeqhklhUTe3xmOn6bWxolyZEeyaFpjGtl4= +k8s.io/utils v0.0.0-20221107191617-1a15be271d1d h1:0Smp/HP1OH4Rvhe+4B8nWGERtlqAGSftbSbbmm45oFs= +k8s.io/utils v0.0.0-20221107191617-1a15be271d1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k= +sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/pkg/OWNERS b/pkg/OWNERS index d6daa1280..e487d1ff0 100644 --- a/pkg/OWNERS +++ b/pkg/OWNERS @@ -1,8 +1,8 @@ # See the OWNERS docs at https://go.k8s.io/owners approvers: -- caesarxuchao -- deads2k -- lavalamp -- smarterclayton -- liggitt + - caesarxuchao + - deads2k + - lavalamp + - smarterclayton + - liggitt diff --git a/pkg/api/apitesting/fuzzer/valuefuzz.go b/pkg/api/apitesting/fuzzer/valuefuzz.go index cd71c517d..facff57bb 100644 --- a/pkg/api/apitesting/fuzzer/valuefuzz.go +++ b/pkg/api/apitesting/fuzzer/valuefuzz.go @@ -40,7 +40,7 @@ func valueFuzz(obj reflect.Value) { valueFuzz(obj.Index(i)) } } - case reflect.Interface, reflect.Ptr: + case reflect.Interface, reflect.Pointer: if obj.IsNil() { // TODO: set non-nil value } else { diff --git a/pkg/api/apitesting/naming/naming.go b/pkg/api/apitesting/naming/naming.go index 2b8567453..c235d017e 100644 --- a/pkg/api/apitesting/naming/naming.go +++ b/pkg/api/apitesting/naming/naming.go @@ -39,7 +39,7 @@ func VerifyGroupNames(scheme *runtime.Scheme, legacyUnsuffixedGroups sets.String errs := []error{} for _, gv := range scheme.PrioritizedVersionsAllGroups() { if !strings.HasSuffix(gv.Group, ".k8s.io") && !legacyUnsuffixedGroups.Has(gv.Group) { - errs = append(errs, fmt.Errorf("Group %s does not have the standard kubernetes API group suffix of .k8s.io", gv.Group)) + errs = append(errs, fmt.Errorf("group %s does not have the standard kubernetes API group suffix of .k8s.io", gv.Group)) } } return errors.NewAggregate(errs) @@ -76,7 +76,7 @@ func ensureNoTags(gvk schema.GroupVersionKind, tp reflect.Type, parents []reflec parents = append(parents, tp) switch tp.Kind() { - case reflect.Map, reflect.Slice, reflect.Ptr: + case reflect.Map, reflect.Slice, reflect.Pointer: errs = append(errs, ensureNoTags(gvk, tp.Elem(), parents, typesAllowedTags)...) case reflect.String, reflect.Bool, reflect.Float32, reflect.Float64, reflect.Int32, reflect.Int64, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, reflect.Interface: @@ -91,14 +91,14 @@ func ensureNoTags(gvk schema.GroupVersionKind, tp reflect.Type, parents []reflec jsonTag := f.Tag.Get("json") protoTag := f.Tag.Get("protobuf") if len(jsonTag) > 0 || len(protoTag) > 0 { - errs = append(errs, fmt.Errorf("Internal types should not have json or protobuf tags. %#v has tag on field %v: %v.\n%s", gvk, f.Name, f.Tag, fmtParentString(parents))) + errs = append(errs, fmt.Errorf("internal types should not have json or protobuf tags. %#v has tag on field %v: %v.\n%s", gvk, f.Name, f.Tag, fmtParentString(parents))) } errs = append(errs, ensureNoTags(gvk, f.Type, parents, typesAllowedTags)...) } default: - errs = append(errs, fmt.Errorf("Unexpected type %v in %#v.\n%s", tp.Kind(), gvk, fmtParentString(parents))) + errs = append(errs, fmt.Errorf("unexpected type %v in %#v.\n%s", tp.Kind(), gvk, fmtParentString(parents))) } return errs } @@ -106,7 +106,7 @@ func ensureNoTags(gvk schema.GroupVersionKind, tp reflect.Type, parents []reflec func ensureTags(gvk schema.GroupVersionKind, tp reflect.Type, parents []reflect.Type, allowedNonstandardJSONNames map[reflect.Type]string) []error { errs := []error{} // This type handles its own encoding/decoding and doesn't need json tags - if tp.Implements(marshalerType) && (tp.Implements(unmarshalerType) || reflect.PtrTo(tp).Implements(unmarshalerType)) { + if tp.Implements(marshalerType) && (tp.Implements(unmarshalerType) || reflect.PointerTo(tp).Implements(unmarshalerType)) { return errs } @@ -117,7 +117,7 @@ func ensureTags(gvk schema.GroupVersionKind, tp reflect.Type, parents []reflect. parents = append(parents, tp) switch tp.Kind() { - case reflect.Map, reflect.Slice, reflect.Ptr: + case reflect.Map, reflect.Slice, reflect.Pointer: errs = append(errs, ensureTags(gvk, tp.Elem(), parents, allowedNonstandardJSONNames)...) case reflect.String, reflect.Bool, reflect.Float32, reflect.Float64, reflect.Int32, reflect.Int64, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, reflect.Interface: @@ -140,7 +140,7 @@ func ensureTags(gvk schema.GroupVersionKind, tp reflect.Type, parents []reflect. } default: - errs = append(errs, fmt.Errorf("Unexpected type %v in %#v.\n%s", tp.Kind(), gvk, fmtParentString(parents))) + errs = append(errs, fmt.Errorf("unexpected type %v in %#v.\n%s", tp.Kind(), gvk, fmtParentString(parents))) } return errs } diff --git a/pkg/api/apitesting/roundtrip/compatibility.go b/pkg/api/apitesting/roundtrip/compatibility.go index c65525330..8272f9fa3 100644 --- a/pkg/api/apitesting/roundtrip/compatibility.go +++ b/pkg/api/apitesting/roundtrip/compatibility.go @@ -18,29 +18,23 @@ package roundtrip import ( "bytes" + gojson "encoding/json" "io/ioutil" "os" "os/exec" "path/filepath" + "reflect" "sort" - "strconv" "strings" "testing" - "time" "github.com/google/go-cmp/cmp" - fuzz "github.com/google/gofuzz" apiequality "k8s.io/apimachinery/pkg/api/equality" - apimeta "k8s.io/apimachinery/pkg/api/meta" - genericfuzzer "k8s.io/apimachinery/pkg/apis/meta/fuzzer" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/runtime/serializer/json" "k8s.io/apimachinery/pkg/runtime/serializer/protobuf" - "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/util/sets" ) @@ -49,7 +43,7 @@ import ( // // Example use: `NewCompatibilityTestOptions(scheme).Complete(t).Run(t)` type CompatibilityTestOptions struct { - // Scheme is used to create new objects for fuzzing, decoding, and for constructing serializers. + // Scheme is used to create new objects for filling, decoding, and for constructing serializers. // Required. Scheme *runtime.Scheme @@ -60,7 +54,7 @@ type CompatibilityTestOptions struct { // TestDataDirCurrentVersion points to a directory containing compatibility test data for the current version. // Complete() populates this with "/HEAD" if unset. // Within this directory, `...[json|yaml|pb]` files are required to exist, and are: - // * verified to match serialized FuzzedObjects[GVK] + // * verified to match serialized FilledObjects[GVK] // * verified to decode without error // * verified to round-trip byte-for-byte when re-encoded // * verified to be semantically equal when decoded into memory @@ -78,20 +72,25 @@ type CompatibilityTestOptions struct { // Complete() populates this with Scheme.AllKnownTypes() if unset. Kinds []schema.GroupVersionKind - // FuzzedObjects is an optional set of fuzzed objects to use for verifying HEAD fixtures. - // Complete() populates this with the result of CompatibilityTestObject(Kinds[*], Scheme, FuzzFuncs) for any missing kinds. - // Objects must be deterministically fuzzed and identical on every invocation. - FuzzedObjects map[schema.GroupVersionKind]runtime.Object + // FilledObjects is an optional set of pre-filled objects to use for verifying HEAD fixtures. + // Complete() populates this with the result of CompatibilityTestObject(Kinds[*], Scheme, FillFuncs) for any missing kinds. + // Objects must deterministically populate every field and be identical on every invocation. + FilledObjects map[schema.GroupVersionKind]runtime.Object - // FuzzFuncs is an optional set of custom fuzzing functions to use to construct FuzzedObjects. - // They *must* not use any random source other than the passed-in fuzzer. - FuzzFuncs []interface{} + // FillFuncs is an optional map of custom functions to use to fill instances of particular types. + FillFuncs map[reflect.Type]FillFunc JSON runtime.Serializer YAML runtime.Serializer Proto runtime.Serializer } +// FillFunc is a function that populates all serializable fields in obj. +// s and i are string and integer values relevant to the object being populated +// (for example, the json key or protobuf tag containing the object) +// that can be used when filling the object to make the object content identifiable +type FillFunc func(s string, i int, obj interface{}) + func NewCompatibilityTestOptions(scheme *runtime.Scheme) *CompatibilityTestOptions { return &CompatibilityTestOptions{Scheme: scheme} } @@ -162,19 +161,23 @@ func (c *CompatibilityTestOptions) Complete(t *testing.T) *CompatibilityTestOpti return false }) - // Fuzz any missing objects - if c.FuzzedObjects == nil { - c.FuzzedObjects = map[schema.GroupVersionKind]runtime.Object{} + // Fill any missing objects + if c.FilledObjects == nil { + c.FilledObjects = map[schema.GroupVersionKind]runtime.Object{} + } + fillFuncs := defaultFillFuncs() + for k, v := range c.FillFuncs { + fillFuncs[k] = v } for _, gvk := range c.Kinds { - if _, ok := c.FuzzedObjects[gvk]; ok { + if _, ok := c.FilledObjects[gvk]; ok { continue } - obj, err := CompatibilityTestObject(c.Scheme, gvk, c.FuzzFuncs) + obj, err := CompatibilityTestObject(c.Scheme, gvk, fillFuncs) if err != nil { t.Fatal(err) } - c.FuzzedObjects[gvk] = obj + c.FilledObjects[gvk] = obj } if c.JSON == nil { @@ -190,105 +193,47 @@ func (c *CompatibilityTestOptions) Complete(t *testing.T) *CompatibilityTestOpti return c } -// CompatibilityTestObject returns a deterministically fuzzed object for the specified GVK -func CompatibilityTestObject(scheme *runtime.Scheme, gvk schema.GroupVersionKind, fuzzFuncs []interface{}) (runtime.Object, error) { - // Construct the object - obj, err := scheme.New(gvk) - if err != nil { - return nil, err - } - - // Fuzz it - CompatibilityTestFuzzer(scheme, fuzzFuncs).Fuzz(obj) - - // Set the kind and apiVersion - if typeAcc, err := apimeta.TypeAccessor(obj); err != nil { - return nil, err - } else { - typeAcc.SetKind(gvk.Kind) - typeAcc.SetAPIVersion(gvk.GroupVersion().String()) - } - - return obj, nil -} - -// CompatibilityTestFuzzer returns a fuzzer for the given scheme: -// - fixed seed (deterministic output that lets us generate the same fixtures on every run) -// - 0 nil chance (populate all fields) -// - 1 numelements (populate and bound all lists) -// - 20 max depth (don't recurse infinitely) -// - meta fuzzing functions added -// - custom fuzzing functions to make strings and managedFields more readable in fixtures -func CompatibilityTestFuzzer(scheme *runtime.Scheme, fuzzFuncs []interface{}) *fuzz.Fuzzer { - fuzzer := fuzz.NewWithSeed(0).NilChance(0).NumElements(1, 1).MaxDepth(20) - fuzzer = fuzzer.Funcs(genericfuzzer.Funcs(serializer.NewCodecFactory(scheme))...) - fuzzString := 1 - fuzzIntOrString := 1 - fuzzMicroTime := int64(1) - fuzzer.Funcs( - // avoid crazy strings - func(s *string, c fuzz.Continue) { - fuzzString++ - *s = strconv.Itoa(fuzzString) - }, - func(i **intstr.IntOrString, c fuzz.Continue) { - fuzzIntOrString++ - tmp := intstr.FromInt(fuzzIntOrString) - _ = tmp - *i = &tmp - }, - func(t **metav1.MicroTime, c fuzz.Continue) { - if t != nil && *t != nil { - // use type-defined fuzzing for non-nil objects - (*t).Fuzz(c) - return - } - fuzzMicroTime++ - tmp := metav1.NewMicroTime(time.Unix(fuzzMicroTime, 0)) - *t = &tmp - }, - // limit managed fields to two levels - func(f *[]metav1.ManagedFieldsEntry, c fuzz.Continue) { - field := metav1.ManagedFieldsEntry{} - c.Fuzz(&field) - if field.FieldsV1 != nil { - field.FieldsV1.Raw = []byte("{}") - } - *f = []metav1.ManagedFieldsEntry{field} - }, - func(r *runtime.RawExtension, c fuzz.Continue) { - // generate a raw object in normalized form - // TODO: test non-normalized round-tripping... YAMLToJSON normalizes and makes exact comparisons fail - r.Raw = []byte(`{"apiVersion":"example.com/v1","kind":"CustomType","spec":{"replicas":1},"status":{"available":1}}`) - }, - ) - fuzzer.Funcs(fuzzFuncs...) - return fuzzer -} - func (c *CompatibilityTestOptions) Run(t *testing.T) { + usedHEADFixtures := sets.NewString() + for _, gvk := range c.Kinds { t.Run(makeName(gvk), func(t *testing.T) { t.Run("HEAD", func(t *testing.T) { - c.runCurrentVersionTest(t, gvk) + c.runCurrentVersionTest(t, gvk, usedHEADFixtures) }) for _, previousVersionDir := range c.TestDataDirsPreviousVersions { t.Run(filepath.Base(previousVersionDir), func(t *testing.T) { - c.runPreviousVersionTest(t, gvk, previousVersionDir) + c.runPreviousVersionTest(t, gvk, previousVersionDir, nil) }) } }) } + + // Check for unused HEAD fixtures + t.Run("unused_fixtures", func(t *testing.T) { + files, err := os.ReadDir(c.TestDataDirCurrentVersion) + if err != nil { + t.Fatal(err) + } + allFixtures := sets.NewString() + for _, file := range files { + allFixtures.Insert(file.Name()) + } + + if unused := allFixtures.Difference(usedHEADFixtures); len(unused) > 0 { + t.Fatalf("remove unused fixtures from %s:\n%s", c.TestDataDirCurrentVersion, strings.Join(unused.List(), "\n")) + } + }) } -func (c *CompatibilityTestOptions) runCurrentVersionTest(t *testing.T, gvk schema.GroupVersionKind) { - expectedObject := c.FuzzedObjects[gvk] +func (c *CompatibilityTestOptions) runCurrentVersionTest(t *testing.T, gvk schema.GroupVersionKind, usedFiles sets.String) { + expectedObject := c.FilledObjects[gvk] expectedJSON, expectedYAML, expectedProto := c.encode(t, expectedObject) - actualJSON, actualYAML, actualProto, err := read(c.TestDataDirCurrentVersion, gvk, "") + actualJSON, actualYAML, actualProto, err := read(c.TestDataDirCurrentVersion, gvk, "", usedFiles) if err != nil && !os.IsNotExist(err) { t.Fatal(err) } @@ -336,8 +281,14 @@ func (c *CompatibilityTestOptions) runCurrentVersionTest(t *testing.T, gvk schem t.Fatal(err) } { + // compact before decoding since embedded RawExtension fields retain indenting + compacted := &bytes.Buffer{} + if err := gojson.Compact(compacted, actualJSON); err != nil { + t.Error(err) + } + jsonDecoded := emptyObj.DeepCopyObject() - jsonDecoded, _, err = c.JSON.Decode(actualJSON, &gvk, jsonDecoded) + jsonDecoded, _, err = c.JSON.Decode(compacted.Bytes(), &gvk, jsonDecoded) if err != nil { t.Error(err) } else if !apiequality.Semantic.DeepEqual(expectedObject, jsonDecoded) { @@ -380,10 +331,18 @@ func (c *CompatibilityTestOptions) encode(t *testing.T, obj runtime.Object) (jso return jsonBytes.Bytes(), yamlBytes.Bytes(), protoBytes.Bytes() } -func read(dir string, gvk schema.GroupVersionKind, suffix string) (json, yaml, proto []byte, err error) { - actualJSON, jsonErr := ioutil.ReadFile(filepath.Join(dir, makeName(gvk)+suffix+".json")) - actualYAML, yamlErr := ioutil.ReadFile(filepath.Join(dir, makeName(gvk)+suffix+".yaml")) - actualProto, protoErr := ioutil.ReadFile(filepath.Join(dir, makeName(gvk)+suffix+".pb")) +func read(dir string, gvk schema.GroupVersionKind, suffix string, usedFiles sets.String) (json, yaml, proto []byte, err error) { + jsonFilename := makeName(gvk) + suffix + ".json" + actualJSON, jsonErr := ioutil.ReadFile(filepath.Join(dir, jsonFilename)) + yamlFilename := makeName(gvk) + suffix + ".yaml" + actualYAML, yamlErr := ioutil.ReadFile(filepath.Join(dir, yamlFilename)) + protoFilename := makeName(gvk) + suffix + ".pb" + actualProto, protoErr := ioutil.ReadFile(filepath.Join(dir, protoFilename)) + if usedFiles != nil { + usedFiles.Insert(jsonFilename) + usedFiles.Insert(yamlFilename) + usedFiles.Insert(protoFilename) + } if jsonErr != nil { return actualJSON, actualYAML, actualProto, jsonErr } @@ -405,8 +364,8 @@ func writeFile(t *testing.T, dir string, gvk schema.GroupVersionKind, suffix, ex } } -func (c *CompatibilityTestOptions) runPreviousVersionTest(t *testing.T, gvk schema.GroupVersionKind, previousVersionDir string) { - jsonBeforeRoundTrip, yamlBeforeRoundTrip, protoBeforeRoundTrip, err := read(previousVersionDir, gvk, "") +func (c *CompatibilityTestOptions) runPreviousVersionTest(t *testing.T, gvk schema.GroupVersionKind, previousVersionDir string, usedFiles sets.String) { + jsonBeforeRoundTrip, yamlBeforeRoundTrip, protoBeforeRoundTrip, err := read(previousVersionDir, gvk, "", usedFiles) if os.IsNotExist(err) || (len(jsonBeforeRoundTrip) == 0 && len(yamlBeforeRoundTrip) == 0 && len(protoBeforeRoundTrip) == 0) { t.SkipNow() return @@ -420,8 +379,14 @@ func (c *CompatibilityTestOptions) runPreviousVersionTest(t *testing.T, gvk sche t.Fatal(err) } + // compact before decoding since embedded RawExtension fields retain indenting + compacted := &bytes.Buffer{} + if err := gojson.Compact(compacted, jsonBeforeRoundTrip); err != nil { + t.Fatal(err) + } + jsonDecoded := emptyObj.DeepCopyObject() - jsonDecoded, _, err = c.JSON.Decode(jsonBeforeRoundTrip, &gvk, jsonDecoded) + jsonDecoded, _, err = c.JSON.Decode(compacted.Bytes(), &gvk, jsonDecoded) if err != nil { t.Fatal(err) } @@ -457,7 +422,7 @@ func (c *CompatibilityTestOptions) runPreviousVersionTest(t *testing.T, gvk sche } protoAfterRoundTrip := protoBytes.Bytes() - expectedJSONAfterRoundTrip, expectedYAMLAfterRoundTrip, expectedProtoAfterRoundTrip, _ := read(previousVersionDir, gvk, ".after_roundtrip") + expectedJSONAfterRoundTrip, expectedYAMLAfterRoundTrip, expectedProtoAfterRoundTrip, _ := read(previousVersionDir, gvk, ".after_roundtrip", usedFiles) if len(expectedJSONAfterRoundTrip) == 0 { expectedJSONAfterRoundTrip = jsonBeforeRoundTrip } diff --git a/pkg/api/apitesting/roundtrip/construct.go b/pkg/api/apitesting/roundtrip/construct.go new file mode 100644 index 000000000..e78c1f8fc --- /dev/null +++ b/pkg/api/apitesting/roundtrip/construct.go @@ -0,0 +1,179 @@ +/* +Copyright 2022 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 roundtrip + +import ( + "fmt" + "reflect" + "strconv" + "strings" + "time" + + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/intstr" +) + +func defaultFillFuncs() map[reflect.Type]FillFunc { + funcs := map[reflect.Type]FillFunc{} + funcs[reflect.TypeOf(&runtime.RawExtension{})] = func(s string, i int, obj interface{}) { + // generate a raw object in normalized form + // TODO: test non-normalized round-tripping... YAMLToJSON normalizes and makes exact comparisons fail + obj.(*runtime.RawExtension).Raw = []byte(`{"apiVersion":"example.com/v1","kind":"CustomType","spec":{"replicas":1},"status":{"available":1}}`) + } + funcs[reflect.TypeOf(&metav1.TypeMeta{})] = func(s string, i int, obj interface{}) { + // APIVersion and Kind are not serialized in all formats (notably protobuf), so clear by default for cross-format checking. + obj.(*metav1.TypeMeta).APIVersion = "" + obj.(*metav1.TypeMeta).Kind = "" + } + funcs[reflect.TypeOf(&metav1.FieldsV1{})] = func(s string, i int, obj interface{}) { + obj.(*metav1.FieldsV1).Raw = []byte(`{}`) + } + funcs[reflect.TypeOf(&metav1.Time{})] = func(s string, i int, obj interface{}) { + // use the integer as an offset from the year + obj.(*metav1.Time).Time = time.Date(2000+i, 1, 1, 1, 1, 1, 0, time.UTC) + } + funcs[reflect.TypeOf(&metav1.MicroTime{})] = func(s string, i int, obj interface{}) { + // use the integer as an offset from the year, and as a microsecond + obj.(*metav1.MicroTime).Time = time.Date(2000+i, 1, 1, 1, 1, 1, i*int(time.Microsecond), time.UTC) + } + funcs[reflect.TypeOf(&intstr.IntOrString{})] = func(s string, i int, obj interface{}) { + // use the string as a string value + obj.(*intstr.IntOrString).Type = intstr.String + obj.(*intstr.IntOrString).StrVal = s + "Value" + } + return funcs +} + +// CompatibilityTestObject returns a deterministically filled object for the specified GVK +func CompatibilityTestObject(scheme *runtime.Scheme, gvk schema.GroupVersionKind, fillFuncs map[reflect.Type]FillFunc) (runtime.Object, error) { + // Construct the object + obj, err := scheme.New(gvk) + if err != nil { + return nil, err + } + + fill("", 0, reflect.TypeOf(obj), reflect.ValueOf(obj), fillFuncs, map[reflect.Type]bool{}) + + // Set the kind and apiVersion + if typeAcc, err := apimeta.TypeAccessor(obj); err != nil { + return nil, err + } else { + typeAcc.SetKind(gvk.Kind) + typeAcc.SetAPIVersion(gvk.GroupVersion().String()) + } + + return obj, nil +} + +func fill(dataString string, dataInt int, t reflect.Type, v reflect.Value, fillFuncs map[reflect.Type]FillFunc, filledTypes map[reflect.Type]bool) { + if filledTypes[t] { + // we already filled this type, avoid recursing infinitely + return + } + filledTypes[t] = true + defer delete(filledTypes, t) + + // if nil, populate pointers with a zero-value instance of the underlying type + if t.Kind() == reflect.Pointer && v.IsNil() { + if v.CanSet() { + v.Set(reflect.New(t.Elem())) + } else if v.IsNil() { + panic(fmt.Errorf("unsettable nil pointer of type %v in field %s", t, dataString)) + } + } + + if f, ok := fillFuncs[t]; ok { + // use the custom fill function for this type + f(dataString, dataInt, v.Interface()) + return + } + + switch t.Kind() { + case reflect.Slice: + // populate with a single-item slice + v.Set(reflect.MakeSlice(t, 1, 1)) + // recurse to populate the item, preserving the data context + fill(dataString, dataInt, t.Elem(), v.Index(0), fillFuncs, filledTypes) + + case reflect.Map: + // construct the key, which must be a string type, possibly converted to a type alias of string + key := reflect.ValueOf(dataString + "Key").Convert(t.Key()) + // construct a zero-value item + item := reflect.New(t.Elem()) + // recurse to populate the item, preserving the data context + fill(dataString, dataInt, t.Elem(), item.Elem(), fillFuncs, filledTypes) + // store in the map + v.Set(reflect.MakeMap(t)) + v.SetMapIndex(key, item.Elem()) + + case reflect.Struct: + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + + if !field.IsExported() { + continue + } + + // use the json field name, which must be stable + dataString := strings.Split(field.Tag.Get("json"), ",")[0] + if len(dataString) == 0 { + // fall back to the struct field name if there is no json field name + dataString = " " + field.Name + } + + // use the protobuf tag, which must be stable + dataInt := 0 + if protobufTagParts := strings.Split(field.Tag.Get("protobuf"), ","); len(protobufTagParts) > 1 { + if tag, err := strconv.Atoi(protobufTagParts[1]); err != nil { + panic(err) + } else { + dataInt = tag + } + } + if dataInt == 0 { + // fall back to the length of dataString as a backup + dataInt = -len(dataString) + } + + fieldType := field.Type + fieldValue := v.Field(i) + + fill(dataString, dataInt, reflect.PointerTo(fieldType), fieldValue.Addr(), fillFuncs, filledTypes) + } + + case reflect.Pointer: + fill(dataString, dataInt, t.Elem(), v.Elem(), fillFuncs, filledTypes) + + case reflect.String: + // use Convert to set into string alias types correctly + v.Set(reflect.ValueOf(dataString + "Value").Convert(t)) + + case reflect.Bool: + // set to true to ensure we serialize omitempty fields + v.Set(reflect.ValueOf(true).Convert(t)) + + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + // use Convert to set into int alias types and different int widths correctly + v.Set(reflect.ValueOf(dataInt).Convert(t)) + + default: + panic(fmt.Errorf("unhandled type %v in field %s", t, dataString)) + } +} diff --git a/pkg/api/apitesting/roundtrip/fuzz_norace.go b/pkg/api/apitesting/roundtrip/fuzz_norace.go index 8cf063b1e..c655b3ab7 100644 --- a/pkg/api/apitesting/roundtrip/fuzz_norace.go +++ b/pkg/api/apitesting/roundtrip/fuzz_norace.go @@ -1,3 +1,4 @@ +//go:build !race // +build !race /* diff --git a/pkg/api/apitesting/roundtrip/fuzz_race.go b/pkg/api/apitesting/roundtrip/fuzz_race.go index 1ec3366ae..fa252960f 100644 --- a/pkg/api/apitesting/roundtrip/fuzz_race.go +++ b/pkg/api/apitesting/roundtrip/fuzz_race.go @@ -1,3 +1,4 @@ +//go:build race // +build race /* diff --git a/pkg/api/apitesting/roundtrip/roundtrip.go b/pkg/api/apitesting/roundtrip/roundtrip.go index 0c320e578..61868c82a 100644 --- a/pkg/api/apitesting/roundtrip/roundtrip.go +++ b/pkg/api/apitesting/roundtrip/roundtrip.go @@ -25,7 +25,7 @@ import ( "testing" "github.com/davecgh/go-spew/spew" - //lint:ignore SA1019 Keep using deprecated module; it still seems to be maintained and the api of the recommended replacement differs + //nolint:staticcheck //iccheck // SA1019 Keep using deprecated module; it still seems to be maintained and the api of the recommended replacement differs "github.com/golang/protobuf/proto" fuzz "github.com/google/gofuzz" flag "github.com/spf13/pflag" @@ -107,16 +107,15 @@ var globalNonRoundTrippableTypes = sets.NewString( // GlobalNonRoundTrippableTypes returns the kinds that are effectively reserved across all GroupVersions. // They don't roundtrip and thus can be excluded in any custom/downstream roundtrip tests // -// kinds := scheme.AllKnownTypes() -// for gvk := range kinds { -// if roundtrip.GlobalNonRoundTrippableTypes().Has(gvk.Kind) { -// continue -// } -// t.Run(gvk.Group+"."+gvk.Version+"."+gvk.Kind, func(t *testing.T) { -// // roundtrip test -// }) -// } -// +// kinds := scheme.AllKnownTypes() +// for gvk := range kinds { +// if roundtrip.GlobalNonRoundTrippableTypes().Has(gvk.Kind) { +// continue +// } +// t.Run(gvk.Group+"."+gvk.Version+"."+gvk.Kind, func(t *testing.T) { +// // roundtrip test +// }) +// } func GlobalNonRoundTrippableTypes() sets.String { return sets.NewString(globalNonRoundTrippableTypes.List()...) } @@ -294,11 +293,11 @@ func roundTripOfExternalType(t *testing.T, scheme *runtime.Scheme, codecFactory // // For internal types this means // -// internal -> external -> json/protobuf -> external -> internal. +// internal -> external -> json/protobuf -> external -> internal. // // For external types this means // -// external -> json/protobuf -> external. +// external -> json/protobuf -> external. func roundTrip(t *testing.T, scheme *runtime.Scheme, codec runtime.Codec, object runtime.Object) { printer := spew.ConfigState{DisableMethods: true} original := object diff --git a/pkg/api/errors/OWNERS b/pkg/api/errors/OWNERS index d4c73022f..155648acb 100644 --- a/pkg/api/errors/OWNERS +++ b/pkg/api/errors/OWNERS @@ -1,20 +1,17 @@ # See the OWNERS docs at https://go.k8s.io/owners reviewers: -- thockin -- lavalamp -- smarterclayton -- wojtek-t -- deads2k -- brendandburns -- derekwaynecarr -- caesarxuchao -- mikedanese -- liggitt -- saad-ali -- janetkuo -- tallclair -- dims -- hongchaodeng -- krousey -- cjcullen + - thockin + - lavalamp + - smarterclayton + - wojtek-t + - deads2k + - derekwaynecarr + - caesarxuchao + - mikedanese + - liggitt + - saad-ali + - janetkuo + - tallclair + - dims + - cjcullen diff --git a/pkg/api/errors/errors.go b/pkg/api/errors/errors.go index 31eddfc1e..57e0e71f6 100644 --- a/pkg/api/errors/errors.go +++ b/pkg/api/errors/errors.go @@ -44,6 +44,28 @@ type APIStatus interface { var _ error = &StatusError{} +var knownReasons = map[metav1.StatusReason]struct{}{ + // metav1.StatusReasonUnknown : {} + metav1.StatusReasonUnauthorized: {}, + metav1.StatusReasonForbidden: {}, + metav1.StatusReasonNotFound: {}, + metav1.StatusReasonAlreadyExists: {}, + metav1.StatusReasonConflict: {}, + metav1.StatusReasonGone: {}, + metav1.StatusReasonInvalid: {}, + metav1.StatusReasonServerTimeout: {}, + metav1.StatusReasonTimeout: {}, + metav1.StatusReasonTooManyRequests: {}, + metav1.StatusReasonBadRequest: {}, + metav1.StatusReasonMethodNotAllowed: {}, + metav1.StatusReasonNotAcceptable: {}, + metav1.StatusReasonRequestEntityTooLarge: {}, + metav1.StatusReasonUnsupportedMediaType: {}, + metav1.StatusReasonInternalError: {}, + metav1.StatusReasonExpired: {}, + metav1.StatusReasonServiceUnavailable: {}, +} + // Error implements the Error interface. func (e *StatusError) Error() string { return e.ErrStatus.Message @@ -65,21 +87,21 @@ func (e *StatusError) DebugError() (string, []interface{}) { // HasStatusCause returns true if the provided error has a details cause // with the provided type name. +// It supports wrapped errors and returns false when the error is nil. func HasStatusCause(err error, name metav1.CauseType) bool { _, ok := StatusCause(err, name) return ok } // StatusCause returns the named cause from the provided error if it exists and -// the error is of the type APIStatus. Otherwise it returns false. +// the error unwraps to the type APIStatus. Otherwise it returns false. func StatusCause(err error, name metav1.CauseType) (metav1.StatusCause, bool) { - apierr, ok := err.(APIStatus) - if !ok || apierr == nil || apierr.Status().Details == nil { - return metav1.StatusCause{}, false - } - for _, cause := range apierr.Status().Details.Causes { - if cause.Type == name { - return cause, true + status, ok := err.(APIStatus) + if (ok || errors.As(err, &status)) && status.Status().Details != nil { + for _, cause := range status.Status().Details.Causes { + if cause.Type == name { + return cause, true + } } } return metav1.StatusCause{}, false @@ -148,6 +170,25 @@ func NewAlreadyExists(qualifiedResource schema.GroupResource, name string) *Stat }} } +// NewGenerateNameConflict returns an error indicating the server +// was not able to generate a valid name for a resource. +func NewGenerateNameConflict(qualifiedResource schema.GroupResource, name string, retryAfterSeconds int) *StatusError { + return &StatusError{metav1.Status{ + Status: metav1.StatusFailure, + Code: http.StatusConflict, + Reason: metav1.StatusReasonAlreadyExists, + Details: &metav1.StatusDetails{ + Group: qualifiedResource.Group, + Kind: qualifiedResource.Resource, + Name: name, + RetryAfterSeconds: int32(retryAfterSeconds), + }, + Message: fmt.Sprintf( + "%s %q already exists, the server was not able to generate a unique name for the object", + qualifiedResource.String(), name), + }} +} + // NewUnauthorized returns an error indicating the client is not authorized to perform the requested // action. func NewUnauthorized(reason string) *StatusError { @@ -248,7 +289,7 @@ func NewInvalid(qualifiedKind schema.GroupKind, name string, errs field.ErrorLis Field: err.Field, }) } - return &StatusError{metav1.Status{ + err := &StatusError{metav1.Status{ Status: metav1.StatusFailure, Code: http.StatusUnprocessableEntity, Reason: metav1.StatusReasonInvalid, @@ -258,8 +299,14 @@ func NewInvalid(qualifiedKind schema.GroupKind, name string, errs field.ErrorLis Name: name, Causes: causes, }, - Message: fmt.Sprintf("%s %q is invalid: %v", qualifiedKind.String(), name, errs.ToAggregate()), }} + aggregatedErrs := errs.ToAggregate() + if aggregatedErrs == nil { + err.ErrStatus.Message = fmt.Sprintf("%s %q is invalid", qualifiedKind.String(), name) + } else { + err.ErrStatus.Message = fmt.Sprintf("%s %q is invalid: %v", qualifiedKind.String(), name, aggregatedErrs) + } + return err } // NewBadRequest creates an error that indicates that the request is invalid and can not be processed. @@ -476,138 +523,242 @@ func NewGenericServerResponse(code int, verb string, qualifiedResource schema.Gr } // IsNotFound returns true if the specified error was created by NewNotFound. -// It supports wrapped errors. +// It supports wrapped errors and returns false when the error is nil. func IsNotFound(err error) bool { - return ReasonForError(err) == metav1.StatusReasonNotFound + reason, code := reasonAndCodeForError(err) + if reason == metav1.StatusReasonNotFound { + return true + } + if _, ok := knownReasons[reason]; !ok && code == http.StatusNotFound { + return true + } + return false } // IsAlreadyExists determines if the err is an error which indicates that a specified resource already exists. -// It supports wrapped errors. +// It supports wrapped errors and returns false when the error is nil. func IsAlreadyExists(err error) bool { return ReasonForError(err) == metav1.StatusReasonAlreadyExists } // IsConflict determines if the err is an error which indicates the provided update conflicts. -// It supports wrapped errors. +// It supports wrapped errors and returns false when the error is nil. func IsConflict(err error) bool { - return ReasonForError(err) == metav1.StatusReasonConflict + reason, code := reasonAndCodeForError(err) + if reason == metav1.StatusReasonConflict { + return true + } + if _, ok := knownReasons[reason]; !ok && code == http.StatusConflict { + return true + } + return false } // IsInvalid determines if the err is an error which indicates the provided resource is not valid. -// It supports wrapped errors. +// It supports wrapped errors and returns false when the error is nil. func IsInvalid(err error) bool { - return ReasonForError(err) == metav1.StatusReasonInvalid + reason, code := reasonAndCodeForError(err) + if reason == metav1.StatusReasonInvalid { + return true + } + if _, ok := knownReasons[reason]; !ok && code == http.StatusUnprocessableEntity { + return true + } + return false } // IsGone is true if the error indicates the requested resource is no longer available. -// It supports wrapped errors. +// It supports wrapped errors and returns false when the error is nil. func IsGone(err error) bool { - return ReasonForError(err) == metav1.StatusReasonGone + reason, code := reasonAndCodeForError(err) + if reason == metav1.StatusReasonGone { + return true + } + if _, ok := knownReasons[reason]; !ok && code == http.StatusGone { + return true + } + return false } // IsResourceExpired is true if the error indicates the resource has expired and the current action is // no longer possible. -// It supports wrapped errors. +// It supports wrapped errors and returns false when the error is nil. func IsResourceExpired(err error) bool { return ReasonForError(err) == metav1.StatusReasonExpired } // IsNotAcceptable determines if err is an error which indicates that the request failed due to an invalid Accept header -// It supports wrapped errors. +// It supports wrapped errors and returns false when the error is nil. func IsNotAcceptable(err error) bool { - return ReasonForError(err) == metav1.StatusReasonNotAcceptable + reason, code := reasonAndCodeForError(err) + if reason == metav1.StatusReasonNotAcceptable { + return true + } + if _, ok := knownReasons[reason]; !ok && code == http.StatusNotAcceptable { + return true + } + return false } // IsUnsupportedMediaType determines if err is an error which indicates that the request failed due to an invalid Content-Type header -// It supports wrapped errors. +// It supports wrapped errors and returns false when the error is nil. func IsUnsupportedMediaType(err error) bool { - return ReasonForError(err) == metav1.StatusReasonUnsupportedMediaType + reason, code := reasonAndCodeForError(err) + if reason == metav1.StatusReasonUnsupportedMediaType { + return true + } + if _, ok := knownReasons[reason]; !ok && code == http.StatusUnsupportedMediaType { + return true + } + return false } // IsMethodNotSupported determines if the err is an error which indicates the provided action could not // be performed because it is not supported by the server. -// It supports wrapped errors. +// It supports wrapped errors and returns false when the error is nil. func IsMethodNotSupported(err error) bool { - return ReasonForError(err) == metav1.StatusReasonMethodNotAllowed + reason, code := reasonAndCodeForError(err) + if reason == metav1.StatusReasonMethodNotAllowed { + return true + } + if _, ok := knownReasons[reason]; !ok && code == http.StatusMethodNotAllowed { + return true + } + return false } // IsServiceUnavailable is true if the error indicates the underlying service is no longer available. -// It supports wrapped errors. +// It supports wrapped errors and returns false when the error is nil. func IsServiceUnavailable(err error) bool { - return ReasonForError(err) == metav1.StatusReasonServiceUnavailable + reason, code := reasonAndCodeForError(err) + if reason == metav1.StatusReasonServiceUnavailable { + return true + } + if _, ok := knownReasons[reason]; !ok && code == http.StatusServiceUnavailable { + return true + } + return false } // IsBadRequest determines if err is an error which indicates that the request is invalid. -// It supports wrapped errors. +// It supports wrapped errors and returns false when the error is nil. func IsBadRequest(err error) bool { - return ReasonForError(err) == metav1.StatusReasonBadRequest + reason, code := reasonAndCodeForError(err) + if reason == metav1.StatusReasonBadRequest { + return true + } + if _, ok := knownReasons[reason]; !ok && code == http.StatusBadRequest { + return true + } + return false } // IsUnauthorized determines if err is an error which indicates that the request is unauthorized and // requires authentication by the user. -// It supports wrapped errors. +// It supports wrapped errors and returns false when the error is nil. func IsUnauthorized(err error) bool { - return ReasonForError(err) == metav1.StatusReasonUnauthorized + reason, code := reasonAndCodeForError(err) + if reason == metav1.StatusReasonUnauthorized { + return true + } + if _, ok := knownReasons[reason]; !ok && code == http.StatusUnauthorized { + return true + } + return false } // IsForbidden determines if err is an error which indicates that the request is forbidden and cannot // be completed as requested. -// It supports wrapped errors. +// It supports wrapped errors and returns false when the error is nil. func IsForbidden(err error) bool { - return ReasonForError(err) == metav1.StatusReasonForbidden + reason, code := reasonAndCodeForError(err) + if reason == metav1.StatusReasonForbidden { + return true + } + if _, ok := knownReasons[reason]; !ok && code == http.StatusForbidden { + return true + } + return false } // IsTimeout determines if err is an error which indicates that request times out due to long // processing. -// It supports wrapped errors. +// It supports wrapped errors and returns false when the error is nil. func IsTimeout(err error) bool { - return ReasonForError(err) == metav1.StatusReasonTimeout + reason, code := reasonAndCodeForError(err) + if reason == metav1.StatusReasonTimeout { + return true + } + if _, ok := knownReasons[reason]; !ok && code == http.StatusGatewayTimeout { + return true + } + return false } // IsServerTimeout determines if err is an error which indicates that the request needs to be retried // by the client. -// It supports wrapped errors. +// It supports wrapped errors and returns false when the error is nil. func IsServerTimeout(err error) bool { + // do not check the status code, because no https status code exists that can + // be scoped to retryable timeouts. return ReasonForError(err) == metav1.StatusReasonServerTimeout } // IsInternalError determines if err is an error which indicates an internal server error. -// It supports wrapped errors. +// It supports wrapped errors and returns false when the error is nil. func IsInternalError(err error) bool { - return ReasonForError(err) == metav1.StatusReasonInternalError + reason, code := reasonAndCodeForError(err) + if reason == metav1.StatusReasonInternalError { + return true + } + if _, ok := knownReasons[reason]; !ok && code == http.StatusInternalServerError { + return true + } + return false } // IsTooManyRequests determines if err is an error which indicates that there are too many requests // that the server cannot handle. -// It supports wrapped errors. +// It supports wrapped errors and returns false when the error is nil. func IsTooManyRequests(err error) bool { - if ReasonForError(err) == metav1.StatusReasonTooManyRequests { + reason, code := reasonAndCodeForError(err) + if reason == metav1.StatusReasonTooManyRequests { return true } - if status := APIStatus(nil); errors.As(err, &status) { - return status.Status().Code == http.StatusTooManyRequests + + // IsTooManyRequests' checking of code predates the checking of the code in + // the other Is* functions. In order to maintain backward compatibility, this + // does not check that the reason is unknown. + if code == http.StatusTooManyRequests { + return true } return false } // IsRequestEntityTooLargeError determines if err is an error which indicates // the request entity is too large. -// It supports wrapped errors. +// It supports wrapped errors and returns false when the error is nil. func IsRequestEntityTooLargeError(err error) bool { - if ReasonForError(err) == metav1.StatusReasonRequestEntityTooLarge { + reason, code := reasonAndCodeForError(err) + if reason == metav1.StatusReasonRequestEntityTooLarge { return true } - if status := APIStatus(nil); errors.As(err, &status) { - return status.Status().Code == http.StatusRequestEntityTooLarge + + // IsRequestEntityTooLargeError's checking of code predates the checking of + // the code in the other Is* functions. In order to maintain backward + // compatibility, this does not check that the reason is unknown. + if code == http.StatusRequestEntityTooLarge { + return true } return false } // IsUnexpectedServerError returns true if the server response was not in the expected API format, // and may be the result of another HTTP actor. -// It supports wrapped errors. +// It supports wrapped errors and returns false when the error is nil. func IsUnexpectedServerError(err error) bool { - if status := APIStatus(nil); errors.As(err, &status) && status.Status().Details != nil { + status, ok := err.(APIStatus) + if (ok || errors.As(err, &status)) && status.Status().Details != nil { for _, cause := range status.Status().Details.Causes { if cause.Type == metav1.CauseTypeUnexpectedServerResponse { return true @@ -618,19 +769,20 @@ func IsUnexpectedServerError(err error) bool { } // IsUnexpectedObjectError determines if err is due to an unexpected object from the master. -// It supports wrapped errors. +// It supports wrapped errors and returns false when the error is nil. func IsUnexpectedObjectError(err error) bool { - uoe := &UnexpectedObjectError{} - return err != nil && errors.As(err, &uoe) + uoe, ok := err.(*UnexpectedObjectError) + return err != nil && (ok || errors.As(err, &uoe)) } // SuggestsClientDelay returns true if this error suggests a client delay as well as the // suggested seconds to wait, or false if the error does not imply a wait. It does not // address whether the error *should* be retried, since some errors (like a 3xx) may // request delay without retry. -// It supports wrapped errors. +// It supports wrapped errors and returns false when the error is nil. func SuggestsClientDelay(err error) (int, bool) { - if t := APIStatus(nil); errors.As(err, &t) && t.Status().Details != nil { + t, ok := err.(APIStatus) + if (ok || errors.As(err, &t)) && t.Status().Details != nil { switch t.Status().Reason { // this StatusReason explicitly requests the caller to delay the action case metav1.StatusReasonServerTimeout: @@ -645,14 +797,22 @@ func SuggestsClientDelay(err error) (int, bool) { } // ReasonForError returns the HTTP status for a particular error. -// It supports wrapped errors. +// It supports wrapped errors and returns StatusReasonUnknown when +// the error is nil or doesn't have a status. func ReasonForError(err error) metav1.StatusReason { - if status := APIStatus(nil); errors.As(err, &status) { + if status, ok := err.(APIStatus); ok || errors.As(err, &status) { return status.Status().Reason } return metav1.StatusReasonUnknown } +func reasonAndCodeForError(err error) (metav1.StatusReason, int32) { + if status, ok := err.(APIStatus); ok || errors.As(err, &status) { + return status.Status().Reason, status.Status().Code + } + return metav1.StatusReasonUnknown, 0 +} + // ErrorReporter converts generic errors into runtime.Object errors without // requiring the caller to take a dependency on meta/v1 (where Status lives). // This prevents circular dependencies in core watch code. diff --git a/pkg/api/errors/errors_test.go b/pkg/api/errors/errors_test.go index 6b9e3b810..b67bf885b 100644 --- a/pkg/api/errors/errors_test.go +++ b/pkg/api/errors/errors_test.go @@ -64,7 +64,7 @@ func TestErrorNew(t *testing.T) { } if !IsConflict(NewConflict(resource("tests"), "2", errors.New("message"))) { - t.Errorf("expected to be conflict") + t.Errorf("expected to be %s", metav1.StatusReasonAlreadyExists) } if !IsNotFound(NewNotFound(resource("tests"), "3")) { t.Errorf("expected to be %s", metav1.StatusReasonNotFound) @@ -88,6 +88,13 @@ func TestErrorNew(t *testing.T) { t.Errorf("expected to be %s", metav1.StatusReasonMethodNotAllowed) } + if !IsAlreadyExists(NewGenerateNameConflict(resource("tests"), "3", 1)) { + t.Errorf("expected to be %s", metav1.StatusReasonAlreadyExists) + } + if time, ok := SuggestsClientDelay(NewGenerateNameConflict(resource("tests"), "3", 1)); time != 1 || !ok { + t.Errorf("unexpected %d", time) + } + if time, ok := SuggestsClientDelay(NewServerTimeout(resource("tests"), "doing something", 10)); time != 10 || !ok { t.Errorf("unexpected %d", time) } @@ -118,6 +125,7 @@ func TestNewInvalid(t *testing.T) { testCases := []struct { Err *field.Error Details *metav1.StatusDetails + Msg string }{ { field.Duplicate(field.NewPath("field[0].name"), "bar"), @@ -129,6 +137,7 @@ func TestNewInvalid(t *testing.T) { Field: "field[0].name", }}, }, + `Kind "name" is invalid: field[0].name: Duplicate value: "bar"`, }, { field.Invalid(field.NewPath("field[0].name"), "bar", "detail"), @@ -140,6 +149,7 @@ func TestNewInvalid(t *testing.T) { Field: "field[0].name", }}, }, + `Kind "name" is invalid: field[0].name: Invalid value: "bar": detail`, }, { field.NotFound(field.NewPath("field[0].name"), "bar"), @@ -151,6 +161,7 @@ func TestNewInvalid(t *testing.T) { Field: "field[0].name", }}, }, + `Kind "name" is invalid: field[0].name: Not found: "bar"`, }, { field.NotSupported(field.NewPath("field[0].name"), "bar", nil), @@ -162,6 +173,7 @@ func TestNewInvalid(t *testing.T) { Field: "field[0].name", }}, }, + `Kind "name" is invalid: field[0].name: Unsupported value: "bar"`, }, { field.Required(field.NewPath("field[0].name"), ""), @@ -173,12 +185,28 @@ func TestNewInvalid(t *testing.T) { Field: "field[0].name", }}, }, + `Kind "name" is invalid: field[0].name: Required value`, + }, + { + nil, + &metav1.StatusDetails{ + Kind: "Kind", + Name: "name", + Causes: []metav1.StatusCause{}, + }, + `Kind "name" is invalid`, }, } for i, testCase := range testCases { vErr, expected := testCase.Err, testCase.Details - expected.Causes[0].Message = vErr.ErrorBody() - err := NewInvalid(kind("Kind"), "name", field.ErrorList{vErr}) + if vErr != nil && expected != nil { + expected.Causes[0].Message = vErr.ErrorBody() + } + var errList field.ErrorList + if vErr != nil { + errList = append(errList, vErr) + } + err := NewInvalid(kind("Kind"), "name", errList) status := err.ErrStatus if status.Code != 422 || status.Reason != metav1.StatusReasonInvalid { t.Errorf("%d: unexpected status: %#v", i, status) @@ -186,6 +214,9 @@ func TestNewInvalid(t *testing.T) { if !reflect.DeepEqual(expected, status.Details) { t.Errorf("%d: expected %#v, got %#v", i, expected, status.Details) } + if testCase.Msg != status.Message { + t.Errorf("%d: expected\n%s\ngot\n%s", i, testCase.Msg, status.Message) + } } } @@ -248,6 +279,10 @@ func TestReasonForErrorSupportsWrappedErrors(t *testing.T) { err: fmt.Errorf("wrapping: %w", fmt.Errorf("some more: %w", errors.New("hello"))), expectedReason: metav1.StatusReasonUnknown, }, + { + name: "Nil", + expectedReason: metav1.StatusReasonUnknown, + }, } for _, tc := range testCases { @@ -295,6 +330,10 @@ func TestIsTooManyRequestsSupportsWrappedErrors(t *testing.T) { err: fmt.Errorf("Wrapping: %w", &StatusError{ErrStatus: metav1.Status{Code: http.StatusNotFound}}), expectMatch: false, }, + { + name: "Nil", + expectMatch: false, + }, } for _, tc := range testCases { @@ -339,6 +378,10 @@ func TestIsRequestEntityTooLargeErrorSupportsWrappedErrors(t *testing.T) { err: fmt.Errorf("Wrapping: %w", &StatusError{ErrStatus: metav1.Status{Code: http.StatusNotFound}}), expectMatch: false, }, + { + name: "Nil", + expectMatch: false, + }, } for _, tc := range testCases { @@ -383,6 +426,10 @@ func TestIsUnexpectedServerError(t *testing.T) { err: fmt.Errorf("wrapping: %w", fmt.Errorf("some more: %w", errors.New("hello"))), expectMatch: false, }, + { + name: "Nil", + expectMatch: false, + }, } for _, tc := range testCases { @@ -423,6 +470,10 @@ func TestIsUnexpectedObjectError(t *testing.T) { err: fmt.Errorf("wrapping: %w", fmt.Errorf("some more: %w", errors.New("hello"))), expectMatch: false, }, + { + name: "Nil", + expectMatch: false, + }, } for _, tc := range testCases { @@ -468,6 +519,10 @@ func TestSuggestsClientDelaySupportsWrapping(t *testing.T) { err: fmt.Errorf("wrapping: %w", fmt.Errorf("some more: %w", errors.New("hello"))), expectMatch: false, }, + { + name: "Nil", + expectMatch: false, + }, } for _, tc := range testCases { @@ -478,3 +533,173 @@ func TestSuggestsClientDelaySupportsWrapping(t *testing.T) { }) } } + +func TestIsErrorTypesByReasonAndCode(t *testing.T) { + testCases := []struct { + name string + knownReason metav1.StatusReason + otherReason metav1.StatusReason + otherReasonConsidered bool + code int32 + fn func(error) bool + }{ + { + name: "IsRequestEntityTooLarge", + knownReason: metav1.StatusReasonRequestEntityTooLarge, + otherReason: metav1.StatusReasonForbidden, + otherReasonConsidered: false, + code: http.StatusRequestEntityTooLarge, + fn: IsRequestEntityTooLargeError, + }, { + name: "TooManyRequests", + knownReason: metav1.StatusReasonTooManyRequests, + otherReason: metav1.StatusReasonForbidden, + otherReasonConsidered: false, + code: http.StatusTooManyRequests, + fn: IsTooManyRequests, + }, { + name: "Forbidden", + knownReason: metav1.StatusReasonForbidden, + otherReason: metav1.StatusReasonNotFound, + otherReasonConsidered: true, + code: http.StatusForbidden, + fn: IsForbidden, + }, { + name: "NotFound", + knownReason: metav1.StatusReasonNotFound, + otherReason: metav1.StatusReasonForbidden, + otherReasonConsidered: true, + code: http.StatusNotFound, + fn: IsNotFound, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Run("by known reason", func(t *testing.T) { + err := &StatusError{ + metav1.Status{ + Reason: tc.knownReason, + }, + } + + got := tc.fn(err) + if !got { + t.Errorf("expected reason %s to match", tc.knownReason) + } + }) + + t.Run("by code and unknown reason", func(t *testing.T) { + err := &StatusError{ + metav1.Status{ + Reason: metav1.StatusReasonUnknown, // this could be _any_ reason that isn't in knownReasons. + Code: tc.code, + }, + } + + got := tc.fn(err) + if !got { + t.Errorf("expected code %d with reason %s to match", tc.code, tc.otherReason) + } + }) + + if !tc.otherReasonConsidered { + return + } + + t.Run("by code and other known reason", func(t *testing.T) { + err := &StatusError{ + metav1.Status{ + Reason: tc.otherReason, + Code: tc.code, + }, + } + + got := tc.fn(err) + if got { + t.Errorf("expected code %d with reason %s to not match", tc.code, tc.otherReason) + } + }) + + }) + + } +} + +func TestStatusCauseSupportsWrappedErrors(t *testing.T) { + err := &StatusError{ErrStatus: metav1.Status{ + Details: &metav1.StatusDetails{ + Causes: []metav1.StatusCause{{Type: "SomeCause"}}, + }, + }} + + if cause, ok := StatusCause(nil, "SomeCause"); ok { + t.Errorf("expected no cause for nil, got %v: %#v", ok, cause) + } + if cause, ok := StatusCause(errors.New("boom"), "SomeCause"); ok { + t.Errorf("expected no cause for wrong type, got %v: %#v", ok, cause) + } + + if cause, ok := StatusCause(err, "Other"); ok { + t.Errorf("expected no cause for wrong name, got %v: %#v", ok, cause) + } + if cause, ok := StatusCause(err, "SomeCause"); !ok || cause != err.ErrStatus.Details.Causes[0] { + t.Errorf("expected cause, got %v: %#v", ok, cause) + } + + wrapped := fmt.Errorf("once: %w", err) + if cause, ok := StatusCause(wrapped, "SomeCause"); !ok || cause != err.ErrStatus.Details.Causes[0] { + t.Errorf("expected cause when wrapped, got %v: %#v", ok, cause) + } + + nested := fmt.Errorf("twice: %w", wrapped) + if cause, ok := StatusCause(nested, "SomeCause"); !ok || cause != err.ErrStatus.Details.Causes[0] { + t.Errorf("expected cause when nested, got %v: %#v", ok, cause) + } +} + +func BenchmarkIsAlreadyExistsWrappedErrors(b *testing.B) { + err := NewAlreadyExists(schema.GroupResource{}, "") + wrapped := fmt.Errorf("once: %w", err) + + b.Run("Nil", func(b *testing.B) { + for i := 0; i < b.N; i++ { + IsAlreadyExists(nil) + } + }) + + b.Run("Bare", func(b *testing.B) { + for i := 0; i < b.N; i++ { + IsAlreadyExists(err) + } + }) + + b.Run("Wrapped", func(b *testing.B) { + for i := 0; i < b.N; i++ { + IsAlreadyExists(wrapped) + } + }) +} + +func BenchmarkIsNotFoundWrappedErrors(b *testing.B) { + err := NewNotFound(schema.GroupResource{}, "") + wrapped := fmt.Errorf("once: %w", err) + + b.Run("Nil", func(b *testing.B) { + for i := 0; i < b.N; i++ { + IsNotFound(nil) + } + }) + + b.Run("Bare", func(b *testing.B) { + for i := 0; i < b.N; i++ { + IsNotFound(err) + } + }) + + b.Run("Wrapped", func(b *testing.B) { + for i := 0; i < b.N; i++ { + IsNotFound(wrapped) + } + }) +} diff --git a/pkg/api/meta/OWNERS b/pkg/api/meta/OWNERS index f929e061d..1e1330fff 100644 --- a/pkg/api/meta/OWNERS +++ b/pkg/api/meta/OWNERS @@ -1,18 +1,14 @@ # See the OWNERS docs at https://go.k8s.io/owners reviewers: -- thockin -- smarterclayton -- wojtek-t -- deads2k -- brendandburns -- derekwaynecarr -- caesarxuchao -- mikedanese -- liggitt -- janetkuo -- ncdc -- dims -- krousey -- resouer -- mfojtik + - thockin + - smarterclayton + - wojtek-t + - deads2k + - derekwaynecarr + - caesarxuchao + - mikedanese + - liggitt + - janetkuo + - ncdc + - dims diff --git a/pkg/api/meta/conditions.go b/pkg/api/meta/conditions.go index 00874f89c..60c8209de 100644 --- a/pkg/api/meta/conditions.go +++ b/pkg/api/meta/conditions.go @@ -24,9 +24,9 @@ import ( // SetStatusCondition sets the corresponding condition in conditions to newCondition. // conditions must be non-nil. -// 1. if the condition of the specified type already exists (all fields of the existing condition are updated to -// newCondition, LastTransitionTime is set to now if the new status differs from the old status) -// 2. if a condition of the specified type does not exist (LastTransitionTime is set to now() if unset, and newCondition is appended) +// 1. if the condition of the specified type already exists (all fields of the existing condition are updated to +// newCondition, LastTransitionTime is set to now if the new status differs from the old status) +// 2. if a condition of the specified type does not exist (LastTransitionTime is set to now() if unset, and newCondition is appended) func SetStatusCondition(conditions *[]metav1.Condition, newCondition metav1.Condition) { if conditions == nil { return diff --git a/pkg/api/meta/errors.go b/pkg/api/meta/errors.go index cbf5d0263..f36aa4ec2 100644 --- a/pkg/api/meta/errors.go +++ b/pkg/api/meta/errors.go @@ -17,6 +17,7 @@ limitations under the License. package meta import ( + "errors" "fmt" "k8s.io/apimachinery/pkg/runtime/schema" @@ -43,6 +44,11 @@ func (e *AmbiguousResourceError) Error() string { return fmt.Sprintf("%v matches multiple resources or kinds", e.PartialResource) } +func (*AmbiguousResourceError) Is(target error) bool { + _, ok := target.(*AmbiguousResourceError) + return ok +} + // AmbiguousKindError is returned if the RESTMapper finds multiple matches for a kind type AmbiguousKindError struct { PartialKind schema.GroupVersionKind @@ -63,16 +69,16 @@ func (e *AmbiguousKindError) Error() string { return fmt.Sprintf("%v matches multiple resources or kinds", e.PartialKind) } +func (*AmbiguousKindError) Is(target error) bool { + _, ok := target.(*AmbiguousKindError) + return ok +} + func IsAmbiguousError(err error) bool { if err == nil { return false } - switch err.(type) { - case *AmbiguousResourceError, *AmbiguousKindError: - return true - default: - return false - } + return errors.Is(err, &AmbiguousResourceError{}) || errors.Is(err, &AmbiguousKindError{}) } // NoResourceMatchError is returned if the RESTMapper can't find any match for a resource @@ -84,6 +90,11 @@ func (e *NoResourceMatchError) Error() string { return fmt.Sprintf("no matches for %v", e.PartialResource) } +func (*NoResourceMatchError) Is(target error) bool { + _, ok := target.(*NoResourceMatchError) + return ok +} + // NoKindMatchError is returned if the RESTMapper can't find any match for a kind type NoKindMatchError struct { // GroupKind is the API group and kind that was searched @@ -108,14 +119,14 @@ func (e *NoKindMatchError) Error() string { } } +func (*NoKindMatchError) Is(target error) bool { + _, ok := target.(*NoKindMatchError) + return ok +} + func IsNoMatchError(err error) bool { if err == nil { return false } - switch err.(type) { - case *NoResourceMatchError, *NoKindMatchError: - return true - default: - return false - } + return errors.Is(err, &NoResourceMatchError{}) || errors.Is(err, &NoKindMatchError{}) } diff --git a/pkg/api/meta/errors_test.go b/pkg/api/meta/errors_test.go new file mode 100644 index 000000000..56e5d030b --- /dev/null +++ b/pkg/api/meta/errors_test.go @@ -0,0 +1,79 @@ +/* +Copyright 2022 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 meta + +import ( + "errors" + "fmt" + "testing" + + "k8s.io/apimachinery/pkg/runtime/schema" +) + +func TestErrorMatching(t *testing.T) { + testCases := []struct { + name string + // input should contain an error that is _not_ empty, otherwise the naive reflectlite.DeepEqual matching of + // the errors lib will always succeed, but for all of these we want to verify that the matching is based on + // type. + input error + new func() error + matcherFunc func(error) bool + }{ + { + name: "AmbiguousResourceError", + input: &AmbiguousResourceError{MatchingResources: []schema.GroupVersionResource{{}}}, + new: func() error { return &AmbiguousResourceError{} }, + matcherFunc: IsAmbiguousError, + }, + { + name: "AmbiguousKindError", + input: &AmbiguousKindError{MatchingResources: []schema.GroupVersionResource{{}}}, + new: func() error { return &AmbiguousKindError{} }, + matcherFunc: IsAmbiguousError, + }, + { + name: "NoResourceMatchError", + input: &NoResourceMatchError{PartialResource: schema.GroupVersionResource{Group: "foo"}}, + new: func() error { return &NoResourceMatchError{} }, + matcherFunc: IsNoMatchError, + }, + { + name: "NoKindMatchError", + input: &NoKindMatchError{SearchedVersions: []string{"foo"}}, + new: func() error { return &NoKindMatchError{} }, + matcherFunc: IsNoMatchError, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if !errors.Is(tc.input, tc.new()) { + t.Error("error doesn't match itself directly") + } + if !errors.Is(fmt.Errorf("wrapepd: %w", tc.input), tc.new()) { + t.Error("error doesn't match itself when wrapped") + } + if !tc.matcherFunc(tc.input) { + t.Errorf("error doesn't get matched by matcherfunc") + } + if errors.Is(tc.input, errors.New("foo")) { + t.Error("error incorrectly matches other error") + } + }) + } +} diff --git a/pkg/api/meta/firsthit_restmapper.go b/pkg/api/meta/firsthit_restmapper.go index fd2210022..1bc816fe3 100644 --- a/pkg/api/meta/firsthit_restmapper.go +++ b/pkg/api/meta/firsthit_restmapper.go @@ -23,6 +23,10 @@ import ( utilerrors "k8s.io/apimachinery/pkg/util/errors" ) +var ( + _ ResettableRESTMapper = &FirstHitRESTMapper{} +) + // FirstHitRESTMapper is a wrapper for multiple RESTMappers which returns the // first successful result for the singular requests type FirstHitRESTMapper struct { @@ -75,6 +79,10 @@ func (m FirstHitRESTMapper) RESTMapping(gk schema.GroupKind, versions ...string) return nil, collapseAggregateErrors(errors) } +func (m FirstHitRESTMapper) Reset() { + m.MultiRESTMapper.Reset() +} + // collapseAggregateErrors returns the minimal errors. it handles empty as nil, handles one item in a list // by returning the item, and collapses all NoMatchErrors to a single one (since they should all be the same) func collapseAggregateErrors(errors []error) error { diff --git a/pkg/api/meta/help.go b/pkg/api/meta/help.go index 50468b533..899d3e8a6 100644 --- a/pkg/api/meta/help.go +++ b/pkg/api/meta/help.go @@ -40,7 +40,8 @@ var ( // IsListType returns true if the provided Object has a slice called Items. // TODO: Replace the code in this check with an interface comparison by -// creating and enforcing that lists implement a list accessor. +// +// creating and enforcing that lists implement a list accessor. func IsListType(obj runtime.Object) bool { switch t := obj.(type) { case runtime.Unstructured: @@ -97,7 +98,7 @@ func getItemsPtr(list runtime.Object) (interface{}, error) { return nil, errExpectFieldItems } switch items.Kind() { - case reflect.Interface, reflect.Ptr: + case reflect.Interface, reflect.Pointer: target := reflect.TypeOf(items.Interface()).Elem() if target.Kind() != reflect.Slice { return nil, errExpectSliceItems @@ -130,7 +131,7 @@ func EachListItem(obj runtime.Object, fn func(runtime.Object) error) error { return nil } takeAddr := false - if elemType := items.Type().Elem(); elemType.Kind() != reflect.Ptr && elemType.Kind() != reflect.Interface { + if elemType := items.Type().Elem(); elemType.Kind() != reflect.Pointer && elemType.Kind() != reflect.Interface { if !items.Index(0).CanAddr() { return fmt.Errorf("unable to take address of items in %T for EachListItem", obj) } diff --git a/pkg/api/meta/interfaces.go b/pkg/api/meta/interfaces.go index 42eac3af0..a35ce3bd0 100644 --- a/pkg/api/meta/interfaces.go +++ b/pkg/api/meta/interfaces.go @@ -132,3 +132,12 @@ type RESTMapper interface { ResourceSingularizer(resource string) (singular string, err error) } + +// ResettableRESTMapper is a RESTMapper which is capable of resetting itself +// from discovery. +// All rest mappers that delegate to other rest mappers must implement this interface and dynamically +// check if the delegate mapper supports the Reset() operation. +type ResettableRESTMapper interface { + RESTMapper + Reset() +} diff --git a/pkg/api/meta/lazy.go b/pkg/api/meta/lazy.go index 431a0a635..a4298114b 100644 --- a/pkg/api/meta/lazy.go +++ b/pkg/api/meta/lazy.go @@ -32,7 +32,7 @@ type lazyObject struct { mapper RESTMapper } -// NewLazyObjectLoader handles unrecoverable errors when creating a RESTMapper / ObjectTyper by +// NewLazyRESTMapperLoader handles unrecoverable errors when creating a RESTMapper / ObjectTyper by // returning those initialization errors when the interface methods are invoked. This defers the // initialization and any server calls until a client actually needs to perform the action. func NewLazyRESTMapperLoader(fn func() (RESTMapper, error)) RESTMapper { @@ -52,7 +52,7 @@ func (o *lazyObject) init() error { return o.err } -var _ RESTMapper = &lazyObject{} +var _ ResettableRESTMapper = &lazyObject{} func (o *lazyObject) KindFor(resource schema.GroupVersionResource) (schema.GroupVersionKind, error) { if err := o.init(); err != nil { @@ -102,3 +102,11 @@ func (o *lazyObject) ResourceSingularizer(resource string) (singular string, err } return o.mapper.ResourceSingularizer(resource) } + +func (o *lazyObject) Reset() { + o.lock.Lock() + defer o.lock.Unlock() + if o.loaded && o.err == nil { + MaybeResetRESTMapper(o.mapper) + } +} diff --git a/pkg/api/meta/meta.go b/pkg/api/meta/meta.go index 6a4116a04..2551f07f5 100644 --- a/pkg/api/meta/meta.go +++ b/pkg/api/meta/meta.go @@ -130,7 +130,6 @@ func AsPartialObjectMetadata(m metav1.Object) *metav1.PartialObjectMetadata { Annotations: m.GetAnnotations(), OwnerReferences: m.GetOwnerReferences(), Finalizers: m.GetFinalizers(), - ClusterName: m.GetClusterName(), ManagedFields: m.GetManagedFields(), }, } @@ -600,7 +599,7 @@ func (a genericAccessor) SetFinalizers(finalizers []string) { func (a genericAccessor) GetOwnerReferences() []metav1.OwnerReference { var ret []metav1.OwnerReference s := a.ownerReferences - if s.Kind() != reflect.Ptr || s.Elem().Kind() != reflect.Slice { + if s.Kind() != reflect.Pointer || s.Elem().Kind() != reflect.Slice { klog.Errorf("expect %v to be a pointer to slice", s) return ret } @@ -618,7 +617,7 @@ func (a genericAccessor) GetOwnerReferences() []metav1.OwnerReference { func (a genericAccessor) SetOwnerReferences(references []metav1.OwnerReference) { s := a.ownerReferences - if s.Kind() != reflect.Ptr || s.Elem().Kind() != reflect.Slice { + if s.Kind() != reflect.Pointer || s.Elem().Kind() != reflect.Slice { klog.Errorf("expect %v to be a pointer to slice", s) } s = s.Elem() diff --git a/pkg/api/meta/multirestmapper.go b/pkg/api/meta/multirestmapper.go index 6b01bf197..b7e971250 100644 --- a/pkg/api/meta/multirestmapper.go +++ b/pkg/api/meta/multirestmapper.go @@ -24,11 +24,15 @@ import ( utilerrors "k8s.io/apimachinery/pkg/util/errors" ) +var ( + _ ResettableRESTMapper = MultiRESTMapper{} +) + // MultiRESTMapper is a wrapper for multiple RESTMappers. type MultiRESTMapper []RESTMapper func (m MultiRESTMapper) String() string { - nested := []string{} + nested := make([]string, 0, len(m)) for _, t := range m { currString := fmt.Sprintf("%v", t) splitStrings := strings.Split(currString, "\n") @@ -208,3 +212,9 @@ func (m MultiRESTMapper) RESTMappings(gk schema.GroupKind, versions ...string) ( } return allMappings, nil } + +func (m MultiRESTMapper) Reset() { + for _, t := range m { + MaybeResetRESTMapper(t) + } +} diff --git a/pkg/api/meta/priority.go b/pkg/api/meta/priority.go index fa11c580f..4f097c9c9 100644 --- a/pkg/api/meta/priority.go +++ b/pkg/api/meta/priority.go @@ -29,6 +29,10 @@ const ( AnyKind = "*" ) +var ( + _ ResettableRESTMapper = PriorityRESTMapper{} +) + // PriorityRESTMapper is a wrapper for automatically choosing a particular Resource or Kind // when multiple matches are possible type PriorityRESTMapper struct { @@ -220,3 +224,7 @@ func (m PriorityRESTMapper) ResourcesFor(partiallySpecifiedResource schema.Group func (m PriorityRESTMapper) KindsFor(partiallySpecifiedResource schema.GroupVersionResource) (gvk []schema.GroupVersionKind, err error) { return m.Delegate.KindsFor(partiallySpecifiedResource) } + +func (m PriorityRESTMapper) Reset() { + MaybeResetRESTMapper(m.Delegate) +} diff --git a/pkg/api/meta/restmapper.go b/pkg/api/meta/restmapper.go index 00bd86f51..91cb98cae 100644 --- a/pkg/api/meta/restmapper.go +++ b/pkg/api/meta/restmapper.go @@ -519,3 +519,11 @@ func (m *DefaultRESTMapper) RESTMappings(gk schema.GroupKind, versions ...string } return mappings, nil } + +// MaybeResetRESTMapper calls Reset() on the mapper if it is a ResettableRESTMapper. +func MaybeResetRESTMapper(mapper RESTMapper) { + m, ok := mapper.(ResettableRESTMapper) + if ok { + m.Reset() + } +} diff --git a/pkg/api/meta/testrestmapper/test_restmapper.go b/pkg/api/meta/testrestmapper/test_restmapper.go index 0e8837e09..44d877ecf 100644 --- a/pkg/api/meta/testrestmapper/test_restmapper.go +++ b/pkg/api/meta/testrestmapper/test_restmapper.go @@ -27,6 +27,7 @@ import ( // 1. legacy kube group preferred version, extensions preferred version, metrics preferred version, legacy // kube any version, extensions any version, metrics any version, all other groups alphabetical preferred version, // all other groups alphabetical. +// // TODO callers of this method should be updated to build their own specific restmapper based on their scheme for their tests // TODO the things being tested are related to whether various cases are handled, not tied to the particular types being checked. func TestOnlyStaticRESTMapper(scheme *runtime.Scheme, versionPatterns ...schema.GroupVersion) meta.RESTMapper { diff --git a/pkg/api/resource/OWNERS b/pkg/api/resource/OWNERS index 15bded17a..d1c9f5307 100644 --- a/pkg/api/resource/OWNERS +++ b/pkg/api/resource/OWNERS @@ -1,12 +1,11 @@ # See the OWNERS docs at https://go.k8s.io/owners reviewers: -- thockin -- lavalamp -- smarterclayton -- wojtek-t -- derekwaynecarr -- mikedanese -- saad-ali -- janetkuo -- xiang90 + - thockin + - lavalamp + - smarterclayton + - wojtek-t + - derekwaynecarr + - mikedanese + - saad-ali + - janetkuo diff --git a/pkg/api/resource/generated.pb.go b/pkg/api/resource/generated.pb.go index 2e09f4fac..53a25d344 100644 --- a/pkg/api/resource/generated.pb.go +++ b/pkg/api/resource/generated.pb.go @@ -61,8 +61,32 @@ func (m *Quantity) XXX_DiscardUnknown() { var xxx_messageInfo_Quantity proto.InternalMessageInfo +func (m *QuantityValue) Reset() { *m = QuantityValue{} } +func (*QuantityValue) ProtoMessage() {} +func (*QuantityValue) Descriptor() ([]byte, []int) { + return fileDescriptor_612bba87bd70906c, []int{1} +} +func (m *QuantityValue) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_QuantityValue.Unmarshal(m, b) +} +func (m *QuantityValue) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_QuantityValue.Marshal(b, m, deterministic) +} +func (m *QuantityValue) XXX_Merge(src proto.Message) { + xxx_messageInfo_QuantityValue.Merge(m, src) +} +func (m *QuantityValue) XXX_Size() int { + return xxx_messageInfo_QuantityValue.Size(m) +} +func (m *QuantityValue) XXX_DiscardUnknown() { + xxx_messageInfo_QuantityValue.DiscardUnknown(m) +} + +var xxx_messageInfo_QuantityValue proto.InternalMessageInfo + func init() { proto.RegisterType((*Quantity)(nil), "k8s.io.apimachinery.pkg.api.resource.Quantity") + proto.RegisterType((*QuantityValue)(nil), "k8s.io.apimachinery.pkg.api.resource.QuantityValue") } func init() { @@ -70,20 +94,21 @@ func init() { } var fileDescriptor_612bba87bd70906c = []byte{ - // 237 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x4c, 0x8e, 0xb1, 0x4e, 0xc3, 0x30, - 0x10, 0x40, 0xcf, 0x0b, 0x2a, 0x19, 0x2b, 0x84, 0x10, 0xc3, 0xa5, 0x42, 0x0c, 0x2c, 0xd8, 0x6b, - 0xc5, 0xc8, 0xce, 0x00, 0x23, 0x5b, 0x92, 0x1e, 0xae, 0x15, 0xd5, 0x8e, 0x2e, 0x36, 0x52, 0xb7, - 0x8e, 0x8c, 0x1d, 0x19, 0x9b, 0xbf, 0xe9, 0xd8, 0xb1, 0x03, 0x03, 0x31, 0x3f, 0x82, 0xea, 0x36, - 0x52, 0xb7, 0x7b, 0xef, 0xf4, 0x4e, 0x97, 0xbd, 0xd4, 0xd3, 0x56, 0x1a, 0xa7, 0xea, 0x50, 0x12, - 0x5b, 0xf2, 0xd4, 0xaa, 0x4f, 0xb2, 0x33, 0xc7, 0xea, 0xb4, 0x28, 0x1a, 0xb3, 0x28, 0xaa, 0xb9, - 0xb1, 0xc4, 0x4b, 0xd5, 0xd4, 0xfa, 0x20, 0x14, 0x53, 0xeb, 0x02, 0x57, 0xa4, 0x34, 0x59, 0xe2, - 0xc2, 0xd3, 0x4c, 0x36, 0xec, 0xbc, 0x1b, 0xdf, 0x1f, 0x2b, 0x79, 0x5e, 0xc9, 0xa6, 0xd6, 0x07, - 0x21, 0x87, 0xea, 0xf6, 0x51, 0x1b, 0x3f, 0x0f, 0xa5, 0xac, 0xdc, 0x42, 0x69, 0xa7, 0x9d, 0x4a, - 0x71, 0x19, 0x3e, 0x12, 0x25, 0x48, 0xd3, 0xf1, 0xe8, 0xdd, 0x34, 0x1b, 0xbd, 0x86, 0xc2, 0x7a, - 0xe3, 0x97, 0xe3, 0xeb, 0xec, 0xa2, 0xf5, 0x6c, 0xac, 0xbe, 0x11, 0x13, 0xf1, 0x70, 0xf9, 0x76, - 0xa2, 0xa7, 0xab, 0xef, 0x4d, 0x0e, 0x5f, 0x5d, 0x0e, 0xeb, 0x2e, 0x87, 0x4d, 0x97, 0xc3, 0xea, - 0x67, 0x02, 0xcf, 0x72, 0xdb, 0x23, 0xec, 0x7a, 0x84, 0x7d, 0x8f, 0xb0, 0x8a, 0x28, 0xb6, 0x11, - 0xc5, 0x2e, 0xa2, 0xd8, 0x47, 0x14, 0xbf, 0x11, 0xc5, 0xfa, 0x0f, 0xe1, 0x7d, 0x34, 0x3c, 0xf6, - 0x1f, 0x00, 0x00, 0xff, 0xff, 0x3c, 0x08, 0x88, 0x49, 0x0e, 0x01, 0x00, 0x00, + // 254 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xf2, 0xcd, 0xb6, 0x28, 0xd6, + 0xcb, 0xcc, 0xd7, 0xcf, 0x2e, 0x4d, 0x4a, 0x2d, 0xca, 0x4b, 0x2d, 0x49, 0x2d, 0xd6, 0x2f, 0x4b, + 0xcd, 0x4b, 0xc9, 0x2f, 0xd2, 0x87, 0x4a, 0x24, 0x16, 0x64, 0xe6, 0x26, 0x26, 0x67, 0x64, 0xe6, + 0xa5, 0x16, 0x55, 0xea, 0x17, 0x64, 0xa7, 0x83, 0x04, 0xf4, 0x8b, 0x52, 0x8b, 0xf3, 0x4b, 0x8b, + 0x92, 0x53, 0xf5, 0xd3, 0x53, 0xf3, 0x52, 0x8b, 0x12, 0x4b, 0x52, 0x53, 0xf4, 0x0a, 0x8a, 0xf2, + 0x4b, 0xf2, 0x85, 0x54, 0x20, 0xba, 0xf4, 0x90, 0x75, 0xe9, 0x15, 0x64, 0xa7, 0x83, 0x04, 0xf4, + 0x60, 0xba, 0xa4, 0x74, 0xd3, 0x33, 0x4b, 0x32, 0x4a, 0x93, 0xf4, 0x92, 0xf3, 0x73, 0xf5, 0xd3, + 0xf3, 0xd3, 0xf3, 0xf5, 0xc1, 0x9a, 0x93, 0x4a, 0xd3, 0xc0, 0x3c, 0x30, 0x07, 0xcc, 0x82, 0x18, + 0xaa, 0x64, 0xc1, 0xc5, 0x11, 0x58, 0x9a, 0x98, 0x57, 0x92, 0x59, 0x52, 0x29, 0x24, 0xc6, 0xc5, + 0x56, 0x5c, 0x52, 0x94, 0x99, 0x97, 0x2e, 0xc1, 0xa8, 0xc0, 0xa8, 0xc1, 0x19, 0x04, 0xe5, 0x59, + 0x89, 0xcc, 0x58, 0x20, 0xcf, 0xd0, 0xb1, 0x50, 0x9e, 0x61, 0xc2, 0x42, 0x79, 0x86, 0x05, 0x0b, + 0xe5, 0x19, 0x1a, 0xee, 0x28, 0x30, 0x28, 0xd9, 0x72, 0xf1, 0xc2, 0x74, 0x86, 0x25, 0xe6, 0x94, + 0xa6, 0x92, 0xa6, 0xdd, 0xc9, 0xeb, 0xc4, 0x43, 0x39, 0x86, 0x0b, 0x0f, 0xe5, 0x18, 0x6e, 0x3c, + 0x94, 0x63, 0x68, 0x78, 0x24, 0xc7, 0x78, 0xe2, 0x91, 0x1c, 0xe3, 0x85, 0x47, 0x72, 0x8c, 0x37, + 0x1e, 0xc9, 0x31, 0x3e, 0x78, 0x24, 0xc7, 0x38, 0xe1, 0xb1, 0x1c, 0x43, 0x94, 0x0a, 0x31, 0x21, + 0x05, 0x08, 0x00, 0x00, 0xff, 0xff, 0x8e, 0x70, 0x98, 0xa3, 0x69, 0x01, 0x00, 0x00, } diff --git a/pkg/api/resource/generated.proto b/pkg/api/resource/generated.proto index 472104d54..ddd0db8fb 100644 --- a/pkg/api/resource/generated.proto +++ b/pkg/api/resource/generated.proto @@ -22,7 +22,7 @@ syntax = "proto2"; package k8s.io.apimachinery.pkg.api.resource; // Package-wide variables from generator "generated". -option go_package = "resource"; +option go_package = "k8s.io/apimachinery/pkg/api/resource"; // Quantity is a fixed-point representation of a number. // It provides convenient marshaling/unmarshaling in JSON and YAML, @@ -30,8 +30,11 @@ option go_package = "resource"; // // The serialization format is: // +// ``` // ::= -// (Note that may be empty, from the "" case in .) +// +// (Note that may be empty, from the "" case in .) +// // ::= 0 | 1 | ... | 9 // ::= | // ::= | . | . | . @@ -39,10 +42,15 @@ option go_package = "resource"; // ::= | // ::= | | // ::= Ki | Mi | Gi | Ti | Pi | Ei -// (International System of units; See: http://physics.nist.gov/cuu/Units/binary.html) +// +// (International System of units; See: http://physics.nist.gov/cuu/Units/binary.html) +// // ::= m | "" | k | M | G | T | P | E -// (Note that 1024 = 1Ki but 1000 = 1k; I didn't choose the capitalization.) +// +// (Note that 1024 = 1Ki but 1000 = 1k; I didn't choose the capitalization.) +// // ::= "e" | "E" +// ``` // // No matter which of the three exponent forms is used, no quantity may represent // a number greater than 2^63-1 in magnitude, nor may it have more than 3 decimal @@ -56,14 +64,17 @@ option go_package = "resource"; // Before serializing, Quantity will be put in "canonical form". // This means that Exponent/suffix will be adjusted up or down (with a // corresponding increase or decrease in Mantissa) such that: -// a. No precision is lost -// b. No fractional digits will be emitted -// c. The exponent (or suffix) is as large as possible. +// +// - No precision is lost +// - No fractional digits will be emitted +// - The exponent (or suffix) is as large as possible. +// // The sign will be omitted unless the number is negative. // // Examples: -// 1.5 will be serialized as "1500m" -// 1.5Gi will be serialized as "1536Mi" +// +// - 1.5 will be serialized as "1500m" +// - 1.5Gi will be serialized as "1536Mi" // // Note that the quantity will NEVER be internally represented by a // floating point number. That is the whole point of this exercise. @@ -86,3 +97,15 @@ message Quantity { optional string string = 1; } +// QuantityValue makes it possible to use a Quantity as value for a command +// line parameter. +// +// +protobuf=true +// +protobuf.embed=string +// +protobuf.options.marshal=false +// +protobuf.options.(gogoproto.goproto_stringer)=false +// +k8s:deepcopy-gen=true +message QuantityValue { + optional string string = 1; +} + diff --git a/pkg/api/resource/quantity.go b/pkg/api/resource/quantity.go index 2395656cc..b47d554b3 100644 --- a/pkg/api/resource/quantity.go +++ b/pkg/api/resource/quantity.go @@ -34,8 +34,11 @@ import ( // // The serialization format is: // +// ``` // ::= -// (Note that may be empty, from the "" case in .) +// +// (Note that may be empty, from the "" case in .) +// // ::= 0 | 1 | ... | 9 // ::= | // ::= | . | . | . @@ -43,10 +46,15 @@ import ( // ::= | // ::= | | // ::= Ki | Mi | Gi | Ti | Pi | Ei -// (International System of units; See: http://physics.nist.gov/cuu/Units/binary.html) +// +// (International System of units; See: http://physics.nist.gov/cuu/Units/binary.html) +// // ::= m | "" | k | M | G | T | P | E -// (Note that 1024 = 1Ki but 1000 = 1k; I didn't choose the capitalization.) +// +// (Note that 1024 = 1Ki but 1000 = 1k; I didn't choose the capitalization.) +// // ::= "e" | "E" +// ``` // // No matter which of the three exponent forms is used, no quantity may represent // a number greater than 2^63-1 in magnitude, nor may it have more than 3 decimal @@ -60,14 +68,17 @@ import ( // Before serializing, Quantity will be put in "canonical form". // This means that Exponent/suffix will be adjusted up or down (with a // corresponding increase or decrease in Mantissa) such that: -// a. No precision is lost -// b. No fractional digits will be emitted -// c. The exponent (or suffix) is as large as possible. +// +// - No precision is lost +// - No fractional digits will be emitted +// - The exponent (or suffix) is as large as possible. +// // The sign will be omitted unless the number is negative. // // Examples: -// 1.5 will be serialized as "1500m" -// 1.5Gi will be serialized as "1536Mi" +// +// - 1.5 will be serialized as "1500m" +// - 1.5Gi will be serialized as "1536Mi" // // Note that the quantity will NEVER be internally represented by a // floating point number. That is the whole point of this exercise. @@ -397,13 +408,17 @@ func (_ Quantity) OpenAPISchemaType() []string { return []string{"string"} } // the OpenAPI spec of this type. func (_ Quantity) OpenAPISchemaFormat() string { return "" } +// OpenAPIV3OneOfTypes is used by the kube-openapi generator when constructing +// the OpenAPI v3 spec of this type. +func (Quantity) OpenAPIV3OneOfTypes() []string { return []string{"string", "number"} } + // CanonicalizeBytes returns the canonical form of q and its suffix (see comment on Quantity). // // Note about BinarySI: -// * If q.Format is set to BinarySI and q.Amount represents a non-zero value between -// -1 and +1, it will be emitted as if q.Format were DecimalSI. -// * Otherwise, if q.Format is set to BinarySI, fractional parts of q.Amount will be -// rounded up. (1.1i becomes 2i.) +// - If q.Format is set to BinarySI and q.Amount represents a non-zero value between +// -1 and +1, it will be emitted as if q.Format were DecimalSI. +// - Otherwise, if q.Format is set to BinarySI, fractional parts of q.Amount will be +// rounded up. (1.1i becomes 2i.) func (q *Quantity) CanonicalizeBytes(out []byte) (result, suffix []byte) { if q.IsZero() { return zeroBytes, nil @@ -460,17 +475,7 @@ func (q *Quantity) AsApproximateFloat64() float64 { return base } - // multiply by the appropriate exponential scale - switch q.Format { - case DecimalExponent, DecimalSI: - return base * math.Pow10(exponent) - default: - // fast path for exponents that can fit in 64 bits - if exponent > 0 && exponent < 7 { - return base * float64(int64(1)<<(exponent*10)) - } - return base * math.Pow(2, float64(exponent*10)) - } + return base * math.Pow10(exponent) } // AsInt64 returns a representation of the current value as an int64 if a fast conversion @@ -649,7 +654,7 @@ func (q Quantity) MarshalJSON() ([]byte, error) { copy(out[1:], q.s) return out, nil } - result := make([]byte, int64QuantityExpectedBytes, int64QuantityExpectedBytes) + result := make([]byte, int64QuantityExpectedBytes) result[0] = '"' number, suffix := q.CanonicalizeBytes(result[1:1]) // if the same slice was returned to us that we passed in, avoid another allocation by copying number into @@ -774,3 +779,30 @@ func (q *Quantity) SetScaled(value int64, scale Scale) { q.d.Dec = nil q.i = int64Amount{value: value, scale: scale} } + +// QuantityValue makes it possible to use a Quantity as value for a command +// line parameter. +// +// +protobuf=true +// +protobuf.embed=string +// +protobuf.options.marshal=false +// +protobuf.options.(gogoproto.goproto_stringer)=false +// +k8s:deepcopy-gen=true +type QuantityValue struct { + Quantity +} + +// Set implements pflag.Value.Set and Go flag.Value.Set. +func (q *QuantityValue) Set(s string) error { + quantity, err := ParseQuantity(s) + if err != nil { + return err + } + q.Quantity = quantity + return nil +} + +// Type implements pflag.Value.Type. +func (q QuantityValue) Type() string { + return "quantity" +} diff --git a/pkg/api/resource/quantity_test.go b/pkg/api/resource/quantity_test.go index cb459b69d..320477358 100644 --- a/pkg/api/resource/quantity_test.go +++ b/pkg/api/resource/quantity_test.go @@ -21,11 +21,13 @@ import ( "fmt" "math" "math/rand" + "os" "strings" "testing" "unicode" fuzz "github.com/google/gofuzz" + "github.com/spf13/pflag" inf "gopkg.in/inf.v0" ) @@ -1207,11 +1209,11 @@ func TestQuantityAsApproximateFloat64(t *testing.T) { {decQuantity(1024, 0, BinarySI), 1024}, {decQuantity(8*1024, 0, BinarySI), 8 * 1024}, {decQuantity(7*1024*1024, 0, BinarySI), 7 * 1024 * 1024}, - {decQuantity(7*1024*1024, 1, BinarySI), (7 * 1024 * 1024) * 1024}, - {decQuantity(7*1024*1024, 4, BinarySI), (7 * 1024 * 1024) * (1024 * 1024 * 1024 * 1024)}, - {decQuantity(7*1024*1024, 8, BinarySI), (7 * 1024 * 1024) * (1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024)}, - {decQuantity(7*1024*1024, -1, BinarySI), (7 * 1024 * 1024) / float64(1024)}, - {decQuantity(7*1024*1024, -8, BinarySI), (7 * 1024 * 1024) / float64(1024*1024*1024*1024*1024*1024*1024*1024)}, + {decQuantity(7*1024*1024, 1, BinarySI), (7 * 1024 * 1024) * 10}, + {decQuantity(7*1024*1024, 4, BinarySI), (7 * 1024 * 1024) * 10000}, + {decQuantity(7*1024*1024, 8, BinarySI), (7 * 1024 * 1024) * 100000000}, + {decQuantity(7*1024*1024, -1, BinarySI), (7 * 1024 * 1024) * math.Pow10(-1)}, // '* Pow10' and '/ float(10)' do not round the same way + {decQuantity(7*1024*1024, -8, BinarySI), (7 * 1024 * 1024) / float64(100000000)}, {decQuantity(1024, 0, DecimalSI), 1024}, {decQuantity(8*1024, 0, DecimalSI), 8 * 1024}, @@ -1260,6 +1262,40 @@ func TestQuantityAsApproximateFloat64(t *testing.T) { } } +func TestStringQuantityAsApproximateFloat64(t *testing.T) { + table := []struct { + in string + out float64 + }{ + {"2Ki", 2048}, + {"1.1Ki", 1126.4e+0}, + {"1Mi", 1.048576e+06}, + {"2Gi", 2.147483648e+09}, + } + + for _, item := range table { + t.Run(item.in, func(t *testing.T) { + in, err := ParseQuantity(item.in) + if err != nil { + t.Fatal(err) + } + out := in.AsApproximateFloat64() + if out != item.out { + t.Fatalf("expected %v, got %v", item.out, out) + } + if in.d.Dec != nil { + if i, ok := in.AsInt64(); ok { + q := intQuantity(i, 0, in.Format) + out := q.AsApproximateFloat64() + if out != item.out { + t.Fatalf("as int quantity: expected %v, got %v", item.out, out) + } + } + } + }) + } +} + func benchmarkQuantities() []Quantity { return []Quantity{ intQuantity(1024*1024*1024, 0, BinarySI), @@ -1441,3 +1477,42 @@ func BenchmarkQuantityAsApproximateFloat64(b *testing.B) { } b.StopTimer() } + +var _ pflag.Value = &QuantityValue{} + +func TestQuantityValueSet(t *testing.T) { + q := QuantityValue{} + + if err := q.Set("invalid"); err == nil { + + t.Error("'invalid' did not trigger a parse error") + } + + if err := q.Set("1Mi"); err != nil { + t.Errorf("parsing 1Mi should have worked, got: %v", err) + } + if q.Value() != 1024*1024 { + t.Errorf("quantity should have been set to 1Mi, got: %v", q) + } + + data, err := json.Marshal(q) + if err != nil { + t.Errorf("unexpected encoding error: %v", err) + } + expected := `"1Mi"` + if string(data) != expected { + t.Errorf("expected 1Mi value to be encoded as %q, got: %q", expected, string(data)) + } +} + +func ExampleQuantityValue() { + q := QuantityValue{ + Quantity: MustParse("1Mi"), + } + fs := pflag.FlagSet{} + fs.SetOutput(os.Stdout) + fs.Var(&q, "mem", "sets amount of memory") + fs.PrintDefaults() + // Output: + // --mem quantity sets amount of memory (default 1Mi) +} diff --git a/pkg/api/resource/suffix.go b/pkg/api/resource/suffix.go index 5ed7abe66..6ec527f9c 100644 --- a/pkg/api/resource/suffix.go +++ b/pkg/api/resource/suffix.go @@ -165,7 +165,7 @@ func (sh *suffixHandler) constructBytes(base, exponent int32, format Format) (s if exponent == 0 { return nil, true } - result := make([]byte, 8, 8) + result := make([]byte, 8) result[0] = 'e' number := strconv.AppendInt(result[1:1], int64(exponent), 10) if &result[1] == &number[0] { diff --git a/pkg/api/resource/zz_generated.deepcopy.go b/pkg/api/resource/zz_generated.deepcopy.go index ab4740790..abb00f38e 100644 --- a/pkg/api/resource/zz_generated.deepcopy.go +++ b/pkg/api/resource/zz_generated.deepcopy.go @@ -1,3 +1,4 @@ +//go:build !ignore_autogenerated // +build !ignore_autogenerated /* @@ -25,3 +26,20 @@ func (in *Quantity) DeepCopyInto(out *Quantity) { *out = in.DeepCopy() return } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *QuantityValue) DeepCopyInto(out *QuantityValue) { + *out = *in + out.Quantity = in.Quantity.DeepCopy() + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new QuantityValue. +func (in *QuantityValue) DeepCopy() *QuantityValue { + if in == nil { + return nil + } + out := new(QuantityValue) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/api/validation/objectmeta.go b/pkg/api/validation/objectmeta.go index 66e8d677a..593d7ba8c 100644 --- a/pkg/api/validation/objectmeta.go +++ b/pkg/api/validation/objectmeta.go @@ -40,13 +40,11 @@ var BannedOwners = map[schema.GroupVersionKind]struct{}{ {Group: "", Version: "v1", Kind: "Event"}: {}, } -// ValidateClusterName can be used to check whether the given cluster name is valid. -var ValidateClusterName = NameIsDNS1035Label - // ValidateAnnotations validates that a set of annotations are correctly defined. func ValidateAnnotations(annotations map[string]string, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} for k := range annotations { + // The rule is QualifiedName except that case doesn't matter, so convert to lowercase before checking. for _, msg := range validation.IsQualifiedName(strings.ToLower(k)) { allErrs = append(allErrs, field.Invalid(fldPath, k, msg)) } @@ -93,15 +91,16 @@ func validateOwnerReference(ownerReference metav1.OwnerReference, fldPath *field // ValidateOwnerReferences validates that a set of owner references are correctly defined. func ValidateOwnerReferences(ownerReferences []metav1.OwnerReference, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} - controllerName := "" + firstControllerName := "" for _, ref := range ownerReferences { allErrs = append(allErrs, validateOwnerReference(ref, fldPath)...) if ref.Controller != nil && *ref.Controller { - if controllerName != "" { + curControllerName := ref.Kind + "/" + ref.Name + if firstControllerName != "" { allErrs = append(allErrs, field.Invalid(fldPath, ownerReferences, - fmt.Sprintf("Only one reference can have Controller set to true. Found \"true\" in references for %v and %v", controllerName, ref.Name))) + fmt.Sprintf("Only one reference can have Controller set to true. Found \"true\" in references for %v and %v", firstControllerName, curControllerName))) } else { - controllerName = ref.Name + firstControllerName = curControllerName } } } @@ -184,11 +183,6 @@ func ValidateObjectMetaAccessor(meta metav1.Object, requiresNamespace bool, name allErrs = append(allErrs, field.Forbidden(fldPath.Child("namespace"), "not allowed on this type")) } } - if len(meta.GetClusterName()) != 0 { - for _, msg := range ValidateClusterName(meta.GetClusterName(), false) { - allErrs = append(allErrs, field.Invalid(fldPath.Child("clusterName"), meta.GetClusterName(), msg)) - } - } allErrs = append(allErrs, ValidateNonnegativeField(meta.GetGeneration(), fldPath.Child("generation"))...) allErrs = append(allErrs, v1validation.ValidateLabels(meta.GetLabels(), fldPath.Child("labels"))...) @@ -261,7 +255,6 @@ func ValidateObjectMetaAccessorUpdate(newMeta, oldMeta metav1.Object, fldPath *f allErrs = append(allErrs, ValidateImmutableField(newMeta.GetCreationTimestamp(), oldMeta.GetCreationTimestamp(), fldPath.Child("creationTimestamp"))...) allErrs = append(allErrs, ValidateImmutableField(newMeta.GetDeletionTimestamp(), oldMeta.GetDeletionTimestamp(), fldPath.Child("deletionTimestamp"))...) allErrs = append(allErrs, ValidateImmutableField(newMeta.GetDeletionGracePeriodSeconds(), oldMeta.GetDeletionGracePeriodSeconds(), fldPath.Child("deletionGracePeriodSeconds"))...) - allErrs = append(allErrs, ValidateImmutableField(newMeta.GetClusterName(), oldMeta.GetClusterName(), fldPath.Child("clusterName"))...) allErrs = append(allErrs, v1validation.ValidateLabels(newMeta.GetLabels(), fldPath.Child("labels"))...) allErrs = append(allErrs, ValidateAnnotations(newMeta.GetAnnotations(), fldPath.Child("annotations"))...) diff --git a/pkg/api/validation/objectmeta_test.go b/pkg/api/validation/objectmeta_test.go index 349da67f8..0331cf9f3 100644 --- a/pkg/api/validation/objectmeta_test.go +++ b/pkg/api/validation/objectmeta_test.go @@ -163,34 +163,34 @@ func TestValidateObjectMetaOwnerReferences(t *testing.T) { ownerReferences: []metav1.OwnerReference{ { APIVersion: "customresourceVersion", - Kind: "customresourceKind", + Kind: "customresourceKind1", Name: "name", UID: "1", Controller: &falseVar, }, { APIVersion: "customresourceVersion", - Kind: "customresourceKind", + Kind: "customresourceKind2", Name: "name", UID: "2", Controller: &trueVar, }, { APIVersion: "customresourceVersion", - Kind: "customresourceKind", + Kind: "customresourceKind3", Name: "name", UID: "3", Controller: &trueVar, }, { APIVersion: "customresourceVersion", - Kind: "customresourceKind", + Kind: "customresourceKind4", Name: "name", UID: "4", }, }, expectError: true, - expectedErrorMessage: "Only one reference can have Controller set to true", + expectedErrorMessage: "Only one reference can have Controller set to true. Found \"true\" in references for customresourceKind2/name and customresourceKind3/name", }, } diff --git a/pkg/apis/OWNERS b/pkg/apis/OWNERS index 38336c177..402373247 100644 --- a/pkg/apis/OWNERS +++ b/pkg/apis/OWNERS @@ -4,8 +4,8 @@ options: no_parent_owners: true approvers: -- api-approvers + - api-approvers reviewers: -- api-reviewers + - api-reviewers labels: -- kind/api-change + - kind/api-change diff --git a/pkg/apis/meta/internalversion/zz_generated.conversion.go b/pkg/apis/meta/internalversion/zz_generated.conversion.go index a9b28f244..6d212b846 100644 --- a/pkg/apis/meta/internalversion/zz_generated.conversion.go +++ b/pkg/apis/meta/internalversion/zz_generated.conversion.go @@ -1,3 +1,4 @@ +//go:build !ignore_autogenerated // +build !ignore_autogenerated /* diff --git a/pkg/apis/meta/internalversion/zz_generated.deepcopy.go b/pkg/apis/meta/internalversion/zz_generated.deepcopy.go index d5e4fc680..6e1eac5c7 100644 --- a/pkg/apis/meta/internalversion/zz_generated.deepcopy.go +++ b/pkg/apis/meta/internalversion/zz_generated.deepcopy.go @@ -1,3 +1,4 @@ +//go:build !ignore_autogenerated // +build !ignore_autogenerated /* diff --git a/pkg/apis/meta/v1/OWNERS b/pkg/apis/meta/v1/OWNERS index 5731b9ee2..e7e5c152d 100644 --- a/pkg/apis/meta/v1/OWNERS +++ b/pkg/apis/meta/v1/OWNERS @@ -1,23 +1,16 @@ # See the OWNERS docs at https://go.k8s.io/owners reviewers: -- thockin -- smarterclayton -- wojtek-t -- deads2k -- brendandburns -- caesarxuchao -- liggitt -- davidopp -- sttts -- quinton-hoole -- luxas -- janetkuo -- justinsb -- ncdc -- soltysh -- dims -- hongchaodeng -- krousey -- therc -- kevin-wangzefeng + - thockin + - smarterclayton + - wojtek-t + - deads2k + - caesarxuchao + - liggitt + - sttts + - luxas + - janetkuo + - justinsb + - ncdc + - soltysh + - dims diff --git a/pkg/apis/meta/v1/generated.pb.go b/pkg/apis/meta/v1/generated.pb.go index 326f68812..7e00eb7d9 100644 --- a/pkg/apis/meta/v1/generated.pb.go +++ b/pkg/apis/meta/v1/generated.pb.go @@ -1326,184 +1326,185 @@ func init() { } var fileDescriptor_cf52fa777ced5367 = []byte{ - // 2829 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xcc, 0x3a, 0xcb, 0x6f, 0x24, 0x47, - 0xf9, 0xee, 0x19, 0x8f, 0x3d, 0xf3, 0x8d, 0xc7, 0x8f, 0x5a, 0xef, 0xef, 0x37, 0x6b, 0x84, 0xc7, - 0xe9, 0xa0, 0x68, 0x03, 0xc9, 0x38, 0x5e, 0x42, 0xb4, 0xd9, 0x90, 0x80, 0xc7, 0xb3, 0xde, 0x98, - 0xac, 0x63, 0xab, 0xbc, 0xbb, 0x40, 0x88, 0x50, 0xda, 0xdd, 0xe5, 0x71, 0xe3, 0x9e, 0xee, 0x49, - 0x55, 0x8f, 0x37, 0x03, 0x07, 0x72, 0x00, 0x01, 0x12, 0x44, 0xe1, 0xc6, 0x09, 0x25, 0x82, 0xbf, - 0x80, 0x0b, 0xfc, 0x01, 0x48, 0xe4, 0x18, 0xc4, 0x25, 0x12, 0x68, 0x94, 0x98, 0x03, 0x47, 0xc4, - 0xd5, 0x17, 0x50, 0x3d, 0xba, 0xbb, 0x7a, 0x1e, 0xeb, 0x9e, 0xec, 0x12, 0x71, 0x9b, 0xfe, 0xde, - 0x55, 0xf5, 0xd5, 0xf7, 0xaa, 0x81, 0xdd, 0x93, 0xeb, 0xac, 0xee, 0x06, 0xeb, 0x27, 0xdd, 0x43, - 0x42, 0x7d, 0x12, 0x12, 0xb6, 0x7e, 0x4a, 0x7c, 0x27, 0xa0, 0xeb, 0x0a, 0x61, 0x75, 0xdc, 0xb6, - 0x65, 0x1f, 0xbb, 0x3e, 0xa1, 0xbd, 0xf5, 0xce, 0x49, 0x8b, 0x03, 0xd8, 0x7a, 0x9b, 0x84, 0xd6, - 0xfa, 0xe9, 0xc6, 0x7a, 0x8b, 0xf8, 0x84, 0x5a, 0x21, 0x71, 0xea, 0x1d, 0x1a, 0x84, 0x01, 0xfa, - 0x82, 0xe4, 0xaa, 0xeb, 0x5c, 0xf5, 0xce, 0x49, 0x8b, 0x03, 0x58, 0x9d, 0x73, 0xd5, 0x4f, 0x37, - 0x56, 0x9e, 0x6e, 0xb9, 0xe1, 0x71, 0xf7, 0xb0, 0x6e, 0x07, 0xed, 0xf5, 0x56, 0xd0, 0x0a, 0xd6, - 0x05, 0xf3, 0x61, 0xf7, 0x48, 0x7c, 0x89, 0x0f, 0xf1, 0x4b, 0x0a, 0x5d, 0x19, 0x6b, 0x0a, 0xed, - 0xfa, 0xa1, 0xdb, 0x26, 0x83, 0x56, 0xac, 0x3c, 0x77, 0x11, 0x03, 0xb3, 0x8f, 0x49, 0xdb, 0x1a, - 0xe4, 0x33, 0xff, 0x94, 0x87, 0xe2, 0xe6, 0xfe, 0xce, 0x2d, 0x1a, 0x74, 0x3b, 0x68, 0x0d, 0xa6, - 0x7d, 0xab, 0x4d, 0xaa, 0xc6, 0x9a, 0x71, 0xb5, 0xd4, 0x98, 0xfb, 0xa0, 0x5f, 0x9b, 0x3a, 0xeb, - 0xd7, 0xa6, 0x5f, 0xb5, 0xda, 0x04, 0x0b, 0x0c, 0xf2, 0xa0, 0x78, 0x4a, 0x28, 0x73, 0x03, 0x9f, - 0x55, 0x73, 0x6b, 0xf9, 0xab, 0xe5, 0x6b, 0x2f, 0xd5, 0xb3, 0xac, 0xbf, 0x2e, 0x14, 0xdc, 0x93, - 0xac, 0xdb, 0x01, 0x6d, 0xba, 0xcc, 0x0e, 0x4e, 0x09, 0xed, 0x35, 0x16, 0x95, 0x96, 0xa2, 0x42, - 0x32, 0x1c, 0x6b, 0x40, 0x3f, 0x32, 0x60, 0xb1, 0x43, 0xc9, 0x11, 0xa1, 0x94, 0x38, 0x0a, 0x5f, - 0xcd, 0xaf, 0x19, 0x8f, 0x40, 0x6d, 0x55, 0xa9, 0x5d, 0xdc, 0x1f, 0x90, 0x8f, 0x87, 0x34, 0xa2, - 0xdf, 0x18, 0xb0, 0xc2, 0x08, 0x3d, 0x25, 0x74, 0xd3, 0x71, 0x28, 0x61, 0xac, 0xd1, 0xdb, 0xf2, - 0x5c, 0xe2, 0x87, 0x5b, 0x3b, 0x4d, 0xcc, 0xaa, 0xd3, 0x62, 0x1f, 0xbe, 0x96, 0xcd, 0xa0, 0x83, - 0x71, 0x72, 0x1a, 0xa6, 0xb2, 0x68, 0x65, 0x2c, 0x09, 0xc3, 0x0f, 0x30, 0xc3, 0x3c, 0x82, 0xb9, - 0xe8, 0x20, 0x6f, 0xbb, 0x2c, 0x44, 0xf7, 0x60, 0xa6, 0xc5, 0x3f, 0x58, 0xd5, 0x10, 0x06, 0xd6, - 0xb3, 0x19, 0x18, 0xc9, 0x68, 0xcc, 0x2b, 0x7b, 0x66, 0xc4, 0x27, 0xc3, 0x4a, 0x9a, 0xf9, 0xb3, - 0x69, 0x28, 0x6f, 0xee, 0xef, 0x60, 0xc2, 0x82, 0x2e, 0xb5, 0x49, 0x06, 0xa7, 0xb9, 0x0e, 0x73, - 0xcc, 0xf5, 0x5b, 0x5d, 0xcf, 0xa2, 0x1c, 0x5a, 0x9d, 0x11, 0x94, 0xcb, 0x8a, 0x72, 0xee, 0x40, - 0xc3, 0xe1, 0x14, 0x25, 0xba, 0x06, 0xc0, 0x25, 0xb0, 0x8e, 0x65, 0x13, 0xa7, 0x9a, 0x5b, 0x33, - 0xae, 0x16, 0x1b, 0x48, 0xf1, 0xc1, 0xab, 0x31, 0x06, 0x6b, 0x54, 0xe8, 0x71, 0x28, 0x08, 0x4b, - 0xab, 0x45, 0xa1, 0xa6, 0xa2, 0xc8, 0x0b, 0x62, 0x19, 0x58, 0xe2, 0xd0, 0x93, 0x30, 0xab, 0xbc, - 0xac, 0x5a, 0x12, 0x64, 0x0b, 0x8a, 0x6c, 0x36, 0x72, 0x83, 0x08, 0xcf, 0xd7, 0x77, 0xe2, 0xfa, - 0x8e, 0xf0, 0x3b, 0x6d, 0x7d, 0xaf, 0xb8, 0xbe, 0x83, 0x05, 0x06, 0xdd, 0x86, 0xc2, 0x29, 0xa1, - 0x87, 0xdc, 0x13, 0xb8, 0x6b, 0x7e, 0x29, 0xdb, 0x46, 0xdf, 0xe3, 0x2c, 0x8d, 0x12, 0x37, 0x4d, - 0xfc, 0xc4, 0x52, 0x08, 0xaa, 0x03, 0xb0, 0xe3, 0x80, 0x86, 0x62, 0x79, 0xd5, 0xc2, 0x5a, 0xfe, - 0x6a, 0xa9, 0x31, 0xcf, 0xd7, 0x7b, 0x10, 0x43, 0xb1, 0x46, 0xc1, 0xe9, 0x6d, 0x2b, 0x24, 0xad, - 0x80, 0xba, 0x84, 0x55, 0x67, 0x13, 0xfa, 0xad, 0x18, 0x8a, 0x35, 0x0a, 0xf4, 0x0d, 0x40, 0x2c, - 0x0c, 0xa8, 0xd5, 0x22, 0x6a, 0xa9, 0x2f, 0x5b, 0xec, 0xb8, 0x0a, 0x62, 0x75, 0x2b, 0x6a, 0x75, - 0xe8, 0x60, 0x88, 0x02, 0x8f, 0xe0, 0x32, 0x7f, 0x67, 0xc0, 0x82, 0xe6, 0x0b, 0xc2, 0xef, 0xae, - 0xc3, 0x5c, 0x4b, 0xbb, 0x75, 0xca, 0x2f, 0xe2, 0xd3, 0xd6, 0x6f, 0x24, 0x4e, 0x51, 0x22, 0x02, - 0x25, 0xaa, 0x24, 0x45, 0xd1, 0x65, 0x23, 0xb3, 0xd3, 0x46, 0x36, 0x24, 0x9a, 0x34, 0x20, 0xc3, - 0x89, 0x64, 0xf3, 0x1f, 0x86, 0x70, 0xe0, 0x28, 0xde, 0xa0, 0xab, 0x5a, 0x4c, 0x33, 0xc4, 0xf6, - 0xcd, 0x8d, 0x89, 0x47, 0x17, 0x04, 0x82, 0xdc, 0xff, 0x44, 0x20, 0xb8, 0x51, 0xfc, 0xd5, 0x7b, - 0xb5, 0xa9, 0xb7, 0xff, 0xb6, 0x36, 0x65, 0xfe, 0xd2, 0x80, 0xb9, 0xcd, 0x4e, 0xc7, 0xeb, 0xed, - 0x75, 0x42, 0xb1, 0x00, 0x13, 0x66, 0x1c, 0xda, 0xc3, 0x5d, 0x5f, 0x2d, 0x14, 0xf8, 0xfd, 0x6e, - 0x0a, 0x08, 0x56, 0x18, 0x7e, 0x7f, 0x8e, 0x02, 0x6a, 0x13, 0x75, 0xdd, 0xe2, 0xfb, 0xb3, 0xcd, - 0x81, 0x58, 0xe2, 0xf8, 0x21, 0x1f, 0xb9, 0xc4, 0x73, 0x76, 0x2d, 0xdf, 0x6a, 0x11, 0xaa, 0x2e, - 0x47, 0xbc, 0xf5, 0xdb, 0x1a, 0x0e, 0xa7, 0x28, 0xcd, 0x7f, 0xe7, 0xa0, 0xb4, 0x15, 0xf8, 0x8e, - 0x1b, 0xaa, 0xcb, 0x15, 0xf6, 0x3a, 0x43, 0xc1, 0xe3, 0x4e, 0xaf, 0x43, 0xb0, 0xc0, 0xa0, 0xe7, - 0x61, 0x86, 0x85, 0x56, 0xd8, 0x65, 0xc2, 0x9e, 0x52, 0xe3, 0xb1, 0x28, 0x2c, 0x1d, 0x08, 0xe8, - 0x79, 0xbf, 0xb6, 0x10, 0x8b, 0x93, 0x20, 0xac, 0x18, 0xb8, 0xa7, 0x07, 0x87, 0x62, 0xa3, 0x9c, - 0x5b, 0x32, 0xed, 0x45, 0xf9, 0x23, 0x9f, 0x78, 0xfa, 0xde, 0x10, 0x05, 0x1e, 0xc1, 0x85, 0x4e, - 0x01, 0x79, 0x16, 0x0b, 0xef, 0x50, 0xcb, 0x67, 0x42, 0xd7, 0x1d, 0xb7, 0x4d, 0xd4, 0x85, 0xff, - 0x62, 0xb6, 0x13, 0xe7, 0x1c, 0x89, 0xde, 0xdb, 0x43, 0xd2, 0xf0, 0x08, 0x0d, 0xe8, 0x09, 0x98, - 0xa1, 0xc4, 0x62, 0x81, 0x5f, 0x2d, 0x88, 0xe5, 0xc7, 0x51, 0x19, 0x0b, 0x28, 0x56, 0x58, 0x1e, - 0xd0, 0xda, 0x84, 0x31, 0xab, 0x15, 0x85, 0xd7, 0x38, 0xa0, 0xed, 0x4a, 0x30, 0x8e, 0xf0, 0x66, - 0x1b, 0x2a, 0x5b, 0x94, 0x58, 0x21, 0x99, 0xc4, 0x2b, 0x3e, 0xfd, 0x81, 0xff, 0x24, 0x0f, 0x95, - 0x26, 0xf1, 0x48, 0xa2, 0x6f, 0x1b, 0x50, 0x8b, 0x5a, 0x36, 0xd9, 0x27, 0xd4, 0x0d, 0x9c, 0x03, - 0x62, 0x07, 0xbe, 0xc3, 0x84, 0x0b, 0xe4, 0x1b, 0xff, 0xc7, 0xf7, 0xe6, 0xd6, 0x10, 0x16, 0x8f, - 0xe0, 0x40, 0x1e, 0x54, 0x3a, 0x54, 0xfc, 0x16, 0xfb, 0x25, 0x3d, 0xa4, 0x7c, 0xed, 0xcb, 0xd9, - 0x8e, 0x63, 0x5f, 0x67, 0x6d, 0x2c, 0x9d, 0xf5, 0x6b, 0x95, 0x14, 0x08, 0xa7, 0x85, 0xa3, 0xaf, - 0xc3, 0x62, 0x40, 0x3b, 0xc7, 0x96, 0xdf, 0x24, 0x1d, 0xe2, 0x3b, 0xc4, 0x0f, 0x99, 0xd8, 0x85, - 0x62, 0x63, 0x99, 0xd7, 0x11, 0x7b, 0x03, 0x38, 0x3c, 0x44, 0x8d, 0x5e, 0x83, 0xa5, 0x0e, 0x0d, - 0x3a, 0x56, 0x4b, 0xb8, 0xd4, 0x7e, 0xe0, 0xb9, 0x76, 0x4f, 0xb8, 0x50, 0xa9, 0xf1, 0xd4, 0x59, - 0xbf, 0xb6, 0xb4, 0x3f, 0x88, 0x3c, 0xef, 0xd7, 0x2e, 0x89, 0xad, 0xe3, 0x90, 0x04, 0x89, 0x87, - 0xc5, 0x68, 0x67, 0x58, 0x18, 0x77, 0x86, 0xe6, 0x0e, 0x14, 0x9b, 0x5d, 0xe5, 0xcf, 0x2f, 0x42, - 0xd1, 0x51, 0xbf, 0xd5, 0xce, 0x47, 0x17, 0x2b, 0xa6, 0x39, 0xef, 0xd7, 0x2a, 0xbc, 0x74, 0xac, - 0x47, 0x00, 0x1c, 0xb3, 0x98, 0x4f, 0x40, 0x51, 0x1c, 0x39, 0xbb, 0xb7, 0x81, 0x16, 0x21, 0x8f, - 0xad, 0xfb, 0x42, 0xca, 0x1c, 0xe6, 0x3f, 0xb5, 0x08, 0xb4, 0x07, 0x70, 0x8b, 0x84, 0xd1, 0xc1, - 0x6f, 0xc2, 0x42, 0x14, 0x86, 0xd3, 0xd9, 0xe1, 0xff, 0x95, 0xee, 0x05, 0x9c, 0x46, 0xe3, 0x41, - 0x7a, 0xf3, 0x75, 0x28, 0x89, 0x0c, 0xc2, 0xd3, 0x6f, 0x92, 0xea, 0x8d, 0x07, 0xa4, 0xfa, 0x28, - 0x7f, 0xe7, 0xc6, 0xe5, 0x6f, 0xcd, 0x5c, 0x0f, 0x2a, 0x92, 0x37, 0x2a, 0x6e, 0x32, 0x69, 0x78, - 0x0a, 0x8a, 0x91, 0x99, 0x4a, 0x4b, 0x5c, 0xd4, 0x46, 0x82, 0x70, 0x4c, 0xa1, 0x69, 0x3b, 0x86, - 0x54, 0x36, 0xcc, 0xa6, 0x4c, 0xab, 0x5c, 0x72, 0x0f, 0xae, 0x5c, 0x34, 0x4d, 0x3f, 0x84, 0xea, - 0xb8, 0x4a, 0xf8, 0x21, 0xf2, 0x75, 0x76, 0x53, 0xcc, 0x77, 0x0c, 0x58, 0xd4, 0x25, 0x65, 0x3f, - 0xbe, 0xec, 0x4a, 0x2e, 0xae, 0xd4, 0xb4, 0x1d, 0xf9, 0xb5, 0x01, 0xcb, 0xa9, 0xa5, 0x4d, 0x74, - 0xe2, 0x13, 0x18, 0xa5, 0x3b, 0x47, 0x7e, 0x02, 0xe7, 0xf8, 0x4b, 0x0e, 0x2a, 0xb7, 0xad, 0x43, - 0xe2, 0x1d, 0x10, 0x8f, 0xd8, 0x61, 0x40, 0xd1, 0x0f, 0xa0, 0xdc, 0xb6, 0x42, 0xfb, 0x58, 0x40, - 0xa3, 0xaa, 0xbe, 0x99, 0x2d, 0xd8, 0xa5, 0x24, 0xd5, 0x77, 0x13, 0x31, 0x37, 0xfd, 0x90, 0xf6, - 0x1a, 0x97, 0x94, 0x49, 0x65, 0x0d, 0x83, 0x75, 0x6d, 0xa2, 0x15, 0x13, 0xdf, 0x37, 0xdf, 0xea, - 0xf0, 0x92, 0x63, 0xf2, 0x0e, 0x30, 0x65, 0x02, 0x26, 0x6f, 0x76, 0x5d, 0x4a, 0xda, 0xc4, 0x0f, - 0x93, 0x56, 0x6c, 0x77, 0x40, 0x3e, 0x1e, 0xd2, 0xb8, 0xf2, 0x12, 0x2c, 0x0e, 0x1a, 0xcf, 0xe3, - 0xcf, 0x09, 0xe9, 0xc9, 0xf3, 0xc2, 0xfc, 0x27, 0x5a, 0x86, 0xc2, 0xa9, 0xe5, 0x75, 0xd5, 0x6d, - 0xc4, 0xf2, 0xe3, 0x46, 0xee, 0xba, 0x61, 0xfe, 0xd6, 0x80, 0xea, 0x38, 0x43, 0xd0, 0xe7, 0x35, - 0x41, 0x8d, 0xb2, 0xb2, 0x2a, 0xff, 0x0a, 0xe9, 0x49, 0xa9, 0x37, 0xa1, 0x18, 0x74, 0x78, 0x3d, - 0x10, 0x50, 0x75, 0xea, 0x4f, 0x46, 0x27, 0xb9, 0xa7, 0xe0, 0xe7, 0xfd, 0xda, 0xe5, 0x94, 0xf8, - 0x08, 0x81, 0x63, 0x56, 0x1e, 0xa9, 0x85, 0x3d, 0x3c, 0x7b, 0xc4, 0x91, 0xfa, 0x9e, 0x80, 0x60, - 0x85, 0x31, 0xff, 0x60, 0xc0, 0xb4, 0x28, 0xa6, 0x5f, 0x87, 0x22, 0xdf, 0x3f, 0xc7, 0x0a, 0x2d, - 0x61, 0x57, 0xe6, 0x36, 0x8e, 0x73, 0xef, 0x92, 0xd0, 0x4a, 0xbc, 0x2d, 0x82, 0xe0, 0x58, 0x22, - 0xc2, 0x50, 0x70, 0x43, 0xd2, 0x8e, 0x0e, 0xf2, 0xe9, 0xb1, 0xa2, 0xd5, 0x10, 0xa1, 0x8e, 0xad, - 0xfb, 0x37, 0xdf, 0x0a, 0x89, 0xcf, 0x0f, 0x23, 0xb9, 0x1a, 0x3b, 0x5c, 0x06, 0x96, 0xa2, 0xcc, - 0x7f, 0x19, 0x10, 0xab, 0xe2, 0xce, 0xcf, 0x88, 0x77, 0x74, 0xdb, 0xf5, 0x4f, 0xd4, 0xb6, 0xc6, - 0xe6, 0x1c, 0x28, 0x38, 0x8e, 0x29, 0x46, 0xa5, 0x87, 0xdc, 0x64, 0xe9, 0x81, 0x2b, 0xb4, 0x03, - 0x3f, 0x74, 0xfd, 0xee, 0xd0, 0x6d, 0xdb, 0x52, 0x70, 0x1c, 0x53, 0xf0, 0x42, 0x84, 0x92, 0xb6, - 0xe5, 0xfa, 0xae, 0xdf, 0xe2, 0x8b, 0xd8, 0x0a, 0xba, 0x7e, 0x28, 0x32, 0xb2, 0x2a, 0x44, 0xf0, - 0x10, 0x16, 0x8f, 0xe0, 0x30, 0x7f, 0x3f, 0x0d, 0x65, 0xbe, 0xe6, 0x28, 0xcf, 0xbd, 0x00, 0x15, - 0x4f, 0xf7, 0x02, 0xb5, 0xf6, 0xcb, 0xca, 0x94, 0xf4, 0xbd, 0xc6, 0x69, 0x5a, 0xce, 0x2c, 0xea, - 0xa7, 0x98, 0x39, 0x97, 0x66, 0xde, 0xd6, 0x91, 0x38, 0x4d, 0xcb, 0xa3, 0xd7, 0x7d, 0x7e, 0x3f, - 0x54, 0x65, 0x12, 0x1f, 0xd1, 0x37, 0x39, 0x10, 0x4b, 0x1c, 0xda, 0x85, 0x4b, 0x96, 0xe7, 0x05, - 0xf7, 0x05, 0xb0, 0x11, 0x04, 0x27, 0x6d, 0x8b, 0x9e, 0x30, 0xd1, 0x08, 0x17, 0x1b, 0x9f, 0x53, - 0x2c, 0x97, 0x36, 0x87, 0x49, 0xf0, 0x28, 0xbe, 0x51, 0xc7, 0x36, 0x3d, 0xe1, 0xb1, 0x1d, 0xc3, - 0xf2, 0x00, 0x48, 0xdc, 0x72, 0xd5, 0x95, 0x3e, 0xab, 0xe4, 0x2c, 0xe3, 0x11, 0x34, 0xe7, 0x63, - 0xe0, 0x78, 0xa4, 0x44, 0x74, 0x03, 0xe6, 0xb9, 0x27, 0x07, 0xdd, 0x30, 0xaa, 0x3b, 0x0b, 0xe2, - 0xb8, 0xd1, 0x59, 0xbf, 0x36, 0x7f, 0x27, 0x85, 0xc1, 0x03, 0x94, 0x7c, 0x73, 0x3d, 0xb7, 0xed, - 0x86, 0xd5, 0x59, 0xc1, 0x12, 0x6f, 0xee, 0x6d, 0x0e, 0xc4, 0x12, 0x97, 0xf2, 0xc0, 0xe2, 0x45, - 0x1e, 0x68, 0xfe, 0x39, 0x0f, 0x48, 0x16, 0xca, 0x8e, 0xac, 0xa7, 0x64, 0x48, 0xe3, 0xd5, 0xbc, - 0x2a, 0xb4, 0x8d, 0x81, 0x6a, 0x5e, 0xd5, 0xd8, 0x11, 0x1e, 0xed, 0x42, 0x49, 0x86, 0x96, 0xe4, - 0xba, 0xac, 0x2b, 0xe2, 0xd2, 0x5e, 0x84, 0x38, 0xef, 0xd7, 0x56, 0x52, 0x6a, 0x62, 0x8c, 0xe8, - 0xb4, 0x12, 0x09, 0xe8, 0x1a, 0x80, 0xd5, 0x71, 0xf5, 0x59, 0x5b, 0x29, 0x99, 0xb8, 0x24, 0x5d, - 0x33, 0xd6, 0xa8, 0xd0, 0xcb, 0x30, 0x1d, 0x7e, 0xba, 0x6e, 0xa8, 0x28, 0x9a, 0x3d, 0xde, 0xfb, - 0x08, 0x09, 0x5c, 0xbb, 0xf0, 0x67, 0xc6, 0xcd, 0x52, 0x8d, 0x4c, 0xac, 0x7d, 0x3b, 0xc6, 0x60, - 0x8d, 0x0a, 0x7d, 0x0b, 0x8a, 0x47, 0xaa, 0x14, 0x15, 0x07, 0x93, 0x39, 0x44, 0x46, 0x05, 0xac, - 0x6c, 0xf7, 0xa3, 0x2f, 0x1c, 0x4b, 0x43, 0x5f, 0x81, 0x32, 0xeb, 0x1e, 0xc6, 0xd9, 0x5b, 0x9e, - 0x66, 0x9c, 0x2a, 0x0f, 0x12, 0x14, 0xd6, 0xe9, 0xcc, 0x37, 0xa1, 0xb4, 0xeb, 0xda, 0x34, 0x10, - 0xfd, 0xdb, 0x93, 0x30, 0xcb, 0x52, 0x0d, 0x4e, 0x7c, 0x92, 0x91, 0x97, 0x45, 0x78, 0xee, 0x5e, - 0xbe, 0xe5, 0x07, 0xb2, 0x8d, 0x29, 0x24, 0xee, 0xf5, 0x2a, 0x07, 0x62, 0x89, 0xbb, 0xb1, 0xcc, - 0x0b, 0x84, 0x9f, 0xbe, 0x5f, 0x9b, 0x7a, 0xf7, 0xfd, 0xda, 0xd4, 0x7b, 0xef, 0xab, 0x62, 0xe1, - 0x1c, 0x00, 0xf6, 0x0e, 0xbf, 0x47, 0x6c, 0x19, 0x76, 0x33, 0x8d, 0xe4, 0xa2, 0x49, 0xb0, 0x18, - 0xc9, 0xe5, 0x06, 0x8a, 0x3e, 0x0d, 0x87, 0x53, 0x94, 0x68, 0x1d, 0x4a, 0xf1, 0xb0, 0x4d, 0xf9, - 0xc7, 0x52, 0xe4, 0x6f, 0xf1, 0x44, 0x0e, 0x27, 0x34, 0xa9, 0x1c, 0x30, 0x7d, 0x61, 0x0e, 0x68, - 0x40, 0xbe, 0xeb, 0x3a, 0xaa, 0xd9, 0x7d, 0x26, 0xca, 0xc1, 0x77, 0x77, 0x9a, 0xe7, 0xfd, 0xda, - 0x63, 0xe3, 0x66, 0xdc, 0x61, 0xaf, 0x43, 0x58, 0xfd, 0xee, 0x4e, 0x13, 0x73, 0xe6, 0x51, 0x01, - 0x69, 0x66, 0xc2, 0x80, 0x74, 0x0d, 0xa0, 0x95, 0x8c, 0x0c, 0xe4, 0x7d, 0x8f, 0x1d, 0x51, 0x1b, - 0x15, 0x68, 0x54, 0x88, 0xc1, 0x92, 0xcd, 0xfb, 0x6a, 0xd5, 0xba, 0xb3, 0xd0, 0x6a, 0xcb, 0x21, - 0xe4, 0x64, 0x77, 0xe2, 0x8a, 0x52, 0xb3, 0xb4, 0x35, 0x28, 0x0c, 0x0f, 0xcb, 0x47, 0x01, 0x2c, - 0x39, 0xaa, 0x43, 0x4c, 0x94, 0x96, 0x26, 0x56, 0x7a, 0x99, 0x2b, 0x6c, 0x0e, 0x0a, 0xc2, 0xc3, - 0xb2, 0xd1, 0x77, 0x61, 0x25, 0x02, 0x0e, 0xb7, 0xe9, 0x22, 0x60, 0xe7, 0x1b, 0xab, 0x67, 0xfd, - 0xda, 0x4a, 0x73, 0x2c, 0x15, 0x7e, 0x80, 0x04, 0xe4, 0xc0, 0x8c, 0x27, 0x0b, 0xdc, 0xb2, 0x28, - 0x4a, 0xbe, 0x9a, 0x6d, 0x15, 0x89, 0xf7, 0xd7, 0xf5, 0xc2, 0x36, 0x1e, 0x97, 0xa8, 0x9a, 0x56, - 0xc9, 0x46, 0x6f, 0x41, 0xd9, 0xf2, 0xfd, 0x20, 0xb4, 0xe4, 0xe0, 0x60, 0x4e, 0xa8, 0xda, 0x9c, - 0x58, 0xd5, 0x66, 0x22, 0x63, 0xa0, 0x90, 0xd6, 0x30, 0x58, 0x57, 0x85, 0xee, 0xc3, 0x42, 0x70, - 0xdf, 0x27, 0x14, 0x93, 0x23, 0x42, 0x89, 0x6f, 0x13, 0x56, 0xad, 0x08, 0xed, 0xcf, 0x66, 0xd4, - 0x9e, 0x62, 0x4e, 0x5c, 0x3a, 0x0d, 0x67, 0x78, 0x50, 0x0b, 0xaa, 0xf3, 0xd8, 0xea, 0x5b, 0x9e, - 0xfb, 0x7d, 0x42, 0x59, 0x75, 0x3e, 0x99, 0x13, 0x6f, 0xc7, 0x50, 0xac, 0x51, 0xf0, 0xe8, 0x67, - 0x7b, 0x5d, 0x16, 0x12, 0x39, 0xb4, 0x5f, 0x48, 0x47, 0xbf, 0xad, 0x04, 0x85, 0x75, 0x3a, 0xd4, - 0x85, 0x4a, 0x5b, 0xcf, 0x34, 0xd5, 0x25, 0xb1, 0xba, 0xeb, 0xd9, 0x56, 0x37, 0x9c, 0x0b, 0x93, - 0xc2, 0x27, 0x85, 0xc3, 0x69, 0x2d, 0x2b, 0xcf, 0x43, 0xf9, 0x53, 0xf6, 0x04, 0xbc, 0xa7, 0x18, - 0x3c, 0xc7, 0x89, 0x7a, 0x8a, 0x3f, 0xe6, 0x60, 0x3e, 0xbd, 0xfb, 0x03, 0x59, 0xb4, 0x90, 0x29, - 0x8b, 0x46, 0xdd, 0xab, 0x31, 0xf6, 0x9d, 0x21, 0x0a, 0xeb, 0xf9, 0xb1, 0x61, 0x5d, 0x45, 0xcf, - 0xe9, 0x87, 0x89, 0x9e, 0x75, 0x00, 0x5e, 0x9e, 0xd0, 0xc0, 0xf3, 0x08, 0x15, 0x81, 0xb3, 0xa8, - 0xde, 0x13, 0x62, 0x28, 0xd6, 0x28, 0x78, 0x11, 0x7d, 0xe8, 0x05, 0xf6, 0x89, 0xd8, 0x82, 0xe8, - 0xd2, 0x8b, 0x90, 0x59, 0x94, 0x45, 0x74, 0x63, 0x08, 0x8b, 0x47, 0x70, 0x98, 0x3d, 0xb8, 0xbc, - 0x6f, 0xd1, 0xd0, 0xb5, 0xbc, 0xe4, 0x82, 0x89, 0x2e, 0xe5, 0x8d, 0xa1, 0x1e, 0xe8, 0x99, 0x49, - 0x2f, 0x6a, 0xb2, 0xf9, 0x09, 0x2c, 0xe9, 0x83, 0xcc, 0xbf, 0x1a, 0x70, 0x65, 0xa4, 0xee, 0xcf, - 0xa0, 0x07, 0x7b, 0x23, 0xdd, 0x83, 0xbd, 0x90, 0x71, 0x78, 0x39, 0xca, 0xda, 0x31, 0x1d, 0xd9, - 0x2c, 0x14, 0xf6, 0x79, 0xed, 0x6b, 0xfe, 0xc2, 0x80, 0x39, 0xf1, 0x6b, 0x92, 0xc1, 0x6f, 0x2d, - 0xfd, 0x1c, 0x50, 0x7a, 0x84, 0x4f, 0x01, 0xef, 0x18, 0x90, 0x1e, 0xb9, 0xa2, 0x97, 0xa4, 0xff, - 0x1a, 0xf1, 0x4c, 0x74, 0x42, 0xdf, 0x7d, 0x71, 0x5c, 0x07, 0x79, 0x29, 0xd3, 0x70, 0xf1, 0x29, - 0x28, 0xe1, 0x20, 0x08, 0xf7, 0xad, 0xf0, 0x98, 0xf1, 0x85, 0x77, 0xf8, 0x0f, 0xb5, 0x37, 0x62, - 0xe1, 0x02, 0x83, 0x25, 0xdc, 0xfc, 0xb9, 0x01, 0x57, 0xc6, 0x3e, 0xd1, 0xf0, 0x10, 0x60, 0xc7, - 0x5f, 0x6a, 0x45, 0xb1, 0x17, 0x26, 0x74, 0x58, 0xa3, 0xe2, 0xad, 0x5f, 0xea, 0x5d, 0x67, 0xb0, - 0xf5, 0x4b, 0x69, 0xc3, 0x69, 0x5a, 0xf3, 0x9f, 0x39, 0x50, 0x6f, 0x22, 0xff, 0x65, 0x8f, 0x7d, - 0x62, 0xe0, 0x45, 0x66, 0x3e, 0xfd, 0x22, 0x13, 0x3f, 0xbf, 0x68, 0x4f, 0x12, 0xf9, 0x07, 0x3f, - 0x49, 0xa0, 0xe7, 0xe2, 0x57, 0x0e, 0x19, 0xba, 0x56, 0xd3, 0xaf, 0x1c, 0xe7, 0xfd, 0xda, 0x9c, - 0x12, 0x9e, 0x7e, 0xf5, 0x78, 0x0d, 0x66, 0x1d, 0x12, 0x5a, 0xae, 0x27, 0xdb, 0xb8, 0xcc, 0xb3, - 0x7f, 0x29, 0xac, 0x29, 0x59, 0x1b, 0x65, 0x6e, 0x93, 0xfa, 0xc0, 0x91, 0x40, 0x1e, 0x6d, 0xed, - 0xc0, 0x91, 0x5d, 0x48, 0x21, 0x89, 0xb6, 0x5b, 0x81, 0x43, 0xb0, 0xc0, 0x98, 0xef, 0x1a, 0x50, - 0x96, 0x92, 0xb6, 0xac, 0x2e, 0x23, 0x68, 0x23, 0x5e, 0x85, 0x3c, 0xee, 0x2b, 0xfa, 0x73, 0xd6, - 0x79, 0xbf, 0x56, 0x12, 0x64, 0xa2, 0x81, 0x19, 0xf1, 0x6c, 0x93, 0xbb, 0x60, 0x8f, 0x1e, 0x87, - 0x82, 0xb8, 0x3d, 0x6a, 0x33, 0x93, 0x77, 0x39, 0x0e, 0xc4, 0x12, 0x67, 0x7e, 0x9c, 0x83, 0x4a, - 0x6a, 0x71, 0x19, 0x7a, 0x81, 0x78, 0xe2, 0x99, 0xcb, 0x30, 0x45, 0x1f, 0xff, 0x0a, 0xae, 0x72, - 0xcf, 0xcc, 0xc3, 0xe4, 0x9e, 0x6f, 0xc3, 0x8c, 0xcd, 0xf7, 0x28, 0xfa, 0x53, 0xc5, 0xc6, 0x24, - 0xc7, 0x29, 0x76, 0x37, 0xf1, 0x46, 0xf1, 0xc9, 0xb0, 0x12, 0x88, 0x6e, 0xc1, 0x12, 0x25, 0x21, - 0xed, 0x6d, 0x1e, 0x85, 0x84, 0xea, 0xbd, 0x7f, 0x21, 0xa9, 0xb8, 0xf1, 0x20, 0x01, 0x1e, 0xe6, - 0x31, 0x0f, 0x61, 0xee, 0x8e, 0x75, 0xe8, 0xc5, 0xaf, 0x59, 0x18, 0x2a, 0xae, 0x6f, 0x7b, 0x5d, - 0x87, 0xc8, 0x68, 0x1c, 0x45, 0xaf, 0xe8, 0xd2, 0xee, 0xe8, 0xc8, 0xf3, 0x7e, 0xed, 0x52, 0x0a, - 0x20, 0x9f, 0x6f, 0x70, 0x5a, 0x84, 0xe9, 0xc1, 0xf4, 0x67, 0xd8, 0x3d, 0x7e, 0x07, 0x4a, 0x49, - 0x7d, 0xff, 0x88, 0x55, 0x9a, 0x6f, 0x40, 0x91, 0x7b, 0x7c, 0xd4, 0x97, 0x5e, 0x50, 0xe2, 0xa4, - 0x0b, 0xa7, 0x5c, 0x96, 0xc2, 0xc9, 0x6c, 0x43, 0xe5, 0x6e, 0xc7, 0x79, 0xc8, 0xf7, 0xcc, 0x5c, - 0xe6, 0xac, 0x75, 0x0d, 0xe4, 0xff, 0x35, 0x78, 0x82, 0x90, 0x99, 0x5b, 0x4b, 0x10, 0x7a, 0xe2, - 0xd5, 0x86, 0xf9, 0x3f, 0x36, 0x00, 0xc4, 0xd4, 0xec, 0xe6, 0x29, 0xf1, 0xc3, 0x0c, 0xaf, 0xde, - 0x77, 0x61, 0x26, 0x90, 0xde, 0x24, 0xdf, 0x34, 0x27, 0x1c, 0xcd, 0xc6, 0x97, 0x40, 0xfa, 0x13, - 0x56, 0xc2, 0x1a, 0x57, 0x3f, 0xf8, 0x64, 0x75, 0xea, 0xc3, 0x4f, 0x56, 0xa7, 0x3e, 0xfa, 0x64, - 0x75, 0xea, 0xed, 0xb3, 0x55, 0xe3, 0x83, 0xb3, 0x55, 0xe3, 0xc3, 0xb3, 0x55, 0xe3, 0xa3, 0xb3, - 0x55, 0xe3, 0xe3, 0xb3, 0x55, 0xe3, 0xdd, 0xbf, 0xaf, 0x4e, 0xbd, 0x96, 0x3b, 0xdd, 0xf8, 0x4f, - 0x00, 0x00, 0x00, 0xff, 0xff, 0x0d, 0x18, 0xc5, 0x8c, 0x25, 0x27, 0x00, 0x00, + // 2842 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xcc, 0x1a, 0x4b, 0x6f, 0x24, 0x47, + 0xd9, 0x3d, 0x0f, 0x7b, 0xe6, 0x9b, 0x19, 0x3f, 0x6a, 0xbd, 0x30, 0x6b, 0x84, 0xc7, 0xe9, 0x44, + 0xd1, 0x06, 0x92, 0x71, 0x76, 0x09, 0xd1, 0x66, 0x43, 0x02, 0x1e, 0xcf, 0x7a, 0xe3, 0x64, 0x1d, + 0x5b, 0xe5, 0xdd, 0x05, 0x42, 0x84, 0xd2, 0x9e, 0x2e, 0x8f, 0x1b, 0xf7, 0x74, 0x4f, 0xaa, 0x7a, + 0xbc, 0x19, 0x38, 0x90, 0x03, 0x08, 0x90, 0x50, 0x14, 0x6e, 0x9c, 0x50, 0x22, 0xf8, 0x01, 0x88, + 0x0b, 0xdc, 0x41, 0x22, 0xc7, 0x20, 0x2e, 0x91, 0x40, 0xa3, 0xc4, 0x1c, 0x38, 0x22, 0xae, 0xbe, + 0x80, 0xea, 0xd1, 0xdd, 0xd5, 0xf3, 0x58, 0xf7, 0x64, 0x97, 0x88, 0xdb, 0xf4, 0xf7, 0xae, 0xaa, + 0xaf, 0xbe, 0x47, 0x7d, 0x03, 0x3b, 0xc7, 0xd7, 0x58, 0xdd, 0xf1, 0xd7, 0x8f, 0x7b, 0x07, 0x84, + 0x7a, 0x24, 0x20, 0x6c, 0xfd, 0x84, 0x78, 0xb6, 0x4f, 0xd7, 0x15, 0xc2, 0xea, 0x3a, 0x1d, 0xab, + 0x75, 0xe4, 0x78, 0x84, 0xf6, 0xd7, 0xbb, 0xc7, 0x6d, 0x0e, 0x60, 0xeb, 0x1d, 0x12, 0x58, 0xeb, + 0x27, 0x57, 0xd6, 0xdb, 0xc4, 0x23, 0xd4, 0x0a, 0x88, 0x5d, 0xef, 0x52, 0x3f, 0xf0, 0xd1, 0x63, + 0x92, 0xab, 0xae, 0x73, 0xd5, 0xbb, 0xc7, 0x6d, 0x0e, 0x60, 0x75, 0xce, 0x55, 0x3f, 0xb9, 0xb2, + 0xf2, 0x54, 0xdb, 0x09, 0x8e, 0x7a, 0x07, 0xf5, 0x96, 0xdf, 0x59, 0x6f, 0xfb, 0x6d, 0x7f, 0x5d, + 0x30, 0x1f, 0xf4, 0x0e, 0xc5, 0x97, 0xf8, 0x10, 0xbf, 0xa4, 0xd0, 0x95, 0x89, 0xa6, 0xd0, 0x9e, + 0x17, 0x38, 0x1d, 0x32, 0x6c, 0xc5, 0xca, 0xb3, 0xe7, 0x31, 0xb0, 0xd6, 0x11, 0xe9, 0x58, 0xc3, + 0x7c, 0xe6, 0x9f, 0xb3, 0x50, 0xd8, 0xd8, 0xdb, 0xbe, 0x49, 0xfd, 0x5e, 0x17, 0xad, 0x41, 0xce, + 0xb3, 0x3a, 0xa4, 0x6a, 0xac, 0x19, 0x97, 0x8b, 0x8d, 0xf2, 0x07, 0x83, 0xda, 0xcc, 0xe9, 0xa0, + 0x96, 0x7b, 0xd5, 0xea, 0x10, 0x2c, 0x30, 0xc8, 0x85, 0xc2, 0x09, 0xa1, 0xcc, 0xf1, 0x3d, 0x56, + 0xcd, 0xac, 0x65, 0x2f, 0x97, 0xae, 0xbe, 0x58, 0x4f, 0xb3, 0xfe, 0xba, 0x50, 0x70, 0x57, 0xb2, + 0x6e, 0xf9, 0xb4, 0xe9, 0xb0, 0x96, 0x7f, 0x42, 0x68, 0xbf, 0xb1, 0xa8, 0xb4, 0x14, 0x14, 0x92, + 0xe1, 0x48, 0x03, 0xfa, 0x91, 0x01, 0x8b, 0x5d, 0x4a, 0x0e, 0x09, 0xa5, 0xc4, 0x56, 0xf8, 0x6a, + 0x76, 0xcd, 0x78, 0x08, 0x6a, 0xab, 0x4a, 0xed, 0xe2, 0xde, 0x90, 0x7c, 0x3c, 0xa2, 0x11, 0xfd, + 0xda, 0x80, 0x15, 0x46, 0xe8, 0x09, 0xa1, 0x1b, 0xb6, 0x4d, 0x09, 0x63, 0x8d, 0xfe, 0xa6, 0xeb, + 0x10, 0x2f, 0xd8, 0xdc, 0x6e, 0x62, 0x56, 0xcd, 0x89, 0x7d, 0xf8, 0x7a, 0x3a, 0x83, 0xf6, 0x27, + 0xc9, 0x69, 0x98, 0xca, 0xa2, 0x95, 0x89, 0x24, 0x0c, 0xdf, 0xc7, 0x0c, 0xf3, 0x10, 0xca, 0xe1, + 0x41, 0xde, 0x72, 0x58, 0x80, 0xee, 0xc2, 0x6c, 0x9b, 0x7f, 0xb0, 0xaa, 0x21, 0x0c, 0xac, 0xa7, + 0x33, 0x30, 0x94, 0xd1, 0x98, 0x57, 0xf6, 0xcc, 0x8a, 0x4f, 0x86, 0x95, 0x34, 0xf3, 0x67, 0x39, + 0x28, 0x6d, 0xec, 0x6d, 0x63, 0xc2, 0xfc, 0x1e, 0x6d, 0x91, 0x14, 0x4e, 0x73, 0x0d, 0xca, 0xcc, + 0xf1, 0xda, 0x3d, 0xd7, 0xa2, 0x1c, 0x5a, 0x9d, 0x15, 0x94, 0xcb, 0x8a, 0xb2, 0xbc, 0xaf, 0xe1, + 0x70, 0x82, 0x12, 0x5d, 0x05, 0xe0, 0x12, 0x58, 0xd7, 0x6a, 0x11, 0xbb, 0x9a, 0x59, 0x33, 0x2e, + 0x17, 0x1a, 0x48, 0xf1, 0xc1, 0xab, 0x11, 0x06, 0x6b, 0x54, 0xe8, 0x51, 0xc8, 0x0b, 0x4b, 0xab, + 0x05, 0xa1, 0xa6, 0xa2, 0xc8, 0xf3, 0x62, 0x19, 0x58, 0xe2, 0xd0, 0x13, 0x30, 0xa7, 0xbc, 0xac, + 0x5a, 0x14, 0x64, 0x0b, 0x8a, 0x6c, 0x2e, 0x74, 0x83, 0x10, 0xcf, 0xd7, 0x77, 0xec, 0x78, 0xb6, + 0xf0, 0x3b, 0x6d, 0x7d, 0xaf, 0x38, 0x9e, 0x8d, 0x05, 0x06, 0xdd, 0x82, 0xfc, 0x09, 0xa1, 0x07, + 0xdc, 0x13, 0xb8, 0x6b, 0x7e, 0x39, 0xdd, 0x46, 0xdf, 0xe5, 0x2c, 0x8d, 0x22, 0x37, 0x4d, 0xfc, + 0xc4, 0x52, 0x08, 0xaa, 0x03, 0xb0, 0x23, 0x9f, 0x06, 0x62, 0x79, 0xd5, 0xfc, 0x5a, 0xf6, 0x72, + 0xb1, 0x31, 0xcf, 0xd7, 0xbb, 0x1f, 0x41, 0xb1, 0x46, 0xc1, 0xe9, 0x5b, 0x56, 0x40, 0xda, 0x3e, + 0x75, 0x08, 0xab, 0xce, 0xc5, 0xf4, 0x9b, 0x11, 0x14, 0x6b, 0x14, 0xe8, 0x65, 0x40, 0x2c, 0xf0, + 0xa9, 0xd5, 0x26, 0x6a, 0xa9, 0x2f, 0x59, 0xec, 0xa8, 0x0a, 0x62, 0x75, 0x2b, 0x6a, 0x75, 0x68, + 0x7f, 0x84, 0x02, 0x8f, 0xe1, 0x32, 0x7f, 0x67, 0xc0, 0x82, 0xe6, 0x0b, 0xc2, 0xef, 0xae, 0x41, + 0xb9, 0xad, 0xdd, 0x3a, 0xe5, 0x17, 0xd1, 0x69, 0xeb, 0x37, 0x12, 0x27, 0x28, 0x11, 0x81, 0x22, + 0x55, 0x92, 0xc2, 0xe8, 0x72, 0x25, 0xb5, 0xd3, 0x86, 0x36, 0xc4, 0x9a, 0x34, 0x20, 0xc3, 0xb1, + 0x64, 0xf3, 0x9f, 0x86, 0x70, 0xe0, 0x30, 0xde, 0xa0, 0xcb, 0x5a, 0x4c, 0x33, 0xc4, 0xf6, 0x95, + 0x27, 0xc4, 0xa3, 0x73, 0x02, 0x41, 0xe6, 0xff, 0x22, 0x10, 0x5c, 0x2f, 0xfc, 0xf2, 0xbd, 0xda, + 0xcc, 0xdb, 0x7f, 0x5f, 0x9b, 0x31, 0x7f, 0x61, 0x40, 0x79, 0xa3, 0xdb, 0x75, 0xfb, 0xbb, 0xdd, + 0x40, 0x2c, 0xc0, 0x84, 0x59, 0x9b, 0xf6, 0x71, 0xcf, 0x53, 0x0b, 0x05, 0x7e, 0xbf, 0x9b, 0x02, + 0x82, 0x15, 0x86, 0xdf, 0x9f, 0x43, 0x9f, 0xb6, 0x88, 0xba, 0x6e, 0xd1, 0xfd, 0xd9, 0xe2, 0x40, + 0x2c, 0x71, 0xfc, 0x90, 0x0f, 0x1d, 0xe2, 0xda, 0x3b, 0x96, 0x67, 0xb5, 0x09, 0x55, 0x97, 0x23, + 0xda, 0xfa, 0x2d, 0x0d, 0x87, 0x13, 0x94, 0xe6, 0x7f, 0x32, 0x50, 0xdc, 0xf4, 0x3d, 0xdb, 0x09, + 0xd4, 0xe5, 0x0a, 0xfa, 0xdd, 0x91, 0xe0, 0x71, 0xbb, 0xdf, 0x25, 0x58, 0x60, 0xd0, 0x73, 0x30, + 0xcb, 0x02, 0x2b, 0xe8, 0x31, 0x61, 0x4f, 0xb1, 0xf1, 0x48, 0x18, 0x96, 0xf6, 0x05, 0xf4, 0x6c, + 0x50, 0x5b, 0x88, 0xc4, 0x49, 0x10, 0x56, 0x0c, 0xdc, 0xd3, 0xfd, 0x03, 0xb1, 0x51, 0xf6, 0x4d, + 0x99, 0xf6, 0xc2, 0xfc, 0x91, 0x8d, 0x3d, 0x7d, 0x77, 0x84, 0x02, 0x8f, 0xe1, 0x42, 0x27, 0x80, + 0x5c, 0x8b, 0x05, 0xb7, 0xa9, 0xe5, 0x31, 0xa1, 0xeb, 0xb6, 0xd3, 0x21, 0xea, 0xc2, 0x7f, 0x29, + 0xdd, 0x89, 0x73, 0x8e, 0x58, 0xef, 0xad, 0x11, 0x69, 0x78, 0x8c, 0x06, 0xf4, 0x38, 0xcc, 0x52, + 0x62, 0x31, 0xdf, 0xab, 0xe6, 0xc5, 0xf2, 0xa3, 0xa8, 0x8c, 0x05, 0x14, 0x2b, 0x2c, 0x0f, 0x68, + 0x1d, 0xc2, 0x98, 0xd5, 0x0e, 0xc3, 0x6b, 0x14, 0xd0, 0x76, 0x24, 0x18, 0x87, 0x78, 0xf3, 0xb7, + 0x06, 0x54, 0x36, 0x29, 0xb1, 0x02, 0x32, 0x8d, 0x5b, 0x7c, 0xea, 0x13, 0x47, 0x1b, 0xb0, 0x20, + 0xbe, 0xef, 0x5a, 0xae, 0x63, 0xcb, 0x33, 0xc8, 0x09, 0xe6, 0xcf, 0x2b, 0xe6, 0x85, 0xad, 0x24, + 0x1a, 0x0f, 0xd3, 0x9b, 0x3f, 0xc9, 0x42, 0xa5, 0x49, 0x5c, 0x12, 0x9b, 0xbc, 0x05, 0xa8, 0x4d, + 0xad, 0x16, 0xd9, 0x23, 0xd4, 0xf1, 0xed, 0x7d, 0xd2, 0xf2, 0x3d, 0x9b, 0x09, 0x37, 0xca, 0x36, + 0x3e, 0xc7, 0xf7, 0xf7, 0xe6, 0x08, 0x16, 0x8f, 0xe1, 0x40, 0x2e, 0x54, 0xba, 0x54, 0xfc, 0x16, + 0x7b, 0x2e, 0xbd, 0xac, 0x74, 0xf5, 0x2b, 0xe9, 0x8e, 0x74, 0x4f, 0x67, 0x6d, 0x2c, 0x9d, 0x0e, + 0x6a, 0x95, 0x04, 0x08, 0x27, 0x85, 0xa3, 0x6f, 0xc0, 0xa2, 0x4f, 0xbb, 0x47, 0x96, 0xd7, 0x24, + 0x5d, 0xe2, 0xd9, 0xc4, 0x0b, 0x98, 0xd8, 0xc8, 0x42, 0x63, 0x99, 0xd7, 0x22, 0xbb, 0x43, 0x38, + 0x3c, 0x42, 0x8d, 0x5e, 0x83, 0xa5, 0x2e, 0xf5, 0xbb, 0x56, 0x5b, 0x6c, 0xcc, 0x9e, 0xef, 0x3a, + 0xad, 0xbe, 0xda, 0xce, 0x27, 0x4f, 0x07, 0xb5, 0xa5, 0xbd, 0x61, 0xe4, 0xd9, 0xa0, 0x76, 0x41, + 0x6c, 0x1d, 0x87, 0xc4, 0x48, 0x3c, 0x2a, 0x46, 0x73, 0x83, 0xfc, 0x24, 0x37, 0x30, 0xb7, 0xa1, + 0xd0, 0xec, 0xa9, 0x3b, 0xf1, 0x02, 0x14, 0x6c, 0xf5, 0x5b, 0xed, 0x7c, 0x78, 0x39, 0x23, 0x9a, + 0xb3, 0x41, 0xad, 0xc2, 0xcb, 0xcf, 0x7a, 0x08, 0xc0, 0x11, 0x8b, 0xf9, 0x38, 0x14, 0xc4, 0xc1, + 0xb3, 0xbb, 0x57, 0xd0, 0x22, 0x64, 0xb1, 0x75, 0x4f, 0x48, 0x29, 0x63, 0xfe, 0x53, 0x8b, 0x62, + 0xbb, 0x00, 0x37, 0x49, 0x10, 0x1e, 0xfc, 0x06, 0x2c, 0x84, 0xa1, 0x3c, 0x99, 0x61, 0x22, 0x6f, + 0xc2, 0x49, 0x34, 0x1e, 0xa6, 0x37, 0x5f, 0x87, 0xa2, 0xc8, 0x42, 0x3c, 0x85, 0xc7, 0xe5, 0x82, + 0x71, 0x9f, 0x72, 0x21, 0xac, 0x01, 0x32, 0x93, 0x6a, 0x00, 0xcd, 0x5c, 0x17, 0x2a, 0x92, 0x37, + 0x2c, 0x90, 0x52, 0x69, 0x78, 0x12, 0x0a, 0xa1, 0x99, 0x4a, 0x4b, 0x54, 0x18, 0x87, 0x82, 0x70, + 0x44, 0xa1, 0x69, 0x3b, 0x82, 0x44, 0x46, 0x4d, 0xa7, 0x4c, 0xab, 0x7e, 0x32, 0xf7, 0xaf, 0x7e, + 0x34, 0x4d, 0x3f, 0x84, 0xea, 0xa4, 0x6a, 0xfa, 0x01, 0x72, 0x7e, 0x7a, 0x53, 0xcc, 0x77, 0x0c, + 0x58, 0xd4, 0x25, 0xa5, 0x3f, 0xbe, 0xf4, 0x4a, 0xce, 0xaf, 0xf6, 0xb4, 0x1d, 0xf9, 0x95, 0x01, + 0xcb, 0x89, 0xa5, 0x4d, 0x75, 0xe2, 0x53, 0x18, 0xa5, 0x3b, 0x47, 0x76, 0x0a, 0xe7, 0xf8, 0x6b, + 0x06, 0x2a, 0xb7, 0xac, 0x03, 0xe2, 0xee, 0x13, 0x97, 0xb4, 0x02, 0x9f, 0xa2, 0x1f, 0x40, 0xa9, + 0x63, 0x05, 0xad, 0x23, 0x01, 0x0d, 0x3b, 0x83, 0x66, 0xba, 0x60, 0x97, 0x90, 0x54, 0xdf, 0x89, + 0xc5, 0xdc, 0xf0, 0x02, 0xda, 0x6f, 0x5c, 0x50, 0x26, 0x95, 0x34, 0x0c, 0xd6, 0xb5, 0x89, 0x76, + 0x4e, 0x7c, 0xdf, 0x78, 0xab, 0xcb, 0xcb, 0x96, 0xe9, 0xbb, 0xc8, 0x84, 0x09, 0x98, 0xbc, 0xd9, + 0x73, 0x28, 0xe9, 0x10, 0x2f, 0x88, 0xdb, 0xb9, 0x9d, 0x21, 0xf9, 0x78, 0x44, 0xe3, 0xca, 0x8b, + 0xb0, 0x38, 0x6c, 0x3c, 0x8f, 0x3f, 0xc7, 0xa4, 0x2f, 0xcf, 0x0b, 0xf3, 0x9f, 0x68, 0x19, 0xf2, + 0x27, 0x96, 0xdb, 0x53, 0xb7, 0x11, 0xcb, 0x8f, 0xeb, 0x99, 0x6b, 0x86, 0xf9, 0x1b, 0x03, 0xaa, + 0x93, 0x0c, 0x41, 0x5f, 0xd4, 0x04, 0x35, 0x4a, 0xca, 0xaa, 0xec, 0x2b, 0xa4, 0x2f, 0xa5, 0xde, + 0x80, 0x82, 0xdf, 0xe5, 0x35, 0x85, 0x4f, 0xd5, 0xa9, 0x3f, 0x11, 0x9e, 0xe4, 0xae, 0x82, 0x9f, + 0x0d, 0x6a, 0x17, 0x13, 0xe2, 0x43, 0x04, 0x8e, 0x58, 0x79, 0xa4, 0x16, 0xf6, 0xf0, 0xec, 0x11, + 0x45, 0xea, 0xbb, 0x02, 0x82, 0x15, 0xc6, 0xfc, 0x83, 0x01, 0x39, 0x51, 0x90, 0xbf, 0x0e, 0x05, + 0xbe, 0x7f, 0xb6, 0x15, 0x58, 0xc2, 0xae, 0xd4, 0xad, 0x20, 0xe7, 0xde, 0x21, 0x81, 0x15, 0x7b, + 0x5b, 0x08, 0xc1, 0x91, 0x44, 0x84, 0x21, 0xef, 0x04, 0xa4, 0x13, 0x1e, 0xe4, 0x53, 0x13, 0x45, + 0xab, 0x87, 0x88, 0x3a, 0xb6, 0xee, 0xdd, 0x78, 0x2b, 0x20, 0x1e, 0x3f, 0x8c, 0xf8, 0x6a, 0x6c, + 0x73, 0x19, 0x58, 0x8a, 0x32, 0xff, 0x6d, 0x40, 0xa4, 0x8a, 0x3b, 0x3f, 0x23, 0xee, 0xe1, 0x2d, + 0xc7, 0x3b, 0x56, 0xdb, 0x1a, 0x99, 0xb3, 0xaf, 0xe0, 0x38, 0xa2, 0x18, 0x97, 0x1e, 0x32, 0xd3, + 0xa5, 0x07, 0xae, 0xb0, 0xe5, 0x7b, 0x81, 0xe3, 0xf5, 0x46, 0x6e, 0xdb, 0xa6, 0x82, 0xe3, 0x88, + 0x82, 0x17, 0x22, 0x94, 0x74, 0x2c, 0xc7, 0x73, 0xbc, 0x36, 0x5f, 0xc4, 0xa6, 0xdf, 0xf3, 0x02, + 0x91, 0x91, 0x55, 0x21, 0x82, 0x47, 0xb0, 0x78, 0x0c, 0x87, 0xf9, 0xfb, 0x1c, 0x94, 0xf8, 0x9a, + 0xc3, 0x3c, 0xf7, 0x3c, 0x54, 0x5c, 0xdd, 0x0b, 0xd4, 0xda, 0x2f, 0x2a, 0x53, 0x92, 0xf7, 0x1a, + 0x27, 0x69, 0x39, 0xb3, 0x28, 0xa1, 0x22, 0xe6, 0x4c, 0x92, 0x79, 0x4b, 0x47, 0xe2, 0x24, 0x2d, + 0x8f, 0x5e, 0xf7, 0xf8, 0xfd, 0x50, 0x95, 0x49, 0x74, 0x44, 0xdf, 0xe4, 0x40, 0x2c, 0x71, 0x68, + 0x07, 0x2e, 0x58, 0xae, 0xeb, 0xdf, 0x13, 0xc0, 0x86, 0xef, 0x1f, 0x77, 0x2c, 0x7a, 0xcc, 0x44, + 0x33, 0x5d, 0x68, 0x7c, 0x41, 0xb1, 0x5c, 0xd8, 0x18, 0x25, 0xc1, 0xe3, 0xf8, 0xc6, 0x1d, 0x5b, + 0x6e, 0xca, 0x63, 0x3b, 0x82, 0xe5, 0x21, 0x90, 0xb8, 0xe5, 0xaa, 0xb3, 0x7d, 0x46, 0xc9, 0x59, + 0xc6, 0x63, 0x68, 0xce, 0x26, 0xc0, 0xf1, 0x58, 0x89, 0xe8, 0x3a, 0xcc, 0x73, 0x4f, 0xf6, 0x7b, + 0x41, 0x58, 0x77, 0xe6, 0xc5, 0x71, 0xa3, 0xd3, 0x41, 0x6d, 0xfe, 0x76, 0x02, 0x83, 0x87, 0x28, + 0xf9, 0xe6, 0xba, 0x4e, 0xc7, 0x09, 0xaa, 0x73, 0x82, 0x25, 0xda, 0xdc, 0x5b, 0x1c, 0x88, 0x25, + 0x2e, 0xe1, 0x81, 0x85, 0xf3, 0x3c, 0xd0, 0xfc, 0x4b, 0x16, 0x90, 0xac, 0xb5, 0x6d, 0x59, 0x4f, + 0xc9, 0x90, 0xc6, 0x3b, 0x02, 0x55, 0xab, 0x1b, 0x43, 0x1d, 0x81, 0x2a, 0xd3, 0x43, 0x3c, 0xda, + 0x81, 0xa2, 0x0c, 0x2d, 0xf1, 0x75, 0x59, 0x57, 0xc4, 0xc5, 0xdd, 0x10, 0x71, 0x36, 0xa8, 0xad, + 0x24, 0xd4, 0x44, 0x18, 0xd1, 0xad, 0xc5, 0x12, 0xd0, 0x55, 0x00, 0xab, 0xeb, 0xe8, 0xef, 0x75, + 0xc5, 0xf8, 0xd5, 0x26, 0xee, 0xbc, 0xb1, 0x46, 0x85, 0x5e, 0x82, 0x5c, 0xf0, 0xe9, 0x3a, 0xaa, + 0x82, 0x68, 0x18, 0x79, 0xff, 0x24, 0x24, 0x70, 0xed, 0xc2, 0x9f, 0x19, 0x37, 0x4b, 0x35, 0x43, + 0x91, 0xf6, 0xad, 0x08, 0x83, 0x35, 0x2a, 0xf4, 0x2d, 0x28, 0x1c, 0xaa, 0x52, 0x54, 0x1c, 0x4c, + 0xea, 0x10, 0x19, 0x16, 0xb0, 0xf2, 0xc9, 0x20, 0xfc, 0xc2, 0x91, 0x34, 0xf4, 0x55, 0x28, 0xb1, + 0xde, 0x41, 0x94, 0xbd, 0xe5, 0x69, 0x46, 0xa9, 0x72, 0x3f, 0x46, 0x61, 0x9d, 0xce, 0x7c, 0x13, + 0x8a, 0x3b, 0x4e, 0x8b, 0xfa, 0xa2, 0x07, 0x7c, 0x02, 0xe6, 0x58, 0xa2, 0xc1, 0x89, 0x4e, 0x32, + 0xf4, 0xb2, 0x10, 0xcf, 0xdd, 0xcb, 0xb3, 0x3c, 0x5f, 0xb6, 0x31, 0xf9, 0xd8, 0xbd, 0x5e, 0xe5, + 0x40, 0x2c, 0x71, 0xd7, 0x97, 0x79, 0x81, 0xf0, 0xd3, 0xf7, 0x6b, 0x33, 0xef, 0xbe, 0x5f, 0x9b, + 0x79, 0xef, 0x7d, 0x55, 0x2c, 0xfc, 0x11, 0x00, 0x76, 0x0f, 0xbe, 0x47, 0x5a, 0x32, 0xec, 0xa6, + 0x7a, 0xd6, 0x0b, 0x5f, 0x93, 0xc5, 0xb3, 0x5e, 0x66, 0xa8, 0xe8, 0xd3, 0x70, 0x38, 0x41, 0x89, + 0xd6, 0xa1, 0x18, 0x3d, 0xd8, 0x29, 0xff, 0x58, 0x0a, 0xfd, 0x2d, 0x7a, 0xd5, 0xc3, 0x31, 0x4d, + 0x22, 0x07, 0xe4, 0xce, 0xcd, 0x01, 0x0d, 0xc8, 0xf6, 0x1c, 0x5b, 0x35, 0xcc, 0x4f, 0x87, 0x39, + 0xf8, 0xce, 0x76, 0xf3, 0x6c, 0x50, 0x7b, 0x64, 0xd2, 0x3b, 0x79, 0xd0, 0xef, 0x12, 0x56, 0xbf, + 0xb3, 0xdd, 0xc4, 0x9c, 0x79, 0x5c, 0x40, 0x9a, 0x9d, 0x32, 0x20, 0x5d, 0x05, 0x68, 0xc7, 0xcf, + 0x0e, 0xf2, 0xbe, 0x47, 0x8e, 0xa8, 0x3d, 0x37, 0x68, 0x54, 0x88, 0xc1, 0x52, 0x8b, 0xb7, 0xe6, + 0xaa, 0xfd, 0x67, 0x81, 0xd5, 0x91, 0x0f, 0x99, 0xd3, 0xdd, 0x89, 0x4b, 0x4a, 0xcd, 0xd2, 0xe6, + 0xb0, 0x30, 0x3c, 0x2a, 0x1f, 0xf9, 0xb0, 0x64, 0xab, 0x0e, 0x31, 0x56, 0x5a, 0x9c, 0x5a, 0xe9, + 0x45, 0xae, 0xb0, 0x39, 0x2c, 0x08, 0x8f, 0xca, 0x46, 0xdf, 0x85, 0x95, 0x10, 0x38, 0xda, 0xa6, + 0x8b, 0x80, 0x9d, 0x6d, 0xac, 0x9e, 0x0e, 0x6a, 0x2b, 0xcd, 0x89, 0x54, 0xf8, 0x3e, 0x12, 0x90, + 0x0d, 0xb3, 0xae, 0x2c, 0x70, 0x4b, 0xa2, 0x28, 0xf9, 0x5a, 0xba, 0x55, 0xc4, 0xde, 0x5f, 0xd7, + 0x0b, 0xdb, 0xe8, 0xc9, 0x45, 0xd5, 0xb4, 0x4a, 0x36, 0x7a, 0x0b, 0x4a, 0x96, 0xe7, 0xf9, 0x81, + 0x25, 0x1f, 0x0e, 0xca, 0x42, 0xd5, 0xc6, 0xd4, 0xaa, 0x36, 0x62, 0x19, 0x43, 0x85, 0xb4, 0x86, + 0xc1, 0xba, 0x2a, 0x74, 0x0f, 0x16, 0xfc, 0x7b, 0x1e, 0xa1, 0x98, 0x1c, 0x12, 0x4a, 0xbc, 0x16, + 0x61, 0xd5, 0x8a, 0xd0, 0xfe, 0x4c, 0x4a, 0xed, 0x09, 0xe6, 0xd8, 0xa5, 0x93, 0x70, 0x86, 0x87, + 0xb5, 0xa0, 0x3a, 0x8f, 0xad, 0x9e, 0xe5, 0x3a, 0xdf, 0x27, 0x94, 0x55, 0xe7, 0xe3, 0xb7, 0xe6, + 0xad, 0x08, 0x8a, 0x35, 0x0a, 0xd4, 0x83, 0x4a, 0x47, 0x4f, 0x19, 0xd5, 0x25, 0x61, 0xe6, 0xb5, + 0x74, 0x66, 0x8e, 0x26, 0xb5, 0xb8, 0x82, 0x49, 0xe0, 0x70, 0x52, 0xcb, 0xca, 0x73, 0x50, 0xfa, + 0x94, 0xc5, 0x3d, 0x6f, 0x0e, 0x86, 0x0f, 0x64, 0xaa, 0xe6, 0xe0, 0x4f, 0x19, 0x98, 0x4f, 0x6e, + 0xe3, 0x50, 0x3a, 0xcc, 0xa7, 0x4a, 0x87, 0x61, 0x1b, 0x6a, 0x4c, 0x1c, 0x3a, 0x84, 0xf1, 0x39, + 0x3b, 0x31, 0x3e, 0xab, 0x30, 0x98, 0x7b, 0x90, 0x30, 0x58, 0x07, 0xe0, 0x75, 0x06, 0xf5, 0x5d, + 0x97, 0x50, 0x11, 0x01, 0x0b, 0x6a, 0xb8, 0x10, 0x41, 0xb1, 0x46, 0xc1, 0xab, 0xe1, 0x03, 0xd7, + 0x6f, 0x1d, 0x8b, 0x2d, 0x08, 0x6f, 0xaf, 0x88, 0x7d, 0x05, 0x59, 0x0d, 0x37, 0x46, 0xb0, 0x78, + 0x0c, 0x87, 0xd9, 0x87, 0x8b, 0x7b, 0x16, 0x0d, 0x1c, 0xcb, 0x8d, 0x6f, 0x8a, 0x68, 0x37, 0xde, + 0x18, 0x69, 0x66, 0x9e, 0x9e, 0xf6, 0xc6, 0xc5, 0x9b, 0x1f, 0xc3, 0xe2, 0x86, 0xc6, 0xfc, 0x9b, + 0x01, 0x97, 0xc6, 0xea, 0xfe, 0x0c, 0x9a, 0xa9, 0x37, 0x92, 0xcd, 0xd4, 0xf3, 0x29, 0x5f, 0x21, + 0xc7, 0x59, 0x3b, 0xa1, 0xb5, 0x9a, 0x83, 0xfc, 0x1e, 0x2f, 0x62, 0xcd, 0x0f, 0x0d, 0x28, 0x8b, + 0x5f, 0xd3, 0x3c, 0x02, 0xd7, 0x92, 0xb3, 0x81, 0xe2, 0xc3, 0x9b, 0x0b, 0x3c, 0x8c, 0x57, 0xe2, + 0x77, 0x0c, 0x48, 0x3e, 0xbf, 0xa2, 0x17, 0xe5, 0x15, 0x30, 0xa2, 0xf7, 0xd1, 0x29, 0xdd, 0xff, + 0x85, 0x49, 0xdd, 0xe4, 0x85, 0x54, 0x0f, 0x8d, 0x4f, 0x42, 0x11, 0xfb, 0x7e, 0xb0, 0x67, 0x05, + 0x47, 0x8c, 0xef, 0x5d, 0x97, 0xff, 0x50, 0xdb, 0x2b, 0xf6, 0x4e, 0x60, 0xb0, 0x84, 0x9b, 0x3f, + 0x37, 0xe0, 0xd2, 0xc4, 0x91, 0x0f, 0x8f, 0x22, 0xad, 0xe8, 0x4b, 0xad, 0x28, 0x72, 0xe4, 0x98, + 0x0e, 0x6b, 0x54, 0xbc, 0x0d, 0x4c, 0xcc, 0x89, 0x86, 0xdb, 0xc0, 0x84, 0x36, 0x9c, 0xa4, 0x35, + 0xff, 0x95, 0x01, 0x35, 0x63, 0xf9, 0x1f, 0x3b, 0xfd, 0xe3, 0x43, 0x13, 0x9e, 0xf9, 0xe4, 0x84, + 0x27, 0x1a, 0xe7, 0x68, 0x23, 0x8e, 0xec, 0xfd, 0x47, 0x1c, 0xe8, 0xd9, 0x68, 0x6a, 0x22, 0x7d, + 0x68, 0x35, 0x39, 0x35, 0x39, 0x1b, 0xd4, 0xca, 0x4a, 0x78, 0x72, 0x8a, 0xf2, 0x1a, 0xcc, 0xd9, + 0x24, 0xb0, 0x1c, 0x57, 0xb6, 0x74, 0xa9, 0xe7, 0x00, 0x52, 0x58, 0x53, 0xb2, 0x36, 0x4a, 0xdc, + 0x26, 0xf5, 0x81, 0x43, 0x81, 0x3c, 0x60, 0xb7, 0x7c, 0x5b, 0x76, 0x24, 0xf9, 0x38, 0x60, 0x6f, + 0xfa, 0x36, 0xc1, 0x02, 0x63, 0xbe, 0x6b, 0x40, 0x49, 0x4a, 0xda, 0xb4, 0x7a, 0x8c, 0xa0, 0x2b, + 0xd1, 0x2a, 0xe4, 0x71, 0x5f, 0xd2, 0xc7, 0x63, 0x67, 0x83, 0x5a, 0x51, 0x90, 0x89, 0x66, 0x66, + 0xcc, 0x18, 0x28, 0x73, 0xce, 0x1e, 0x3d, 0x0a, 0x79, 0x71, 0x81, 0xd4, 0x66, 0xc6, 0x73, 0x3e, + 0x0e, 0xc4, 0x12, 0x67, 0x7e, 0x9c, 0x81, 0x4a, 0x62, 0x71, 0x29, 0xfa, 0x82, 0xe8, 0xf5, 0x33, + 0x93, 0xe2, 0x45, 0x7d, 0xf2, 0x54, 0x5d, 0xa5, 0xaf, 0xd9, 0x07, 0x49, 0x5f, 0xdf, 0x86, 0xd9, + 0x16, 0xdf, 0xa3, 0xf0, 0x4f, 0x1a, 0x57, 0xa6, 0x39, 0x4e, 0xb1, 0xbb, 0xb1, 0x37, 0x8a, 0x4f, + 0x86, 0x95, 0x40, 0x74, 0x13, 0x96, 0x28, 0x09, 0x68, 0x7f, 0xe3, 0x30, 0x20, 0x54, 0x7f, 0x07, + 0xc8, 0xc7, 0xd5, 0x37, 0x1e, 0x26, 0xc0, 0xa3, 0x3c, 0xe6, 0x01, 0x94, 0x6f, 0x5b, 0x07, 0x6e, + 0x34, 0xd9, 0xc2, 0x50, 0x71, 0xbc, 0x96, 0xdb, 0xb3, 0x89, 0x0c, 0xe8, 0x61, 0xf4, 0x0a, 0x2f, + 0xed, 0xb6, 0x8e, 0x3c, 0x1b, 0xd4, 0x2e, 0x24, 0x00, 0x72, 0x94, 0x83, 0x93, 0x22, 0x4c, 0x17, + 0x72, 0x9f, 0x61, 0x27, 0xf9, 0x1d, 0x28, 0xc6, 0xb5, 0xfe, 0x43, 0x56, 0x69, 0xbe, 0x01, 0x05, + 0xee, 0xf1, 0x61, 0x8f, 0x7a, 0x4e, 0x95, 0x94, 0xac, 0xbd, 0x32, 0x69, 0x6a, 0x2f, 0x31, 0x1f, + 0xbd, 0xd3, 0xb5, 0x1f, 0x70, 0x3e, 0x9a, 0x79, 0x90, 0xcc, 0x97, 0x9d, 0x32, 0xf3, 0x5d, 0x05, + 0xf9, 0x1f, 0x12, 0x9e, 0x64, 0x64, 0x01, 0xa1, 0x25, 0x19, 0x3d, 0xff, 0x6b, 0xc3, 0x81, 0x1f, + 0x1b, 0x00, 0xe2, 0x15, 0xee, 0xc6, 0x09, 0xf1, 0x82, 0x14, 0x93, 0xf8, 0x3b, 0x30, 0xeb, 0x4b, + 0x8f, 0x94, 0x33, 0xd2, 0x29, 0x9f, 0x7a, 0xa3, 0x8b, 0x24, 0x7d, 0x12, 0x2b, 0x61, 0x8d, 0x97, + 0x3f, 0xf8, 0x64, 0x75, 0xe6, 0xc3, 0x4f, 0x56, 0x67, 0x3e, 0xfa, 0x64, 0x75, 0xe6, 0xed, 0xd3, + 0x55, 0xe3, 0x83, 0xd3, 0x55, 0xe3, 0xc3, 0xd3, 0x55, 0xe3, 0xa3, 0xd3, 0x55, 0xe3, 0xe3, 0xd3, + 0x55, 0xe3, 0xdd, 0x7f, 0xac, 0xce, 0xbc, 0xf6, 0x58, 0x9a, 0xff, 0xe6, 0xfd, 0x37, 0x00, 0x00, + 0xff, 0xff, 0x0b, 0x4d, 0x51, 0xc5, 0xdb, 0x27, 0x00, 0x00, } func (m *APIGroup) Marshal() (dAtA []byte, err error) { @@ -1909,6 +1910,11 @@ func (m *CreateOptions) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + i -= len(m.FieldValidation) + copy(dAtA[i:], m.FieldValidation) + i = encodeVarintGenerated(dAtA, i, uint64(len(m.FieldValidation))) + i-- + dAtA[i] = 0x22 i -= len(m.FieldManager) copy(dAtA[i:], m.FieldManager) i = encodeVarintGenerated(dAtA, i, uint64(len(m.FieldManager))) @@ -2657,11 +2663,6 @@ func (m *ObjectMeta) MarshalToSizedBuffer(dAtA []byte) (int, error) { dAtA[i] = 0x8a } } - i -= len(m.ClusterName) - copy(dAtA[i:], m.ClusterName) - i = encodeVarintGenerated(dAtA, i, uint64(len(m.ClusterName))) - i-- - dAtA[i] = 0x7a if len(m.Finalizers) > 0 { for iNdEx := len(m.Finalizers) - 1; iNdEx >= 0; iNdEx-- { i -= len(m.Finalizers[iNdEx]) @@ -2982,6 +2983,11 @@ func (m *PatchOptions) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + i -= len(m.FieldValidation) + copy(dAtA[i:], m.FieldValidation) + i = encodeVarintGenerated(dAtA, i, uint64(len(m.FieldValidation))) + i-- + dAtA[i] = 0x22 i -= len(m.FieldManager) copy(dAtA[i:], m.FieldManager) i = encodeVarintGenerated(dAtA, i, uint64(len(m.FieldManager))) @@ -3382,6 +3388,11 @@ func (m *UpdateOptions) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + i -= len(m.FieldValidation) + copy(dAtA[i:], m.FieldValidation) + i = encodeVarintGenerated(dAtA, i, uint64(len(m.FieldValidation))) + i-- + dAtA[i] = 0x1a i -= len(m.FieldManager) copy(dAtA[i:], m.FieldManager) i = encodeVarintGenerated(dAtA, i, uint64(len(m.FieldManager))) @@ -3648,6 +3659,8 @@ func (m *CreateOptions) Size() (n int) { } l = len(m.FieldManager) n += 1 + l + sovGenerated(uint64(l)) + l = len(m.FieldValidation) + n += 1 + l + sovGenerated(uint64(l)) return n } @@ -3981,8 +3994,6 @@ func (m *ObjectMeta) Size() (n int) { n += 1 + l + sovGenerated(uint64(l)) } } - l = len(m.ClusterName) - n += 1 + l + sovGenerated(uint64(l)) if len(m.ManagedFields) > 0 { for _, e := range m.ManagedFields { l = e.Size() @@ -4069,6 +4080,8 @@ func (m *PatchOptions) Size() (n int) { } l = len(m.FieldManager) n += 1 + l + sovGenerated(uint64(l)) + l = len(m.FieldValidation) + n += 1 + l + sovGenerated(uint64(l)) return n } @@ -4227,6 +4240,8 @@ func (m *UpdateOptions) Size() (n int) { } l = len(m.FieldManager) n += 1 + l + sovGenerated(uint64(l)) + l = len(m.FieldValidation) + n += 1 + l + sovGenerated(uint64(l)) return n } @@ -4371,6 +4386,7 @@ func (this *CreateOptions) String() string { s := strings.Join([]string{`&CreateOptions{`, `DryRun:` + fmt.Sprintf("%v", this.DryRun) + `,`, `FieldManager:` + fmt.Sprintf("%v", this.FieldManager) + `,`, + `FieldValidation:` + fmt.Sprintf("%v", this.FieldValidation) + `,`, `}`, }, "") return s @@ -4570,7 +4586,6 @@ func (this *ObjectMeta) String() string { `Annotations:` + mapStringForAnnotations + `,`, `OwnerReferences:` + repeatedStringForOwnerReferences + `,`, `Finalizers:` + fmt.Sprintf("%v", this.Finalizers) + `,`, - `ClusterName:` + fmt.Sprintf("%v", this.ClusterName) + `,`, `ManagedFields:` + repeatedStringForManagedFields + `,`, `}`, }, "") @@ -4634,6 +4649,7 @@ func (this *PatchOptions) String() string { `DryRun:` + fmt.Sprintf("%v", this.DryRun) + `,`, `Force:` + valueToStringGenerated(this.Force) + `,`, `FieldManager:` + fmt.Sprintf("%v", this.FieldManager) + `,`, + `FieldValidation:` + fmt.Sprintf("%v", this.FieldValidation) + `,`, `}`, }, "") return s @@ -4756,6 +4772,7 @@ func (this *UpdateOptions) String() string { s := strings.Join([]string{`&UpdateOptions{`, `DryRun:` + fmt.Sprintf("%v", this.DryRun) + `,`, `FieldManager:` + fmt.Sprintf("%v", this.FieldManager) + `,`, + `FieldValidation:` + fmt.Sprintf("%v", this.FieldValidation) + `,`, `}`, }, "") return s @@ -6097,6 +6114,38 @@ func (m *CreateOptions) Unmarshal(dAtA []byte) error { } m.FieldManager = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field FieldValidation", 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.FieldValidation = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenerated(dAtA[iNdEx:]) @@ -9153,38 +9202,6 @@ func (m *ObjectMeta) Unmarshal(dAtA []byte) error { } m.Finalizers = append(m.Finalizers, string(dAtA[iNdEx:postIndex])) iNdEx = postIndex - case 15: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field ClusterName", 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.ClusterName = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex case 17: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field ManagedFields", wireType) @@ -9824,6 +9841,38 @@ func (m *PatchOptions) Unmarshal(dAtA []byte) error { } m.FieldManager = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field FieldValidation", 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.FieldValidation = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenerated(dAtA[iNdEx:]) @@ -11145,6 +11194,38 @@ func (m *UpdateOptions) Unmarshal(dAtA []byte) error { } m.FieldManager = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field FieldValidation", 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.FieldValidation = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenerated(dAtA[iNdEx:]) diff --git a/pkg/apis/meta/v1/generated.proto b/pkg/apis/meta/v1/generated.proto index 7d450644d..2be188a6a 100644 --- a/pkg/apis/meta/v1/generated.proto +++ b/pkg/apis/meta/v1/generated.proto @@ -25,7 +25,7 @@ import "k8s.io/apimachinery/pkg/runtime/generated.proto"; import "k8s.io/apimachinery/pkg/runtime/schema/generated.proto"; // Package-wide variables from generator "generated". -option go_package = "v1"; +option go_package = "k8s.io/apimachinery/pkg/apis/meta/v1"; // APIGroup contains the name, the supported versions, and the preferred version // of a group. @@ -162,17 +162,18 @@ message ApplyOptions { // Condition contains details for one aspect of the current state of this API Resource. // --- // This struct is intended for direct use as an array at the field path .status.conditions. For example, -// type FooStatus struct{ -// // Represents the observations of a foo's current state. -// // Known .status.conditions.type are: "Available", "Progressing", and "Degraded" -// // +patchMergeKey=type -// // +patchStrategy=merge -// // +listType=map -// // +listMapKey=type -// Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` // -// // other fields -// } +// type FooStatus struct{ +// // Represents the observations of a foo's current state. +// // Known .status.conditions.type are: "Available", "Progressing", and "Degraded" +// // +patchMergeKey=type +// // +patchStrategy=merge +// // +listType=map +// // +listMapKey=type +// Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` +// +// // other fields +// } message Condition { // type of condition in CamelCase or in foo.example.com/CamelCase. // --- @@ -242,6 +243,28 @@ message CreateOptions { // as defined by https://golang.org/pkg/unicode/#IsPrint. // +optional optional string fieldManager = 3; + + // fieldValidation instructs the server on how to handle + // objects in the request (POST/PUT/PATCH) containing unknown + // or duplicate fields, provided that the `ServerSideFieldValidation` + // feature gate is also enabled. Valid values are: + // - Ignore: This will ignore any unknown fields that are silently + // dropped from the object, and will ignore all but the last duplicate + // field that the decoder encounters. This is the default behavior + // prior to v1.23 and is the default behavior when the + // `ServerSideFieldValidation` feature gate is disabled. + // - Warn: This will send a warning via the standard warning response + // header for each unknown field that is dropped from the object, and + // for each duplicate field that is encountered. The request will + // still succeed if there are no other errors, and will only persist + // the last of any duplicate fields. This is the default when the + // `ServerSideFieldValidation` feature gate is enabled. + // - Strict: This will fail the request with a BadRequest error if + // any unknown fields would be dropped from the object, or if any + // duplicate fields are present. The error returned from the server + // will contain all unknown and duplicate fields encountered. + // +optional + optional string fieldValidation = 4; } // DeleteOptions may be provided when deleting an API object. @@ -362,7 +385,7 @@ message GroupVersionForDiscovery { } // GroupVersionKind unambiguously identifies a kind. It doesn't anonymously include GroupVersion -// to avoid automatic coersion. It doesn't use a GroupVersion to avoid custom marshalling +// to avoid automatic coercion. It doesn't use a GroupVersion to avoid custom marshalling // // +protobuf.options.(gogoproto.goproto_stringer)=false message GroupVersionKind { @@ -374,7 +397,7 @@ message GroupVersionKind { } // GroupVersionResource unambiguously identifies a resource. It doesn't anonymously include GroupVersion -// to avoid automatic coersion. It doesn't use a GroupVersion to avoid custom marshalling +// to avoid automatic coercion. It doesn't use a GroupVersion to avoid custom marshalling // // +protobuf.options.(gogoproto.goproto_stringer)=false message GroupVersionResource { @@ -435,13 +458,7 @@ message List { // ListMeta describes metadata that synthetic resources must have, including lists and // various status objects. A resource may have only one of {ObjectMeta, ListMeta}. message ListMeta { - // selfLink is a URL representing this object. - // Populated by the system. - // Read-only. - // - // DEPRECATED - // Kubernetes will stop propagating this field in 1.20 release and the field is planned - // to be removed in 1.21 release. + // Deprecated: selfLink is a legacy read-only field that is no longer populated by the system. // +optional optional string selfLink = 1; @@ -576,7 +593,11 @@ message ManagedFieldsEntry { // set because it cannot be automatically converted. optional string apiVersion = 3; - // Time is timestamp of when these fields were set. It should always be empty if Operation is 'Apply' + // Time is the timestamp of when the ManagedFields entry was added. The + // timestamp will also be updated if a field is added, the manager + // changes any of the owned fields value or removes a field. The + // timestamp does not update when a field is removed from the entry + // because another manager took it over. // +optional optional Time time = 4; @@ -636,10 +657,7 @@ message ObjectMeta { // and may be truncated by the length of the suffix required to make the value // unique on the server. // - // If this field is specified and the generated name exists, the server will - // NOT return a 409 - instead, it will either return 201 Created or 500 with Reason - // ServerTimeout indicating a unique name could not be found in the time allotted, and the client - // should retry (optionally after the time indicated in the Retry-After header). + // If this field is specified and the generated name exists, the server will return a 409. // // Applied only if Name is not specified. // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency @@ -657,13 +675,7 @@ message ObjectMeta { // +optional optional string namespace = 3; - // SelfLink is a URL representing this object. - // Populated by the system. - // Read-only. - // - // DEPRECATED - // Kubernetes will stop propagating this field in 1.20 release and the field is planned - // to be removed in 1.21 release. + // Deprecated: selfLink is a legacy read-only field that is no longer populated by the system. // +optional optional string selfLink = 4; @@ -774,12 +786,6 @@ message ObjectMeta { // +patchStrategy=merge repeated string finalizers = 14; - // The name of the cluster which the object belongs to. - // This is used to distinguish resources with same name and namespace in different clusters. - // This field is not set anywhere right now and apiserver is going to ignore it if set in create or update request. - // +optional - optional string clusterName = 15; - // ManagedFields maps workflow-id and version to the set of fields // that are managed by that workflow. This is mostly for internal // housekeeping, and users typically shouldn't need to set or @@ -819,6 +825,8 @@ message OwnerReference { // If true, AND if the owner has the "foregroundDeletion" finalizer, then // the owner cannot be deleted from the key-value store until this // reference is removed. + // See https://kubernetes.io/docs/concepts/architecture/garbage-collection/#foreground-deletion + // for how the garbage collector interacts with this field and enforces the foreground deletion. // Defaults to false. // To set this field, a user needs "delete" permission of the owner, // otherwise 422 (Unprocessable Entity) will be returned. @@ -878,6 +886,28 @@ message PatchOptions { // types (JsonPatch, MergePatch, StrategicMergePatch). // +optional optional string fieldManager = 3; + + // fieldValidation instructs the server on how to handle + // objects in the request (POST/PUT/PATCH) containing unknown + // or duplicate fields, provided that the `ServerSideFieldValidation` + // feature gate is also enabled. Valid values are: + // - Ignore: This will ignore any unknown fields that are silently + // dropped from the object, and will ignore all but the last duplicate + // field that the decoder encounters. This is the default behavior + // prior to v1.23 and is the default behavior when the + // `ServerSideFieldValidation` feature gate is disabled. + // - Warn: This will send a warning via the standard warning response + // header for each unknown field that is dropped from the object, and + // for each duplicate field that is encountered. The request will + // still succeed if there are no other errors, and will only persist + // the last of any duplicate fields. This is the default when the + // `ServerSideFieldValidation` feature gate is enabled. + // - Strict: This will fail the request with a BadRequest error if + // any unknown fields would be dropped from the object, or if any + // duplicate fields are present. The error returned from the server + // will contain all unknown and duplicate fields encountered. + // +optional + optional string fieldValidation = 4; } // Preconditions must be fulfilled before an operation (update, delete, etc.) is carried out. @@ -1095,6 +1125,28 @@ message UpdateOptions { // as defined by https://golang.org/pkg/unicode/#IsPrint. // +optional optional string fieldManager = 2; + + // fieldValidation instructs the server on how to handle + // objects in the request (POST/PUT/PATCH) containing unknown + // or duplicate fields, provided that the `ServerSideFieldValidation` + // feature gate is also enabled. Valid values are: + // - Ignore: This will ignore any unknown fields that are silently + // dropped from the object, and will ignore all but the last duplicate + // field that the decoder encounters. This is the default behavior + // prior to v1.23 and is the default behavior when the + // `ServerSideFieldValidation` feature gate is disabled. + // - Warn: This will send a warning via the standard warning response + // header for each unknown field that is dropped from the object, and + // for each duplicate field that is encountered. The request will + // still succeed if there are no other errors, and will only persist + // the last of any duplicate fields. This is the default when the + // `ServerSideFieldValidation` feature gate is enabled. + // - Strict: This will fail the request with a BadRequest error if + // any unknown fields would be dropped from the object, or if any + // duplicate fields are present. The error returned from the server + // will contain all unknown and duplicate fields encountered. + // +optional + optional string fieldValidation = 3; } // Verbs masks the value so protobuf can generate diff --git a/pkg/apis/meta/v1/group_version.go b/pkg/apis/meta/v1/group_version.go index 54a0944af..fc9e521e2 100644 --- a/pkg/apis/meta/v1/group_version.go +++ b/pkg/apis/meta/v1/group_version.go @@ -44,7 +44,7 @@ func (gr *GroupResource) String() string { } // GroupVersionResource unambiguously identifies a resource. It doesn't anonymously include GroupVersion -// to avoid automatic coersion. It doesn't use a GroupVersion to avoid custom marshalling +// to avoid automatic coercion. It doesn't use a GroupVersion to avoid custom marshalling // // +protobuf.options.(gogoproto.goproto_stringer)=false type GroupVersionResource struct { @@ -80,7 +80,7 @@ func (gk *GroupKind) String() string { } // GroupVersionKind unambiguously identifies a kind. It doesn't anonymously include GroupVersion -// to avoid automatic coersion. It doesn't use a GroupVersion to avoid custom marshalling +// to avoid automatic coercion. It doesn't use a GroupVersion to avoid custom marshalling // // +protobuf.options.(gogoproto.goproto_stringer)=false type GroupVersionKind struct { diff --git a/pkg/apis/meta/v1/group_version_test.go b/pkg/apis/meta/v1/group_version_test.go index 0ad415cf8..c50a1902e 100644 --- a/pkg/apis/meta/v1/group_version_test.go +++ b/pkg/apis/meta/v1/group_version_test.go @@ -21,7 +21,7 @@ import ( "reflect" "testing" - "k8s.io/apimachinery/pkg/runtime/serializer/json" + utiljson "k8s.io/apimachinery/pkg/util/json" ) type GroupVersionHolder struct { @@ -46,13 +46,12 @@ func TestGroupVersionUnmarshalJSON(t *testing.T) { if !reflect.DeepEqual(result.GV, c.expect) { t.Errorf("JSON codec failed to unmarshal input '%s': expected %+v, got %+v", c.input, c.expect, result.GV) } - // test the json-iterator codec - iter := json.CaseSensitiveJSONIterator() - if err := iter.Unmarshal(c.input, &result); err != nil { - t.Errorf("json-iterator codec failed to unmarshal input '%v': %v", c.input, err) + // test the utiljson codec + if err := utiljson.Unmarshal(c.input, &result); err != nil { + t.Errorf("util/json codec failed to unmarshal input '%v': %v", c.input, err) } if !reflect.DeepEqual(result.GV, c.expect) { - t.Errorf("json-iterator codec failed to unmarshal input '%s': expected %+v, got %+v", c.input, c.expect, result.GV) + t.Errorf("util/json codec failed to unmarshal input '%s': expected %+v, got %+v", c.input, c.expect, result.GV) } } } diff --git a/pkg/apis/meta/v1/helpers.go b/pkg/apis/meta/v1/helpers.go index 2b6a30b65..592dcb8a7 100644 --- a/pkg/apis/meta/v1/helpers.go +++ b/pkg/apis/meta/v1/helpers.go @@ -58,7 +58,7 @@ func LabelSelectorAsSelector(ps *LabelSelector) (labels.Selector, error) { case LabelSelectorOpDoesNotExist: op = selection.DoesNotExist default: - return nil, fmt.Errorf("%q is not a valid pod selector operator", expr.Operator) + return nil, fmt.Errorf("%q is not a valid label selector operator", expr.Operator) } r, err := labels.NewRequirement(expr.Key, op, append([]string(nil), expr.Values...)) if err != nil { diff --git a/pkg/apis/meta/v1/helpers_test.go b/pkg/apis/meta/v1/helpers_test.go index ff22f6bab..af6d9cf7d 100644 --- a/pkg/apis/meta/v1/helpers_test.go +++ b/pkg/apis/meta/v1/helpers_test.go @@ -205,7 +205,6 @@ func TestResetObjectMetaForStatus(t *testing.T) { existingMeta.SetUID(types.UID("")) existingMeta.SetName("") existingMeta.SetNamespace("") - existingMeta.SetClusterName("") existingMeta.SetCreationTimestamp(Time{}) existingMeta.SetDeletionTimestamp(nil) existingMeta.SetDeletionGracePeriodSeconds(nil) diff --git a/pkg/apis/meta/v1/meta.go b/pkg/apis/meta/v1/meta.go index 2002f91b0..92d3ed5e0 100644 --- a/pkg/apis/meta/v1/meta.go +++ b/pkg/apis/meta/v1/meta.go @@ -59,8 +59,6 @@ type Object interface { SetFinalizers(finalizers []string) GetOwnerReferences() []OwnerReference SetOwnerReferences([]OwnerReference) - GetClusterName() string - SetClusterName(clusterName string) GetManagedFields() []ManagedFieldsEntry SetManagedFields(managedFields []ManagedFieldsEntry) } @@ -172,8 +170,6 @@ func (meta *ObjectMeta) GetOwnerReferences() []OwnerReference { return m func (meta *ObjectMeta) SetOwnerReferences(references []OwnerReference) { meta.OwnerReferences = references } -func (meta *ObjectMeta) GetClusterName() string { return meta.ClusterName } -func (meta *ObjectMeta) SetClusterName(clusterName string) { meta.ClusterName = clusterName } func (meta *ObjectMeta) GetManagedFields() []ManagedFieldsEntry { return meta.ManagedFields } func (meta *ObjectMeta) SetManagedFields(managedFields []ManagedFieldsEntry) { meta.ManagedFields = managedFields diff --git a/pkg/apis/meta/v1/micro_time_fuzz.go b/pkg/apis/meta/v1/micro_time_fuzz.go index befab16f7..3cf9d48e9 100644 --- a/pkg/apis/meta/v1/micro_time_fuzz.go +++ b/pkg/apis/meta/v1/micro_time_fuzz.go @@ -1,3 +1,4 @@ +//go:build !notest // +build !notest /* diff --git a/pkg/apis/meta/v1/micro_time_proto.go b/pkg/apis/meta/v1/micro_time_proto.go index 6dd6d8999..ab68181e9 100644 --- a/pkg/apis/meta/v1/micro_time_proto.go +++ b/pkg/apis/meta/v1/micro_time_proto.go @@ -27,9 +27,12 @@ func (m *MicroTime) ProtoMicroTime() *Timestamp { if m == nil { return &Timestamp{} } + + // truncate precision to microseconds to match JSON marshaling/unmarshaling + truncatedNanoseconds := time.Duration(m.Time.Nanosecond()).Truncate(time.Microsecond) return &Timestamp{ Seconds: m.Time.Unix(), - Nanos: int32(m.Time.Nanosecond()), + Nanos: int32(truncatedNanoseconds), } } @@ -51,7 +54,10 @@ func (m *MicroTime) Unmarshal(data []byte) error { if err := p.Unmarshal(data); err != nil { return err } - m.Time = time.Unix(p.Seconds, int64(p.Nanos)).Local() + + // truncate precision to microseconds to match JSON marshaling/unmarshaling + truncatedNanoseconds := time.Duration(p.Nanos).Truncate(time.Microsecond) + m.Time = time.Unix(p.Seconds, int64(truncatedNanoseconds)).Local() return nil } diff --git a/pkg/apis/meta/v1/micro_time_test.go b/pkg/apis/meta/v1/micro_time_test.go index 3b2745786..9c13b928e 100644 --- a/pkg/apis/meta/v1/micro_time_test.go +++ b/pkg/apis/meta/v1/micro_time_test.go @@ -118,7 +118,7 @@ func TestMicroTimeProto(t *testing.T) { input MicroTime }{ {MicroTime{}}, - {DateMicro(1998, time.May, 5, 1, 5, 5, 50, time.Local)}, + {DateMicro(1998, time.May, 5, 1, 5, 5, 1000, time.Local)}, {DateMicro(1998, time.May, 5, 5, 5, 5, 0, time.Local)}, } @@ -253,3 +253,68 @@ func TestMicroTimeIsZero(t *testing.T) { }) } } + +func TestMicroTimeUnmarshalJSONAndProtoEqual(t *testing.T) { + cases := []struct { + name string + input MicroTime + result bool + }{ + {"nanosecond level precision", UnixMicro(123, 123123123), true}, + {"microsecond level precision", UnixMicro(123, 123123000), true}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + jsonData, err := c.input.MarshalJSON() + if err != nil { + t.Fatalf("Failed to marshal input to JSON: '%v': %v", c.input, err) + } + + protoData, err := c.input.Marshal() + if err != nil { + t.Fatalf("Failed to marshal input to proto: '%v': %v", c.input, err) + } + + var tJSON, tProto MicroTime + if err = tJSON.UnmarshalJSON(jsonData); err != nil { + t.Fatalf("Failed to unmarshal JSON: '%v': %v", jsonData, err) + } + if err = tProto.Unmarshal(protoData); err != nil { + t.Fatalf("Failed to unmarshal proto: '%v': %v", protoData, err) + } + + result := tJSON.Equal(&tProto) + if result != c.result { + t.Errorf("Failed equality test for '%v': expected %+v, got %+v", c.input, c.result, result) + } + }) + } +} + +func TestMicroTimeProtoUnmarshalRaw(t *testing.T) { + cases := []struct { + name string + input []byte + expected MicroTime + }{ + // input is generated by Timestamp{123, 123123123}.Marshal() + {"nanosecond level precision", []byte{8, 123, 16, 179, 235, 218, 58}, UnixMicro(123, 123123000)}, + // input is generated by Timestamp{123, 123123000}.Marshal() + {"microsecond level precision", []byte{8, 123, 16, 184, 234, 218, 58}, UnixMicro(123, 123123000)}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var actual MicroTime + if err := actual.Unmarshal(c.input); err != nil { + t.Fatalf("Failed to unmarshal proto: '%v': %v", c.input, err) + } + + if !actual.Equal(&c.expected) { + t.Errorf("Failed unmarshal from nanosecond-precise raw for '%v': expected %+v, got %+v", c.input, c.expected, actual) + } + }) + } + +} diff --git a/pkg/apis/meta/v1/time_fuzz.go b/pkg/apis/meta/v1/time_fuzz.go index 94ad8d7cf..bf9e21b5b 100644 --- a/pkg/apis/meta/v1/time_fuzz.go +++ b/pkg/apis/meta/v1/time_fuzz.go @@ -1,3 +1,4 @@ +//go:build !notest // +build !notest /* diff --git a/pkg/apis/meta/v1/types.go b/pkg/apis/meta/v1/types.go index 9660282c4..152f99296 100644 --- a/pkg/apis/meta/v1/types.go +++ b/pkg/apis/meta/v1/types.go @@ -17,10 +17,11 @@ limitations under the License. // Package v1 contains API types that are common to all versions. // // The package contains two categories of types: -// - external (serialized) types that lack their own version (e.g TypeMeta) -// - internal (never-serialized) types that are needed by several different -// api groups, and so live here, to avoid duplication and/or import loops -// (e.g. LabelSelector). +// - external (serialized) types that lack their own version (e.g TypeMeta) +// - internal (never-serialized) types that are needed by several different +// api groups, and so live here, to avoid duplication and/or import loops +// (e.g. LabelSelector). +// // In the future, we will probably move these categories of objects into // separate packages. package v1 @@ -58,13 +59,7 @@ type TypeMeta struct { // ListMeta describes metadata that synthetic resources must have, including lists and // various status objects. A resource may have only one of {ObjectMeta, ListMeta}. type ListMeta struct { - // selfLink is a URL representing this object. - // Populated by the system. - // Read-only. - // - // DEPRECATED - // Kubernetes will stop propagating this field in 1.20 release and the field is planned - // to be removed in 1.21 release. + // Deprecated: selfLink is a legacy read-only field that is no longer populated by the system. // +optional SelfLink string `json:"selfLink,omitempty" protobuf:"bytes,1,opt,name=selfLink"` @@ -131,10 +126,7 @@ type ObjectMeta struct { // and may be truncated by the length of the suffix required to make the value // unique on the server. // - // If this field is specified and the generated name exists, the server will - // NOT return a 409 - instead, it will either return 201 Created or 500 with Reason - // ServerTimeout indicating a unique name could not be found in the time allotted, and the client - // should retry (optionally after the time indicated in the Retry-After header). + // If this field is specified and the generated name exists, the server will return a 409. // // Applied only if Name is not specified. // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency @@ -152,13 +144,7 @@ type ObjectMeta struct { // +optional Namespace string `json:"namespace,omitempty" protobuf:"bytes,3,opt,name=namespace"` - // SelfLink is a URL representing this object. - // Populated by the system. - // Read-only. - // - // DEPRECATED - // Kubernetes will stop propagating this field in 1.20 release and the field is planned - // to be removed in 1.21 release. + // Deprecated: selfLink is a legacy read-only field that is no longer populated by the system. // +optional SelfLink string `json:"selfLink,omitempty" protobuf:"bytes,4,opt,name=selfLink"` @@ -269,11 +255,9 @@ type ObjectMeta struct { // +patchStrategy=merge Finalizers []string `json:"finalizers,omitempty" patchStrategy:"merge" protobuf:"bytes,14,rep,name=finalizers"` - // The name of the cluster which the object belongs to. - // This is used to distinguish resources with same name and namespace in different clusters. - // This field is not set anywhere right now and apiserver is going to ignore it if set in create or update request. - // +optional - ClusterName string `json:"clusterName,omitempty" protobuf:"bytes,15,opt,name=clusterName"` + // Tombstone: ClusterName was a legacy field that was always cleared by + // the system and never used. + // ClusterName string `json:"clusterName,omitempty" protobuf:"bytes,15,opt,name=clusterName"` // ManagedFields maps workflow-id and version to the set of fields // that are managed by that workflow. This is mostly for internal @@ -322,6 +306,8 @@ type OwnerReference struct { // If true, AND if the owner has the "foregroundDeletion" finalizer, then // the owner cannot be deleted from the key-value store until this // reference is removed. + // See https://kubernetes.io/docs/concepts/architecture/garbage-collection/#foreground-deletion + // for how the garbage collector interacts with this field and enforces the foreground deletion. // Defaults to false. // To set this field, a user needs "delete" permission of the owner, // otherwise 422 (Unprocessable Entity) will be returned. @@ -522,6 +508,15 @@ type DeleteOptions struct { DryRun []string `json:"dryRun,omitempty" protobuf:"bytes,5,rep,name=dryRun"` } +const ( + // FieldValidationIgnore ignores unknown/duplicate fields + FieldValidationIgnore = "Ignore" + // FieldValidationWarn responds with a warning, but successfully serve the request + FieldValidationWarn = "Warn" + // FieldValidationStrict fails the request on unknown/duplicate fields + FieldValidationStrict = "Strict" +) + // +k8s:conversion-gen:explicit-from=net/url.Values // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -544,6 +539,28 @@ type CreateOptions struct { // as defined by https://golang.org/pkg/unicode/#IsPrint. // +optional FieldManager string `json:"fieldManager,omitempty" protobuf:"bytes,3,name=fieldManager"` + + // fieldValidation instructs the server on how to handle + // objects in the request (POST/PUT/PATCH) containing unknown + // or duplicate fields, provided that the `ServerSideFieldValidation` + // feature gate is also enabled. Valid values are: + // - Ignore: This will ignore any unknown fields that are silently + // dropped from the object, and will ignore all but the last duplicate + // field that the decoder encounters. This is the default behavior + // prior to v1.23 and is the default behavior when the + // `ServerSideFieldValidation` feature gate is disabled. + // - Warn: This will send a warning via the standard warning response + // header for each unknown field that is dropped from the object, and + // for each duplicate field that is encountered. The request will + // still succeed if there are no other errors, and will only persist + // the last of any duplicate fields. This is the default when the + // `ServerSideFieldValidation` feature gate is enabled. + // - Strict: This will fail the request with a BadRequest error if + // any unknown fields would be dropped from the object, or if any + // duplicate fields are present. The error returned from the server + // will contain all unknown and duplicate fields encountered. + // +optional + FieldValidation string `json:"fieldValidation,omitempty" protobuf:"bytes,4,name=fieldValidation"` } // +k8s:conversion-gen:explicit-from=net/url.Values @@ -577,6 +594,28 @@ type PatchOptions struct { // types (JsonPatch, MergePatch, StrategicMergePatch). // +optional FieldManager string `json:"fieldManager,omitempty" protobuf:"bytes,3,name=fieldManager"` + + // fieldValidation instructs the server on how to handle + // objects in the request (POST/PUT/PATCH) containing unknown + // or duplicate fields, provided that the `ServerSideFieldValidation` + // feature gate is also enabled. Valid values are: + // - Ignore: This will ignore any unknown fields that are silently + // dropped from the object, and will ignore all but the last duplicate + // field that the decoder encounters. This is the default behavior + // prior to v1.23 and is the default behavior when the + // `ServerSideFieldValidation` feature gate is disabled. + // - Warn: This will send a warning via the standard warning response + // header for each unknown field that is dropped from the object, and + // for each duplicate field that is encountered. The request will + // still succeed if there are no other errors, and will only persist + // the last of any duplicate fields. This is the default when the + // `ServerSideFieldValidation` feature gate is enabled. + // - Strict: This will fail the request with a BadRequest error if + // any unknown fields would be dropped from the object, or if any + // duplicate fields are present. The error returned from the server + // will contain all unknown and duplicate fields encountered. + // +optional + FieldValidation string `json:"fieldValidation,omitempty" protobuf:"bytes,4,name=fieldValidation"` } // ApplyOptions may be provided when applying an API object. @@ -632,6 +671,28 @@ type UpdateOptions struct { // as defined by https://golang.org/pkg/unicode/#IsPrint. // +optional FieldManager string `json:"fieldManager,omitempty" protobuf:"bytes,2,name=fieldManager"` + + // fieldValidation instructs the server on how to handle + // objects in the request (POST/PUT/PATCH) containing unknown + // or duplicate fields, provided that the `ServerSideFieldValidation` + // feature gate is also enabled. Valid values are: + // - Ignore: This will ignore any unknown fields that are silently + // dropped from the object, and will ignore all but the last duplicate + // field that the decoder encounters. This is the default behavior + // prior to v1.23 and is the default behavior when the + // `ServerSideFieldValidation` feature gate is disabled. + // - Warn: This will send a warning via the standard warning response + // header for each unknown field that is dropped from the object, and + // for each duplicate field that is encountered. The request will + // still succeed if there are no other errors, and will only persist + // the last of any duplicate fields. This is the default when the + // `ServerSideFieldValidation` feature gate is enabled. + // - Strict: This will fail the request with a BadRequest error if + // any unknown fields would be dropped from the object, or if any + // duplicate fields are present. The error returned from the server + // will contain all unknown and duplicate fields encountered. + // +optional + FieldValidation string `json:"fieldValidation,omitempty" protobuf:"bytes,3,name=fieldValidation"` } // Preconditions must be fulfilled before an operation (update, delete, etc.) is carried out. @@ -1166,7 +1227,11 @@ type ManagedFieldsEntry struct { // APIVersion field. It is necessary to track the version of a field // set because it cannot be automatically converted. APIVersion string `json:"apiVersion,omitempty" protobuf:"bytes,3,opt,name=apiVersion"` - // Time is timestamp of when these fields were set. It should always be empty if Operation is 'Apply' + // Time is the timestamp of when the ManagedFields entry was added. The + // timestamp will also be updated if a field is added, the manager + // changes any of the owned fields value or removes a field. The + // timestamp does not update when a field is removed from the entry + // because another manager took it over. // +optional Time *Time `json:"time,omitempty" protobuf:"bytes,4,opt,name=time"` @@ -1384,17 +1449,18 @@ type PartialObjectMetadataList struct { // Condition contains details for one aspect of the current state of this API Resource. // --- // This struct is intended for direct use as an array at the field path .status.conditions. For example, -// type FooStatus struct{ -// // Represents the observations of a foo's current state. -// // Known .status.conditions.type are: "Available", "Progressing", and "Degraded" -// // +patchMergeKey=type -// // +patchStrategy=merge -// // +listType=map -// // +listMapKey=type -// Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` // -// // other fields -// } +// type FooStatus struct{ +// // Represents the observations of a foo's current state. +// // Known .status.conditions.type are: "Available", "Progressing", and "Degraded" +// // +patchMergeKey=type +// // +patchStrategy=merge +// // +listType=map +// // +listMapKey=type +// Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` +// +// // other fields +// } type Condition struct { // type of condition in CamelCase or in foo.example.com/CamelCase. // --- diff --git a/pkg/apis/meta/v1/types_swagger_doc_generated.go b/pkg/apis/meta/v1/types_swagger_doc_generated.go index 3eae04d07..9570726a0 100644 --- a/pkg/apis/meta/v1/types_swagger_doc_generated.go +++ b/pkg/apis/meta/v1/types_swagger_doc_generated.go @@ -112,9 +112,10 @@ func (Condition) SwaggerDoc() map[string]string { } var map_CreateOptions = map[string]string{ - "": "CreateOptions may be provided when creating an API object.", - "dryRun": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", - "fieldManager": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + "": "CreateOptions may be provided when creating an API object.", + "dryRun": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "fieldManager": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + "fieldValidation": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields, provided that the `ServerSideFieldValidation` feature gate is also enabled. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23 and is the default behavior when the `ServerSideFieldValidation` feature gate is disabled. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default when the `ServerSideFieldValidation` feature gate is enabled. - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", } func (CreateOptions) SwaggerDoc() map[string]string { @@ -194,7 +195,7 @@ func (List) SwaggerDoc() map[string]string { var map_ListMeta = map[string]string{ "": "ListMeta describes metadata that synthetic resources must have, including lists and various status objects. A resource may have only one of {ObjectMeta, ListMeta}.", - "selfLink": "selfLink is a URL representing this object. Populated by the system. Read-only.\n\nDEPRECATED Kubernetes will stop propagating this field in 1.20 release and the field is planned to be removed in 1.21 release.", + "selfLink": "Deprecated: selfLink is a legacy read-only field that is no longer populated by the system.", "resourceVersion": "String that identifies the server's internal version of this object that can be used by clients to determine when objects have changed. Value must be treated as opaque by clients and passed unmodified back to the server. Populated by the system. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", "continue": "continue may be set if the user set a limit on the number of items returned, and indicates that the server has more data available. The value is opaque and may be used to issue another request to the endpoint that served this list to retrieve the next set of available objects. Continuing a consistent list may not be possible if the server configuration has changed or more than a few minutes have passed. The resourceVersion field returned when using this continue value will be identical to the value in the first response, unless you have received this token from an error message.", "remainingItemCount": "remainingItemCount is the number of subsequent items in the list which are not included in this list response. If the list request contained label or field selectors, then the number of remaining items is unknown and the field will be left unset and omitted during serialization. If the list is complete (either because it is not chunking or because this is the last chunk), then there are no more remaining items and this field will be left unset and omitted during serialization. Servers older than v1.15 do not set this field. The intended use of the remainingItemCount is *estimating* the size of a collection. Clients should not rely on the remainingItemCount to be set or to be exact.", @@ -226,7 +227,7 @@ var map_ManagedFieldsEntry = map[string]string{ "manager": "Manager is an identifier of the workflow managing these fields.", "operation": "Operation is the type of operation which lead to this ManagedFieldsEntry being created. The only valid values for this field are 'Apply' and 'Update'.", "apiVersion": "APIVersion defines the version of this resource that this field set applies to. The format is \"group/version\" just like the top-level APIVersion field. It is necessary to track the version of a field set because it cannot be automatically converted.", - "time": "Time is timestamp of when these fields were set. It should always be empty if Operation is 'Apply'", + "time": "Time is the timestamp of when the ManagedFields entry was added. The timestamp will also be updated if a field is added, the manager changes any of the owned fields value or removes a field. The timestamp does not update when a field is removed from the entry because another manager took it over.", "fieldsType": "FieldsType is the discriminator for the different fields format and version. There is currently only one possible value: \"FieldsV1\"", "fieldsV1": "FieldsV1 holds the first JSON version format as described in the \"FieldsV1\" type.", "subresource": "Subresource is the name of the subresource used to update that object, or empty string if the object was updated through the main resource. The value of this field is used to distinguish between managers, even if they share the same name. For example, a status update will be distinct from a regular update using the same manager name. Note that the APIVersion field is not related to the Subresource field and it always corresponds to the version of the main resource.", @@ -239,9 +240,9 @@ func (ManagedFieldsEntry) SwaggerDoc() map[string]string { var map_ObjectMeta = map[string]string{ "": "ObjectMeta is metadata that all persisted resources must have, which includes all objects users must create.", "name": "Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: http://kubernetes.io/docs/user-guide/identifiers#names", - "generateName": "GenerateName is an optional prefix, used by the server, to generate a unique name ONLY IF the Name field has not been provided. If this field is used, the name returned to the client will be different than the name passed. This value will also be combined with a unique suffix. The provided value has the same validation rules as the Name field, and may be truncated by the length of the suffix required to make the value unique on the server.\n\nIf this field is specified and the generated name exists, the server will NOT return a 409 - instead, it will either return 201 Created or 500 with Reason ServerTimeout indicating a unique name could not be found in the time allotted, and the client should retry (optionally after the time indicated in the Retry-After header).\n\nApplied only if Name is not specified. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency", + "generateName": "GenerateName is an optional prefix, used by the server, to generate a unique name ONLY IF the Name field has not been provided. If this field is used, the name returned to the client will be different than the name passed. This value will also be combined with a unique suffix. The provided value has the same validation rules as the Name field, and may be truncated by the length of the suffix required to make the value unique on the server.\n\nIf this field is specified and the generated name exists, the server will return a 409.\n\nApplied only if Name is not specified. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency", "namespace": "Namespace defines the space within which each name must be unique. An empty namespace is equivalent to the \"default\" namespace, but \"default\" is the canonical representation. Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty.\n\nMust be a DNS_LABEL. Cannot be updated. More info: http://kubernetes.io/docs/user-guide/namespaces", - "selfLink": "SelfLink is a URL representing this object. Populated by the system. Read-only.\n\nDEPRECATED Kubernetes will stop propagating this field in 1.20 release and the field is planned to be removed in 1.21 release.", + "selfLink": "Deprecated: selfLink is a legacy read-only field that is no longer populated by the system.", "uid": "UID is the unique in time and space value for this object. It is typically generated by the server on successful creation of a resource and is not allowed to change on PUT operations.\n\nPopulated by the system. Read-only. More info: http://kubernetes.io/docs/user-guide/identifiers#uids", "resourceVersion": "An opaque value that represents the internal version of this object that can be used by clients to determine when objects have changed. May be used for optimistic concurrency, change detection, and the watch operation on a resource or set of resources. Clients must treat these values as opaque and passed unmodified back to the server. They may only be valid for a particular resource or set of resources.\n\nPopulated by the system. Read-only. Value must be treated as opaque by clients and . More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", "generation": "A sequence number representing a specific generation of the desired state. Populated by the system. Read-only.", @@ -252,7 +253,6 @@ var map_ObjectMeta = map[string]string{ "annotations": "Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. More info: http://kubernetes.io/docs/user-guide/annotations", "ownerReferences": "List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. If this object is managed by a controller, then an entry in this list will point to this controller, with the controller field set to true. There cannot be more than one managing controller.", "finalizers": "Must be empty before the object is deleted from the registry. Each entry is an identifier for the responsible component that will remove the entry from the list. If the deletionTimestamp of the object is non-nil, entries in this list can only be removed. Finalizers may be processed and removed in any order. Order is NOT enforced because it introduces significant risk of stuck finalizers. finalizers is a shared field, any actor with permission can reorder it. If the finalizer list is processed in order, then this can lead to a situation in which the component responsible for the first finalizer in the list is waiting for a signal (field value, external system, or other) produced by a component responsible for a finalizer later in the list, resulting in a deadlock. Without enforced ordering finalizers are free to order amongst themselves and are not vulnerable to ordering changes in the list.", - "clusterName": "The name of the cluster which the object belongs to. This is used to distinguish resources with same name and namespace in different clusters. This field is not set anywhere right now and apiserver is going to ignore it if set in create or update request.", "managedFields": "ManagedFields maps workflow-id and version to the set of fields that are managed by that workflow. This is mostly for internal housekeeping, and users typically shouldn't need to set or understand this field. A workflow can be the user's name, a controller's name, or the name of a specific apply path like \"ci-cd\". The set of fields is always in the version that the workflow used when modifying the object.", } @@ -267,7 +267,7 @@ var map_OwnerReference = map[string]string{ "name": "Name of the referent. More info: http://kubernetes.io/docs/user-guide/identifiers#names", "uid": "UID of the referent. More info: http://kubernetes.io/docs/user-guide/identifiers#uids", "controller": "If true, this reference points to the managing controller.", - "blockOwnerDeletion": "If true, AND if the owner has the \"foregroundDeletion\" finalizer, then the owner cannot be deleted from the key-value store until this reference is removed. Defaults to false. To set this field, a user needs \"delete\" permission of the owner, otherwise 422 (Unprocessable Entity) will be returned.", + "blockOwnerDeletion": "If true, AND if the owner has the \"foregroundDeletion\" finalizer, then the owner cannot be deleted from the key-value store until this reference is removed. See https://kubernetes.io/docs/concepts/architecture/garbage-collection/#foreground-deletion for how the garbage collector interacts with this field and enforces the foreground deletion. Defaults to false. To set this field, a user needs \"delete\" permission of the owner, otherwise 422 (Unprocessable Entity) will be returned.", } func (OwnerReference) SwaggerDoc() map[string]string { @@ -302,10 +302,11 @@ func (Patch) SwaggerDoc() map[string]string { } var map_PatchOptions = map[string]string{ - "": "PatchOptions may be provided when patching an API object. PatchOptions is meant to be a superset of UpdateOptions.", - "dryRun": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", - "force": "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.", - "fieldManager": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).", + "": "PatchOptions may be provided when patching an API object. PatchOptions is meant to be a superset of UpdateOptions.", + "dryRun": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "force": "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.", + "fieldManager": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).", + "fieldValidation": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields, provided that the `ServerSideFieldValidation` feature gate is also enabled. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23 and is the default behavior when the `ServerSideFieldValidation` feature gate is disabled. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default when the `ServerSideFieldValidation` feature gate is enabled. - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", } func (PatchOptions) SwaggerDoc() map[string]string { @@ -447,9 +448,10 @@ func (TypeMeta) SwaggerDoc() map[string]string { } var map_UpdateOptions = map[string]string{ - "": "UpdateOptions may be provided when updating an API object. All fields in UpdateOptions should also be present in PatchOptions.", - "dryRun": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", - "fieldManager": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + "": "UpdateOptions may be provided when updating an API object. All fields in UpdateOptions should also be present in PatchOptions.", + "dryRun": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "fieldManager": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + "fieldValidation": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields, provided that the `ServerSideFieldValidation` feature gate is also enabled. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23 and is the default behavior when the `ServerSideFieldValidation` feature gate is disabled. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default when the `ServerSideFieldValidation` feature gate is enabled. - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", } func (UpdateOptions) SwaggerDoc() map[string]string { diff --git a/pkg/apis/meta/v1/types_test.go b/pkg/apis/meta/v1/types_test.go index 648e8b58a..f216527dd 100644 --- a/pkg/apis/meta/v1/types_test.go +++ b/pkg/apis/meta/v1/types_test.go @@ -21,7 +21,7 @@ import ( "reflect" "testing" - "k8s.io/apimachinery/pkg/runtime/serializer/json" + utiljson "k8s.io/apimachinery/pkg/util/json" ) func TestVerbsMarshalJSON(t *testing.T) { @@ -56,10 +56,9 @@ func TestVerbsJsonIterUnmarshalJSON(t *testing.T) { {`{"verbs":["delete"]}`, APIResource{Verbs: Verbs([]string{"delete"})}}, } - iter := json.CaseSensitiveJSONIterator() for i, c := range cases { var result APIResource - if err := iter.Unmarshal([]byte(c.input), &result); err != nil { + if err := utiljson.Unmarshal([]byte(c.input), &result); err != nil { t.Errorf("[%d] Failed to unmarshal input '%v': %v", i, c.input, err) } if !reflect.DeepEqual(result, c.result) { diff --git a/pkg/apis/meta/v1/unstructured/helpers.go b/pkg/apis/meta/v1/unstructured/helpers.go index 7b101ea51..2e33283ef 100644 --- a/pkg/apis/meta/v1/unstructured/helpers.go +++ b/pkg/apis/meta/v1/unstructured/helpers.go @@ -340,6 +340,7 @@ func (s unstructuredJSONScheme) Decode(data []byte, _ *schema.GroupVersionKind, if len(gvk.Kind) == 0 { return nil, &gvk, runtime.NewMissingKindErr(string(data)) } + // TODO(109023): require apiVersion here as well return obj, &gvk, nil } @@ -382,7 +383,7 @@ func (unstructuredJSONScheme) Identifier() runtime.Identifier { func (s unstructuredJSONScheme) decode(data []byte) (runtime.Object, error) { type detector struct { - Items gojson.RawMessage + Items gojson.RawMessage `json:"items"` } var det detector if err := json.Unmarshal(data, &det); err != nil { @@ -425,7 +426,7 @@ func (unstructuredJSONScheme) decodeToUnstructured(data []byte, unstruct *Unstru func (s unstructuredJSONScheme) decodeToList(data []byte, list *UnstructuredList) error { type decodeList struct { - Items []gojson.RawMessage + Items []gojson.RawMessage `json:"items"` } var dList decodeList diff --git a/pkg/apis/meta/v1/unstructured/helpers_test.go b/pkg/apis/meta/v1/unstructured/helpers_test.go index eb1cd6579..c6c0358d0 100644 --- a/pkg/apis/meta/v1/unstructured/helpers_test.go +++ b/pkg/apis/meta/v1/unstructured/helpers_test.go @@ -165,3 +165,63 @@ func TestNestedFieldCopy(t *testing.T) { func TestCacheableObject(t *testing.T) { runtimetesting.CacheableObjectTest(t, UnstructuredJSONScheme) } + +func TestSetNestedStringSlice(t *testing.T) { + obj := map[string]interface{}{ + "x": map[string]interface{}{ + "y": 1, + "a": "foo", + }, + } + + err := SetNestedStringSlice(obj, []string{"bar"}, "x", "z") + assert.NoError(t, err) + assert.Len(t, obj["x"], 3) + assert.Len(t, obj["x"].(map[string]interface{})["z"], 1) + assert.Equal(t, obj["x"].(map[string]interface{})["z"].([]interface{})[0], "bar") +} + +func TestSetNestedSlice(t *testing.T) { + obj := map[string]interface{}{ + "x": map[string]interface{}{ + "y": 1, + "a": "foo", + }, + } + + err := SetNestedSlice(obj, []interface{}{"bar"}, "x", "z") + assert.NoError(t, err) + assert.Len(t, obj["x"], 3) + assert.Len(t, obj["x"].(map[string]interface{})["z"], 1) + assert.Equal(t, obj["x"].(map[string]interface{})["z"].([]interface{})[0], "bar") +} + +func TestSetNestedStringMap(t *testing.T) { + obj := map[string]interface{}{ + "x": map[string]interface{}{ + "y": 1, + "a": "foo", + }, + } + + err := SetNestedStringMap(obj, map[string]string{"b": "bar"}, "x", "z") + assert.NoError(t, err) + assert.Len(t, obj["x"], 3) + assert.Len(t, obj["x"].(map[string]interface{})["z"], 1) + assert.Equal(t, obj["x"].(map[string]interface{})["z"].(map[string]interface{})["b"], "bar") +} + +func TestSetNestedMap(t *testing.T) { + obj := map[string]interface{}{ + "x": map[string]interface{}{ + "y": 1, + "a": "foo", + }, + } + + err := SetNestedMap(obj, map[string]interface{}{"b": "bar"}, "x", "z") + assert.NoError(t, err) + assert.Len(t, obj["x"], 3) + assert.Len(t, obj["x"].(map[string]interface{})["z"], 1) + assert.Equal(t, obj["x"].(map[string]interface{})["z"].(map[string]interface{})["b"], "bar") +} diff --git a/pkg/apis/meta/v1/unstructured/unstructured.go b/pkg/apis/meta/v1/unstructured/unstructured.go index d1903394d..a499eee8e 100644 --- a/pkg/apis/meta/v1/unstructured/unstructured.go +++ b/pkg/apis/meta/v1/unstructured/unstructured.go @@ -444,18 +444,6 @@ func (u *Unstructured) SetFinalizers(finalizers []string) { u.setNestedStringSlice(finalizers, "metadata", "finalizers") } -func (u *Unstructured) GetClusterName() string { - return getNestedString(u.Object, "metadata", "clusterName") -} - -func (u *Unstructured) SetClusterName(clusterName string) { - if len(clusterName) == 0 { - RemoveNestedField(u.Object, "metadata", "clusterName") - return - } - u.setNestedField(clusterName, "metadata", "clusterName") -} - func (u *Unstructured) GetManagedFields() []metav1.ManagedFieldsEntry { items, found, err := NestedSlice(u.Object, "metadata", "managedFields") if !found || err != nil { diff --git a/pkg/apis/meta/v1/unstructured/unstructured_test.go b/pkg/apis/meta/v1/unstructured/unstructured_test.go index 7ee74a3ee..be9a48f71 100644 --- a/pkg/apis/meta/v1/unstructured/unstructured_test.go +++ b/pkg/apis/meta/v1/unstructured/unstructured_test.go @@ -105,7 +105,6 @@ func TestUnstructuredMetadataOmitempty(t *testing.T) { u.SetAnnotations(nil) u.SetOwnerReferences(nil) u.SetFinalizers(nil) - u.SetClusterName("") u.SetManagedFields(nil) gotMetadata, _, err := unstructured.NestedFieldNoCopy(u.UnstructuredContent(), "metadata") @@ -147,6 +146,5 @@ func setObjectMetaUsingAccessors(u, uCopy *unstructured.Unstructured) { uCopy.SetAnnotations(u.GetAnnotations()) uCopy.SetOwnerReferences(u.GetOwnerReferences()) uCopy.SetFinalizers(u.GetFinalizers()) - uCopy.SetClusterName(u.GetClusterName()) uCopy.SetManagedFields(u.GetManagedFields()) } diff --git a/pkg/apis/meta/v1/unstructured/zz_generated.deepcopy.go b/pkg/apis/meta/v1/unstructured/zz_generated.deepcopy.go index 9a9f25e8f..fe8250dd6 100644 --- a/pkg/apis/meta/v1/unstructured/zz_generated.deepcopy.go +++ b/pkg/apis/meta/v1/unstructured/zz_generated.deepcopy.go @@ -1,3 +1,4 @@ +//go:build !ignore_autogenerated // +build !ignore_autogenerated /* diff --git a/pkg/apis/meta/v1/validation/validation.go b/pkg/apis/meta/v1/validation/validation.go index bd82a9d65..a0f709ad8 100644 --- a/pkg/apis/meta/v1/validation/validation.go +++ b/pkg/apis/meta/v1/validation/validation.go @@ -28,19 +28,46 @@ import ( "k8s.io/apimachinery/pkg/util/validation/field" ) -func ValidateLabelSelector(ps *metav1.LabelSelector, fldPath *field.Path) field.ErrorList { +// LabelSelectorValidationOptions is a struct that can be passed to ValidateLabelSelector to record the validate options +type LabelSelectorValidationOptions struct { + // Allow invalid label value in selector + AllowInvalidLabelValueInSelector bool +} + +// LabelSelectorHasInvalidLabelValue returns true if the given selector contains an invalid label value in a match expression. +// This is useful for determining whether AllowInvalidLabelValueInSelector should be set to true when validating an update +// based on existing persisted invalid values. +func LabelSelectorHasInvalidLabelValue(ps *metav1.LabelSelector) bool { + if ps == nil { + return false + } + for _, e := range ps.MatchExpressions { + for _, v := range e.Values { + if len(validation.IsValidLabelValue(v)) > 0 { + return true + } + } + } + return false +} + +// ValidateLabelSelector validate the LabelSelector according to the opts and returns any validation errors. +// opts.AllowInvalidLabelValueInSelector is only expected to be set to true when required for backwards compatibility with existing invalid data. +func ValidateLabelSelector(ps *metav1.LabelSelector, opts LabelSelectorValidationOptions, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} if ps == nil { return allErrs } allErrs = append(allErrs, ValidateLabels(ps.MatchLabels, fldPath.Child("matchLabels"))...) for i, expr := range ps.MatchExpressions { - allErrs = append(allErrs, ValidateLabelSelectorRequirement(expr, fldPath.Child("matchExpressions").Index(i))...) + allErrs = append(allErrs, ValidateLabelSelectorRequirement(expr, opts, fldPath.Child("matchExpressions").Index(i))...) } return allErrs } -func ValidateLabelSelectorRequirement(sr metav1.LabelSelectorRequirement, fldPath *field.Path) field.ErrorList { +// ValidateLabelSelectorRequirement validate the requirement according to the opts and returns any validation errors. +// opts.AllowInvalidLabelValueInSelector is only expected to be set to true when required for backwards compatibility with existing invalid data. +func ValidateLabelSelectorRequirement(sr metav1.LabelSelectorRequirement, opts LabelSelectorValidationOptions, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} switch sr.Operator { case metav1.LabelSelectorOpIn, metav1.LabelSelectorOpNotIn: @@ -55,6 +82,13 @@ func ValidateLabelSelectorRequirement(sr metav1.LabelSelectorRequirement, fldPat allErrs = append(allErrs, field.Invalid(fldPath.Child("operator"), sr.Operator, "not a valid selector operator")) } allErrs = append(allErrs, ValidateLabelName(sr.Key, fldPath.Child("key"))...) + if !opts.AllowInvalidLabelValueInSelector { + for valueIndex, value := range sr.Values { + for _, msg := range validation.IsValidLabelValue(value) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("values").Index(valueIndex), value, msg)) + } + } + } return allErrs } @@ -96,17 +130,19 @@ func ValidateDeleteOptions(options *metav1.DeleteOptions) field.ErrorList { } func ValidateCreateOptions(options *metav1.CreateOptions) field.ErrorList { - return append( - ValidateFieldManager(options.FieldManager, field.NewPath("fieldManager")), - ValidateDryRun(field.NewPath("dryRun"), options.DryRun)..., - ) + allErrs := field.ErrorList{} + allErrs = append(allErrs, ValidateFieldManager(options.FieldManager, field.NewPath("fieldManager"))...) + allErrs = append(allErrs, ValidateDryRun(field.NewPath("dryRun"), options.DryRun)...) + allErrs = append(allErrs, ValidateFieldValidation(field.NewPath("fieldValidation"), options.FieldValidation)...) + return allErrs } func ValidateUpdateOptions(options *metav1.UpdateOptions) field.ErrorList { - return append( - ValidateFieldManager(options.FieldManager, field.NewPath("fieldManager")), - ValidateDryRun(field.NewPath("dryRun"), options.DryRun)..., - ) + allErrs := field.ErrorList{} + allErrs = append(allErrs, ValidateFieldManager(options.FieldManager, field.NewPath("fieldManager"))...) + allErrs = append(allErrs, ValidateDryRun(field.NewPath("dryRun"), options.DryRun)...) + allErrs = append(allErrs, ValidateFieldValidation(field.NewPath("fieldValidation"), options.FieldValidation)...) + return allErrs } func ValidatePatchOptions(options *metav1.PatchOptions, patchType types.PatchType) field.ErrorList { @@ -123,6 +159,7 @@ func ValidatePatchOptions(options *metav1.PatchOptions, patchType types.PatchTyp } allErrs = append(allErrs, ValidateFieldManager(options.FieldManager, field.NewPath("fieldManager"))...) allErrs = append(allErrs, ValidateDryRun(field.NewPath("dryRun"), options.DryRun)...) + allErrs = append(allErrs, ValidateFieldValidation(field.NewPath("fieldValidation"), options.FieldValidation)...) return allErrs } @@ -159,6 +196,18 @@ func ValidateDryRun(fldPath *field.Path, dryRun []string) field.ErrorList { return allErrs } +var allowedFieldValidationValues = sets.NewString("", metav1.FieldValidationIgnore, metav1.FieldValidationWarn, metav1.FieldValidationStrict) + +// ValidateFieldValidation validates that a fieldValidation query param only contains allowed values. +func ValidateFieldValidation(fldPath *field.Path, fieldValidation string) field.ErrorList { + allErrs := field.ErrorList{} + if !allowedFieldValidationValues.Has(fieldValidation) { + allErrs = append(allErrs, field.NotSupported(fldPath, fieldValidation, allowedFieldValidationValues.List())) + } + return allErrs + +} + const UninitializedStatusUpdateErrorMsg string = `must not update status when the object is uninitialized` // ValidateTableOptions returns any invalid flags on TableOptions. diff --git a/pkg/apis/meta/v1/validation/validation_test.go b/pkg/apis/meta/v1/validation/validation_test.go index 93c3284f7..60b0cd3db 100644 --- a/pkg/apis/meta/v1/validation/validation_test.go +++ b/pkg/apis/meta/v1/validation/validation_test.go @@ -427,6 +427,98 @@ func TestValidateConditions(t *testing.T) { } } +func TestLabelSelectorMatchExpression(t *testing.T) { + testCases := []struct { + name string + labelSelector *metav1.LabelSelector + wantErrorNumber int + validateErrs func(t *testing.T, errs field.ErrorList) + }{ + { + name: "Valid LabelSelector", + labelSelector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "key", + Operator: metav1.LabelSelectorOpIn, + Values: []string{"value"}, + }, + }, + }, + wantErrorNumber: 0, + validateErrs: nil, + }, + { + name: "MatchExpression's key name isn't valid", + labelSelector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "-key", + Operator: metav1.LabelSelectorOpIn, + Values: []string{"value"}, + }, + }, + }, + wantErrorNumber: 1, + validateErrs: func(t *testing.T, errs field.ErrorList) { + errMessage := "name part must consist of alphanumeric characters" + if !partStringInErrorMessage(errs, errMessage) { + t.Errorf("missing %q in\n%v", errMessage, errorsAsString(errs)) + } + }, + }, + { + name: "MatchExpression's operator isn't valid", + labelSelector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "key", + Operator: "abc", + Values: []string{"value"}, + }, + }, + }, + wantErrorNumber: 1, + validateErrs: func(t *testing.T, errs field.ErrorList) { + errMessage := "not a valid selector operator" + if !partStringInErrorMessage(errs, errMessage) { + t.Errorf("missing %q in\n%v", errMessage, errorsAsString(errs)) + } + }, + }, + { + name: "MatchExpression's value name isn't valid", + labelSelector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "key", + Operator: metav1.LabelSelectorOpIn, + Values: []string{"-value"}, + }, + }, + }, + wantErrorNumber: 1, + validateErrs: func(t *testing.T, errs field.ErrorList) { + errMessage := "a valid label must be an empty string or consist of" + if !partStringInErrorMessage(errs, errMessage) { + t.Errorf("missing %q in\n%v", errMessage, errorsAsString(errs)) + } + }, + }, + } + for index, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + allErrs := ValidateLabelSelector(testCase.labelSelector, LabelSelectorValidationOptions{false}, field.NewPath("labelSelector")) + if len(allErrs) != testCase.wantErrorNumber { + t.Errorf("case[%d]: expected failure", index) + } + if len(allErrs) >= 1 && testCase.validateErrs != nil { + testCase.validateErrs(t, allErrs) + } + }) + } +} + func hasError(errs field.ErrorList, needle string) bool { for _, curr := range errs { if curr.Error() == needle { @@ -445,6 +537,15 @@ func hasPrefixError(errs field.ErrorList, prefix string) bool { return false } +func partStringInErrorMessage(errs field.ErrorList, prefix string) bool { + for _, curr := range errs { + if strings.Contains(curr.Error(), prefix) { + return true + } + } + return false +} + func errorsAsString(errs field.ErrorList) string { messages := []string{} for _, curr := range errs { diff --git a/pkg/apis/meta/v1/zz_generated.conversion.go b/pkg/apis/meta/v1/zz_generated.conversion.go index 3ecb67c82..b7590f0b3 100644 --- a/pkg/apis/meta/v1/zz_generated.conversion.go +++ b/pkg/apis/meta/v1/zz_generated.conversion.go @@ -1,3 +1,4 @@ +//go:build !ignore_autogenerated // +build !ignore_autogenerated /* @@ -293,6 +294,13 @@ func autoConvert_url_Values_To_v1_CreateOptions(in *url.Values, out *CreateOptio } else { out.FieldManager = "" } + if values, ok := map[string][]string(*in)["fieldValidation"]; ok && len(values) > 0 { + if err := runtime.Convert_Slice_string_To_string(&values, &out.FieldValidation, s); err != nil { + return err + } + } else { + out.FieldValidation = "" + } return nil } @@ -448,6 +456,13 @@ func autoConvert_url_Values_To_v1_PatchOptions(in *url.Values, out *PatchOptions } else { out.FieldManager = "" } + if values, ok := map[string][]string(*in)["fieldValidation"]; ok && len(values) > 0 { + if err := runtime.Convert_Slice_string_To_string(&values, &out.FieldValidation, s); err != nil { + return err + } + } else { + out.FieldValidation = "" + } return nil } @@ -496,6 +511,13 @@ func autoConvert_url_Values_To_v1_UpdateOptions(in *url.Values, out *UpdateOptio } else { out.FieldManager = "" } + if values, ok := map[string][]string(*in)["fieldValidation"]; ok && len(values) > 0 { + if err := runtime.Convert_Slice_string_To_string(&values, &out.FieldValidation, s); err != nil { + return err + } + } else { + out.FieldValidation = "" + } return nil } diff --git a/pkg/apis/meta/v1/zz_generated.deepcopy.go b/pkg/apis/meta/v1/zz_generated.deepcopy.go index d43020da5..418e6099f 100644 --- a/pkg/apis/meta/v1/zz_generated.deepcopy.go +++ b/pkg/apis/meta/v1/zz_generated.deepcopy.go @@ -1,3 +1,4 @@ +//go:build !ignore_autogenerated // +build !ignore_autogenerated /* diff --git a/pkg/apis/meta/v1/zz_generated.defaults.go b/pkg/apis/meta/v1/zz_generated.defaults.go index cce2e603a..dac177e93 100644 --- a/pkg/apis/meta/v1/zz_generated.defaults.go +++ b/pkg/apis/meta/v1/zz_generated.defaults.go @@ -1,3 +1,4 @@ +//go:build !ignore_autogenerated // +build !ignore_autogenerated /* diff --git a/pkg/apis/meta/v1beta1/generated.pb.go b/pkg/apis/meta/v1beta1/generated.pb.go index a5a949679..a2abc67c1 100644 --- a/pkg/apis/meta/v1beta1/generated.pb.go +++ b/pkg/apis/meta/v1beta1/generated.pb.go @@ -82,26 +82,26 @@ func init() { var fileDescriptor_90ec10f86b91f9a8 = []byte{ // 317 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x91, 0x41, 0x4b, 0xf3, 0x30, - 0x1c, 0xc6, 0x9b, 0xf7, 0x65, 0x38, 0x3a, 0x04, 0xd9, 0x69, 0xee, 0x90, 0x0d, 0x4f, 0xf3, 0xb0, - 0x84, 0x0d, 0x11, 0xc1, 0xdb, 0x6e, 0x82, 0xa2, 0xec, 0x28, 0x1e, 0x4c, 0xbb, 0xbf, 0x5d, 0xac, - 0x69, 0x4a, 0xf2, 0xef, 0xc0, 0x9b, 0x1f, 0xc1, 0x8f, 0xb5, 0xe3, 0x8e, 0x03, 0x61, 0xb8, 0xf8, - 0x45, 0x24, 0x5d, 0x15, 0x19, 0x0a, 0xbb, 0xf5, 0x79, 0xca, 0xef, 0x97, 0x27, 0x24, 0x1c, 0xa7, - 0x67, 0x96, 0x49, 0xcd, 0xd3, 0x22, 0x02, 0x93, 0x01, 0x82, 0xe5, 0x33, 0xc8, 0x26, 0xda, 0xf0, - 0xea, 0x87, 0xc8, 0xa5, 0x12, 0xf1, 0x54, 0x66, 0x60, 0x9e, 0x79, 0x9e, 0x26, 0xbe, 0xb0, 0x5c, - 0x01, 0x0a, 0x3e, 0x1b, 0x44, 0x80, 0x62, 0xc0, 0x13, 0xc8, 0xc0, 0x08, 0x84, 0x09, 0xcb, 0x8d, - 0x46, 0xdd, 0x3c, 0xde, 0xa0, 0xec, 0x27, 0xca, 0xf2, 0x34, 0xf1, 0x85, 0x65, 0x1e, 0x65, 0x15, - 0xda, 0xee, 0x27, 0x12, 0xa7, 0x45, 0xc4, 0x62, 0xad, 0x78, 0xa2, 0x13, 0xcd, 0x4b, 0x43, 0x54, - 0x3c, 0x94, 0xa9, 0x0c, 0xe5, 0xd7, 0xc6, 0xdc, 0x3e, 0xd9, 0x65, 0xd4, 0xf6, 0x9e, 0xf6, 0xe9, - 0x5f, 0x94, 0x29, 0x32, 0x94, 0x0a, 0xb8, 0x8d, 0xa7, 0xa0, 0xc4, 0x36, 0x77, 0xf4, 0x46, 0xc2, - 0xc3, 0x1b, 0x61, 0x50, 0x8a, 0xa7, 0xeb, 0xe8, 0x11, 0x62, 0xbc, 0x02, 0x14, 0x13, 0x81, 0xe2, - 0x52, 0x5a, 0x6c, 0xde, 0x85, 0x75, 0x55, 0xe5, 0xd6, 0xbf, 0x2e, 0xe9, 0x35, 0x86, 0x8c, 0xed, - 0x72, 0x71, 0xe6, 0x69, 0x6f, 0x1a, 0x1d, 0xcc, 0x57, 0x9d, 0xc0, 0xad, 0x3a, 0xf5, 0xaf, 0x66, - 0xfc, 0x6d, 0x6c, 0xde, 0x87, 0x35, 0x89, 0xa0, 0x6c, 0x8b, 0x74, 0xff, 0xf7, 0x1a, 0xc3, 0xf3, - 0xdd, 0xd4, 0xbf, 0xae, 0x1d, 0xed, 0x57, 0xe7, 0xd4, 0x2e, 0xbc, 0x71, 0xbc, 0x11, 0x8f, 0xfa, - 0xf3, 0x35, 0x0d, 0x16, 0x6b, 0x1a, 0x2c, 0xd7, 0x34, 0x78, 0x71, 0x94, 0xcc, 0x1d, 0x25, 0x0b, - 0x47, 0xc9, 0xd2, 0x51, 0xf2, 0xee, 0x28, 0x79, 0xfd, 0xa0, 0xc1, 0xed, 0x5e, 0xf5, 0x52, 0x9f, - 0x01, 0x00, 0x00, 0xff, 0xff, 0xf7, 0x82, 0x5b, 0x80, 0x29, 0x02, 0x00, 0x00, + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x92, 0x41, 0x4b, 0xf3, 0x30, + 0x1c, 0xc6, 0x9b, 0xf7, 0x65, 0x30, 0x3a, 0x04, 0xd9, 0x69, 0xee, 0x90, 0x0d, 0x4f, 0xdb, 0xc1, + 0x84, 0x0d, 0x11, 0xc1, 0xdb, 0x6e, 0x82, 0x32, 0xd9, 0x51, 0x3c, 0x98, 0x76, 0x7f, 0xbb, 0x58, + 0xd3, 0x94, 0xe4, 0xdf, 0x81, 0x37, 0x3f, 0x82, 0x1f, 0x6b, 0xc7, 0x1d, 0x07, 0xc2, 0x70, 0xf5, + 0x8b, 0x48, 0xda, 0x2a, 0x32, 0x14, 0x7a, 0xeb, 0xf3, 0x94, 0xdf, 0x2f, 0x4f, 0x20, 0xfe, 0x2c, + 0x3e, 0xb7, 0x4c, 0x6a, 0x1e, 0x67, 0x01, 0x98, 0x04, 0x10, 0x2c, 0x5f, 0x42, 0x32, 0xd7, 0x86, + 0x57, 0x3f, 0x44, 0x2a, 0x95, 0x08, 0x17, 0x32, 0x01, 0xf3, 0xcc, 0xd3, 0x38, 0x72, 0x85, 0xe5, + 0x0a, 0x50, 0xf0, 0xe5, 0x28, 0x00, 0x14, 0x23, 0x1e, 0x41, 0x02, 0x46, 0x20, 0xcc, 0x59, 0x6a, + 0x34, 0xea, 0xf6, 0xb0, 0x44, 0xd9, 0x4f, 0x94, 0xa5, 0x71, 0xe4, 0x0a, 0xcb, 0x1c, 0xca, 0x2a, + 0xb4, 0x7b, 0x12, 0x49, 0x5c, 0x64, 0x01, 0x0b, 0xb5, 0xe2, 0x91, 0x8e, 0x34, 0x2f, 0x0c, 0x41, + 0xf6, 0x50, 0xa4, 0x22, 0x14, 0x5f, 0xa5, 0xb9, 0x7b, 0x5a, 0x67, 0xd4, 0xfe, 0x9e, 0xee, 0xd9, + 0x5f, 0x94, 0xc9, 0x12, 0x94, 0x0a, 0xb8, 0x0d, 0x17, 0xa0, 0xc4, 0x3e, 0x77, 0xfc, 0x46, 0xfc, + 0xa3, 0x1b, 0x61, 0x50, 0x8a, 0xa7, 0x69, 0xf0, 0x08, 0x21, 0x5e, 0x03, 0x8a, 0xb9, 0x40, 0x71, + 0x25, 0x2d, 0xb6, 0xef, 0xfc, 0xa6, 0xaa, 0x72, 0xe7, 0x5f, 0x9f, 0x0c, 0x5a, 0x63, 0xc6, 0xea, + 0x5c, 0x9c, 0x39, 0xda, 0x99, 0x26, 0x87, 0xab, 0x6d, 0xcf, 0xcb, 0xb7, 0xbd, 0xe6, 0x57, 0x33, + 0xfb, 0x36, 0xb6, 0xef, 0xfd, 0x86, 0x44, 0x50, 0xb6, 0x43, 0xfa, 0xff, 0x07, 0xad, 0xf1, 0x45, + 0x3d, 0xf5, 0xaf, 0x6b, 0x27, 0x07, 0xd5, 0x39, 0x8d, 0x4b, 0x67, 0x9c, 0x95, 0xe2, 0xc9, 0x74, + 0xb5, 0xa3, 0xde, 0x7a, 0x47, 0xbd, 0xcd, 0x8e, 0x7a, 0x2f, 0x39, 0x25, 0xab, 0x9c, 0x92, 0x75, + 0x4e, 0xc9, 0x26, 0xa7, 0xe4, 0x3d, 0xa7, 0xe4, 0xf5, 0x83, 0x7a, 0xb7, 0xc3, 0xda, 0xcf, 0xe0, + 0x33, 0x00, 0x00, 0xff, 0xff, 0x30, 0x97, 0x8b, 0x11, 0x4b, 0x02, 0x00, 0x00, } func (m *PartialObjectMetadataList) Marshal() (dAtA []byte, err error) { diff --git a/pkg/apis/meta/v1beta1/generated.proto b/pkg/apis/meta/v1beta1/generated.proto index a209dd456..d14d42591 100644 --- a/pkg/apis/meta/v1beta1/generated.proto +++ b/pkg/apis/meta/v1beta1/generated.proto @@ -25,7 +25,7 @@ import "k8s.io/apimachinery/pkg/apis/meta/v1/generated.proto"; import "k8s.io/apimachinery/pkg/runtime/schema/generated.proto"; // Package-wide variables from generator "generated". -option go_package = "v1beta1"; +option go_package = "k8s.io/apimachinery/pkg/apis/meta/v1beta1"; // PartialObjectMetadataList contains a list of objects containing only their metadata. // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/apis/meta/v1beta1/zz_generated.deepcopy.go b/pkg/apis/meta/v1beta1/zz_generated.deepcopy.go index 89053b981..972b7f03e 100644 --- a/pkg/apis/meta/v1beta1/zz_generated.deepcopy.go +++ b/pkg/apis/meta/v1beta1/zz_generated.deepcopy.go @@ -1,3 +1,4 @@ +//go:build !ignore_autogenerated // +build !ignore_autogenerated /* diff --git a/pkg/apis/meta/v1beta1/zz_generated.defaults.go b/pkg/apis/meta/v1beta1/zz_generated.defaults.go index 73e63fc11..198b5be4a 100644 --- a/pkg/apis/meta/v1beta1/zz_generated.defaults.go +++ b/pkg/apis/meta/v1beta1/zz_generated.defaults.go @@ -1,3 +1,4 @@ +//go:build !ignore_autogenerated // +build !ignore_autogenerated /* diff --git a/pkg/apis/testapigroup/v1/generated.pb.go b/pkg/apis/testapigroup/v1/generated.pb.go index f044a0820..d68950795 100644 --- a/pkg/apis/testapigroup/v1/generated.pb.go +++ b/pkg/apis/testapigroup/v1/generated.pb.go @@ -199,73 +199,73 @@ func init() { } var fileDescriptor_b7eb07c7d80facdf = []byte{ - // 1048 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x55, 0xc1, 0x6e, 0xdb, 0x46, - 0x10, 0x35, 0x2d, 0xc9, 0x96, 0xd6, 0x56, 0x62, 0x6f, 0x62, 0x80, 0x35, 0x10, 0xc9, 0xf1, 0xc1, - 0x70, 0x8b, 0x94, 0x8a, 0x8d, 0x26, 0x70, 0x9b, 0x43, 0x11, 0xda, 0x45, 0xed, 0xc2, 0x71, 0x84, - 0x95, 0x8b, 0x14, 0x45, 0x0f, 0x59, 0x51, 0x53, 0x99, 0x95, 0xc8, 0x25, 0x76, 0x57, 0x2a, 0x74, - 0x2b, 0xfa, 0x05, 0xfd, 0x88, 0xde, 0x7a, 0xee, 0x07, 0xf4, 0x50, 0xc0, 0xc7, 0x1c, 0x73, 0x12, - 0x6a, 0xf5, 0x2f, 0x7c, 0x2a, 0x76, 0xb9, 0xa4, 0x28, 0xcb, 0x55, 0xa2, 0xdc, 0xb8, 0x33, 0xef, - 0xbd, 0x99, 0xdd, 0x19, 0xce, 0xa0, 0x6f, 0x3b, 0x07, 0xc2, 0xf1, 0x59, 0xad, 0xd3, 0x6b, 0x02, - 0x0f, 0x41, 0x82, 0xa8, 0xf5, 0x21, 0x6c, 0x31, 0x5e, 0x33, 0x0e, 0x1a, 0xf9, 0x01, 0xf5, 0x2e, - 0xfc, 0x10, 0xf8, 0xa0, 0x16, 0x75, 0xda, 0xca, 0x20, 0x6a, 0x12, 0x84, 0xa4, 0x91, 0xdf, 0xe6, - 0xac, 0x17, 0xd5, 0xfa, 0x7b, 0xb5, 0x36, 0x84, 0xc0, 0xa9, 0x84, 0x96, 0x13, 0x71, 0x26, 0x19, - 0x7e, 0x14, 0xb3, 0x9d, 0x2c, 0xdb, 0x89, 0x3a, 0x6d, 0x65, 0x10, 0x4e, 0x96, 0xed, 0xf4, 0xf7, - 0x36, 0x3f, 0x6d, 0xfb, 0xf2, 0xa2, 0xd7, 0x74, 0x3c, 0x16, 0xd4, 0xda, 0xac, 0xcd, 0x6a, 0x5a, - 0xa4, 0xd9, 0xfb, 0x51, 0x9f, 0xf4, 0x41, 0x7f, 0xc5, 0xe2, 0x9b, 0x9f, 0xcd, 0x4c, 0x2d, 0x00, - 0x49, 0x6f, 0x49, 0x69, 0xf3, 0x7f, 0x2f, 0xc4, 0x7b, 0xa1, 0xf4, 0x03, 0x98, 0x22, 0x3c, 0x7d, - 0x17, 0x41, 0x78, 0x17, 0x10, 0xd0, 0x9b, 0xbc, 0xed, 0xdf, 0x17, 0x51, 0xfe, 0x90, 0xf2, 0x08, - 0xbf, 0x46, 0x45, 0x95, 0x4c, 0x8b, 0x4a, 0x6a, 0x5b, 0x5b, 0xd6, 0xee, 0xca, 0xfe, 0x63, 0x67, - 0xe6, 0xbb, 0x28, 0xb4, 0xd3, 0xdf, 0x73, 0x5e, 0x36, 0x7f, 0x02, 0x4f, 0xbe, 0x00, 0x49, 0x5d, - 0x7c, 0x39, 0xac, 0x2e, 0x8c, 0x86, 0x55, 0x34, 0xb6, 0x91, 0x54, 0x15, 0x7f, 0x87, 0xf2, 0x22, - 0x02, 0xcf, 0x5e, 0xd4, 0xea, 0x4f, 0x9d, 0x79, 0x5e, 0xdd, 0x51, 0x39, 0x36, 0x22, 0xf0, 0xdc, - 0x55, 0x13, 0x23, 0xaf, 0x4e, 0x44, 0x2b, 0xe2, 0xd7, 0x68, 0x49, 0x48, 0x2a, 0x7b, 0xc2, 0xce, - 0x69, 0xed, 0x83, 0x0f, 0xd0, 0xd6, 0x7c, 0xf7, 0x8e, 0x51, 0x5f, 0x8a, 0xcf, 0xc4, 0xe8, 0x6e, - 0xff, 0x99, 0x43, 0x65, 0x05, 0x3b, 0x64, 0x61, 0xcb, 0x97, 0x3e, 0x0b, 0xf1, 0x13, 0x94, 0x97, - 0x83, 0x08, 0xf4, 0x5b, 0x95, 0xdc, 0x87, 0x49, 0x56, 0xe7, 0x83, 0x08, 0xae, 0x87, 0xd5, 0xf5, - 0x09, 0xb0, 0x32, 0x12, 0x0d, 0xc7, 0x9f, 0xa7, 0xa9, 0x2e, 0x4e, 0x10, 0x4d, 0xc0, 0xeb, 0x61, - 0xf5, 0x6e, 0x4a, 0x9b, 0xcc, 0x01, 0xb7, 0x51, 0xb9, 0x4b, 0x85, 0xac, 0x73, 0xd6, 0x84, 0x73, - 0x3f, 0x00, 0x73, 0xd9, 0x4f, 0xde, 0xaf, 0x4c, 0x8a, 0xe1, 0x6e, 0x98, 0x68, 0xe5, 0xd3, 0xac, - 0x10, 0x99, 0xd4, 0xc5, 0x7d, 0x84, 0x95, 0xe1, 0x9c, 0xd3, 0x50, 0xc4, 0xf9, 0xab, 0x68, 0xf9, - 0xb9, 0xa3, 0x6d, 0x9a, 0x68, 0xf8, 0x74, 0x4a, 0x8d, 0xdc, 0x12, 0x01, 0xef, 0xa0, 0x25, 0x0e, - 0x54, 0xb0, 0xd0, 0x2e, 0xe8, 0xb7, 0x49, 0x8b, 0x41, 0xb4, 0x95, 0x18, 0x2f, 0xfe, 0x18, 0x2d, - 0x07, 0x20, 0x04, 0x6d, 0x83, 0xbd, 0xa4, 0x81, 0x77, 0x0d, 0x70, 0xf9, 0x45, 0x6c, 0x26, 0x89, - 0x7f, 0xfb, 0x2f, 0x0b, 0x15, 0x55, 0x29, 0x4e, 0x7d, 0x21, 0xf1, 0x0f, 0x53, 0x2d, 0xee, 0xbc, - 0xdf, 0x6d, 0x14, 0x5b, 0x37, 0xf8, 0x9a, 0x09, 0x54, 0x4c, 0x2c, 0x99, 0xf6, 0x7e, 0x85, 0x0a, - 0xbe, 0x84, 0x40, 0x15, 0x36, 0xb7, 0xbb, 0xb2, 0xbf, 0x3f, 0x7f, 0x0f, 0xba, 0x65, 0x23, 0x5f, - 0x38, 0x51, 0x42, 0x24, 0xd6, 0xdb, 0xfe, 0x7b, 0x39, 0xbe, 0x83, 0x6a, 0x78, 0x7c, 0x8a, 0xca, - 0x5c, 0x51, 0xb9, 0xac, 0xb3, 0xae, 0xef, 0x0d, 0x74, 0x13, 0x94, 0xdc, 0x9d, 0xa4, 0xb0, 0x24, - 0xeb, 0xbc, 0xbe, 0x69, 0x20, 0x93, 0x64, 0xdc, 0x46, 0x0f, 0x24, 0xf0, 0xc0, 0x0f, 0xa9, 0x2a, - 0xc2, 0xd7, 0x9c, 0x7a, 0x50, 0x07, 0xee, 0xb3, 0x56, 0x03, 0x3c, 0x16, 0xb6, 0x84, 0x2e, 0x7a, - 0xce, 0x7d, 0x38, 0x1a, 0x56, 0x1f, 0x9c, 0xcf, 0x02, 0x92, 0xd9, 0x3a, 0xf8, 0x25, 0xda, 0xa0, - 0x9e, 0xf4, 0xfb, 0x70, 0x04, 0xb4, 0xd5, 0xf5, 0x43, 0x48, 0x02, 0x14, 0x74, 0x80, 0x8f, 0x46, - 0xc3, 0xea, 0xc6, 0xf3, 0xdb, 0x00, 0xe4, 0x76, 0x1e, 0xfe, 0xd5, 0x42, 0xab, 0x21, 0x6b, 0x41, - 0x03, 0xba, 0xe0, 0x49, 0xc6, 0xed, 0x65, 0xfd, 0xea, 0xc7, 0x1f, 0x36, 0x55, 0x9c, 0xb3, 0x8c, - 0xd4, 0x57, 0xa1, 0xe4, 0x03, 0xf7, 0xbe, 0x79, 0xd1, 0xd5, 0xac, 0x8b, 0x4c, 0xc4, 0xc4, 0xdf, - 0x20, 0x2c, 0x80, 0xf7, 0x7d, 0x0f, 0x9e, 0x7b, 0x1e, 0xeb, 0x85, 0xf2, 0x8c, 0x06, 0x60, 0x17, - 0x75, 0x45, 0xd2, 0xe6, 0x6f, 0x4c, 0x21, 0xc8, 0x2d, 0x2c, 0x7c, 0x8c, 0xee, 0x4c, 0x5a, 0xed, - 0x92, 0xd6, 0xd9, 0x32, 0x3a, 0xf6, 0x11, 0x44, 0x1c, 0x3c, 0x35, 0xba, 0x27, 0x15, 0xc9, 0x0d, - 0x1e, 0x7e, 0x84, 0x8a, 0x2a, 0x4b, 0x9d, 0x0b, 0xd2, 0x1a, 0x69, 0xdb, 0x9e, 0x19, 0x3b, 0x49, - 0x11, 0xf8, 0x09, 0x5a, 0xb9, 0x60, 0x42, 0x9e, 0x81, 0xfc, 0x99, 0xf1, 0x8e, 0xbd, 0xb2, 0x65, - 0xed, 0x16, 0xdd, 0x7b, 0x86, 0xb0, 0x72, 0x3c, 0x76, 0x91, 0x2c, 0x4e, 0xfd, 0x83, 0xea, 0x58, - 0x3f, 0x39, 0xb2, 0x57, 0x35, 0x25, 0xfd, 0x07, 0x8f, 0x63, 0x33, 0x49, 0xfc, 0x09, 0xf4, 0xa4, - 0x7e, 0x68, 0x97, 0xa7, 0xa1, 0x27, 0xf5, 0x43, 0x92, 0xf8, 0x55, 0xea, 0xea, 0x33, 0x54, 0xa9, - 0xaf, 0x4d, 0xa6, 0x7e, 0x6c, 0xec, 0x24, 0x45, 0xe0, 0x1a, 0x2a, 0x89, 0x5e, 0xb3, 0xc5, 0x02, - 0xea, 0x87, 0xf6, 0xba, 0x86, 0xaf, 0x1b, 0x78, 0xa9, 0x91, 0x38, 0xc8, 0x18, 0x83, 0x9f, 0xa1, - 0xb2, 0x5a, 0x83, 0xad, 0x5e, 0x17, 0xb8, 0x8e, 0x71, 0x4f, 0x93, 0xd2, 0xa9, 0xd8, 0x48, 0x9c, - 0xfa, 0x8d, 0x26, 0xb1, 0x9b, 0x5f, 0xa2, 0xf5, 0xa9, 0x2e, 0xc1, 0x6b, 0x28, 0xd7, 0x81, 0x41, - 0xbc, 0x04, 0x88, 0xfa, 0xc4, 0xf7, 0x51, 0xa1, 0x4f, 0xbb, 0x3d, 0x88, 0xe7, 0x3b, 0x89, 0x0f, - 0x5f, 0x2c, 0x1e, 0x58, 0xdb, 0x7f, 0xe4, 0x10, 0x1a, 0xaf, 0x1a, 0xfc, 0x18, 0x15, 0xa2, 0x0b, - 0x2a, 0x92, 0x0d, 0x92, 0xf4, 0x4b, 0xa1, 0xae, 0x8c, 0xd7, 0xc3, 0x6a, 0x49, 0x61, 0xf5, 0x81, - 0xc4, 0x40, 0xcc, 0x10, 0xf2, 0x92, 0xdd, 0x90, 0x8c, 0x99, 0x67, 0xf3, 0x37, 0x7c, 0xba, 0x5f, - 0xc6, 0xfb, 0x3a, 0x35, 0x09, 0x92, 0x09, 0x91, 0x1d, 0xb4, 0xb9, 0xd9, 0x83, 0x36, 0x33, 0xbb, - 0xf3, 0x33, 0x67, 0xf7, 0x0e, 0x5a, 0x8a, 0x8b, 0x7d, 0x73, 0xc6, 0xc7, 0xbd, 0x40, 0x8c, 0x57, - 0xe1, 0x3c, 0xca, 0xa3, 0x93, 0xba, 0x19, 0xf1, 0x29, 0xee, 0x50, 0x5b, 0x89, 0xf1, 0xe2, 0x57, - 0xa8, 0xa4, 0x07, 0x9a, 0x5e, 0x51, 0xcb, 0x73, 0xaf, 0xa8, 0xb2, 0xee, 0x95, 0x44, 0x80, 0x8c, - 0xb5, 0xdc, 0xdd, 0xcb, 0xab, 0xca, 0xc2, 0x9b, 0xab, 0xca, 0xc2, 0xdb, 0xab, 0xca, 0xc2, 0x2f, - 0xa3, 0x8a, 0x75, 0x39, 0xaa, 0x58, 0x6f, 0x46, 0x15, 0xeb, 0xed, 0xa8, 0x62, 0xfd, 0x33, 0xaa, - 0x58, 0xbf, 0xfd, 0x5b, 0x59, 0xf8, 0x7e, 0xb1, 0xbf, 0xf7, 0x5f, 0x00, 0x00, 0x00, 0xff, 0xff, - 0xd7, 0x22, 0x1b, 0x36, 0x96, 0x0a, 0x00, 0x00, + // 1051 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, 0xe6, 0x50, 0x84, 0x76, 0x51, 0xbb, 0x70, 0x1c, + 0x61, 0xe5, 0x22, 0x45, 0xd1, 0x43, 0x56, 0xd4, 0x54, 0x66, 0x25, 0x72, 0x89, 0xdd, 0x95, 0x0a, + 0xdd, 0x8a, 0x3e, 0x41, 0x1f, 0xa2, 0xb7, 0x9e, 0xfb, 0x00, 0x3d, 0x14, 0xf0, 0x31, 0xc7, 0x9c, + 0x84, 0x5a, 0x7d, 0x0b, 0x9f, 0x8a, 0x5d, 0x2e, 0x29, 0xca, 0x72, 0xd5, 0x28, 0x37, 0xee, 0xcc, + 0xf7, 0x7d, 0x33, 0xbb, 0x33, 0x9a, 0x11, 0xfa, 0xba, 0x73, 0x28, 0x1c, 0x9f, 0xd5, 0x3a, 0xbd, + 0x26, 0xf0, 0x10, 0x24, 0x88, 0x5a, 0x1f, 0xc2, 0x16, 0xe3, 0x35, 0xe3, 0xa0, 0x91, 0x1f, 0x50, + 0xef, 0xd2, 0x0f, 0x81, 0x0f, 0x6a, 0x51, 0xa7, 0xad, 0x0c, 0xa2, 0x26, 0x41, 0x48, 0x1a, 0xf9, + 0x6d, 0xce, 0x7a, 0x51, 0xad, 0xbf, 0x5f, 0x6b, 0x43, 0x08, 0x9c, 0x4a, 0x68, 0x39, 0x11, 0x67, + 0x92, 0xe1, 0xbd, 0x98, 0xed, 0x64, 0xd9, 0x4e, 0xd4, 0x69, 0x2b, 0x83, 0x70, 0xb2, 0x6c, 0xa7, + 0xbf, 0xbf, 0xf9, 0x71, 0xdb, 0x97, 0x97, 0xbd, 0xa6, 0xe3, 0xb1, 0xa0, 0xd6, 0x66, 0x6d, 0x56, + 0xd3, 0x22, 0xcd, 0xde, 0xf7, 0xfa, 0xa4, 0x0f, 0xfa, 0x2b, 0x16, 0xdf, 0xfc, 0x64, 0x66, 0x6a, + 0x01, 0x48, 0x7a, 0x47, 0x4a, 0x9b, 0xff, 0x7a, 0x21, 0xde, 0x0b, 0xa5, 0x1f, 0xc0, 0x14, 0xe1, + 0xd9, 0x7f, 0x11, 0x84, 0x77, 0x09, 0x01, 0xbd, 0xcd, 0xdb, 0xfe, 0x75, 0x11, 0xe5, 0x8f, 0x28, + 0x8f, 0xf0, 0x1b, 0x54, 0x54, 0xc9, 0xb4, 0xa8, 0xa4, 0xb6, 0xb5, 0x65, 0xed, 0xae, 0x1c, 0x3c, + 0x71, 0x66, 0xbe, 0x8b, 0x42, 0x3b, 0xfd, 0x7d, 0xe7, 0x55, 0xf3, 0x07, 0xf0, 0xe4, 0x4b, 0x90, + 0xd4, 0xc5, 0x57, 0xc3, 0xea, 0xc2, 0x68, 0x58, 0x45, 0x63, 0x1b, 0x49, 0x55, 0xf1, 0x37, 0x28, + 0x2f, 0x22, 0xf0, 0xec, 0x45, 0xad, 0xfe, 0xcc, 0x99, 0xe7, 0xd5, 0x1d, 0x95, 0x63, 0x23, 0x02, + 0xcf, 0x5d, 0x35, 0x31, 0xf2, 0xea, 0x44, 0xb4, 0x22, 0x7e, 0x83, 0x96, 0x84, 0xa4, 0xb2, 0x27, + 0xec, 0x9c, 0xd6, 0x3e, 0xfc, 0x00, 0x6d, 0xcd, 0x77, 0xef, 0x19, 0xf5, 0xa5, 0xf8, 0x4c, 0x8c, + 0xee, 0xf6, 0xef, 0x39, 0x54, 0x56, 0xb0, 0x23, 0x16, 0xb6, 0x7c, 0xe9, 0xb3, 0x10, 0x3f, 0x45, + 0x79, 0x39, 0x88, 0x40, 0xbf, 0x55, 0xc9, 0x7d, 0x9c, 0x64, 0x75, 0x31, 0x88, 0xe0, 0x66, 0x58, + 0x5d, 0x9f, 0x00, 0x2b, 0x23, 0xd1, 0x70, 0xfc, 0x69, 0x9a, 0xea, 0xe2, 0x04, 0xd1, 0x04, 0xbc, + 0x19, 0x56, 0xef, 0xa7, 0xb4, 0xc9, 0x1c, 0x70, 0x1b, 0x95, 0xbb, 0x54, 0xc8, 0x3a, 0x67, 0x4d, + 0xb8, 0xf0, 0x03, 0x30, 0x97, 0xfd, 0xe8, 0xfd, 0xca, 0xa4, 0x18, 0xee, 0x86, 0x89, 0x56, 0x3e, + 0xcb, 0x0a, 0x91, 0x49, 0x5d, 0xdc, 0x47, 0x58, 0x19, 0x2e, 0x38, 0x0d, 0x45, 0x9c, 0xbf, 0x8a, + 0x96, 0x9f, 0x3b, 0xda, 0xa6, 0x89, 0x86, 0xcf, 0xa6, 0xd4, 0xc8, 0x1d, 0x11, 0xf0, 0x0e, 0x5a, + 0xe2, 0x40, 0x05, 0x0b, 0xed, 0x82, 0x7e, 0x9b, 0xb4, 0x18, 0x44, 0x5b, 0x89, 0xf1, 0xe2, 0xff, + 0xa3, 0xe5, 0x00, 0x84, 0xa0, 0x6d, 0xb0, 0x97, 0x34, 0xf0, 0xbe, 0x01, 0x2e, 0xbf, 0x8c, 0xcd, + 0x24, 0xf1, 0x6f, 0xff, 0x61, 0xa1, 0xa2, 0x2a, 0xc5, 0x99, 0x2f, 0x24, 0xfe, 0x6e, 0xaa, 0xc5, + 0x9d, 0xf7, 0xbb, 0x8d, 0x62, 0xeb, 0x06, 0x5f, 0x33, 0x81, 0x8a, 0x89, 0x25, 0xd3, 0xde, 0xaf, + 0x51, 0xc1, 0x97, 0x10, 0xa8, 0xc2, 0xe6, 0x76, 0x57, 0x0e, 0x0e, 0xe6, 0xef, 0x41, 0xb7, 0x6c, + 0xe4, 0x0b, 0xa7, 0x4a, 0x88, 0xc4, 0x7a, 0xdb, 0x7f, 0x2e, 0xc7, 0x77, 0x50, 0x0d, 0x8f, 0xcf, + 0x50, 0x99, 0x2b, 0x2a, 0x97, 0x75, 0xd6, 0xf5, 0xbd, 0x81, 0x6e, 0x82, 0x92, 0xbb, 0x93, 0x14, + 0x96, 0x64, 0x9d, 0x37, 0xb7, 0x0d, 0x64, 0x92, 0x8c, 0xdb, 0xe8, 0x91, 0x04, 0x1e, 0xf8, 0x21, + 0x55, 0x45, 0xf8, 0x92, 0x53, 0x0f, 0xea, 0xc0, 0x7d, 0xd6, 0x6a, 0x80, 0xc7, 0xc2, 0x96, 0xd0, + 0x45, 0xcf, 0xb9, 0x8f, 0x47, 0xc3, 0xea, 0xa3, 0x8b, 0x59, 0x40, 0x32, 0x5b, 0x07, 0xbf, 0x42, + 0x1b, 0xd4, 0x93, 0x7e, 0x1f, 0x8e, 0x81, 0xb6, 0xba, 0x7e, 0x08, 0x49, 0x80, 0x82, 0x0e, 0xf0, + 0xbf, 0xd1, 0xb0, 0xba, 0xf1, 0xe2, 0x2e, 0x00, 0xb9, 0x9b, 0x87, 0x7f, 0xb6, 0xd0, 0x6a, 0xc8, + 0x5a, 0xd0, 0x80, 0x2e, 0x78, 0x92, 0x71, 0x7b, 0x59, 0xbf, 0xfa, 0xc9, 0x87, 0x4d, 0x15, 0xe7, + 0x3c, 0x23, 0xf5, 0x45, 0x28, 0xf9, 0xc0, 0x7d, 0x68, 0x5e, 0x74, 0x35, 0xeb, 0x22, 0x13, 0x31, + 0xf1, 0x57, 0x08, 0x0b, 0xe0, 0x7d, 0xdf, 0x83, 0x17, 0x9e, 0xc7, 0x7a, 0xa1, 0x3c, 0xa7, 0x01, + 0xd8, 0x45, 0x5d, 0x91, 0xb4, 0xf9, 0x1b, 0x53, 0x08, 0x72, 0x07, 0x0b, 0x9f, 0xa0, 0x7b, 0x93, + 0x56, 0xbb, 0xa4, 0x75, 0xb6, 0x8c, 0x8e, 0x7d, 0x0c, 0x11, 0x07, 0x4f, 0x8d, 0xee, 0x49, 0x45, + 0x72, 0x8b, 0x87, 0xf7, 0x50, 0x51, 0x65, 0xa9, 0x73, 0x41, 0x5a, 0x23, 0x6d, 0xdb, 0x73, 0x63, + 0x27, 0x29, 0x02, 0x3f, 0x45, 0x2b, 0x97, 0x4c, 0xc8, 0x73, 0x90, 0x3f, 0x32, 0xde, 0xb1, 0x57, + 0xb6, 0xac, 0xdd, 0xa2, 0xfb, 0xc0, 0x10, 0x56, 0x4e, 0xc6, 0x2e, 0x92, 0xc5, 0xa9, 0xdf, 0xa0, + 0x3a, 0xd6, 0x4f, 0x8f, 0xed, 0x55, 0x4d, 0x49, 0x7f, 0x83, 0x27, 0xb1, 0x99, 0x24, 0xfe, 0x04, + 0x7a, 0x5a, 0x3f, 0xb2, 0xcb, 0xd3, 0xd0, 0xd3, 0xfa, 0x11, 0x49, 0xfc, 0x2a, 0x75, 0xf5, 0x19, + 0xaa, 0xd4, 0xd7, 0x26, 0x53, 0x3f, 0x31, 0x76, 0x92, 0x22, 0x70, 0x0d, 0x95, 0x44, 0xaf, 0xd9, + 0x62, 0x01, 0xf5, 0x43, 0x7b, 0x5d, 0xc3, 0xd7, 0x0d, 0xbc, 0xd4, 0x48, 0x1c, 0x64, 0x8c, 0xc1, + 0xcf, 0x51, 0x59, 0xad, 0xc1, 0x56, 0xaf, 0x0b, 0x5c, 0xc7, 0x78, 0xa0, 0x49, 0xe9, 0x54, 0x6c, + 0x24, 0x4e, 0xfd, 0x46, 0x93, 0xd8, 0xcd, 0xcf, 0xd1, 0xfa, 0x54, 0x97, 0xe0, 0x35, 0x94, 0xeb, + 0xc0, 0x20, 0x5e, 0x02, 0x44, 0x7d, 0xe2, 0x87, 0xa8, 0xd0, 0xa7, 0xdd, 0x1e, 0xc4, 0xf3, 0x9d, + 0xc4, 0x87, 0xcf, 0x16, 0x0f, 0xad, 0xed, 0xdf, 0x72, 0x08, 0x8d, 0x57, 0x0d, 0x7e, 0x82, 0x0a, + 0xd1, 0x25, 0x15, 0xc9, 0x06, 0x49, 0xfa, 0xa5, 0x50, 0x57, 0xc6, 0x9b, 0x61, 0xb5, 0xa4, 0xb0, + 0xfa, 0x40, 0x62, 0x20, 0x66, 0x08, 0x79, 0xc9, 0x6e, 0x48, 0xc6, 0xcc, 0xf3, 0xf9, 0x1b, 0x3e, + 0xdd, 0x2f, 0xe3, 0x7d, 0x9d, 0x9a, 0x04, 0xc9, 0x84, 0xc8, 0x0e, 0xda, 0xdc, 0xec, 0x41, 0x9b, + 0x99, 0xdd, 0xf9, 0x99, 0xb3, 0x7b, 0x07, 0x2d, 0xc5, 0xc5, 0xbe, 0x3d, 0xe3, 0xe3, 0x5e, 0x20, + 0xc6, 0xab, 0x70, 0x1e, 0xe5, 0xd1, 0x69, 0xdd, 0x8c, 0xf8, 0x14, 0x77, 0xa4, 0xad, 0xc4, 0x78, + 0xf1, 0x6b, 0x54, 0xd2, 0x03, 0x4d, 0xaf, 0xa8, 0xe5, 0xb9, 0x57, 0x54, 0x59, 0xf7, 0x4a, 0x22, + 0x40, 0xc6, 0x5a, 0x2e, 0xb9, 0xba, 0xae, 0x2c, 0xbc, 0xbd, 0xae, 0x2c, 0xbc, 0xbb, 0xae, 0x2c, + 0xfc, 0x34, 0xaa, 0x58, 0x57, 0xa3, 0x8a, 0xf5, 0x76, 0x54, 0xb1, 0xde, 0x8d, 0x2a, 0xd6, 0x5f, + 0xa3, 0x8a, 0xf5, 0xcb, 0xdf, 0x95, 0x85, 0x6f, 0xf7, 0xe6, 0xf9, 0xe3, 0xf9, 0x4f, 0x00, 0x00, + 0x00, 0xff, 0xff, 0x1e, 0x86, 0x1f, 0x63, 0xc0, 0x0a, 0x00, 0x00, } func (m *Carp) Marshal() (dAtA []byte, err error) { diff --git a/pkg/apis/testapigroup/v1/generated.proto b/pkg/apis/testapigroup/v1/generated.proto index e2fab00d3..bfdfb4dbb 100644 --- a/pkg/apis/testapigroup/v1/generated.proto +++ b/pkg/apis/testapigroup/v1/generated.proto @@ -26,7 +26,7 @@ import "k8s.io/apimachinery/pkg/runtime/generated.proto"; import "k8s.io/apimachinery/pkg/runtime/schema/generated.proto"; // Package-wide variables from generator "generated". -option go_package = "v1"; +option go_package = "k8s.io/apimachinery/pkg/apis/testapigroup/v1"; // Carp is a collection of containers, used as either input (create, update) or as output (list, get). message Carp { diff --git a/pkg/apis/testapigroup/v1/zz_generated.conversion.go b/pkg/apis/testapigroup/v1/zz_generated.conversion.go index 1df2cb338..22b1c276e 100644 --- a/pkg/apis/testapigroup/v1/zz_generated.conversion.go +++ b/pkg/apis/testapigroup/v1/zz_generated.conversion.go @@ -1,3 +1,4 @@ +//go:build !ignore_autogenerated // +build !ignore_autogenerated /* diff --git a/pkg/apis/testapigroup/v1/zz_generated.deepcopy.go b/pkg/apis/testapigroup/v1/zz_generated.deepcopy.go index af9d00949..36a3da3bd 100644 --- a/pkg/apis/testapigroup/v1/zz_generated.deepcopy.go +++ b/pkg/apis/testapigroup/v1/zz_generated.deepcopy.go @@ -1,3 +1,4 @@ +//go:build !ignore_autogenerated // +build !ignore_autogenerated /* diff --git a/pkg/apis/testapigroup/v1/zz_generated.defaults.go b/pkg/apis/testapigroup/v1/zz_generated.defaults.go index cce2e603a..dac177e93 100644 --- a/pkg/apis/testapigroup/v1/zz_generated.defaults.go +++ b/pkg/apis/testapigroup/v1/zz_generated.defaults.go @@ -1,3 +1,4 @@ +//go:build !ignore_autogenerated // +build !ignore_autogenerated /* diff --git a/pkg/apis/testapigroup/zz_generated.deepcopy.go b/pkg/apis/testapigroup/zz_generated.deepcopy.go index e80ccfa90..8536973cd 100644 --- a/pkg/apis/testapigroup/zz_generated.deepcopy.go +++ b/pkg/apis/testapigroup/zz_generated.deepcopy.go @@ -1,3 +1,4 @@ +//go:build !ignore_autogenerated // +build !ignore_autogenerated /* diff --git a/pkg/conversion/converter.go b/pkg/conversion/converter.go index 791348476..76b76247c 100644 --- a/pkg/conversion/converter.go +++ b/pkg/conversion/converter.go @@ -44,23 +44,16 @@ type Converter struct { generatedConversionFuncs ConversionFuncs // Set of conversions that should be treated as a no-op - ignoredConversions map[typePair]struct{} ignoredUntypedConversions map[typePair]struct{} - - // nameFunc is called to retrieve the name of a type; this name is used for the - // purpose of deciding whether two types match or not (i.e., will we attempt to - // do a conversion). The default returns the go type name. - nameFunc func(t reflect.Type) string } // NewConverter creates a new Converter object. -func NewConverter(nameFn NameFunc) *Converter { +// Arg NameFunc is just for backward compatibility. +func NewConverter(NameFunc) *Converter { c := &Converter{ conversionFuncs: NewConversionFuncs(), generatedConversionFuncs: NewConversionFuncs(), - ignoredConversions: make(map[typePair]struct{}), ignoredUntypedConversions: make(map[typePair]struct{}), - nameFunc: nameFn, } c.RegisterUntypedConversionFunc( (*[]byte)(nil), (*[]byte)(nil), @@ -122,10 +115,10 @@ type ConversionFuncs struct { // previously defined functions. func (c ConversionFuncs) AddUntyped(a, b interface{}, fn ConversionFunc) error { tA, tB := reflect.TypeOf(a), reflect.TypeOf(b) - if tA.Kind() != reflect.Ptr { + if tA.Kind() != reflect.Pointer { return fmt.Errorf("the type %T must be a pointer to register as an untyped conversion", a) } - if tB.Kind() != reflect.Ptr { + if tB.Kind() != reflect.Pointer { return fmt.Errorf("the type %T must be a pointer to register as an untyped conversion", b) } c.untyped[typePair{tA, tB}] = fn @@ -186,13 +179,12 @@ func (c *Converter) RegisterGeneratedUntypedConversionFunc(a, b interface{}, fn func (c *Converter) RegisterIgnoredConversion(from, to interface{}) error { typeFrom := reflect.TypeOf(from) typeTo := reflect.TypeOf(to) - if reflect.TypeOf(from).Kind() != reflect.Ptr { + if typeFrom.Kind() != reflect.Pointer { return fmt.Errorf("expected pointer arg for 'from' param 0, got: %v", typeFrom) } - if typeTo.Kind() != reflect.Ptr { + if typeTo.Kind() != reflect.Pointer { return fmt.Errorf("expected pointer arg for 'to' param 1, got: %v", typeTo) } - c.ignoredConversions[typePair{typeFrom.Elem(), typeTo.Elem()}] = struct{}{} c.ignoredUntypedConversions[typePair{typeFrom, typeTo}] = struct{}{} return nil } diff --git a/pkg/conversion/converter_test.go b/pkg/conversion/converter_test.go index 8bcdb9f9b..584068772 100644 --- a/pkg/conversion/converter_test.go +++ b/pkg/conversion/converter_test.go @@ -24,7 +24,7 @@ import ( ) func TestConverter_byteSlice(t *testing.T) { - c := NewConverter(DefaultNameFunc) + c := NewConverter(nil) src := []byte{1, 2, 3} dest := []byte{} err := c.Convert(&src, &dest, nil) @@ -37,7 +37,7 @@ func TestConverter_byteSlice(t *testing.T) { } func TestConverter_MismatchedTypes(t *testing.T) { - c := NewConverter(DefaultNameFunc) + c := NewConverter(nil) convertFn := func(in *[]string, out *int, s Scope) error { if str, err := strconv.Atoi((*in)[0]); err != nil { @@ -76,7 +76,7 @@ func TestConverter_CallsRegisteredFunctions(t *testing.T) { Baz int } type C struct{} - c := NewConverter(DefaultNameFunc) + c := NewConverter(nil) convertFn1 := func(in *A, out *B, s Scope) error { out.Bar = in.Foo out.Baz = in.Baz @@ -151,7 +151,7 @@ func TestConverter_IgnoredConversion(t *testing.T) { type B struct{} count := 0 - c := NewConverter(DefaultNameFunc) + c := NewConverter(nil) convertFn := func(in *A, out *B, s Scope) error { count++ return nil @@ -180,7 +180,7 @@ func TestConverter_IgnoredConversion(t *testing.T) { func TestConverter_GeneratedConversionOverridden(t *testing.T) { type A struct{} type B struct{} - c := NewConverter(DefaultNameFunc) + c := NewConverter(nil) convertFn1 := func(in *A, out *B, s Scope) error { return nil } @@ -214,7 +214,7 @@ func TestConverter_GeneratedConversionOverridden(t *testing.T) { func TestConverter_WithConversionOverridden(t *testing.T) { type A struct{} type B struct{} - c := NewConverter(DefaultNameFunc) + c := NewConverter(nil) convertFn1 := func(in *A, out *B, s Scope) error { return fmt.Errorf("conversion function should be overridden") } @@ -260,7 +260,7 @@ func TestConverter_WithConversionOverridden(t *testing.T) { func TestConverter_meta(t *testing.T) { type Foo struct{ A string } type Bar struct{ A string } - c := NewConverter(DefaultNameFunc) + c := NewConverter(nil) checks := 0 convertFn1 := func(in *Foo, out *Bar, s Scope) error { if s.Meta() == nil { diff --git a/pkg/conversion/deep_equal.go b/pkg/conversion/deep_equal.go index f21abe1e5..25b2923f2 100644 --- a/pkg/conversion/deep_equal.go +++ b/pkg/conversion/deep_equal.go @@ -34,3 +34,14 @@ func EqualitiesOrDie(funcs ...interface{}) Equalities { } return e } + +// Performs a shallow copy of the equalities map +func (e Equalities) Copy() Equalities { + result := Equalities{reflect.Equalities{}} + + for key, value := range e.Equalities { + result.Equalities[key] = value + } + + return result +} diff --git a/pkg/conversion/helper.go b/pkg/conversion/helper.go index 4ebc1ebc5..7fadd27a4 100644 --- a/pkg/conversion/helper.go +++ b/pkg/conversion/helper.go @@ -26,7 +26,7 @@ import ( // Returns an error if this is not possible. func EnforcePtr(obj interface{}) (reflect.Value, error) { v := reflect.ValueOf(obj) - if v.Kind() != reflect.Ptr { + if v.Kind() != reflect.Pointer { if v.Kind() == reflect.Invalid { return reflect.Value{}, fmt.Errorf("expected pointer, but got invalid kind") } diff --git a/pkg/conversion/queryparams/convert.go b/pkg/conversion/queryparams/convert.go index 2f0dd0074..b0a9246d9 100644 --- a/pkg/conversion/queryparams/convert.go +++ b/pkg/conversion/queryparams/convert.go @@ -55,7 +55,7 @@ func jsonTag(field reflect.StructField) (string, bool) { } func isPointerKind(kind reflect.Kind) bool { - return kind == reflect.Ptr + return kind == reflect.Pointer } func isStructKind(kind reflect.Kind) bool { @@ -139,7 +139,7 @@ func Convert(obj interface{}) (url.Values, error) { } var sv reflect.Value switch reflect.TypeOf(obj).Kind() { - case reflect.Ptr, reflect.Interface: + case reflect.Pointer, reflect.Interface: sv = reflect.ValueOf(obj).Elem() default: return nil, fmt.Errorf("expecting a pointer or interface") diff --git a/pkg/labels/selector.go b/pkg/labels/selector.go index 9eea34579..891004389 100644 --- a/pkg/labels/selector.go +++ b/pkg/labels/selector.go @@ -22,12 +22,12 @@ import ( "strconv" "strings" - "github.com/google/go-cmp/cmp" "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" + stringslices "k8s.io/utils/strings/slices" ) var ( @@ -74,9 +74,12 @@ type Selector interface { RequiresExactMatch(label string) (value string, found bool) } +// Sharing this saves 1 alloc per use; this is safe because it's immutable. +var sharedEverythingSelector Selector = internalSelector{} + // Everything returns a selector that matches all labels. func Everything() Selector { - return internalSelector{} + return sharedEverythingSelector } type nothingSelector struct{} @@ -91,9 +94,12 @@ func (n nothingSelector) RequiresExactMatch(label string) (value string, found b return "", false } +// Sharing this saves 1 alloc per use; this is safe because it's immutable. +var sharedNothingSelector Selector = nothingSelector{} + // Nothing returns a selector that matches no labels func Nothing() Selector { - return nothingSelector{} + return sharedNothingSelector } // NewSelector returns a nil selector @@ -149,7 +155,8 @@ type Requirement struct { // (4) If the operator is Exists or DoesNotExist, the value set must be empty. // (5) If the operator is Gt or Lt, the values set must contain only one value, which will be interpreted as an integer. // (6) The key is invalid due to its length, or sequence -// of characters. See validateLabelKey for more details. +// +// of characters. See validateLabelKey for more details. // // The empty string is a valid value in the input values set. // Returned error, if not nil, is guaranteed to be an aggregated field.ErrorList @@ -208,13 +215,20 @@ func (r *Requirement) hasValue(value string) bool { // There is a match in the following cases: // (1) The operator is Exists and Labels has the Requirement's key. // (2) The operator is In, Labels has the Requirement's key and Labels' -// value for that key is in Requirement's value set. +// +// value for that key is in Requirement's value set. +// // (3) The operator is NotIn, Labels has the Requirement's key and -// Labels' value for that key is not in Requirement's value set. +// +// Labels' value for that key is not in Requirement's value set. +// // (4) The operator is DoesNotExist or NotIn and Labels does not have the -// Requirement's key. +// +// Requirement's key. +// // (5) The operator is GreaterThanOperator or LessThanOperator, and Labels has -// the Requirement's key and the corresponding value satisfies mathematical inequality. +// +// the Requirement's key and the corresponding value satisfies mathematical inequality. func (r *Requirement) Matches(ls Labels) bool { switch r.operator { case selection.In, selection.Equals, selection.DoubleEquals: @@ -288,7 +302,7 @@ func (r Requirement) Equal(x Requirement) bool { if r.operator != x.operator { return false } - return cmp.Equal(r.strValues, x.strValues) + return stringslices.Equal(r.strValues, x.strValues) } // Empty returns true if the internalSelector doesn't restrict selection space @@ -840,32 +854,33 @@ func (p *Parser) parseExactValue() (sets.String, error) { // as they parse different selectors with different syntaxes. // The input will cause an error if it does not follow this form: // -// ::= | "," -// ::= [!] KEY [ | ] -// ::= "" | -// ::= | -// ::= "notin" -// ::= "in" -// ::= "(" ")" -// ::= VALUE | VALUE "," -// ::= ["="|"=="|"!="] VALUE +// ::= | "," +// ::= [!] KEY [ | ] +// ::= "" | +// ::= | +// ::= "notin" +// ::= "in" +// ::= "(" ")" +// ::= VALUE | VALUE "," +// ::= ["="|"=="|"!="] VALUE // // KEY is a sequence of one or more characters following [ DNS_SUBDOMAIN "/" ] DNS_LABEL. Max length is 63 characters. // VALUE is a sequence of zero or more characters "([A-Za-z0-9_-\.])". Max length is 63 characters. // Delimiter is white space: (' ', '\t') // Example of valid syntax: -// "x in (foo,,baz),y,z notin ()" +// +// "x in (foo,,baz),y,z notin ()" // // Note: -// (1) Inclusion - " in " - denotes that the KEY exists and is equal to any of the -// VALUEs in its requirement -// (2) Exclusion - " notin " - denotes that the KEY is not equal to any -// of the VALUEs in its requirement or does not exist -// (3) The empty string is a valid VALUE -// (4) A requirement with just a KEY - as in "y" above - denotes that -// the KEY exists and can be any VALUE. -// (5) A requirement with just !KEY requires that the KEY not exist. // +// (1) Inclusion - " in " - denotes that the KEY exists and is equal to any of the +// VALUEs in its requirement +// (2) Exclusion - " notin " - denotes that the KEY is not equal to any +// of the VALUEs in its requirement or does not exist +// (3) The empty string is a valid VALUE +// (4) A requirement with just a KEY - as in "y" above - denotes that +// the KEY exists and can be any VALUE. +// (5) A requirement with just !KEY requires that the KEY not exist. func Parse(selector string, opts ...field.PathOption) (Selector, error) { parsedSelector, err := parse(selector, field.ToPath(opts...)) if err == nil { diff --git a/pkg/labels/selector_test.go b/pkg/labels/selector_test.go index 7c061462d..9d2730284 100644 --- a/pkg/labels/selector_test.go +++ b/pkg/labels/selector_test.go @@ -148,8 +148,7 @@ func expectMatchDirect(t *testing.T, selector, ls Set) { } } -//lint:ignore U1000 currently commented out in TODO of TestSetMatches -//nolint:unused,deadcode +//nolint:staticcheck,unused //iccheck // U1000 currently commented out in TODO of TestSetMatches func expectNoMatchDirect(t *testing.T, selector, ls Set) { if SelectorFromSet(selector).Matches(ls) { t.Errorf("Wanted '%s' to not match '%s', but it did.", selector, ls) diff --git a/pkg/labels/zz_generated.deepcopy.go b/pkg/labels/zz_generated.deepcopy.go index 4d482947f..fdf4c31e1 100644 --- a/pkg/labels/zz_generated.deepcopy.go +++ b/pkg/labels/zz_generated.deepcopy.go @@ -1,3 +1,4 @@ +//go:build !ignore_autogenerated // +build !ignore_autogenerated /* diff --git a/pkg/runtime/allocator.go b/pkg/runtime/allocator.go new file mode 100644 index 000000000..8bf22ae8a --- /dev/null +++ b/pkg/runtime/allocator.go @@ -0,0 +1,76 @@ +/* +Copyright 2022 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 runtime + +import ( + "sync" +) + +// AllocatorPool simply stores Allocator objects to avoid additional memory allocations +// by caching created but unused items for later reuse, relieving pressure on the garbage collector. +// +// Usage: +// +// memoryAllocator := runtime.AllocatorPool.Get().(*runtime.Allocator) +// defer runtime.AllocatorPool.Put(memoryAllocator) +// +// A note for future: +// +// consider introducing multiple pools for storing buffers of different sizes +// perhaps this could allow us to be more efficient. +var AllocatorPool = sync.Pool{ + New: func() interface{} { + return &Allocator{} + }, +} + +// Allocator knows how to allocate memory +// It exists to make the cost of object serialization cheaper. +// In some cases, it allows for allocating memory only once and then reusing it. +// This approach puts less load on GC and leads to less fragmented memory in general. +type Allocator struct { + buf []byte +} + +var _ MemoryAllocator = &Allocator{} + +// Allocate reserves memory for n bytes only if the underlying array doesn't have enough capacity +// otherwise it returns previously allocated block of memory. +// +// Note that the returned array is not zeroed, it is the caller's +// responsibility to clean the memory if needed. +func (a *Allocator) Allocate(n uint64) []byte { + if uint64(cap(a.buf)) >= n { + a.buf = a.buf[:n] + return a.buf + } + // grow the buffer + size := uint64(2*cap(a.buf)) + n + a.buf = make([]byte, size) + a.buf = a.buf[:n] + return a.buf +} + +// SimpleAllocator a wrapper around make([]byte) +// conforms to the MemoryAllocator interface +type SimpleAllocator struct{} + +var _ MemoryAllocator = &SimpleAllocator{} + +func (sa *SimpleAllocator) Allocate(n uint64) []byte { + return make([]byte, n) +} diff --git a/pkg/runtime/allocator_test.go b/pkg/runtime/allocator_test.go new file mode 100644 index 000000000..067a5dda5 --- /dev/null +++ b/pkg/runtime/allocator_test.go @@ -0,0 +1,78 @@ +/* +Copyright 2022 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 runtime + +import ( + "math/rand" + "testing" +) + +func TestAllocatorRandomInputs(t *testing.T) { + maxBytes := 5 * 1000000 // 5 MB + iterations := rand.Intn(10000) + 10 + target := &Allocator{} + + for i := 0; i < iterations; i++ { + bytesToAllocate := rand.Intn(maxBytes) + buff := target.Allocate(uint64(bytesToAllocate)) + if cap(buff) < bytesToAllocate { + t.Fatalf("expected the buffer to allocate: %v bytes whereas it allocated: %v bytes", bytesToAllocate, cap(buff)) + } + if len(buff) != bytesToAllocate { + t.Fatalf("unexpected length of the buffer, expected: %v, got: %v", bytesToAllocate, len(buff)) + } + } +} + +func TestAllocatorNeverShrinks(t *testing.T) { + target := &Allocator{} + initialSize := 1000000 // 1MB + initialBuff := target.Allocate(uint64(initialSize)) + if cap(initialBuff) < initialSize { + t.Fatalf("unexpected size of the buffer, expected at least 1MB, got: %v", cap(initialBuff)) + } + + for i := initialSize; i > 0; i = i / 10 { + newBuff := target.Allocate(uint64(i)) + if cap(newBuff) < initialSize { + t.Fatalf("allocator is now allowed to shrink memory") + } + if len(newBuff) != i { + t.Fatalf("unexpected length of the buffer, expected: %v, got: %v", i, len(newBuff)) + } + } +} + +func TestAllocatorZero(t *testing.T) { + target := &Allocator{} + initialSize := 1000000 // 1MB + buff := target.Allocate(uint64(initialSize)) + if cap(buff) < initialSize { + t.Fatalf("unexpected size of the buffer, expected at least 1MB, got: %v", cap(buff)) + } + if len(buff) != initialSize { + t.Fatalf("unexpected length of the buffer, expected: %v, got: %v", initialSize, len(buff)) + } + + buff = target.Allocate(0) + if cap(buff) < initialSize { + t.Fatalf("unexpected size of the buffer, expected at least 1MB, got: %v", cap(buff)) + } + if len(buff) != 0 { + t.Fatalf("unexpected length of the buffer, expected: 0, got: %v", len(buff)) + } +} diff --git a/pkg/runtime/codec.go b/pkg/runtime/codec.go index a92863139..7fc513dd0 100644 --- a/pkg/runtime/codec.go +++ b/pkg/runtime/codec.go @@ -344,14 +344,15 @@ func NewMultiGroupVersioner(gv schema.GroupVersion, groupKinds ...schema.GroupKi // Incoming kinds that match the provided groupKinds are preferred. // Kind may be empty in the provided group kind, in which case any kind will match. // Examples: -// gv=mygroup/__internal, groupKinds=mygroup/Foo, anothergroup/Bar -// KindForGroupVersionKinds(yetanother/v1/Baz, anothergroup/v1/Bar) -> mygroup/__internal/Bar (matched preferred group/kind) // -// gv=mygroup/__internal, groupKinds=mygroup, anothergroup -// KindForGroupVersionKinds(yetanother/v1/Baz, anothergroup/v1/Bar) -> mygroup/__internal/Bar (matched preferred group) +// gv=mygroup/__internal, groupKinds=mygroup/Foo, anothergroup/Bar +// KindForGroupVersionKinds(yetanother/v1/Baz, anothergroup/v1/Bar) -> mygroup/__internal/Bar (matched preferred group/kind) // -// gv=mygroup/__internal, groupKinds=mygroup, anothergroup -// KindForGroupVersionKinds(yetanother/v1/Baz, yetanother/v1/Bar) -> mygroup/__internal/Baz (no preferred group/kind match, uses first kind in list) +// gv=mygroup/__internal, groupKinds=mygroup, anothergroup +// KindForGroupVersionKinds(yetanother/v1/Baz, anothergroup/v1/Bar) -> mygroup/__internal/Bar (matched preferred group) +// +// gv=mygroup/__internal, groupKinds=mygroup, anothergroup +// KindForGroupVersionKinds(yetanother/v1/Baz, yetanother/v1/Bar) -> mygroup/__internal/Baz (no preferred group/kind match, uses first kind in list) func NewCoercingMultiGroupVersioner(gv schema.GroupVersion, groupKinds ...schema.GroupKind) GroupVersioner { return multiGroupVersioner{target: gv, acceptedGroupKinds: groupKinds, coerce: true} } diff --git a/pkg/runtime/codec_check.go b/pkg/runtime/codec_check.go index 000228061..e88400776 100644 --- a/pkg/runtime/codec_check.go +++ b/pkg/runtime/codec_check.go @@ -30,7 +30,7 @@ import ( // TODO: verify that the correct external version is chosen on encode... func CheckCodec(c Codec, internalType Object, externalTypes ...schema.GroupVersionKind) error { if _, err := Encode(c, internalType); err != nil { - return fmt.Errorf("Internal type not encodable: %v", err) + return fmt.Errorf("internal type not encodable: %v", err) } for _, et := range externalTypes { typeMeta := TypeMeta{ diff --git a/pkg/runtime/converter.go b/pkg/runtime/converter.go index 4a6cc6857..90bf487e3 100644 --- a/pkg/runtime/converter.go +++ b/pkg/runtime/converter.go @@ -22,6 +22,7 @@ import ( "math" "os" "reflect" + "sort" "strconv" "strings" "sync" @@ -109,21 +110,141 @@ type unstructuredConverter struct { // to Go types via reflection. It performs mismatch detection automatically and is intended for use by external // test tools. Use DefaultUnstructuredConverter if you do not explicitly need mismatch detection. func NewTestUnstructuredConverter(comparison conversion.Equalities) UnstructuredConverter { + return NewTestUnstructuredConverterWithValidation(comparison) +} + +// NewTestUnstrucutredConverterWithValidation allows for access to +// FromUnstructuredWithValidation from within tests. +func NewTestUnstructuredConverterWithValidation(comparison conversion.Equalities) *unstructuredConverter { return &unstructuredConverter{ mismatchDetection: true, comparison: comparison, } } -// FromUnstructured converts an object from map[string]interface{} representation into a concrete type. +// fromUnstructuredContext provides options for informing the converter +// the state of its recursive walk through the conversion process. +type fromUnstructuredContext struct { + // isInlined indicates whether the converter is currently in + // an inlined field or not to determine whether it should + // validate the matchedKeys yet or only collect them. + // This should only be set from `structFromUnstructured` + isInlined bool + // matchedKeys is a stack of the set of all fields that exist in the + // concrete go type of the object being converted into. + // This should only be manipulated via `pushMatchedKeyTracker`, + // `recordMatchedKey`, or `popAndVerifyMatchedKeys` + matchedKeys []map[string]struct{} + // parentPath collects the path that the conversion + // takes as it traverses the unstructured json map. + // It is used to report the full path to any unknown + // fields that the converter encounters. + parentPath []string + // returnUnknownFields indicates whether or not + // unknown field errors should be collected and + // returned to the caller + returnUnknownFields bool + // unknownFieldErrors are the collection of + // the full path to each unknown field in the + // object. + unknownFieldErrors []error +} + +// pushMatchedKeyTracker adds a placeholder set for tracking +// matched keys for the given level. This should only be +// called from `structFromUnstructured`. +func (c *fromUnstructuredContext) pushMatchedKeyTracker() { + if !c.returnUnknownFields { + return + } + + c.matchedKeys = append(c.matchedKeys, nil) +} + +// recordMatchedKey initializes the last element of matchedKeys +// (if needed) and sets 'key'. This should only be called from +// `structFromUnstructured`. +func (c *fromUnstructuredContext) recordMatchedKey(key string) { + if !c.returnUnknownFields { + return + } + + last := len(c.matchedKeys) - 1 + if c.matchedKeys[last] == nil { + c.matchedKeys[last] = map[string]struct{}{} + } + c.matchedKeys[last][key] = struct{}{} +} + +// popAndVerifyMatchedKeys pops the last element of matchedKeys, +// checks the matched keys against the data, and adds unknown +// field errors for any matched keys. +// `mapValue` is the value of sv containing all of the keys that exist at this level +// (ie. sv.MapKeys) in the source data. +// `matchedKeys` are all the keys found for that level in the destination object. +// This should only be called from `structFromUnstructured`. +func (c *fromUnstructuredContext) popAndVerifyMatchedKeys(mapValue reflect.Value) { + if !c.returnUnknownFields { + return + } + + last := len(c.matchedKeys) - 1 + curMatchedKeys := c.matchedKeys[last] + c.matchedKeys[last] = nil + c.matchedKeys = c.matchedKeys[:last] + for _, key := range mapValue.MapKeys() { + if _, ok := curMatchedKeys[key.String()]; !ok { + c.recordUnknownField(key.String()) + } + } +} + +func (c *fromUnstructuredContext) recordUnknownField(field string) { + if !c.returnUnknownFields { + return + } + + pathLen := len(c.parentPath) + c.pushKey(field) + errPath := strings.Join(c.parentPath, "") + c.parentPath = c.parentPath[:pathLen] + c.unknownFieldErrors = append(c.unknownFieldErrors, fmt.Errorf(`unknown field "%s"`, errPath)) +} + +func (c *fromUnstructuredContext) pushIndex(index int) { + if !c.returnUnknownFields { + return + } + + c.parentPath = append(c.parentPath, "[", strconv.Itoa(index), "]") +} + +func (c *fromUnstructuredContext) pushKey(key string) { + if !c.returnUnknownFields { + return + } + + if len(c.parentPath) > 0 { + c.parentPath = append(c.parentPath, ".") + } + c.parentPath = append(c.parentPath, key) + +} + +// FromUnstructuredWIthValidation converts an object from map[string]interface{} representation into a concrete type. // It uses encoding/json/Unmarshaler if object implements it or reflection if not. -func (c *unstructuredConverter) FromUnstructured(u map[string]interface{}, obj interface{}) error { +// It takes a validationDirective that indicates how to behave when it encounters unknown fields. +func (c *unstructuredConverter) FromUnstructuredWithValidation(u map[string]interface{}, obj interface{}, returnUnknownFields bool) error { t := reflect.TypeOf(obj) value := reflect.ValueOf(obj) - if t.Kind() != reflect.Ptr || value.IsNil() { + if t.Kind() != reflect.Pointer || value.IsNil() { return fmt.Errorf("FromUnstructured requires a non-nil pointer to an object, got %v", t) } - err := fromUnstructured(reflect.ValueOf(u), value.Elem()) + + fromUnstructuredContext := &fromUnstructuredContext{ + returnUnknownFields: returnUnknownFields, + } + err := fromUnstructured(reflect.ValueOf(u), value.Elem(), fromUnstructuredContext) if c.mismatchDetection { newObj := reflect.New(t.Elem()).Interface() newErr := fromUnstructuredViaJSON(u, newObj) @@ -134,7 +255,23 @@ func (c *unstructuredConverter) FromUnstructured(u map[string]interface{}, obj i klog.Fatalf("FromUnstructured mismatch\nobj1: %#v\nobj2: %#v", obj, newObj) } } - return err + if err != nil { + return err + } + if returnUnknownFields && len(fromUnstructuredContext.unknownFieldErrors) > 0 { + sort.Slice(fromUnstructuredContext.unknownFieldErrors, func(i, j int) bool { + return fromUnstructuredContext.unknownFieldErrors[i].Error() < + fromUnstructuredContext.unknownFieldErrors[j].Error() + }) + return NewStrictDecodingError(fromUnstructuredContext.unknownFieldErrors) + } + return nil +} + +// FromUnstructured converts an object from map[string]interface{} representation into a concrete type. +// It uses encoding/json/Unmarshaler if object implements it or reflection if not. +func (c *unstructuredConverter) FromUnstructured(u map[string]interface{}, obj interface{}) error { + return c.FromUnstructuredWithValidation(u, obj, false) } func fromUnstructuredViaJSON(u map[string]interface{}, obj interface{}) error { @@ -145,7 +282,7 @@ func fromUnstructuredViaJSON(u map[string]interface{}, obj interface{}) error { return json.Unmarshal(data, obj) } -func fromUnstructured(sv, dv reflect.Value) error { +func fromUnstructured(sv, dv reflect.Value, ctx *fromUnstructuredContext) error { sv = unwrapInterface(sv) if !sv.IsValid() { dv.Set(reflect.Zero(dv.Type())) @@ -154,7 +291,7 @@ func fromUnstructured(sv, dv reflect.Value) error { st, dt := sv.Type(), dv.Type() switch dt.Kind() { - case reflect.Map, reflect.Slice, reflect.Ptr, reflect.Struct, reflect.Interface: + case reflect.Map, reflect.Slice, reflect.Pointer, reflect.Struct, reflect.Interface: // Those require non-trivial conversion. default: // This should handle all simple types. @@ -213,18 +350,19 @@ func fromUnstructured(sv, dv reflect.Value) error { switch dt.Kind() { case reflect.Map: - return mapFromUnstructured(sv, dv) + return mapFromUnstructured(sv, dv, ctx) case reflect.Slice: - return sliceFromUnstructured(sv, dv) - case reflect.Ptr: - return pointerFromUnstructured(sv, dv) + return sliceFromUnstructured(sv, dv, ctx) + case reflect.Pointer: + return pointerFromUnstructured(sv, dv, ctx) case reflect.Struct: - return structFromUnstructured(sv, dv) + return structFromUnstructured(sv, dv, ctx) case reflect.Interface: return interfaceFromUnstructured(sv, dv) default: return fmt.Errorf("unrecognized type: %v", dt.Kind()) } + } func fieldInfoFromField(structType reflect.Type, field int) *fieldInfo { @@ -275,7 +413,7 @@ func unwrapInterface(v reflect.Value) reflect.Value { return v } -func mapFromUnstructured(sv, dv reflect.Value) error { +func mapFromUnstructured(sv, dv reflect.Value, ctx *fromUnstructuredContext) error { st, dt := sv.Type(), dv.Type() if st.Kind() != reflect.Map { return fmt.Errorf("cannot restore map from %v", st.Kind()) @@ -293,7 +431,7 @@ func mapFromUnstructured(sv, dv reflect.Value) error { for _, key := range sv.MapKeys() { value := reflect.New(dt.Elem()).Elem() if val := unwrapInterface(sv.MapIndex(key)); val.IsValid() { - if err := fromUnstructured(val, value); err != nil { + if err := fromUnstructured(val, value, ctx); err != nil { return err } } else { @@ -308,7 +446,7 @@ func mapFromUnstructured(sv, dv reflect.Value) error { return nil } -func sliceFromUnstructured(sv, dv reflect.Value) error { +func sliceFromUnstructured(sv, dv reflect.Value, ctx *fromUnstructuredContext) error { st, dt := sv.Type(), dv.Type() if st.Kind() == reflect.String && dt.Elem().Kind() == reflect.Uint8 { // We store original []byte representation as string. @@ -340,56 +478,88 @@ func sliceFromUnstructured(sv, dv reflect.Value) error { return nil } dv.Set(reflect.MakeSlice(dt, sv.Len(), sv.Cap())) + + pathLen := len(ctx.parentPath) + defer func() { + ctx.parentPath = ctx.parentPath[:pathLen] + }() for i := 0; i < sv.Len(); i++ { - if err := fromUnstructured(sv.Index(i), dv.Index(i)); err != nil { + ctx.pushIndex(i) + if err := fromUnstructured(sv.Index(i), dv.Index(i), ctx); err != nil { return err } + ctx.parentPath = ctx.parentPath[:pathLen] } return nil } -func pointerFromUnstructured(sv, dv reflect.Value) error { +func pointerFromUnstructured(sv, dv reflect.Value, ctx *fromUnstructuredContext) error { st, dt := sv.Type(), dv.Type() - if st.Kind() == reflect.Ptr && sv.IsNil() { + if st.Kind() == reflect.Pointer && sv.IsNil() { dv.Set(reflect.Zero(dt)) return nil } dv.Set(reflect.New(dt.Elem())) switch st.Kind() { - case reflect.Ptr, reflect.Interface: - return fromUnstructured(sv.Elem(), dv.Elem()) + case reflect.Pointer, reflect.Interface: + return fromUnstructured(sv.Elem(), dv.Elem(), ctx) default: - return fromUnstructured(sv, dv.Elem()) + return fromUnstructured(sv, dv.Elem(), ctx) } } -func structFromUnstructured(sv, dv reflect.Value) error { +func structFromUnstructured(sv, dv reflect.Value, ctx *fromUnstructuredContext) error { st, dt := sv.Type(), dv.Type() if st.Kind() != reflect.Map { return fmt.Errorf("cannot restore struct from: %v", st.Kind()) } + pathLen := len(ctx.parentPath) + svInlined := ctx.isInlined + defer func() { + ctx.parentPath = ctx.parentPath[:pathLen] + ctx.isInlined = svInlined + }() + if !svInlined { + ctx.pushMatchedKeyTracker() + } for i := 0; i < dt.NumField(); i++ { fieldInfo := fieldInfoFromField(dt, i) fv := dv.Field(i) if len(fieldInfo.name) == 0 { - // This field is inlined. - if err := fromUnstructured(sv, fv); err != nil { + // This field is inlined, recurse into fromUnstructured again + // with the same set of matched keys. + ctx.isInlined = true + if err := fromUnstructured(sv, fv, ctx); err != nil { return err } + ctx.isInlined = svInlined } else { + // This field is not inlined so we recurse into + // child field of sv corresponding to field i of + // dv, with a new set of matchedKeys and updating + // the parentPath to indicate that we are one level + // deeper. + ctx.recordMatchedKey(fieldInfo.name) value := unwrapInterface(sv.MapIndex(fieldInfo.nameValue)) if value.IsValid() { - if err := fromUnstructured(value, fv); err != nil { + ctx.isInlined = false + ctx.pushKey(fieldInfo.name) + if err := fromUnstructured(value, fv, ctx); err != nil { return err } + ctx.parentPath = ctx.parentPath[:pathLen] + ctx.isInlined = svInlined } else { fv.Set(reflect.Zero(fv.Type())) } } } + if !svInlined { + ctx.popAndVerifyMatchedKeys(sv) + } return nil } @@ -409,7 +579,7 @@ func (c *unstructuredConverter) ToUnstructured(obj interface{}) (map[string]inte } else { t := reflect.TypeOf(obj) value := reflect.ValueOf(obj) - if t.Kind() != reflect.Ptr || value.IsNil() { + if t.Kind() != reflect.Pointer || value.IsNil() { return nil, fmt.Errorf("ToUnstructured requires a non-nil pointer to an object, got %v", t) } u = map[string]interface{}{} @@ -516,7 +686,7 @@ func toUnstructured(sv, dv reflect.Value) error { return mapToUnstructured(sv, dv) case reflect.Slice: return sliceToUnstructured(sv, dv) - case reflect.Ptr: + case reflect.Pointer: return pointerToUnstructured(sv, dv) case reflect.Struct: return structToUnstructured(sv, dv) @@ -535,24 +705,13 @@ func mapToUnstructured(sv, dv reflect.Value) error { } if dt.Kind() == reflect.Interface && dv.NumMethod() == 0 { if st.Key().Kind() == reflect.String { - switch st.Elem().Kind() { - // TODO It should be possible to reuse the slice for primitive types. - // However, it is panicing in the following form. - // case reflect.String, reflect.Bool, - // reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, - // reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - // sv.Set(sv) - // return nil - default: - // We need to do a proper conversion. - } + dv.Set(reflect.MakeMap(mapStringInterfaceType)) + dv = dv.Elem() + dt = dv.Type() } - dv.Set(reflect.MakeMap(mapStringInterfaceType)) - dv = dv.Elem() - dt = dv.Type() } if dt.Kind() != reflect.Map { - return fmt.Errorf("cannot convert struct to: %v", dt.Kind()) + return fmt.Errorf("cannot convert map to: %v", dt.Kind()) } if !st.Key().AssignableTo(dt.Key()) && !st.Key().ConvertibleTo(dt.Key()) { @@ -593,20 +752,9 @@ func sliceToUnstructured(sv, dv reflect.Value) error { return nil } if dt.Kind() == reflect.Interface && dv.NumMethod() == 0 { - switch st.Elem().Kind() { - // TODO It should be possible to reuse the slice for primitive types. - // However, it is panicing in the following form. - // case reflect.String, reflect.Bool, - // reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, - // reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - // sv.Set(sv) - // return nil - default: - // We need to do a proper conversion. - dv.Set(reflect.MakeSlice(reflect.SliceOf(dt), sv.Len(), sv.Cap())) - dv = dv.Elem() - dt = dv.Type() - } + dv.Set(reflect.MakeSlice(reflect.SliceOf(dt), sv.Len(), sv.Cap())) + dv = dv.Elem() + dt = dv.Type() } if dt.Kind() != reflect.Slice { return fmt.Errorf("cannot convert slice to: %v", dt.Kind()) @@ -642,7 +790,7 @@ func isZero(v reflect.Value) bool { case reflect.Map, reflect.Slice: // TODO: It seems that 0-len maps are ignored in it. return v.IsNil() || v.Len() == 0 - case reflect.Ptr, reflect.Interface: + case reflect.Pointer, reflect.Interface: return v.IsNil() } return false diff --git a/pkg/runtime/converter_test.go b/pkg/runtime/converter_test.go index 224b2838b..620eeef20 100644 --- a/pkg/runtime/converter_test.go +++ b/pkg/runtime/converter_test.go @@ -24,7 +24,9 @@ import ( encodingjson "encoding/json" "fmt" "reflect" + "regexp" "strconv" + "strings" "testing" "time" @@ -34,6 +36,7 @@ import ( "k8s.io/apimachinery/pkg/util/diff" "k8s.io/apimachinery/pkg/util/json" + fuzz "github.com/google/gofuzz" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -97,6 +100,56 @@ type G struct { CustomPointer2 *CustomPointer `json:"customPointer2"` } +type H struct { + A A `json:"ha"` + C `json:",inline"` +} + +type I struct { + A A `json:"ia"` + H `json:",inline"` + + UL1 UnknownLevel1 `json:"ul1"` +} + +type UnknownLevel1 struct { + A int64 `json:"a"` + InlinedAA `json:",inline"` + InlinedAAA `json:",inline"` +} +type InlinedAA struct { + AA int64 `json:"aa"` +} +type InlinedAAA struct { + AAA int64 `json:"aaa"` + Child UnknownLevel2 `json:"child"` +} + +type UnknownLevel2 struct { + B int64 `json:"b"` + InlinedBB `json:",inline"` + InlinedBBB `json:",inline"` +} +type InlinedBB struct { + BB int64 `json:"bb"` +} +type InlinedBBB struct { + BBB int64 `json:"bbb"` + Child UnknownLevel3 `json:"child"` +} + +type UnknownLevel3 struct { + C int64 `json:"c"` + InlinedCC `json:",inline"` + InlinedCCC `json:",inline"` +} +type InlinedCC struct { + CC int64 `json:"cc"` +} +type InlinedCCC struct { + CCC int64 `json:"ccc"` +} + type CustomValue struct { data []byte } @@ -274,7 +327,7 @@ func TestRoundTrip(t *testing.T) { { // Test slice of interface{} with different values. obj: &D{ - A: []interface{}{3.0, "3.0", nil}, + A: []interface{}{float64(3.5), int64(4), "3.0", nil}, }, }, } @@ -286,6 +339,329 @@ func TestRoundTrip(t *testing.T) { } } +// TestUnknownFields checks for the collection of unknown +// field errors from the various possible locations of +// unknown fields (e.g. fields on struct, inlined struct, slice, etc) +func TestUnknownFields(t *testing.T) { + // simples checks that basic unknown fields are found + // in fields, subfields and slices. + var simplesData = `{ +"ca": [ + { + "aa": 1, + "ab": "ab", + "ac": true, + "unknown1": 24 + } +], +"cc": "ccstring", +"unknown2": "foo" +}` + + var simplesErrs = []string{ + `unknown field "ca[0].unknown1"`, + `unknown field "unknown2"`, + } + + // same-name, different-levels checks that + // fields at a higher level in the json + // are not persisted to unrecognized fields + // at lower levels and vice-versa. + // + // In this case, the field "cc" exists at the root level + // but not in the nested field ul1. If we are + // improperly retaining matched keys, this not + // see an issue with "cc" existing inside "ul1" + // + // The opposite for "aaa", which exists at the + // nested level but not at the root. + var sameNameDiffLevelData = ` + { + "cc": "foo", + "aaa": 1, + "ul1": { + "aa": 1, + "aaa": 1, + "cc": 1 + + } +}` + var sameNameDiffLevelErrs = []string{ + `unknown field "aaa"`, + `unknown field "ul1.cc"`, + } + + // inlined-inlined confirms that we see + // fields that are doubly nested and don't recognize + // those that aren't + var inlinedInlinedData = `{ + "bb": "foo", + "bc": { + "foo": "bar" + }, + "bd": ["d1", "d2"], + "aa": 1 +}` + + var inlinedInlinedErrs = []string{ + `unknown field "aa"`, + } + + // combined tests everything together + var combinedData = ` + { + "ia": { + "aa": 1, + "ab": "ab", + "unknownI": "foo" + }, + "ha": { + "aa": 2, + "ab": "ab2", + "unknownH": "foo" + }, + "ca":[ + { + "aa":1, + "ab":"11", + "ac":true + }, + { + "aa":2, + "ab":"22", + "unknown1": "foo" + }, + { + "aa":3, + "ab":"33", + "unknown2": "foo" + } + ], + "ba":{ + "aa":3, + "ab":"33", + "ac": true, + "unknown3": 26, + "unknown4": "foo" + }, + "unknown5": "foo", + "bb":"bbb", + "bc":{ + "k1":"v1", + "k2":"v2" + }, + "bd":[ + "s1", + "s2" + ], + "cc":"ccc", + "cd":42, + "ce":{ + "k1":1, + "k2":2 + }, + "cf":[ + true, + false, + false + ], + "cg": + [ + 1, + 2, + 5 + ], + "ch":3.3, + "ci":[ + null, + null, + null + ], + "ul1": { + "a": 1, + "aa": 1, + "aaa": 1, + "b": 1, + "bb": 1, + "bbb": 1, + "c": 1, + "cc": 1, + "ccc": 1, + "child": { + "a": 1, + "aa": 1, + "aaa": 1, + "b": 1, + "bb": 1, + "bbb": 1, + "c": 1, + "cc": 1, + "ccc": 1, + "child": { + "a": 1, + "aa": 1, + "aaa": 1, + "b": 1, + "bb": 1, + "bbb": 1, + "c": 1, + "cc": 1, + "ccc": 1 + } + } + } +}` + + var combinedErrs = []string{ + `unknown field "ca[1].unknown1"`, + `unknown field "ca[2].unknown2"`, + `unknown field "ba.unknown3"`, + `unknown field "ba.unknown4"`, + `unknown field "unknown5"`, + `unknown field "ha.unknownH"`, + `unknown field "ia.unknownI"`, + + `unknown field "ul1.b"`, + `unknown field "ul1.bb"`, + `unknown field "ul1.bbb"`, + `unknown field "ul1.c"`, + `unknown field "ul1.cc"`, + `unknown field "ul1.ccc"`, + + `unknown field "ul1.child.a"`, + `unknown field "ul1.child.aa"`, + `unknown field "ul1.child.aaa"`, + `unknown field "ul1.child.c"`, + `unknown field "ul1.child.cc"`, + `unknown field "ul1.child.ccc"`, + + `unknown field "ul1.child.child.a"`, + `unknown field "ul1.child.child.aa"`, + `unknown field "ul1.child.child.aaa"`, + `unknown field "ul1.child.child.b"`, + `unknown field "ul1.child.child.bb"`, + `unknown field "ul1.child.child.bbb"`, + } + + testCases := []struct { + jsonData string + obj interface{} + returnUnknownFields bool + expectedErrs []string + }{ + { + jsonData: simplesData, + obj: &C{}, + returnUnknownFields: true, + expectedErrs: simplesErrs, + }, + { + jsonData: simplesData, + obj: &C{}, + returnUnknownFields: false, + }, + { + jsonData: sameNameDiffLevelData, + obj: &I{}, + returnUnknownFields: true, + expectedErrs: sameNameDiffLevelErrs, + }, + { + jsonData: sameNameDiffLevelData, + obj: &I{}, + returnUnknownFields: false, + }, + { + jsonData: inlinedInlinedData, + obj: &I{}, + returnUnknownFields: true, + expectedErrs: inlinedInlinedErrs, + }, + { + jsonData: inlinedInlinedData, + obj: &I{}, + returnUnknownFields: false, + }, + { + jsonData: combinedData, + obj: &I{}, + returnUnknownFields: true, + expectedErrs: combinedErrs, + }, + { + jsonData: combinedData, + obj: &I{}, + returnUnknownFields: false, + }, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + unstr := make(map[string]interface{}) + err := json.Unmarshal([]byte(tc.jsonData), &unstr) + if err != nil { + t.Errorf("Error when unmarshaling to unstructured: %v", err) + return + } + err = runtime.NewTestUnstructuredConverterWithValidation(simpleEquality).FromUnstructuredWithValidation(unstr, tc.obj, tc.returnUnknownFields) + if len(tc.expectedErrs) == 0 && err != nil { + t.Errorf("unexpected err: %v", err) + } + var errString string + if err != nil { + errString = err.Error() + } + missedErrs := []string{} + failed := false + for _, expected := range tc.expectedErrs { + if !strings.Contains(errString, expected) { + failed = true + missedErrs = append(missedErrs, expected) + } else { + errString = strings.Replace(errString, expected, "", 1) + } + } + if failed { + for _, e := range missedErrs { + t.Errorf("missing err: %v\n", e) + } + } + leftoverErrors := strings.TrimSpace(strings.TrimPrefix(strings.ReplaceAll(errString, ",", ""), "strict decoding error:")) + if leftoverErrors != "" { + t.Errorf("found unexpected errors: %s", leftoverErrors) + } + }) + } +} + +// BenchmarkFromUnstructuredWithValidation benchmarks +// the time and memory required to perform FromUnstructured +// with the various validation directives (Ignore, Warn, Strict) +func BenchmarkFromUnstructuredWithValidation(b *testing.B) { + re := regexp.MustCompile("^I$") + f := fuzz.NewWithSeed(1).NilChance(0.1).SkipFieldsWithPattern(re) + iObj := &I{} + f.Fuzz(&iObj) + + unstr, err := runtime.DefaultUnstructuredConverter.ToUnstructured(iObj) + if err != nil { + b.Fatalf("ToUnstructured failed: %v", err) + return + } + for _, shouldReturn := range []bool{false, true} { + b.Run(fmt.Sprintf("shouldReturn=%t", shouldReturn), func(b *testing.B) { + newObj := reflect.New(reflect.TypeOf(iObj).Elem()).Interface() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + if err = runtime.NewTestUnstructuredConverterWithValidation(simpleEquality).FromUnstructuredWithValidation(unstr, newObj, shouldReturn); err != nil { + b.Fatalf("FromUnstructured failed: %v", err) + return + } + } + }) + } +} + // Verifies that: // 1) serialized json -> object // 2) serialized json -> map[string]interface{} -> object @@ -322,11 +698,11 @@ func TestUnrecognized(t *testing.T) { err error }{ { - data: "{\"da\":[3.0,\"3.0\",null]}", + data: "{\"da\":[3.5,4,\"3.0\",null]}", obj: &D{}, }, { - data: "{\"ea\":[3.0,\"3.0\",null]}", + data: "{\"ea\":[3.5,4,\"3.0\",null]}", obj: &E{}, }, { diff --git a/pkg/runtime/error.go b/pkg/runtime/error.go index be0c5edc8..7dfa45762 100644 --- a/pkg/runtime/error.go +++ b/pkg/runtime/error.go @@ -19,6 +19,7 @@ package runtime import ( "fmt" "reflect" + "strings" "k8s.io/apimachinery/pkg/runtime/schema" ) @@ -124,20 +125,30 @@ func IsMissingVersion(err error) bool { // strictDecodingError is a base error type that is returned by a strict Decoder such // as UniversalStrictDecoder. type strictDecodingError struct { - message string - data string + errors []error } // NewStrictDecodingError creates a new strictDecodingError object. -func NewStrictDecodingError(message string, data string) error { +func NewStrictDecodingError(errors []error) error { return &strictDecodingError{ - message: message, - data: data, + errors: errors, } } func (e *strictDecodingError) Error() string { - return fmt.Sprintf("strict decoder error for %s: %s", e.data, e.message) + var s strings.Builder + s.WriteString("strict decoding error: ") + for i, err := range e.errors { + if i != 0 { + s.WriteString(", ") + } + s.WriteString(err.Error()) + } + return s.String() +} + +func (e *strictDecodingError) Errors() []error { + return e.errors } // IsStrictDecodingError returns true if the error indicates that the provided object @@ -149,3 +160,13 @@ func IsStrictDecodingError(err error) bool { _, ok := err.(*strictDecodingError) return ok } + +// AsStrictDecodingError returns a strict decoding error +// containing all the strictness violations. +func AsStrictDecodingError(err error) (*strictDecodingError, bool) { + if err == nil { + return nil, false + } + strictErr, ok := err.(*strictDecodingError) + return strictErr, ok +} diff --git a/pkg/runtime/generated.pb.go b/pkg/runtime/generated.pb.go index ac428d610..ec677a7d9 100644 --- a/pkg/runtime/generated.pb.go +++ b/pkg/runtime/generated.pb.go @@ -137,31 +137,31 @@ func init() { } var fileDescriptor_9d3c45d7f546725c = []byte{ - // 378 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x8f, 0x4f, 0xab, 0x13, 0x31, - 0x14, 0xc5, 0x27, 0xaf, 0x85, 0x3e, 0xd3, 0xc2, 0x93, 0xb8, 0x70, 0x74, 0x91, 0x79, 0x74, 0xe5, - 0x5b, 0xbc, 0x04, 0x1e, 0x08, 0x6e, 0x3b, 0xa5, 0xa0, 0x88, 0x20, 0xc1, 0x3f, 0xe0, 0xca, 0x74, - 0x26, 0x4e, 0xc3, 0xd0, 0x9b, 0x21, 0xcd, 0x38, 0x76, 0xe7, 0x47, 0xf0, 0x63, 0x75, 0xd9, 0x65, - 0x57, 0xc5, 0x8e, 0x1f, 0xc2, 0xad, 0x34, 0x4d, 0x6b, 0xd5, 0x85, 0xbb, 0xe4, 0x9e, 0xf3, 0x3b, - 0xf7, 0x1e, 0xfc, 0xbc, 0x7c, 0xb6, 0x60, 0xda, 0xf0, 0xb2, 0x9e, 0x2a, 0x0b, 0xca, 0xa9, 0x05, - 0xff, 0xac, 0x20, 0x37, 0x96, 0x07, 0x41, 0x56, 0x7a, 0x2e, 0xb3, 0x99, 0x06, 0x65, 0x97, 0xbc, - 0x2a, 0x0b, 0x6e, 0x6b, 0x70, 0x7a, 0xae, 0x78, 0xa1, 0x40, 0x59, 0xe9, 0x54, 0xce, 0x2a, 0x6b, - 0x9c, 0x21, 0xc9, 0x01, 0x60, 0xe7, 0x00, 0xab, 0xca, 0x82, 0x05, 0xe0, 0xf1, 0x6d, 0xa1, 0xdd, - 0xac, 0x9e, 0xb2, 0xcc, 0xcc, 0x79, 0x61, 0x0a, 0xc3, 0x3d, 0x37, 0xad, 0x3f, 0xf9, 0x9f, 0xff, - 0xf8, 0xd7, 0x21, 0x6f, 0x78, 0x83, 0x07, 0x42, 0x36, 0x93, 0x2f, 0x4e, 0xc1, 0x42, 0x1b, 0x20, - 0x8f, 0x70, 0xc7, 0xca, 0x26, 0x46, 0xd7, 0xe8, 0xc9, 0x20, 0xed, 0xb5, 0xdb, 0xa4, 0x23, 0x64, - 0x23, 0xf6, 0xb3, 0xe1, 0x47, 0x7c, 0xf9, 0x66, 0x59, 0xa9, 0x57, 0xca, 0x49, 0x72, 0x87, 0xb1, - 0xac, 0xf4, 0x3b, 0x65, 0xf7, 0x90, 0x77, 0xdf, 0x4b, 0xc9, 0x6a, 0x9b, 0x44, 0xed, 0x36, 0xc1, - 0xa3, 0xd7, 0x2f, 0x82, 0x22, 0xce, 0x5c, 0xe4, 0x1a, 0x77, 0x4b, 0x0d, 0x79, 0x7c, 0xe1, 0xdd, - 0x83, 0xe0, 0xee, 0xbe, 0xd4, 0x90, 0x0b, 0xaf, 0x0c, 0x7f, 0x22, 0xdc, 0x7b, 0x0b, 0x25, 0x98, - 0x06, 0xc8, 0x7b, 0x7c, 0xe9, 0xc2, 0x36, 0x9f, 0xdf, 0xbf, 0xbb, 0x61, 0xff, 0xe9, 0xce, 0x8e, - 0xe7, 0xa5, 0xf7, 0x43, 0xf8, 0xe9, 0x60, 0x71, 0x0a, 0x3b, 0x36, 0xbc, 0xf8, 0xb7, 0x21, 0x19, - 0xe1, 0xab, 0xcc, 0x80, 0x53, 0xe0, 0x26, 0x90, 0x99, 0x5c, 0x43, 0x11, 0x77, 0xfc, 0xb1, 0x0f, - 0x43, 0xde, 0xd5, 0xf8, 0x4f, 0x59, 0xfc, 0xed, 0x27, 0x4f, 0x71, 0x3f, 0x8c, 0xf6, 0xab, 0xe3, - 0xae, 0xc7, 0x1f, 0x04, 0xbc, 0x3f, 0xfe, 0x2d, 0x89, 0x73, 0x5f, 0x7a, 0xbb, 0xda, 0xd1, 0x68, - 0xbd, 0xa3, 0xd1, 0x66, 0x47, 0xa3, 0xaf, 0x2d, 0x45, 0xab, 0x96, 0xa2, 0x75, 0x4b, 0xd1, 0xa6, - 0xa5, 0xe8, 0x7b, 0x4b, 0xd1, 0xb7, 0x1f, 0x34, 0xfa, 0xd0, 0x0b, 0x45, 0x7f, 0x05, 0x00, 0x00, - 0xff, 0xff, 0xe3, 0x33, 0x18, 0x0b, 0x50, 0x02, 0x00, 0x00, + // 380 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x92, 0xcf, 0xaa, 0x13, 0x31, + 0x14, 0xc6, 0x27, 0xb7, 0x85, 0x7b, 0x4d, 0x0b, 0x57, 0xe2, 0xc2, 0xd1, 0x45, 0xe6, 0xd2, 0x95, + 0x77, 0x61, 0x02, 0x17, 0x04, 0xb7, 0x9d, 0x52, 0x50, 0x44, 0x90, 0xe0, 0x1f, 0x70, 0x65, 0x3a, + 0x13, 0xa7, 0x61, 0xe8, 0xc9, 0x90, 0x66, 0x1c, 0xbb, 0xf3, 0x11, 0x7c, 0xac, 0x2e, 0xbb, 0xec, + 0xaa, 0xd8, 0xf1, 0x21, 0xdc, 0x4a, 0xd3, 0xb4, 0x56, 0x5d, 0x74, 0x97, 0x73, 0xbe, 0xef, 0xf7, + 0x9d, 0x73, 0x20, 0xf8, 0x45, 0xf9, 0x7c, 0xce, 0xb4, 0xe1, 0x65, 0x3d, 0x51, 0x16, 0x94, 0x53, + 0x73, 0xfe, 0x45, 0x41, 0x6e, 0x2c, 0x0f, 0x82, 0xac, 0xf4, 0x4c, 0x66, 0x53, 0x0d, 0xca, 0x2e, + 0x78, 0x55, 0x16, 0xdc, 0xd6, 0xe0, 0xf4, 0x4c, 0xf1, 0x42, 0x81, 0xb2, 0xd2, 0xa9, 0x9c, 0x55, + 0xd6, 0x38, 0x43, 0x92, 0x3d, 0xc0, 0x4e, 0x01, 0x56, 0x95, 0x05, 0x0b, 0xc0, 0xe3, 0xa7, 0x85, + 0x76, 0xd3, 0x7a, 0xc2, 0x32, 0x33, 0xe3, 0x85, 0x29, 0x0c, 0xf7, 0xdc, 0xa4, 0xfe, 0xec, 0x2b, + 0x5f, 0xf8, 0xd7, 0x3e, 0x6f, 0x70, 0x8b, 0xfb, 0x42, 0x36, 0xe3, 0xaf, 0x4e, 0xc1, 0x5c, 0x1b, + 0x20, 0x8f, 0x70, 0xc7, 0xca, 0x26, 0x46, 0x37, 0xe8, 0x49, 0x3f, 0xbd, 0x6c, 0x37, 0x49, 0x47, + 0xc8, 0x46, 0xec, 0x7a, 0x83, 0x4f, 0xf8, 0xea, 0xed, 0xa2, 0x52, 0xaf, 0x95, 0x93, 0xe4, 0x0e, + 0x63, 0x59, 0xe9, 0xf7, 0xca, 0xee, 0x20, 0xef, 0xbe, 0x97, 0x92, 0xe5, 0x26, 0x89, 0xda, 0x4d, + 0x82, 0x87, 0x6f, 0x5e, 0x06, 0x45, 0x9c, 0xb8, 0xc8, 0x0d, 0xee, 0x96, 0x1a, 0xf2, 0xf8, 0xc2, + 0xbb, 0xfb, 0xc1, 0xdd, 0x7d, 0xa5, 0x21, 0x17, 0x5e, 0x19, 0xfc, 0x42, 0xf8, 0xf2, 0x1d, 0x94, + 0x60, 0x1a, 0x20, 0x1f, 0xf0, 0x95, 0x0b, 0xd3, 0x7c, 0x7e, 0xef, 0xee, 0x96, 0x9d, 0xb9, 0x9d, + 0x1d, 0xd6, 0x4b, 0xef, 0x87, 0xf0, 0xe3, 0xc2, 0xe2, 0x18, 0x76, 0xb8, 0xf0, 0xe2, 0xff, 0x0b, + 0xc9, 0x10, 0x5f, 0x67, 0x06, 0x9c, 0x02, 0x37, 0x86, 0xcc, 0xe4, 0x1a, 0x8a, 0xb8, 0xe3, 0x97, + 0x7d, 0x18, 0xf2, 0xae, 0x47, 0x7f, 0xcb, 0xe2, 0x5f, 0x3f, 0x79, 0x86, 0x7b, 0xa1, 0xb5, 0x1b, + 0x1d, 0x77, 0x3d, 0xfe, 0x20, 0xe0, 0xbd, 0xd1, 0x1f, 0x49, 0x9c, 0xfa, 0xd2, 0xf1, 0x72, 0x4b, + 0xa3, 0xd5, 0x96, 0x46, 0xeb, 0x2d, 0x8d, 0xbe, 0xb5, 0x14, 0x2d, 0x5b, 0x8a, 0x56, 0x2d, 0x45, + 0xeb, 0x96, 0xa2, 0x1f, 0x2d, 0x45, 0xdf, 0x7f, 0xd2, 0xe8, 0x63, 0x72, 0xe6, 0xb7, 0xfc, 0x0e, + 0x00, 0x00, 0xff, 0xff, 0x1f, 0x32, 0xd5, 0x68, 0x68, 0x02, 0x00, 0x00, } func (m *RawExtension) Marshal() (dAtA []byte, err error) { diff --git a/pkg/runtime/generated.proto b/pkg/runtime/generated.proto index 3b25391fa..5f06cc574 100644 --- a/pkg/runtime/generated.proto +++ b/pkg/runtime/generated.proto @@ -22,7 +22,7 @@ syntax = "proto2"; package k8s.io.apimachinery.pkg.runtime; // Package-wide variables from generator "generated". -option go_package = "runtime"; +option go_package = "k8s.io/apimachinery/pkg/runtime"; // RawExtension is used to hold extensions in external versions. // @@ -31,32 +31,37 @@ option go_package = "runtime"; // various plugin types. // // // Internal package: -// type MyAPIObject struct { -// runtime.TypeMeta `json:",inline"` -// MyPlugin runtime.Object `json:"myPlugin"` -// } -// type PluginA struct { -// AOption string `json:"aOption"` -// } +// +// type MyAPIObject struct { +// runtime.TypeMeta `json:",inline"` +// MyPlugin runtime.Object `json:"myPlugin"` +// } +// +// type PluginA struct { +// AOption string `json:"aOption"` +// } // // // External package: -// type MyAPIObject struct { -// runtime.TypeMeta `json:",inline"` -// MyPlugin runtime.RawExtension `json:"myPlugin"` -// } -// type PluginA struct { -// AOption string `json:"aOption"` -// } +// +// type MyAPIObject struct { +// runtime.TypeMeta `json:",inline"` +// MyPlugin runtime.RawExtension `json:"myPlugin"` +// } +// +// type PluginA struct { +// AOption string `json:"aOption"` +// } // // // On the wire, the JSON will look something like this: -// { -// "kind":"MyAPIObject", -// "apiVersion":"v1", -// "myPlugin": { -// "kind":"PluginA", -// "aOption":"foo", -// }, -// } +// +// { +// "kind":"MyAPIObject", +// "apiVersion":"v1", +// "myPlugin": { +// "kind":"PluginA", +// "aOption":"foo", +// }, +// } // // So what happens? Decode first uses json or yaml to unmarshal the serialized data into // your external MyAPIObject. That causes the raw JSON to be stored, but not unpacked. @@ -78,10 +83,12 @@ message RawExtension { // TypeMeta is shared by all top level objects. The proper way to use it is to inline it in your type, // like this: -// type MyAwesomeAPIObject struct { -// runtime.TypeMeta `json:",inline"` -// ... // other fields -// } +// +// type MyAwesomeAPIObject struct { +// runtime.TypeMeta `json:",inline"` +// ... // other fields +// } +// // func (obj *MyAwesomeAPIObject) SetGroupVersionKind(gvk *metav1.GroupVersionKind) { metav1.UpdateTypeMeta(obj,gvk) }; GroupVersionKind() *GroupVersionKind // // TypeMeta is provided here for convenience. You may use it directly from this package or define diff --git a/pkg/runtime/interfaces.go b/pkg/runtime/interfaces.go index 3e1fab1d1..710a97795 100644 --- a/pkg/runtime/interfaces.go +++ b/pkg/runtime/interfaces.go @@ -69,6 +69,24 @@ type Encoder interface { Identifier() Identifier } +// MemoryAllocator is responsible for allocating memory. +// By encapsulating memory allocation into its own interface, we can reuse the memory +// across many operations in places we know it can significantly improve the performance. +type MemoryAllocator interface { + // Allocate reserves memory for n bytes. + // Note that implementations of this method are not required to zero the returned array. + // It is the caller's responsibility to clean the memory if needed. + Allocate(n uint64) []byte +} + +// EncoderWithAllocator serializes objects in a way that allows callers to manage any additional memory allocations. +type EncoderWithAllocator interface { + Encoder + // EncodeWithAllocator writes an object to a stream as Encode does. + // In addition, it allows for providing a memory allocator for efficient memory usage during object serialization + EncodeWithAllocator(obj Object, w io.Writer, memAlloc MemoryAllocator) error +} + // Decoder attempts to load an object from data. type Decoder interface { // Decode attempts to deserialize the provided data using either the innate typing of the scheme or the @@ -125,6 +143,9 @@ type SerializerInfo struct { // PrettySerializer, if set, can serialize this object in a form biased towards // readability. PrettySerializer Serializer + // StrictSerializer, if set, deserializes this object strictly, + // erring on unknown fields. + StrictSerializer Serializer // StreamSerializer, if set, describes the streaming serialization format // for this media type. StreamSerializer *StreamSerializerInfo @@ -150,7 +171,7 @@ type NegotiatedSerializer interface { // EncoderForVersion returns an encoder that ensures objects being written to the provided // serializer are in the provided group version. EncoderForVersion(serializer Encoder, gv GroupVersioner) Encoder - // DecoderForVersion returns a decoder that ensures objects being read by the provided + // DecoderToVersion returns a decoder that ensures objects being read by the provided // serializer are in the provided group version by default. DecoderToVersion(serializer Decoder, gv GroupVersioner) Decoder } @@ -204,6 +225,12 @@ type NestedObjectEncoder interface { // NestedObjectDecoder is an optional interface that objects may implement to be given // an opportunity to decode any nested Objects / RawExtensions during serialization. +// It is possible for DecodeNestedObjects to return a non-nil error but for the decoding +// to have succeeded in the case of strict decoding errors (e.g. unknown/duplicate fields). +// As such it is important for callers of DecodeNestedObjects to check to confirm whether +// an error is a runtime.StrictDecodingError before short circuiting. +// Similarly, implementations of DecodeNestedObjects should ensure that a runtime.StrictDecodingError +// is only returned when the rest of decoding has succeeded. type NestedObjectDecoder interface { DecodeNestedObjects(d Decoder) error } @@ -281,14 +308,11 @@ type ResourceVersioner interface { ResourceVersion(obj Object) (string, error) } -// SelfLinker provides methods for setting and retrieving the SelfLink field of an API object. -type SelfLinker interface { - SetSelfLink(obj Object, selfLink string) error - SelfLink(obj Object) (string, error) - - // Knowing Name is sometimes necessary to use a SelfLinker. +// Namer provides methods for retrieving name and namespace of an API object. +type Namer interface { + // Name returns the name of a given object. Name(obj Object) (string, error) - // Knowing Namespace is sometimes necessary to use a SelfLinker + // Namespace returns the name of a given object. Namespace(obj Object) (string, error) } diff --git a/pkg/runtime/schema/generated.pb.go b/pkg/runtime/schema/generated.pb.go index 29d3ac45b..46b1e787b 100644 --- a/pkg/runtime/schema/generated.pb.go +++ b/pkg/runtime/schema/generated.pb.go @@ -43,17 +43,17 @@ func init() { } var fileDescriptor_0462724132518e0d = []byte{ - // 185 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x4c, 0xcc, 0xaf, 0x6e, 0xc3, 0x30, - 0x10, 0xc7, 0x71, 0x9b, 0x0c, 0x0c, 0x0e, 0x0e, 0x1c, 0x1c, 0xda, 0x7c, 0x74, 0xb8, 0x2f, 0x50, - 0x5e, 0xe6, 0x24, 0x57, 0xc7, 0xb2, 0xfc, 0x47, 0x8e, 0x5d, 0xa9, 0xac, 0x8f, 0xd0, 0xc7, 0x0a, - 0x0c, 0x0c, 0x6c, 0xdc, 0x17, 0xa9, 0x64, 0x07, 0x94, 0xdd, 0x4f, 0xa7, 0xcf, 0xf7, 0xf3, 0x68, - 0xfe, 0x27, 0xa1, 0x3d, 0x9a, 0xdc, 0x51, 0x74, 0x94, 0x68, 0xc2, 0x0b, 0xb9, 0xc1, 0x47, 0xdc, - 0x1f, 0x32, 0x68, 0x2b, 0xfb, 0x51, 0x3b, 0x8a, 0x57, 0x0c, 0x46, 0x61, 0xcc, 0x2e, 0x69, 0x4b, - 0x38, 0xf5, 0x23, 0x59, 0x89, 0x8a, 0x1c, 0x45, 0x99, 0x68, 0x10, 0x21, 0xfa, 0xe4, 0xbf, 0x7e, - 0x9a, 0x13, 0xef, 0x4e, 0x04, 0xa3, 0xc4, 0xee, 0x44, 0x73, 0xdf, 0x7f, 0x4a, 0xa7, 0x31, 0x77, - 0xa2, 0xf7, 0x16, 0x95, 0x57, 0x1e, 0x2b, 0xef, 0xf2, 0xb9, 0xae, 0x3a, 0xea, 0xd5, 0xb2, 0x87, - 0xdf, 0x79, 0x03, 0xb6, 0x6c, 0xc0, 0xd6, 0x0d, 0xd8, 0xad, 0x00, 0x9f, 0x0b, 0xf0, 0xa5, 0x00, - 0x5f, 0x0b, 0xf0, 0x47, 0x01, 0x7e, 0x7f, 0x02, 0x3b, 0x7d, 0xb4, 0xf8, 0x2b, 0x00, 0x00, 0xff, - 0xff, 0xba, 0x7e, 0x65, 0xf4, 0xd6, 0x00, 0x00, 0x00, + // 186 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0xce, 0xad, 0x8e, 0xc3, 0x30, + 0x0c, 0xc0, 0xf1, 0x84, 0x1e, 0x3c, 0x78, 0xc0, 0xb0, 0xec, 0x62, 0x7a, 0xf8, 0xf0, 0xa4, 0xf1, + 0xb1, 0xb4, 0xf5, 0xd2, 0x28, 0xca, 0x87, 0xd2, 0x64, 0xd2, 0xd8, 0x1e, 0x61, 0x8f, 0x55, 0x58, + 0x58, 0xb8, 0x66, 0x2f, 0x32, 0x29, 0x2d, 0x18, 0x1c, 0xf3, 0x5f, 0xd6, 0xcf, 0xf2, 0xd7, 0xd1, + 0xfc, 0x8d, 0x42, 0x7b, 0x34, 0xb9, 0xa5, 0xe8, 0x28, 0xd1, 0x88, 0x17, 0x72, 0xbd, 0x8f, 0xb8, + 0x2f, 0x64, 0xd0, 0x56, 0x76, 0x83, 0x76, 0x14, 0xaf, 0x18, 0x8c, 0xc2, 0x98, 0x5d, 0xd2, 0x96, + 0x70, 0xec, 0x06, 0xb2, 0x12, 0x15, 0x39, 0x8a, 0x32, 0x51, 0x2f, 0x42, 0xf4, 0xc9, 0x7f, 0x37, + 0x9b, 0x13, 0xef, 0x4e, 0x04, 0xa3, 0xc4, 0xee, 0xc4, 0xe6, 0x7e, 0x7e, 0x95, 0x4e, 0x43, 0x6e, + 0x45, 0xe7, 0x2d, 0x2a, 0xaf, 0x3c, 0x56, 0xde, 0xe6, 0x73, 0xad, 0x1a, 0x75, 0xda, 0xce, 0xfe, + 0x1f, 0xa6, 0x15, 0xd8, 0xbc, 0x02, 0x5b, 0x56, 0x60, 0xb7, 0x02, 0x7c, 0x2a, 0xc0, 0xe7, 0x02, + 0x7c, 0x29, 0xc0, 0x1f, 0x05, 0xf8, 0xfd, 0x09, 0xec, 0xd4, 0x7c, 0xf6, 0xf4, 0x2b, 0x00, 0x00, + 0xff, 0xff, 0x12, 0xb4, 0xae, 0x48, 0xf6, 0x00, 0x00, 0x00, } diff --git a/pkg/runtime/schema/generated.proto b/pkg/runtime/schema/generated.proto index c50766a4b..01a9c01e5 100644 --- a/pkg/runtime/schema/generated.proto +++ b/pkg/runtime/schema/generated.proto @@ -22,5 +22,5 @@ syntax = "proto2"; package k8s.io.apimachinery.pkg.runtime.schema; // Package-wide variables from generator "generated". -option go_package = "schema"; +option go_package = "k8s.io/apimachinery/pkg/runtime/schema"; diff --git a/pkg/runtime/schema/group_version.go b/pkg/runtime/schema/group_version.go index 994a3e3fa..b21eb664e 100644 --- a/pkg/runtime/schema/group_version.go +++ b/pkg/runtime/schema/group_version.go @@ -191,7 +191,8 @@ func (gv GroupVersion) Identifier() string { // if none of the options match the group. It prefers a match to group and version over just group. // TODO: Move GroupVersion to a package under pkg/runtime, since it's used by scheme. // TODO: Introduce an adapter type between GroupVersion and runtime.GroupVersioner, and use LegacyCodec(GroupVersion) -// in fewer places. +// +// in fewer places. func (gv GroupVersion) KindForGroupVersionKinds(kinds []GroupVersionKind) (target GroupVersionKind, ok bool) { for _, gvk := range kinds { if gvk.Group == gv.Group && gvk.Version == gv.Version { @@ -239,7 +240,8 @@ func (gv GroupVersion) WithResource(resource string) GroupVersionResource { // GroupVersions can be used to represent a set of desired group versions. // TODO: Move GroupVersions to a package under pkg/runtime, since it's used by scheme. // TODO: Introduce an adapter type between GroupVersions and runtime.GroupVersioner, and use LegacyCodec(GroupVersion) -// in fewer places. +// +// in fewer places. type GroupVersions []GroupVersion // Identifier implements runtime.GroupVersioner interface. diff --git a/pkg/runtime/schema/group_version_test.go b/pkg/runtime/schema/group_version_test.go index 3934bc45a..ff28593c5 100644 --- a/pkg/runtime/schema/group_version_test.go +++ b/pkg/runtime/schema/group_version_test.go @@ -178,3 +178,63 @@ func TestParseGroupKind(t *testing.T) { }) } } + +func TestToAPIVersionAndKind(t *testing.T) { + tests := []struct { + desc string + input GroupVersionKind + GroupVersion string + Kind string + }{ + { + desc: "gvk object is not empty", + input: GroupVersionKind{Version: "V1", Kind: "pod"}, + GroupVersion: "V1", + Kind: "pod", + }, + { + desc: "gvk object is empty", + input: GroupVersionKind{}, + GroupVersion: "", + Kind: "", + }, + } + for i, test := range tests { + version, kind := test.input.ToAPIVersionAndKind() + if version != test.GroupVersion { + t.Errorf("%d: expected version: %#v, got: %#v", i, test.GroupVersion, version) + } + if kind != test.Kind { + t.Errorf("%d: expected kind: %#v, got: %#v", i, test.Kind, kind) + } + } +} + +func TestBestMatch(t *testing.T) { + tests := []struct { + desc string + kinds []GroupVersionKind + targets []GroupVersionKind + output GroupVersionKind + }{ + { + desc: "targets and kinds have match items", + kinds: []GroupVersionKind{{Version: "V1", Kind: "pod"}, {Version: "V2", Kind: "pod"}}, + targets: []GroupVersionKind{{Version: "V1", Kind: "pod"}}, + output: GroupVersionKind{Version: "V1", Kind: "pod"}, + }, + { + desc: "targets and kinds do not have match items", + kinds: []GroupVersionKind{{Version: "V1", Kind: "pod"}, {Version: "V2", Kind: "pod"}}, + targets: []GroupVersionKind{{Version: "V3", Kind: "pod"}, {Version: "V4", Kind: "pod"}}, + output: GroupVersionKind{Version: "V3", Kind: "pod"}, + }, + } + + for i, test := range tests { + out := bestMatch(test.kinds, test.targets) + if out != test.output { + t.Errorf("%d: expected out: %#v, got: %#v", i, test.output, out) + } + } +} diff --git a/pkg/runtime/scheme.go b/pkg/runtime/scheme.go index ae47ab3ab..18b25a994 100644 --- a/pkg/runtime/scheme.go +++ b/pkg/runtime/scheme.go @@ -44,11 +44,11 @@ import ( // Schemes are not expected to change at runtime and are only threadsafe after // registration is complete. type Scheme struct { - // versionMap allows one to figure out the go type of an object with + // gvkToType allows one to figure out the go type of an object with // the given version and name. gvkToType map[schema.GroupVersionKind]reflect.Type - // typeToGroupVersion allows one to find metadata for a given go object. + // typeToGVK allows one to find metadata for a given go object. // The reflect.Type we index by should *not* be a pointer. typeToGVK map[reflect.Type][]schema.GroupVersionKind @@ -64,7 +64,7 @@ type Scheme struct { // resource field labels in that version to internal version. fieldLabelConversionFuncs map[schema.GroupVersionKind]FieldLabelConversionFunc - // defaulterFuncs is an array of interfaces to be called with an object to provide defaulting + // defaulterFuncs is a map to funcs to be called with an object to provide defaulting // the provided object must be a pointer. defaulterFuncs map[reflect.Type]func(interface{}) @@ -99,7 +99,7 @@ func NewScheme() *Scheme { versionPriority: map[string][]string{}, schemeName: naming.GetNameFromCallsite(internalPackages...), } - s.converter = conversion.NewConverter(s.nameFunc) + s.converter = conversion.NewConverter(nil) // Enable couple default conversions by default. utilruntime.Must(RegisterEmbeddedConversions(s)) @@ -107,28 +107,6 @@ func NewScheme() *Scheme { return s } -// nameFunc returns the name of the type that we wish to use to determine when two types attempt -// a conversion. Defaults to the go name of the type if the type is not registered. -func (s *Scheme) nameFunc(t reflect.Type) string { - // find the preferred names for this type - gvks, ok := s.typeToGVK[t] - if !ok { - return t.Name() - } - - for _, gvk := range gvks { - internalGV := gvk.GroupVersion() - internalGV.Version = APIVersionInternal // this is hacky and maybe should be passed in - internalGVK := internalGV.WithKind(gvk.Kind) - - if internalType, exists := s.gvkToType[internalGVK]; exists { - return s.typeToGVK[internalType][0].Kind - } - } - - return gvks[0].Kind -} - // Converter allows access to the converter for the scheme func (s *Scheme) Converter() *conversion.Converter { return s.converter @@ -140,7 +118,8 @@ func (s *Scheme) Converter() *conversion.Converter { // API group and version that would never be updated. // // TODO: there is discussion about removing unversioned and replacing it with objects that are manifest into -// every version with particular schemas. Resolve this method at that point. +// +// every version with particular schemas. Resolve this method at that point. func (s *Scheme) AddUnversionedTypes(version schema.GroupVersion, types ...Object) { s.addObservedVersion(version) s.AddKnownTypes(version, types...) @@ -163,7 +142,7 @@ func (s *Scheme) AddKnownTypes(gv schema.GroupVersion, types ...Object) { s.addObservedVersion(gv) for _, obj := range types { t := reflect.TypeOf(obj) - if t.Kind() != reflect.Ptr { + if t.Kind() != reflect.Pointer { panic("All types must be pointers to structs.") } t = t.Elem() @@ -181,7 +160,7 @@ func (s *Scheme) AddKnownTypeWithName(gvk schema.GroupVersionKind, obj Object) { if len(gvk.Version) == 0 { panic(fmt.Sprintf("version is required on all types: %s %v", gvk, t)) } - if t.Kind() != reflect.Ptr { + if t.Kind() != reflect.Pointer { panic("All types must be pointers to structs.") } t = t.Elem() @@ -484,7 +463,7 @@ func (s *Scheme) convertToVersion(copy bool, in Object, target GroupVersioner) ( } else { // determine the incoming kinds with as few allocations as possible. t = reflect.TypeOf(in) - if t.Kind() != reflect.Ptr { + if t.Kind() != reflect.Pointer { return nil, fmt.Errorf("only pointer types may be converted: %v", t) } t = t.Elem() diff --git a/pkg/runtime/serializer/codec_factory.go b/pkg/runtime/serializer/codec_factory.go index e55ab94d1..21944f2d8 100644 --- a/pkg/runtime/serializer/codec_factory.go +++ b/pkg/runtime/serializer/codec_factory.go @@ -40,6 +40,7 @@ type serializerType struct { Serializer runtime.Serializer PrettySerializer runtime.Serializer + StrictSerializer runtime.Serializer AcceptStreamContentTypes []string StreamContentType string @@ -70,10 +71,20 @@ func newSerializersForScheme(scheme *runtime.Scheme, mf json.MetaFactory, option ) } + strictJSONSerializer := json.NewSerializerWithOptions( + mf, scheme, scheme, + json.SerializerOptions{Yaml: false, Pretty: false, Strict: true}, + ) + jsonSerializerType.StrictSerializer = strictJSONSerializer + yamlSerializer := json.NewSerializerWithOptions( mf, scheme, scheme, json.SerializerOptions{Yaml: true, Pretty: false, Strict: options.Strict}, ) + strictYAMLSerializer := json.NewSerializerWithOptions( + mf, scheme, scheme, + json.SerializerOptions{Yaml: true, Pretty: false, Strict: true}, + ) protoSerializer := protobuf.NewSerializer(scheme, scheme) protoRawSerializer := protobuf.NewRawSerializer(scheme, scheme) @@ -85,12 +96,16 @@ func newSerializersForScheme(scheme *runtime.Scheme, mf json.MetaFactory, option FileExtensions: []string{"yaml"}, EncodesAsText: true, Serializer: yamlSerializer, + StrictSerializer: strictYAMLSerializer, }, { AcceptContentTypes: []string{runtime.ContentTypeProtobuf}, ContentType: runtime.ContentTypeProtobuf, FileExtensions: []string{"pb"}, Serializer: protoSerializer, + // note, strict decoding is unsupported for protobuf, + // fall back to regular serializing + StrictSerializer: protoSerializer, Framer: protobuf.LengthDelimitedFramer, StreamSerializer: protoRawSerializer, @@ -187,6 +202,7 @@ func newCodecFactory(scheme *runtime.Scheme, serializers []serializerType) Codec EncodesAsText: d.EncodesAsText, Serializer: d.Serializer, PrettySerializer: d.PrettySerializer, + StrictSerializer: d.StrictSerializer, } mediaType, _, err := mime.ParseMediaType(info.MediaType) @@ -243,7 +259,8 @@ func (f CodecFactory) SupportedMediaTypes() []runtime.SerializerInfo { // invoke CodecForVersions. Callers that need only to read data should use UniversalDecoder(). // // TODO: make this call exist only in pkg/api, and initialize it with the set of default versions. -// All other callers will be forced to request a Codec directly. +// +// All other callers will be forced to request a Codec directly. func (f CodecFactory) LegacyCodec(version ...schema.GroupVersion) runtime.Codec { return versioning.NewDefaultingCodecForScheme(f.scheme, f.legacySerializer, f.universal, schema.GroupVersions(version), runtime.InternalGroupVersioner) } diff --git a/pkg/runtime/serializer/encoder_with_allocator_test.go b/pkg/runtime/serializer/encoder_with_allocator_test.go new file mode 100644 index 000000000..73406d073 --- /dev/null +++ b/pkg/runtime/serializer/encoder_with_allocator_test.go @@ -0,0 +1,139 @@ +/* +Copyright 2022 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 serializer + +import ( + "crypto/rand" + "io/ioutil" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + testapigroupv1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer/protobuf" +) + +func BenchmarkProtobufEncoder(b *testing.B) { + benchmarkEncodeFor(b, protobuf.NewSerializer(nil, nil)) +} + +func BenchmarkProtobufEncodeWithAllocator(b *testing.B) { + benchmarkEncodeWithAllocatorFor(b, protobuf.NewSerializer(nil, nil)) +} + +func BenchmarkRawProtobufEncoder(b *testing.B) { + benchmarkEncodeFor(b, protobuf.NewRawSerializer(nil, nil)) +} + +func BenchmarkRawProtobufEncodeWithAllocator(b *testing.B) { + benchmarkEncodeWithAllocatorFor(b, protobuf.NewRawSerializer(nil, nil)) +} + +func benchmarkEncodeFor(b *testing.B, target runtime.Encoder) { + for _, tc := range benchTestCases() { + b.Run(tc.name, func(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + err := target.Encode(tc.obj, ioutil.Discard) + if err != nil { + b.Fatal(err) + } + } + }) + } +} + +func benchmarkEncodeWithAllocatorFor(b *testing.B, target runtime.EncoderWithAllocator) { + for _, tc := range benchTestCases() { + b.Run(tc.name, func(b *testing.B) { + b.ReportAllocs() + allocator := &runtime.Allocator{} + for n := 0; n < b.N; n++ { + err := target.EncodeWithAllocator(tc.obj, ioutil.Discard, allocator) + if err != nil { + b.Fatal(err) + } + } + }) + } +} + +type benchTestCase struct { + name string + obj runtime.Object +} + +func benchTestCases() []benchTestCase { + return []benchTestCase{ + { + name: "an obj with 1kB payload", + obj: func() runtime.Object { + carpPayload := make([]byte, 1000) // 1 kB + if _, err := rand.Read(carpPayload); err != nil { + panic(err) + } + return carpWithPayload(carpPayload) + }(), + }, + { + name: "an obj with 10kB payload", + obj: func() runtime.Object { + carpPayload := make([]byte, 10000) // 10 kB + if _, err := rand.Read(carpPayload); err != nil { + panic(err) + } + return carpWithPayload(carpPayload) + }(), + }, + { + name: "an obj with 100kB payload", + obj: func() runtime.Object { + carpPayload := make([]byte, 100000) // 100 kB + if _, err := rand.Read(carpPayload); err != nil { + panic(err) + } + return carpWithPayload(carpPayload) + }(), + }, + { + name: "an obj with 1MB payload", + obj: func() runtime.Object { + carpPayload := make([]byte, 1000000) // 1 MB + if _, err := rand.Read(carpPayload); err != nil { + panic(err) + } + return carpWithPayload(carpPayload) + }(), + }, + } +} + +func carpWithPayload(carpPayload []byte) *testapigroupv1.Carp { + gvk := &schema.GroupVersionKind{Group: "group", Version: "version", Kind: "Carp"} + return &testapigroupv1.Carp{ + TypeMeta: metav1.TypeMeta{APIVersion: gvk.GroupVersion().String(), Kind: gvk.Kind}, + ObjectMeta: metav1.ObjectMeta{ + Name: "name", + Namespace: "namespace", + }, + Spec: testapigroupv1.CarpSpec{ + Subdomain: "carp.k8s.io", + NodeSelector: map[string]string{"payload": string(carpPayload)}, + }, + } +} diff --git a/pkg/runtime/serializer/json/json.go b/pkg/runtime/serializer/json/json.go index 48f0777b2..1ae4a32eb 100644 --- a/pkg/runtime/serializer/json/json.go +++ b/pkg/runtime/serializer/json/json.go @@ -20,10 +20,8 @@ import ( "encoding/json" "io" "strconv" - "unsafe" - jsoniter "github.com/json-iterator/go" - "github.com/modern-go/reflect2" + kjson "sigs.k8s.io/json" "sigs.k8s.io/yaml" "k8s.io/apimachinery/pkg/runtime" @@ -68,6 +66,7 @@ func identifier(options SerializerOptions) runtime.Identifier { "name": "json", "yaml": strconv.FormatBool(options.Yaml), "pretty": strconv.FormatBool(options.Pretty), + "strict": strconv.FormatBool(options.Strict), } identifier, err := json.Marshal(result) if err != nil { @@ -110,79 +109,6 @@ type Serializer struct { var _ runtime.Serializer = &Serializer{} var _ recognizer.RecognizingDecoder = &Serializer{} -type customNumberExtension struct { - jsoniter.DummyExtension -} - -func (cne *customNumberExtension) CreateDecoder(typ reflect2.Type) jsoniter.ValDecoder { - if typ.String() == "interface {}" { - return customNumberDecoder{} - } - return nil -} - -type customNumberDecoder struct { -} - -func (customNumberDecoder) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) { - switch iter.WhatIsNext() { - case jsoniter.NumberValue: - var number jsoniter.Number - iter.ReadVal(&number) - i64, err := strconv.ParseInt(string(number), 10, 64) - if err == nil { - *(*interface{})(ptr) = i64 - return - } - f64, err := strconv.ParseFloat(string(number), 64) - if err == nil { - *(*interface{})(ptr) = f64 - return - } - iter.ReportError("DecodeNumber", err.Error()) - default: - *(*interface{})(ptr) = iter.Read() - } -} - -// CaseSensitiveJSONIterator returns a jsoniterator API that's configured to be -// case-sensitive when unmarshalling, and otherwise compatible with -// the encoding/json standard library. -func CaseSensitiveJSONIterator() jsoniter.API { - config := jsoniter.Config{ - EscapeHTML: true, - SortMapKeys: true, - ValidateJsonRawMessage: true, - CaseSensitive: true, - }.Froze() - // Force jsoniter to decode number to interface{} via int64/float64, if possible. - config.RegisterExtension(&customNumberExtension{}) - return config -} - -// StrictCaseSensitiveJSONIterator returns a jsoniterator API that's configured to be -// case-sensitive, but also disallows unknown fields when unmarshalling. It is compatible with -// the encoding/json standard library. -func StrictCaseSensitiveJSONIterator() jsoniter.API { - config := jsoniter.Config{ - EscapeHTML: true, - SortMapKeys: true, - ValidateJsonRawMessage: true, - CaseSensitive: true, - DisallowUnknownFields: true, - }.Froze() - // Force jsoniter to decode number to interface{} via int64/float64, if possible. - config.RegisterExtension(&customNumberExtension{}) - return config -} - -// Private copies of jsoniter to try to shield against possible mutations -// from outside. Still does not protect from package level jsoniter.Register*() functions - someone calling them -// in some other library will mess with every usage of the jsoniter library in the whole program. -// See https://github.com/json-iterator/go/issues/265 -var caseSensitiveJSONIterator = CaseSensitiveJSONIterator() -var strictCaseSensitiveJSONIterator = StrictCaseSensitiveJSONIterator() - // gvkWithDefaults returns group kind and version defaulting from provided default func gvkWithDefaults(actual, defaultGVK schema.GroupVersionKind) schema.GroupVersionKind { if len(actual.Kind) == 0 { @@ -237,9 +163,25 @@ func (s *Serializer) Decode(originalData []byte, gvk *schema.GroupVersionKind, i types, _, err := s.typer.ObjectKinds(into) switch { case runtime.IsNotRegisteredError(err), isUnstructured: - if err := caseSensitiveJSONIterator.Unmarshal(data, into); err != nil { + strictErrs, err := s.unmarshal(into, data, originalData) + if err != nil { return nil, actual, err } + + // when decoding directly into a provided unstructured object, + // extract the actual gvk decoded from the provided data, + // and ensure it is non-empty. + if isUnstructured { + *actual = into.GetObjectKind().GroupVersionKind() + if len(actual.Kind) == 0 { + return nil, actual, runtime.NewMissingKindErr(string(originalData)) + } + // TODO(109023): require apiVersion here as well once unstructuredJSONScheme#Decode does + } + + if len(strictErrs) > 0 { + return into, actual, runtime.NewStrictDecodingError(strictErrs) + } return into, actual, nil case err != nil: return nil, actual, err @@ -261,35 +203,12 @@ func (s *Serializer) Decode(originalData []byte, gvk *schema.GroupVersionKind, i return nil, actual, err } - if err := caseSensitiveJSONIterator.Unmarshal(data, obj); err != nil { - return nil, actual, err - } - - // If the deserializer is non-strict, return successfully here. - if !s.options.Strict { - return obj, actual, nil - } - - // In strict mode pass the data trough the YAMLToJSONStrict converter. - // This is done to catch duplicate fields regardless of encoding (JSON or YAML). For JSON data, - // the output would equal the input, unless there is a parsing error such as duplicate fields. - // As we know this was successful in the non-strict case, the only error that may be returned here - // is because of the newly-added strictness. hence we know we can return the typed strictDecoderError - // the actual error is that the object contains duplicate fields. - altered, err := yaml.YAMLToJSONStrict(originalData) + strictErrs, err := s.unmarshal(obj, data, originalData) if err != nil { - return nil, actual, runtime.NewStrictDecodingError(err.Error(), string(originalData)) - } - // As performance is not an issue for now for the strict deserializer (one has regardless to do - // the unmarshal twice), we take the sanitized, altered data that is guaranteed to have no duplicated - // fields, and unmarshal this into a copy of the already-populated obj. Any error that occurs here is - // due to that a matching field doesn't exist in the object. hence we can return a typed strictDecoderError, - // the actual error is that the object contains unknown field. - strictObj := obj.DeepCopyObject() - if err := strictCaseSensitiveJSONIterator.Unmarshal(altered, strictObj); err != nil { - return nil, actual, runtime.NewStrictDecodingError(err.Error(), string(originalData)) + return nil, actual, err + } else if len(strictErrs) > 0 { + return obj, actual, runtime.NewStrictDecodingError(strictErrs) } - // Always return the same object as the non-strict serializer to avoid any deviations. return obj, actual, nil } @@ -303,7 +222,7 @@ func (s *Serializer) Encode(obj runtime.Object, w io.Writer) error { func (s *Serializer) doEncode(obj runtime.Object, w io.Writer) error { if s.options.Yaml { - json, err := caseSensitiveJSONIterator.Marshal(obj) + json, err := json.Marshal(obj) if err != nil { return err } @@ -316,7 +235,7 @@ func (s *Serializer) doEncode(obj runtime.Object, w io.Writer) error { } if s.options.Pretty { - data, err := caseSensitiveJSONIterator.MarshalIndent(obj, "", " ") + data, err := json.MarshalIndent(obj, "", " ") if err != nil { return err } @@ -327,6 +246,50 @@ func (s *Serializer) doEncode(obj runtime.Object, w io.Writer) error { return encoder.Encode(obj) } +// IsStrict indicates whether the serializer +// uses strict decoding or not +func (s *Serializer) IsStrict() bool { + return s.options.Strict +} + +func (s *Serializer) unmarshal(into runtime.Object, data, originalData []byte) (strictErrs []error, err error) { + // If the deserializer is non-strict, return here. + if !s.options.Strict { + if err := kjson.UnmarshalCaseSensitivePreserveInts(data, into); err != nil { + return nil, err + } + return nil, nil + } + + if s.options.Yaml { + // In strict mode pass the original data through the YAMLToJSONStrict converter. + // This is done to catch duplicate fields in YAML that would have been dropped in the original YAMLToJSON conversion. + // TODO: rework YAMLToJSONStrict to return warnings about duplicate fields without terminating so we don't have to do this twice. + _, err := yaml.YAMLToJSONStrict(originalData) + if err != nil { + strictErrs = append(strictErrs, err) + } + } + + var strictJSONErrs []error + if u, isUnstructured := into.(runtime.Unstructured); isUnstructured { + // Unstructured is a custom unmarshaler that gets delegated + // to, so in order to detect strict JSON errors we need + // to unmarshal directly into the object. + m := map[string]interface{}{} + strictJSONErrs, err = kjson.UnmarshalStrict(data, &m) + u.SetUnstructuredContent(m) + } else { + strictJSONErrs, err = kjson.UnmarshalStrict(data, into) + } + if err != nil { + // fatal decoding error, not due to strictness + return nil, err + } + strictErrs = append(strictErrs, strictJSONErrs...) + return strictErrs, nil +} + // Identifier implements runtime.Encoder interface. func (s *Serializer) Identifier() runtime.Identifier { return s.identifier diff --git a/pkg/runtime/serializer/json/json_limit_test.go b/pkg/runtime/serializer/json/json_limit_test.go index a741541fb..8214d293c 100644 --- a/pkg/runtime/serializer/json/json_limit_test.go +++ b/pkg/runtime/serializer/json/json_limit_test.go @@ -123,7 +123,6 @@ func testcases() []testcase { var decoders = map[string]func([]byte, interface{}) error{ "gojson": gojson.Unmarshal, "utiljson": utiljson.Unmarshal, - "jsoniter": CaseSensitiveJSONIterator().Unmarshal, } func TestJSONLimits(t *testing.T) { diff --git a/pkg/runtime/serializer/json/json_test.go b/pkg/runtime/serializer/json/json_test.go index ee8692062..07a74e215 100644 --- a/pkg/runtime/serializer/json/json_test.go +++ b/pkg/runtime/serializer/json/json_test.go @@ -22,6 +22,8 @@ import ( "strings" "testing" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer/json" @@ -30,15 +32,15 @@ import ( ) type testDecodable struct { + metav1.TypeMeta `json:",inline"` + Other string Value int `json:"value"` Spec DecodableSpec `json:"spec"` Interface interface{} `json:"interface"` - gvk schema.GroupVersionKind } -// DecodableSpec has 15 fields. json-iterator treats struct with more than 10 -// fields differently from struct that has less than 10 fields. +// DecodableSpec has 15 fields. type DecodableSpec struct { A int `json:"A"` B int `json:"B"` @@ -57,9 +59,6 @@ type DecodableSpec struct { O int `json:"o"` } -func (d *testDecodable) GetObjectKind() schema.ObjectKind { return d } -func (d *testDecodable) SetGroupVersionKind(gvk schema.GroupVersionKind) { d.gvk = gvk } -func (d *testDecodable) GroupVersionKind() schema.GroupVersionKind { return d.gvk } func (d *testDecodable) DeepCopyObject() runtime.Object { if d == nil { return nil @@ -74,12 +73,44 @@ func (d *testDecodable) DeepCopyInto(out *testDecodable) { out.Value = d.Value out.Spec = d.Spec out.Interface = d.Interface - out.gvk = d.gvk + return +} + +type testDecodeCoercion struct { + metav1.TypeMeta `json:",inline"` + + Bool bool `json:"bool"` + + Int int `json:"int"` + Int32 int `json:"int32"` + Int64 int `json:"int64"` + + Float32 float32 `json:"float32"` + Float64 float64 `json:"float64"` + + String string `json:"string"` + + Struct testDecodable `json:"struct"` + + Array []string `json:"array"` + Map map[string]string `json:"map"` +} + +func (d *testDecodeCoercion) DeepCopyObject() runtime.Object { + if d == nil { + return nil + } + out := new(testDecodeCoercion) + d.DeepCopyInto(out) + return out +} +func (d *testDecodeCoercion) DeepCopyInto(out *testDecodeCoercion) { + *out = *d return } func TestDecode(t *testing.T) { - testCases := []struct { + type testCase struct { creater runtime.ObjectCreater typer runtime.ObjectTyper yaml bool @@ -93,13 +124,294 @@ func TestDecode(t *testing.T) { errFn func(error) bool expectedObject runtime.Object expectedGVK *schema.GroupVersionKind - }{ + } + + testCases := []testCase{ + // missing metadata without into, typed creater { data: []byte("{}"), expectedGVK: &schema.GroupVersionKind{}, errFn: func(err error) bool { return strings.Contains(err.Error(), "Object 'Kind' is missing in") }, }, + { + data: []byte("{}"), + + expectedGVK: &schema.GroupVersionKind{}, + errFn: func(err error) bool { return strings.Contains(err.Error(), "Object 'Kind' is missing in") }, + strict: true, + }, + + { + data: []byte(`{"kind":"Foo"}`), + + expectedGVK: &schema.GroupVersionKind{Kind: "Foo"}, + errFn: func(err error) bool { return strings.Contains(err.Error(), "Object 'apiVersion' is missing in") }, + }, + { + data: []byte(`{"kind":"Foo"}`), + + expectedGVK: &schema.GroupVersionKind{Kind: "Foo"}, + errFn: func(err error) bool { return strings.Contains(err.Error(), "Object 'apiVersion' is missing in") }, + strict: true, + }, + + { + data: []byte(`{"apiVersion":"foo/v1"}`), + + expectedGVK: &schema.GroupVersionKind{Group: "foo", Version: "v1"}, + errFn: func(err error) bool { return strings.Contains(err.Error(), "Object 'Kind' is missing in") }, + }, + { + data: []byte(`{"apiVersion":"foo/v1"}`), + + expectedGVK: &schema.GroupVersionKind{Group: "foo", Version: "v1"}, + errFn: func(err error) bool { return strings.Contains(err.Error(), "Object 'Kind' is missing in") }, + strict: true, + }, + + { + data: []byte(`{"apiVersion":"/v1","kind":"Foo"}`), + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + creater: &mockCreater{obj: &testDecodable{}}, + + expectedObject: &testDecodable{TypeMeta: metav1.TypeMeta{APIVersion: "/v1", Kind: "Foo"}}, + expectedGVK: &schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Foo"}, + }, + { + data: []byte(`{"apiVersion":"/v1","kind":"Foo"}`), + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + creater: &mockCreater{obj: &testDecodable{}}, + + expectedObject: &testDecodable{TypeMeta: metav1.TypeMeta{APIVersion: "/v1", Kind: "Foo"}}, + expectedGVK: &schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Foo"}, + strict: true, + }, + + // missing metadata with unstructured into + { + data: []byte("{}"), + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + into: &unstructured.Unstructured{}, + + expectedGVK: &schema.GroupVersionKind{}, + errFn: func(err error) bool { return strings.Contains(err.Error(), "Object 'Kind' is missing in") }, + }, + { + data: []byte("{}"), + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + into: &unstructured.Unstructured{}, + + expectedGVK: &schema.GroupVersionKind{}, + errFn: func(err error) bool { return strings.Contains(err.Error(), "Object 'Kind' is missing in") }, + strict: true, + }, + + { + data: []byte(`{"kind":"Foo"}`), + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + into: &unstructured.Unstructured{}, + + expectedGVK: &schema.GroupVersionKind{Kind: "Foo"}, + expectedObject: &unstructured.Unstructured{Object: map[string]interface{}{"kind": "Foo"}}, + // TODO(109023): expect this to error; unstructured decoding currently only requires kind to be set, not apiVersion + }, + { + data: []byte(`{"kind":"Foo"}`), + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + into: &unstructured.Unstructured{}, + + expectedGVK: &schema.GroupVersionKind{Kind: "Foo"}, + expectedObject: &unstructured.Unstructured{Object: map[string]interface{}{"kind": "Foo"}}, + strict: true, + // TODO(109023): expect this to error; unstructured decoding currently only requires kind to be set, not apiVersion + }, + + { + data: []byte(`{"apiVersion":"foo/v1"}`), + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + into: &unstructured.Unstructured{}, + + expectedGVK: &schema.GroupVersionKind{Group: "foo", Version: "v1"}, + errFn: func(err error) bool { return strings.Contains(err.Error(), "Object 'Kind' is missing in") }, + }, + { + data: []byte(`{"apiVersion":"foo/v1"}`), + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + into: &unstructured.Unstructured{}, + + expectedGVK: &schema.GroupVersionKind{Group: "foo", Version: "v1"}, + errFn: func(err error) bool { return strings.Contains(err.Error(), "Object 'Kind' is missing in") }, + strict: true, + }, + + { + data: []byte(`{"apiVersion":"/v1","kind":"Foo"}`), + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + into: &unstructured.Unstructured{}, + + expectedGVK: &schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Foo"}, + expectedObject: &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "/v1", "kind": "Foo"}}, + }, + { + data: []byte(`{"apiVersion":"/v1","kind":"Foo"}`), + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + into: &unstructured.Unstructured{}, + + expectedGVK: &schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Foo"}, + expectedObject: &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "/v1", "kind": "Foo"}}, + strict: true, + }, + + // missing metadata with unstructured into providing metadata + { + data: []byte("{}"), + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + into: &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "into/v1", "kind": "Into"}}, + + expectedGVK: &schema.GroupVersionKind{}, + errFn: func(err error) bool { return strings.Contains(err.Error(), "Object 'Kind' is missing in") }, + }, + { + data: []byte("{}"), + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + into: &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "into/v1", "kind": "Into"}}, + + expectedGVK: &schema.GroupVersionKind{}, + errFn: func(err error) bool { return strings.Contains(err.Error(), "Object 'Kind' is missing in") }, + strict: true, + }, + + { + data: []byte(`{"kind":"Foo"}`), + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + into: &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "into/v1", "kind": "Into"}}, + + expectedGVK: &schema.GroupVersionKind{Kind: "Foo"}, + expectedObject: &unstructured.Unstructured{Object: map[string]interface{}{"kind": "Foo"}}, + // TODO(109023): expect this to error; unstructured decoding currently only requires kind to be set, not apiVersion + }, + { + data: []byte(`{"kind":"Foo"}`), + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + into: &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "into/v1", "kind": "Into"}}, + + expectedGVK: &schema.GroupVersionKind{Kind: "Foo"}, + expectedObject: &unstructured.Unstructured{Object: map[string]interface{}{"kind": "Foo"}}, + strict: true, + // TODO(109023): expect this to error; unstructured decoding currently only requires kind to be set, not apiVersion + }, + + { + data: []byte(`{"apiVersion":"foo/v1"}`), + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + into: &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "into/v1", "kind": "Into"}}, + + expectedGVK: &schema.GroupVersionKind{Group: "foo", Version: "v1"}, + errFn: func(err error) bool { return strings.Contains(err.Error(), "Object 'Kind' is missing in") }, + }, + { + data: []byte(`{"apiVersion":"foo/v1"}`), + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + into: &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "into/v1", "kind": "Into"}}, + + expectedGVK: &schema.GroupVersionKind{Group: "foo", Version: "v1"}, + errFn: func(err error) bool { return strings.Contains(err.Error(), "Object 'Kind' is missing in") }, + strict: true, + }, + + { + data: []byte(`{"apiVersion":"/v1","kind":"Foo"}`), + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + into: &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "into/v1", "kind": "Into"}}, + + expectedGVK: &schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Foo"}, + expectedObject: &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "/v1", "kind": "Foo"}}, + }, + { + data: []byte(`{"apiVersion":"/v1","kind":"Foo"}`), + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + into: &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "into/v1", "kind": "Into"}}, + + expectedGVK: &schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Foo"}, + expectedObject: &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "/v1", "kind": "Foo"}}, + strict: true, + }, + + // missing metadata without into, unstructured creater + { + data: []byte("{}"), + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + creater: &mockCreater{obj: &unstructured.Unstructured{}}, + + expectedGVK: &schema.GroupVersionKind{}, + errFn: func(err error) bool { return strings.Contains(err.Error(), "Object 'Kind' is missing in") }, + }, + { + data: []byte("{}"), + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + creater: &mockCreater{obj: &unstructured.Unstructured{}}, + + expectedGVK: &schema.GroupVersionKind{}, + errFn: func(err error) bool { return strings.Contains(err.Error(), "Object 'Kind' is missing in") }, + strict: true, + }, + + { + data: []byte(`{"kind":"Foo"}`), + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + creater: &mockCreater{obj: &unstructured.Unstructured{}}, + + expectedGVK: &schema.GroupVersionKind{Kind: "Foo"}, + errFn: func(err error) bool { return strings.Contains(err.Error(), "Object 'apiVersion' is missing in") }, + }, + { + data: []byte(`{"kind":"Foo"}`), + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + creater: &mockCreater{obj: &unstructured.Unstructured{}}, + + expectedGVK: &schema.GroupVersionKind{Kind: "Foo"}, + errFn: func(err error) bool { return strings.Contains(err.Error(), "Object 'apiVersion' is missing in") }, + strict: true, + }, + + { + data: []byte(`{"apiVersion":"foo/v1"}`), + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + creater: &mockCreater{obj: &unstructured.Unstructured{}}, + + expectedGVK: &schema.GroupVersionKind{Group: "foo", Version: "v1"}, + errFn: func(err error) bool { return strings.Contains(err.Error(), "Object 'Kind' is missing in") }, + }, + { + data: []byte(`{"apiVersion":"foo/v1"}`), + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + creater: &mockCreater{obj: &unstructured.Unstructured{}}, + + expectedGVK: &schema.GroupVersionKind{Group: "foo", Version: "v1"}, + errFn: func(err error) bool { return strings.Contains(err.Error(), "Object 'Kind' is missing in") }, + strict: true, + }, + + { + data: []byte(`{"apiVersion":"/v1","kind":"Foo"}`), + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + creater: &mockCreater{obj: &unstructured.Unstructured{}}, + + expectedGVK: &schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Foo"}, + expectedObject: &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "/v1", "kind": "Foo"}}, + }, + { + data: []byte(`{"apiVersion":"/v1","kind":"Foo"}`), + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + creater: &mockCreater{obj: &unstructured.Unstructured{}}, + + expectedGVK: &schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Foo"}, + expectedObject: &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "/v1", "kind": "Foo"}}, + strict: true, + }, + + // creator errors { data: []byte("{}"), defaultGVK: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}, @@ -108,12 +420,29 @@ func TestDecode(t *testing.T) { expectedGVK: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}, errFn: func(err error) bool { return err.Error() == "fake error" }, }, + { + data: []byte("{}"), + defaultGVK: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}, + creater: &mockCreater{err: fmt.Errorf("fake error")}, + + expectedGVK: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}, + errFn: func(err error) bool { return err.Error() == "fake error" }, + }, + // creator typed + { + data: []byte("{}"), + defaultGVK: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}, + creater: &mockCreater{obj: &testDecodable{}}, + expectedObject: &testDecodable{}, + expectedGVK: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}, + }, { data: []byte("{}"), defaultGVK: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}, creater: &mockCreater{obj: &testDecodable{}}, expectedObject: &testDecodable{}, expectedGVK: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}, + strict: true, }, // version without group is not defaulted @@ -121,7 +450,7 @@ func TestDecode(t *testing.T) { data: []byte(`{"apiVersion":"blah"}`), defaultGVK: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}, creater: &mockCreater{obj: &testDecodable{}}, - expectedObject: &testDecodable{}, + expectedObject: &testDecodable{TypeMeta: metav1.TypeMeta{APIVersion: "blah"}}, expectedGVK: &schema.GroupVersionKind{Kind: "Test", Group: "", Version: "blah"}, }, // group without version is defaulted @@ -129,7 +458,7 @@ func TestDecode(t *testing.T) { data: []byte(`{"apiVersion":"other/"}`), defaultGVK: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}, creater: &mockCreater{obj: &testDecodable{}}, - expectedObject: &testDecodable{}, + expectedObject: &testDecodable{TypeMeta: metav1.TypeMeta{APIVersion: "other/"}}, expectedGVK: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}, }, // group version, kind is defaulted @@ -137,7 +466,7 @@ func TestDecode(t *testing.T) { data: []byte(`{"apiVersion":"other1/blah1"}`), defaultGVK: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}, creater: &mockCreater{obj: &testDecodable{}}, - expectedObject: &testDecodable{}, + expectedObject: &testDecodable{TypeMeta: metav1.TypeMeta{APIVersion: "other1/blah1"}}, expectedGVK: &schema.GroupVersionKind{Kind: "Test", Group: "other1", Version: "blah1"}, }, // gvk all provided then not defaulted at all @@ -145,17 +474,17 @@ func TestDecode(t *testing.T) { data: []byte(`{"kind":"Test","apiVersion":"other/blah"}`), defaultGVK: &schema.GroupVersionKind{Kind: "Test1", Group: "other1", Version: "blah1"}, creater: &mockCreater{obj: &testDecodable{}}, - expectedObject: &testDecodable{}, + expectedObject: &testDecodable{TypeMeta: metav1.TypeMeta{APIVersion: "other/blah", Kind: "Test"}}, expectedGVK: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}, }, //gvk defaulting if kind not provided in data and defaultGVK use into's kind { data: []byte(`{"apiVersion":"b1/c1"}`), - into: &testDecodable{gvk: schema.GroupVersionKind{Kind: "a3", Group: "b1", Version: "c1"}}, + into: &testDecodable{TypeMeta: metav1.TypeMeta{Kind: "a3", APIVersion: "b1/c1"}}, typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "a3", Group: "b1", Version: "c1"}}, defaultGVK: nil, creater: &mockCreater{obj: &testDecodable{}}, - expectedObject: &testDecodable{gvk: schema.GroupVersionKind{Kind: "a3", Group: "b1", Version: "c1"}}, + expectedObject: &testDecodable{TypeMeta: metav1.TypeMeta{Kind: "a3", APIVersion: "b1/c1"}}, expectedGVK: &schema.GroupVersionKind{Kind: "a3", Group: "b1", Version: "c1"}, }, @@ -199,8 +528,9 @@ func TestDecode(t *testing.T) { typer: &mockTyper{err: runtime.NewNotRegisteredErrForKind("mock", schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"})}, expectedGVK: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}, expectedObject: &testDecodable{ - Other: "test", - Value: 1, + TypeMeta: metav1.TypeMeta{APIVersion: "other/blah", Kind: "Test"}, + Other: "test", + Value: 1, }, }, // registered types get defaulted by the into object kind @@ -232,7 +562,7 @@ func TestDecode(t *testing.T) { creater: &mockCreater{obj: &testDecodable{}}, expectedGVK: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}, errFn: func(err error) bool { - return strings.Contains(err.Error(), `json_test.testDecodable.Interface: DecodeNumber: strconv.ParseFloat: parsing "1e1000": value out of range`) + return strings.Contains(err.Error(), `json: cannot unmarshal number 1e1000 into Go struct field testDecodable.interface of type float64`) }, }, // Unmarshalling is case-sensitive @@ -243,7 +573,8 @@ func TestDecode(t *testing.T) { typer: &mockTyper{err: runtime.NewNotRegisteredErrForKind("mock", schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"})}, expectedGVK: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}, expectedObject: &testDecodable{ - Other: "test", + TypeMeta: metav1.TypeMeta{APIVersion: "other/blah", Kind: "Test"}, + Other: "test", }, }, // Unmarshalling is case-sensitive for big struct. @@ -254,7 +585,8 @@ func TestDecode(t *testing.T) { typer: &mockTyper{err: runtime.NewNotRegisteredErrForKind("mock", schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"})}, expectedGVK: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}, expectedObject: &testDecodable{ - Spec: DecodableSpec{A: 1, H: 3}, + TypeMeta: metav1.TypeMeta{APIVersion: "other/blah", Kind: "Test"}, + Spec: DecodableSpec{A: 1, H: 3}, }, }, // Unknown fields should return an error from the strict JSON deserializer. @@ -264,7 +596,7 @@ func TestDecode(t *testing.T) { typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, expectedGVK: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}, errFn: func(err error) bool { - return strings.Contains(err.Error(), "found unknown field") + return strings.Contains(err.Error(), `unknown field "unknown"`) }, strict: true, }, @@ -275,7 +607,7 @@ func TestDecode(t *testing.T) { typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, expectedGVK: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}, errFn: func(err error) bool { - return strings.Contains(err.Error(), "found unknown field") + return strings.Contains(err.Error(), `unknown field "unknown"`) }, yaml: true, strict: true, @@ -287,7 +619,7 @@ func TestDecode(t *testing.T) { typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, expectedGVK: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}, errFn: func(err error) bool { - return strings.Contains(err.Error(), "already set in map") + return strings.Contains(err.Error(), `duplicate field "value"`) }, strict: true, }, @@ -299,33 +631,32 @@ func TestDecode(t *testing.T) { typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, expectedGVK: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}, errFn: func(err error) bool { - return strings.Contains(err.Error(), "already set in map") + return strings.Contains(err.Error(), `"value" already set in map`) }, yaml: true, strict: true, }, - // Strict JSON decode should fail for untagged fields. + // Duplicate fields should return an error from the strict JSON deserializer for unstructured. { - data: []byte(`{"kind":"Test","apiVersion":"other/blah","value":1,"Other":"test"}`), - into: &testDecodable{}, + data: []byte(`{"kind":"Custom","value":1,"value":1}`), + into: &unstructured.Unstructured{}, typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, - expectedGVK: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}, + expectedGVK: &schema.GroupVersionKind{Kind: "Custom"}, errFn: func(err error) bool { - return strings.Contains(err.Error(), "found unknown field") + return strings.Contains(err.Error(), `duplicate field "value"`) }, strict: true, }, - // Strict YAML decode should fail for untagged fields. + // Duplicate fields should return an error from the strict YAML deserializer for unstructured. { - data: []byte("kind: Test\n" + - "apiVersion: other/blah\n" + + data: []byte("kind: Custom\n" + "value: 1\n" + - "Other: test\n"), - into: &testDecodable{}, + "value: 1\n"), + into: &unstructured.Unstructured{}, typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, - expectedGVK: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}, + expectedGVK: &schema.GroupVersionKind{Kind: "Custom"}, errFn: func(err error) bool { - return strings.Contains(err.Error(), "found unknown field") + return strings.Contains(err.Error(), `"value" already set in map`) }, yaml: true, strict: true, @@ -337,8 +668,9 @@ func TestDecode(t *testing.T) { typer: &mockTyper{err: runtime.NewNotRegisteredErrForKind("mock", schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"})}, expectedGVK: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}, expectedObject: &testDecodable{ - Other: "test", - Value: 1, + TypeMeta: metav1.TypeMeta{APIVersion: "other/blah", Kind: "Test"}, + Other: "test", + Value: 1, }, strict: true, }, @@ -352,8 +684,9 @@ func TestDecode(t *testing.T) { typer: &mockTyper{err: runtime.NewNotRegisteredErrForKind("mock", schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"})}, expectedGVK: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}, expectedObject: &testDecodable{ - Other: "test", - Value: 1, + TypeMeta: metav1.TypeMeta{APIVersion: "other/blah", Kind: "Test"}, + Other: "test", + Value: 1, }, yaml: true, strict: true, @@ -381,6 +714,139 @@ func TestDecode(t *testing.T) { yaml: true, strict: true, }, + // Invalid strict JSON, results in json parse error: + { + data: []byte("foo"), + into: &unstructured.Unstructured{}, + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + errFn: func(err error) bool { + return strings.Contains(err.Error(), `json parse error: invalid character 'o'`) + }, + strict: true, + }, + // empty JSON strict, results in missing kind error + { + data: []byte("{}"), + into: &unstructured.Unstructured{}, + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + expectedGVK: &schema.GroupVersionKind{}, + errFn: func(err error) bool { + return strings.Contains(err.Error(), `Object 'Kind' is missing`) + }, + strict: true, + }, + // coerce from null + { + data: []byte(`{"bool":null,"int":null,"int32":null,"int64":null,"float32":null,"float64":null,"string":null,"array":null,"map":null,"struct":null}`), + into: &testDecodeCoercion{}, + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + expectedGVK: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}, + expectedObject: &testDecodeCoercion{}, + strict: true, + }, + { + data: []byte(`{"bool":null,"int":null,"int32":null,"int64":null,"float32":null,"float64":null,"string":null,"array":null,"map":null,"struct":null}`), + into: &testDecodeCoercion{}, + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + expectedGVK: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}, + expectedObject: &testDecodeCoercion{}, + yaml: true, + strict: true, + }, + // coerce from string + { + data: []byte(`{"string":""}`), + into: &testDecodeCoercion{}, + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + expectedGVK: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}, + expectedObject: &testDecodeCoercion{}, + strict: true, + }, + { + data: []byte(`{"string":""}`), + into: &testDecodeCoercion{}, + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + expectedGVK: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}, + expectedObject: &testDecodeCoercion{}, + yaml: true, + strict: true, + }, + // coerce from array + { + data: []byte(`{"array":[]}`), + into: &testDecodeCoercion{}, + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + expectedGVK: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}, + expectedObject: &testDecodeCoercion{Array: []string{}}, + strict: true, + }, + { + data: []byte(`{"array":[]}`), + into: &testDecodeCoercion{}, + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + expectedGVK: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}, + expectedObject: &testDecodeCoercion{Array: []string{}}, + yaml: true, + strict: true, + }, + // coerce from map + { + data: []byte(`{"map":{},"struct":{}}`), + into: &testDecodeCoercion{}, + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + expectedGVK: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}, + expectedObject: &testDecodeCoercion{Map: map[string]string{}}, + strict: true, + }, + { + data: []byte(`{"map":{},"struct":{}}`), + into: &testDecodeCoercion{}, + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + expectedGVK: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}, + expectedObject: &testDecodeCoercion{Map: map[string]string{}}, + yaml: true, + strict: true, + }, + // coerce from int + { + data: []byte(`{"int":1,"int32":1,"int64":1,"float32":1,"float64":1}`), + into: &testDecodeCoercion{}, + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + expectedGVK: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}, + expectedObject: &testDecodeCoercion{Int: 1, Int32: 1, Int64: 1, Float32: 1, Float64: 1}, + strict: true, + }, + { + data: []byte(`{"int":1,"int32":1,"int64":1,"float32":1,"float64":1}`), + into: &testDecodeCoercion{}, + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + expectedGVK: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}, + expectedObject: &testDecodeCoercion{Int: 1, Int32: 1, Int64: 1, Float32: 1, Float64: 1}, + yaml: true, + strict: true, + }, + // coerce from float + { + data: []byte(`{"float32":1.0,"float64":1.0}`), + into: &testDecodeCoercion{}, + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + expectedGVK: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}, + expectedObject: &testDecodeCoercion{Float32: 1, Float64: 1}, + strict: true, + }, + { + data: []byte(`{"int":1.0,"int32":1.0,"int64":1.0,"float32":1.0,"float64":1.0}`), // floating point gets dropped in yaml -> json step + into: &testDecodeCoercion{}, + typer: &mockTyper{gvk: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}}, + expectedGVK: &schema.GroupVersionKind{Kind: "Test", Group: "other", Version: "blah"}, + expectedObject: &testDecodeCoercion{Int: 1, Int32: 1, Int64: 1, Float32: 1, Float64: 1}, + yaml: true, + strict: true, + }, + } + + logTestCase := func(t *testing.T, tc testCase) { + t.Logf("data=%s\n\tinto=%T, yaml=%v, strict=%v", string(tc.data), tc.into, tc.yaml, tc.strict) } for i, test := range testCases { @@ -393,32 +859,39 @@ func TestDecode(t *testing.T) { obj, gvk, err := s.Decode([]byte(test.data), test.defaultGVK, test.into) if !reflect.DeepEqual(test.expectedGVK, gvk) { + logTestCase(t, test) t.Errorf("%d: unexpected GVK: %v", i, gvk) } switch { case err == nil && test.errFn != nil: + logTestCase(t, test) t.Errorf("%d: failed: not getting the expected error", i) continue case err != nil && test.errFn == nil: + logTestCase(t, test) t.Errorf("%d: failed: %v", i, err) continue case err != nil: if !test.errFn(err) { + logTestCase(t, test) t.Errorf("%d: failed: %v", i, err) } - if obj != nil { + if !runtime.IsStrictDecodingError(err) && obj != nil { + logTestCase(t, test) t.Errorf("%d: should have returned nil object", i) } continue } if test.into != nil && test.into != obj { + logTestCase(t, test) t.Errorf("%d: expected into to be returned: %v", i, obj) continue } if !reflect.DeepEqual(test.expectedObject, obj) { + logTestCase(t, test) t.Errorf("%d: unexpected object:\n%s", i, diff.ObjectGoPrintSideBySide(test.expectedObject, obj)) } } diff --git a/pkg/runtime/serializer/protobuf/protobuf.go b/pkg/runtime/serializer/protobuf/protobuf.go index 8358d77c3..c63e6dc63 100644 --- a/pkg/runtime/serializer/protobuf/protobuf.go +++ b/pkg/runtime/serializer/protobuf/protobuf.go @@ -30,6 +30,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer/recognizer" "k8s.io/apimachinery/pkg/util/framer" + "k8s.io/klog/v2" ) var ( @@ -86,6 +87,7 @@ type Serializer struct { } var _ runtime.Serializer = &Serializer{} +var _ runtime.EncoderWithAllocator = &Serializer{} var _ recognizer.RecognizingDecoder = &Serializer{} const serializerIdentifier runtime.Identifier = "protobuf" @@ -161,22 +163,36 @@ func (s *Serializer) Decode(originalData []byte, gvk *schema.GroupVersionKind, i return unmarshalToObject(s.typer, s.creater, &actual, into, unk.Raw) } +// EncodeWithAllocator writes an object to the provided writer. +// In addition, it allows for providing a memory allocator for efficient memory usage during object serialization. +func (s *Serializer) EncodeWithAllocator(obj runtime.Object, w io.Writer, memAlloc runtime.MemoryAllocator) error { + return s.encode(obj, w, memAlloc) +} + // Encode serializes the provided object to the given writer. func (s *Serializer) Encode(obj runtime.Object, w io.Writer) error { + return s.encode(obj, w, &runtime.SimpleAllocator{}) +} + +func (s *Serializer) encode(obj runtime.Object, w io.Writer, memAlloc runtime.MemoryAllocator) error { if co, ok := obj.(runtime.CacheableObject); ok { - return co.CacheEncode(s.Identifier(), s.doEncode, w) + return co.CacheEncode(s.Identifier(), func(obj runtime.Object, w io.Writer) error { return s.doEncode(obj, w, memAlloc) }, w) } - return s.doEncode(obj, w) + return s.doEncode(obj, w, memAlloc) } -func (s *Serializer) doEncode(obj runtime.Object, w io.Writer) error { +func (s *Serializer) doEncode(obj runtime.Object, w io.Writer, memAlloc runtime.MemoryAllocator) error { + if memAlloc == nil { + klog.Error("a mandatory memory allocator wasn't provided, this might have a negative impact on performance, check invocations of EncodeWithAllocator method, falling back on runtime.SimpleAllocator") + memAlloc = &runtime.SimpleAllocator{} + } prefixSize := uint64(len(s.prefix)) var unk runtime.Unknown switch t := obj.(type) { case *runtime.Unknown: estimatedSize := prefixSize + uint64(t.Size()) - data := make([]byte, estimatedSize) + data := memAlloc.Allocate(estimatedSize) i, err := t.MarshalTo(data[prefixSize:]) if err != nil { return err @@ -196,11 +212,11 @@ func (s *Serializer) doEncode(obj runtime.Object, w io.Writer) error { switch t := obj.(type) { case bufferedMarshaller: - // this path performs a single allocation during write but requires the caller to implement - // the more efficient Size and MarshalToSizedBuffer methods + // this path performs a single allocation during write only when the Allocator wasn't provided + // it also requires the caller to implement the more efficient Size and MarshalToSizedBuffer methods encodedSize := uint64(t.Size()) estimatedSize := prefixSize + estimateUnknownSize(&unk, encodedSize) - data := make([]byte, estimatedSize) + data := memAlloc.Allocate(estimatedSize) i, err := unk.NestedMarshalTo(data[prefixSize:], t, encodedSize) if err != nil { @@ -221,7 +237,7 @@ func (s *Serializer) doEncode(obj runtime.Object, w io.Writer) error { unk.Raw = data estimatedSize := prefixSize + uint64(unk.Size()) - data = make([]byte, estimatedSize) + data = memAlloc.Allocate(estimatedSize) i, err := unk.MarshalTo(data[prefixSize:]) if err != nil { @@ -395,19 +411,33 @@ func unmarshalToObject(typer runtime.ObjectTyper, creater runtime.ObjectCreater, // Encode serializes the provided object to the given writer. Overrides is ignored. func (s *RawSerializer) Encode(obj runtime.Object, w io.Writer) error { + return s.encode(obj, w, &runtime.SimpleAllocator{}) +} + +// EncodeWithAllocator writes an object to the provided writer. +// In addition, it allows for providing a memory allocator for efficient memory usage during object serialization. +func (s *RawSerializer) EncodeWithAllocator(obj runtime.Object, w io.Writer, memAlloc runtime.MemoryAllocator) error { + return s.encode(obj, w, memAlloc) +} + +func (s *RawSerializer) encode(obj runtime.Object, w io.Writer, memAlloc runtime.MemoryAllocator) error { if co, ok := obj.(runtime.CacheableObject); ok { - return co.CacheEncode(s.Identifier(), s.doEncode, w) + return co.CacheEncode(s.Identifier(), func(obj runtime.Object, w io.Writer) error { return s.doEncode(obj, w, memAlloc) }, w) } - return s.doEncode(obj, w) + return s.doEncode(obj, w, memAlloc) } -func (s *RawSerializer) doEncode(obj runtime.Object, w io.Writer) error { +func (s *RawSerializer) doEncode(obj runtime.Object, w io.Writer, memAlloc runtime.MemoryAllocator) error { + if memAlloc == nil { + klog.Error("a mandatory memory allocator wasn't provided, this might have a negative impact on performance, check invocations of EncodeWithAllocator method, falling back on runtime.SimpleAllocator") + memAlloc = &runtime.SimpleAllocator{} + } switch t := obj.(type) { case bufferedReverseMarshaller: - // this path performs a single allocation during write but requires the caller to implement - // the more efficient Size and MarshalToSizedBuffer methods + // this path performs a single allocation during write only when the Allocator wasn't provided + // it also requires the caller to implement the more efficient Size and MarshalToSizedBuffer methods encodedSize := uint64(t.Size()) - data := make([]byte, encodedSize) + data := memAlloc.Allocate(encodedSize) n, err := t.MarshalToSizedBuffer(data) if err != nil { @@ -417,10 +447,10 @@ func (s *RawSerializer) doEncode(obj runtime.Object, w io.Writer) error { return err case bufferedMarshaller: - // this path performs a single allocation during write but requires the caller to implement - // the more efficient Size and MarshalTo methods + // this path performs a single allocation during write only when the Allocator wasn't provided + // it also requires the caller to implement the more efficient Size and MarshalTo methods encodedSize := uint64(t.Size()) - data := make([]byte, encodedSize) + data := memAlloc.Allocate(encodedSize) n, err := t.MarshalTo(data) if err != nil { diff --git a/pkg/runtime/serializer/protobuf/protobuf_test.go b/pkg/runtime/serializer/protobuf/protobuf_test.go index 001b0f647..df9a52d5d 100644 --- a/pkg/runtime/serializer/protobuf/protobuf_test.go +++ b/pkg/runtime/serializer/protobuf/protobuf_test.go @@ -17,8 +17,12 @@ limitations under the License. package protobuf import ( + "bytes" + "reflect" "testing" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + testapigroupv1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" runtimetesting "k8s.io/apimachinery/pkg/runtime/testing" @@ -66,3 +70,111 @@ func (t *mockTyper) ObjectKinds(obj runtime.Object) ([]schema.GroupVersionKind, func (t *mockTyper) Recognizes(_ schema.GroupVersionKind) bool { return false } + +func TestSerializerEncodeWithAllocator(t *testing.T) { + testCases := []struct { + name string + obj runtime.Object + }{ + { + name: "encode a bufferedMarshaller obj", + obj: &testapigroupv1.Carp{ + TypeMeta: metav1.TypeMeta{APIVersion: "group/version", Kind: "Carp"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "name", + Namespace: "namespace", + }, + Spec: testapigroupv1.CarpSpec{ + Subdomain: "carp.k8s.io", + }, + }, + }, + + { + name: "encode a runtime.Unknown obj", + obj: &runtime.Unknown{TypeMeta: runtime.TypeMeta{APIVersion: "group/version", Kind: "Unknown"}, Raw: []byte("hello world")}, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + target := NewSerializer(nil, nil) + + writer := &bytes.Buffer{} + if err := target.Encode(tc.obj, writer); err != nil { + t.Fatal(err) + } + + writer2 := &bytes.Buffer{} + alloc := &testAllocator{} + if err := target.EncodeWithAllocator(tc.obj, writer2, alloc); err != nil { + t.Fatal(err) + } + if alloc.allocateCount != 1 { + t.Fatalf("expected the Allocate method to be called exactly 1 but it was executed: %v times ", alloc.allocateCount) + } + + // to ensure compatibility of the new method with the old one, serialized data must be equal + // also we are not testing decoding since "roundtripping" is tested elsewhere for all known types + if !reflect.DeepEqual(writer.Bytes(), writer2.Bytes()) { + t.Fatal("data mismatch, data serialized with the Encode method is different than serialized with the EncodeWithAllocator method") + } + }) + } +} + +func TestRawSerializerEncodeWithAllocator(t *testing.T) { + testCases := []struct { + name string + obj runtime.Object + }{ + { + name: "encode a bufferedReverseMarshaller obj", + obj: &testapigroupv1.Carp{ + TypeMeta: metav1.TypeMeta{APIVersion: "group/version", Kind: "Carp"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "name", + Namespace: "namespace", + }, + Spec: testapigroupv1.CarpSpec{ + Subdomain: "carp.k8s.io", + }, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + writer := &bytes.Buffer{} + target := NewRawSerializer(nil, nil) + + if err := target.Encode(tc.obj, writer); err != nil { + t.Fatal(err) + } + + writer2 := &bytes.Buffer{} + alloc := &testAllocator{} + if err := target.EncodeWithAllocator(tc.obj, writer2, alloc); err != nil { + t.Fatal(err) + } + if alloc.allocateCount != 1 { + t.Fatalf("expected the Allocate method to be called exactly 1 but it was executed: %v times ", alloc.allocateCount) + } + + // to ensure compatibility of the new method with the old one, serialized data must be equal + // also we are not testing decoding since "roundtripping" is tested elsewhere for all known types + if !reflect.DeepEqual(writer.Bytes(), writer2.Bytes()) { + t.Fatal("data mismatch, data serialized with the Encode method is different than serialized with the EncodeWithAllocator method") + } + }) + } +} + +type testAllocator struct { + buf []byte + allocateCount int +} + +func (ta *testAllocator) Allocate(n uint64) []byte { + ta.buf = make([]byte, n) + ta.allocateCount++ + return ta.buf +} diff --git a/pkg/runtime/serializer/recognizer/recognizer.go b/pkg/runtime/serializer/recognizer/recognizer.go index 709f85291..5a6c200dd 100644 --- a/pkg/runtime/serializer/recognizer/recognizer.go +++ b/pkg/runtime/serializer/recognizer/recognizer.go @@ -109,10 +109,16 @@ func (d *decoder) Decode(data []byte, gvk *schema.GroupVersionKind, into runtime for _, r := range skipped { out, actual, err := r.Decode(data, gvk, into) if err != nil { - lastErr = err - continue + // if we got an object back from the decoder, and the + // error was a strict decoding error (e.g. unknown or + // duplicate fields), we still consider the recognizer + // to have understood the object + if out == nil || !runtime.IsStrictDecodingError(err) { + lastErr = err + continue + } } - return out, actual, nil + return out, actual, err } if lastErr == nil { diff --git a/pkg/runtime/serializer/streaming/streaming.go b/pkg/runtime/serializer/streaming/streaming.go index a60a7c041..87b3fec3f 100644 --- a/pkg/runtime/serializer/streaming/streaming.go +++ b/pkg/runtime/serializer/streaming/streaming.go @@ -90,7 +90,6 @@ func (d *decoder) Decode(defaults *schema.GroupVersionKind, into runtime.Object) } // must read the rest of the frame (until we stop getting ErrShortBuffer) d.resetRead = true - base = 0 return nil, nil, ErrObjectTooLarge } if err != nil { @@ -135,3 +134,23 @@ func (e *encoder) Encode(obj runtime.Object) error { e.buf.Reset() return err } + +type encoderWithAllocator struct { + writer io.Writer + encoder runtime.EncoderWithAllocator + memAllocator runtime.MemoryAllocator +} + +// NewEncoderWithAllocator returns a new streaming encoder +func NewEncoderWithAllocator(w io.Writer, e runtime.EncoderWithAllocator, a runtime.MemoryAllocator) Encoder { + return &encoderWithAllocator{ + writer: w, + encoder: e, + memAllocator: a, + } +} + +// Encode writes the provided object to the nested writer +func (e *encoderWithAllocator) Encode(obj runtime.Object) error { + return e.encoder.EncodeWithAllocator(obj, e.writer, e.memAllocator) +} diff --git a/pkg/runtime/serializer/versioning/versioning.go b/pkg/runtime/serializer/versioning/versioning.go index 718c5dfb7..446633182 100644 --- a/pkg/runtime/serializer/versioning/versioning.go +++ b/pkg/runtime/serializer/versioning/versioning.go @@ -89,6 +89,8 @@ type codec struct { originalSchemeName string } +var _ runtime.EncoderWithAllocator = &codec{} + var identifiersMap sync.Map type codecIdentifier struct { @@ -133,17 +135,34 @@ func (c *codec) Decode(data []byte, defaultGVK *schema.GroupVersionKind, into ru } } + var strictDecodingErrs []error obj, gvk, err := c.decoder.Decode(data, defaultGVK, decodeInto) if err != nil { - return nil, gvk, err + if strictErr, ok := runtime.AsStrictDecodingError(err); obj != nil && ok { + // save the strictDecodingError and let the caller decide what to do with it + strictDecodingErrs = append(strictDecodingErrs, strictErr.Errors()...) + } else { + return nil, gvk, err + } } if d, ok := obj.(runtime.NestedObjectDecoder); ok { if err := d.DecodeNestedObjects(runtime.WithoutVersionDecoder{c.decoder}); err != nil { - return nil, gvk, err + if strictErr, ok := runtime.AsStrictDecodingError(err); ok { + // save the strictDecodingError let and the caller decide what to do with it + strictDecodingErrs = append(strictDecodingErrs, strictErr.Errors()...) + } else { + return nil, gvk, err + + } } } + // aggregate the strict decoding errors into one + var strictDecodingErr error + if len(strictDecodingErrs) > 0 { + strictDecodingErr = runtime.NewStrictDecodingError(strictDecodingErrs) + } // if we specify a target, use generic conversion. if into != nil { // perform defaulting if requested @@ -153,14 +172,14 @@ func (c *codec) Decode(data []byte, defaultGVK *schema.GroupVersionKind, into ru // Short-circuit conversion if the into object is same object if into == obj { - return into, gvk, nil + return into, gvk, strictDecodingErr } if err := c.convertor.Convert(obj, into, c.decodeVersion); err != nil { return nil, gvk, err } - return into, gvk, nil + return into, gvk, strictDecodingErr } // perform defaulting if requested @@ -172,22 +191,43 @@ func (c *codec) Decode(data []byte, defaultGVK *schema.GroupVersionKind, into ru if err != nil { return nil, gvk, err } - return out, gvk, nil + return out, gvk, strictDecodingErr +} + +// EncodeWithAllocator ensures the provided object is output in the appropriate group and version, invoking +// conversion if necessary. Unversioned objects (according to the ObjectTyper) are output as is. +// In addition, it allows for providing a memory allocator for efficient memory usage during object serialization. +func (c *codec) EncodeWithAllocator(obj runtime.Object, w io.Writer, memAlloc runtime.MemoryAllocator) error { + return c.encode(obj, w, memAlloc) } // Encode ensures the provided object is output in the appropriate group and version, invoking // conversion if necessary. Unversioned objects (according to the ObjectTyper) are output as is. func (c *codec) Encode(obj runtime.Object, w io.Writer) error { + return c.encode(obj, w, nil) +} + +func (c *codec) encode(obj runtime.Object, w io.Writer, memAlloc runtime.MemoryAllocator) error { if co, ok := obj.(runtime.CacheableObject); ok { - return co.CacheEncode(c.Identifier(), c.doEncode, w) + return co.CacheEncode(c.Identifier(), func(obj runtime.Object, w io.Writer) error { return c.doEncode(obj, w, memAlloc) }, w) } - return c.doEncode(obj, w) + return c.doEncode(obj, w, memAlloc) } -func (c *codec) doEncode(obj runtime.Object, w io.Writer) error { +func (c *codec) doEncode(obj runtime.Object, w io.Writer, memAlloc runtime.MemoryAllocator) error { + encodeFn := c.encoder.Encode + if memAlloc != nil { + if encoder, supportsAllocator := c.encoder.(runtime.EncoderWithAllocator); supportsAllocator { + encodeFn = func(obj runtime.Object, w io.Writer) error { + return encoder.EncodeWithAllocator(obj, w, memAlloc) + } + } else { + klog.V(6).Infof("a memory allocator was provided but the encoder %s doesn't implement the runtime.EncoderWithAllocator, using regular encoder.Encode method", c.encoder.Identifier()) + } + } switch obj := obj.(type) { case *runtime.Unknown: - return c.encoder.Encode(obj, w) + return encodeFn(obj, w) case runtime.Unstructured: // An unstructured list can contain objects of multiple group version kinds. don't short-circuit just // because the top-level type matches our desired destination type. actually send the object to the converter @@ -196,14 +236,14 @@ func (c *codec) doEncode(obj runtime.Object, w io.Writer) error { // avoid conversion roundtrip if GVK is the right one already or is empty (yes, this is a hack, but the old behaviour we rely on in kubectl) objGVK := obj.GetObjectKind().GroupVersionKind() if len(objGVK.Version) == 0 { - return c.encoder.Encode(obj, w) + return encodeFn(obj, w) } targetGVK, ok := c.encodeVersion.KindForGroupVersionKinds([]schema.GroupVersionKind{objGVK}) if !ok { return runtime.NewNotRegisteredGVKErrForTarget(c.originalSchemeName, objGVK, c.encodeVersion) } if targetGVK == objGVK { - return c.encoder.Encode(obj, w) + return encodeFn(obj, w) } } } @@ -225,7 +265,7 @@ func (c *codec) doEncode(obj runtime.Object, w io.Writer) error { } } objectKind.SetGroupVersionKind(gvks[0]) - return c.encoder.Encode(obj, w) + return encodeFn(obj, w) } // Perform a conversion if necessary @@ -241,7 +281,7 @@ func (c *codec) doEncode(obj runtime.Object, w io.Writer) error { } // Conversion is responsible for setting the proper group, version, and kind onto the outgoing object - return c.encoder.Encode(out, w) + return encodeFn(out, w) } // Identifier implements runtime.Encoder interface. diff --git a/pkg/runtime/serializer/versioning/versioning_test.go b/pkg/runtime/serializer/versioning/versioning_test.go index ee3058c3c..ee12a69ad 100644 --- a/pkg/runtime/serializer/versioning/versioning_test.go +++ b/pkg/runtime/serializer/versioning/versioning_test.go @@ -82,6 +82,23 @@ func TestNestedDecode(t *testing.T) { } } +func TestNestedDecodeStrictDecodingError(t *testing.T) { + strictErr := runtime.NewStrictDecodingError([]error{fmt.Errorf("duplicate field")}) + n := &testNestedDecodable{nestedErr: strictErr} + decoder := &mockSerializer{obj: n} + codec := NewCodec(nil, decoder, nil, nil, nil, nil, nil, nil, "TestNestedDecode") + o, _, err := codec.Decode([]byte(`{}`), nil, n) + if strictErr, ok := runtime.AsStrictDecodingError(err); !ok || err != strictErr { + t.Errorf("unexpected error: %v", err) + } + if o != n { + t.Errorf("did not successfully decode with strict decoding error: %v", o) + } + if !n.nestedCalled { + t.Errorf("did not invoke nested decoder") + } +} + func TestNestedEncode(t *testing.T) { n := &testNestedDecodable{nestedErr: fmt.Errorf("unable to decode")} n2 := &testNestedDecodable{nestedErr: fmt.Errorf("unable to decode 2")} @@ -108,8 +125,9 @@ func TestNestedEncodeError(t *testing.T) { gvk1 := schema.GroupVersionKind{Kind: "test", Group: "other", Version: "v1"} gvk2 := schema.GroupVersionKind{Kind: "test", Group: "other", Version: "v2"} n.SetGroupVersionKind(gvk1) + encoder := &mockSerializer{obj: n} codec := NewCodec( - nil, nil, + encoder, nil, &mockConvertor{}, nil, &mockTyper{gvks: []schema.GroupVersionKind{gvk1, gvk2}}, diff --git a/pkg/runtime/testing/zz_generated.deepcopy.go b/pkg/runtime/testing/zz_generated.deepcopy.go index d67d76dc8..c0d441957 100644 --- a/pkg/runtime/testing/zz_generated.deepcopy.go +++ b/pkg/runtime/testing/zz_generated.deepcopy.go @@ -1,3 +1,4 @@ +//go:build !ignore_autogenerated // +build !ignore_autogenerated /* diff --git a/pkg/runtime/types.go b/pkg/runtime/types.go index 31359f35f..3dc9a5a2f 100644 --- a/pkg/runtime/types.go +++ b/pkg/runtime/types.go @@ -21,10 +21,12 @@ package runtime // TypeMeta is shared by all top level objects. The proper way to use it is to inline it in your type, // like this: -// type MyAwesomeAPIObject struct { -// runtime.TypeMeta `json:",inline"` -// ... // other fields -// } +// +// type MyAwesomeAPIObject struct { +// runtime.TypeMeta `json:",inline"` +// ... // other fields +// } +// // func (obj *MyAwesomeAPIObject) SetGroupVersionKind(gvk *metav1.GroupVersionKind) { metav1.UpdateTypeMeta(obj,gvk) }; GroupVersionKind() *GroupVersionKind // // TypeMeta is provided here for convenience. You may use it directly from this package or define @@ -53,32 +55,37 @@ const ( // various plugin types. // // // Internal package: -// type MyAPIObject struct { -// runtime.TypeMeta `json:",inline"` -// MyPlugin runtime.Object `json:"myPlugin"` -// } -// type PluginA struct { -// AOption string `json:"aOption"` -// } +// +// type MyAPIObject struct { +// runtime.TypeMeta `json:",inline"` +// MyPlugin runtime.Object `json:"myPlugin"` +// } +// +// type PluginA struct { +// AOption string `json:"aOption"` +// } // // // External package: -// type MyAPIObject struct { -// runtime.TypeMeta `json:",inline"` -// MyPlugin runtime.RawExtension `json:"myPlugin"` -// } -// type PluginA struct { -// AOption string `json:"aOption"` -// } +// +// type MyAPIObject struct { +// runtime.TypeMeta `json:",inline"` +// MyPlugin runtime.RawExtension `json:"myPlugin"` +// } +// +// type PluginA struct { +// AOption string `json:"aOption"` +// } // // // On the wire, the JSON will look something like this: -// { -// "kind":"MyAPIObject", -// "apiVersion":"v1", -// "myPlugin": { -// "kind":"PluginA", -// "aOption":"foo", -// }, -// } +// +// { +// "kind":"MyAPIObject", +// "apiVersion":"v1", +// "myPlugin": { +// "kind":"PluginA", +// "aOption":"foo", +// }, +// } // // So what happens? Decode first uses json or yaml to unmarshal the serialized data into // your external MyAPIObject. That causes the raw JSON to be stored, but not unpacked. diff --git a/pkg/runtime/zz_generated.deepcopy.go b/pkg/runtime/zz_generated.deepcopy.go index b0393839e..069ea4f92 100644 --- a/pkg/runtime/zz_generated.deepcopy.go +++ b/pkg/runtime/zz_generated.deepcopy.go @@ -1,3 +1,4 @@ +//go:build !ignore_autogenerated // +build !ignore_autogenerated /* diff --git a/pkg/test/api_meta_meta_test.go b/pkg/test/api_meta_meta_test.go index 49fcf8702..ccd805445 100644 --- a/pkg/test/api_meta_meta_test.go +++ b/pkg/test/api_meta_meta_test.go @@ -298,51 +298,6 @@ func TestResourceVersionerOfAPI(t *testing.T) { } } -func TestTypeMetaSelfLinker(t *testing.T) { - table := map[string]struct { - obj runtime.Object - expect string - try string - succeed bool - }{ - "normal": { - obj: &MyAPIObject{TypeMeta: InternalTypeMeta{SelfLink: "foobar"}}, - expect: "foobar", - try: "newbar", - succeed: true, - }, - "fail": { - obj: &MyIncorrectlyMarkedAsAPIObject{}, - succeed: false, - }, - } - - linker := runtime.SelfLinker(meta.NewAccessor()) - for name, item := range table { - got, err := linker.SelfLink(item.obj) - if e, a := item.succeed, err == nil; e != a { - t.Errorf("%v: expected %v, got %v", name, e, a) - } - if e, a := item.expect, got; item.succeed && e != a { - t.Errorf("%v: expected %v, got %v", name, e, a) - } - - err = linker.SetSelfLink(item.obj, item.try) - if e, a := item.succeed, err == nil; e != a { - t.Errorf("%v: expected %v, got %v", name, e, a) - } - if item.succeed { - got, err := linker.SelfLink(item.obj) - if err != nil { - t.Errorf("%v: expected no err, got %v", name, err) - } - if e, a := item.try, got; e != a { - t.Errorf("%v: expected %v, got %v", name, e, a) - } - } - } -} - type MyAPIObject2 struct { metav1.TypeMeta metav1.ObjectMeta diff --git a/pkg/test/apis_meta_v1_unstructed_unstructure_test.go b/pkg/test/apis_meta_v1_unstructed_unstructure_test.go index 0fb1be7e6..b1a50a41a 100644 --- a/pkg/test/apis_meta_v1_unstructed_unstructure_test.go +++ b/pkg/test/apis_meta_v1_unstructed_unstructure_test.go @@ -170,7 +170,6 @@ func TestUnstructuredGetters(t *testing.T) { "finalizer.1", "finalizer.2", }, - "clusterName": "cluster123", }, }, } @@ -245,9 +244,6 @@ func TestUnstructuredGetters(t *testing.T) { if got, want := unstruct.GetFinalizers(), []string{"finalizer.1", "finalizer.2"}; !reflect.DeepEqual(got, want) { t.Errorf("GetFinalizers()=%v, want %v", got, want) } - if got, want := unstruct.GetClusterName(), "cluster123"; got != want { - t.Errorf("GetClusterName()=%v, want %v", got, want) - } if got, want := unstruct.GetDeletionGracePeriodSeconds(), &ten; !reflect.DeepEqual(got, want) { t.Errorf("GetDeletionGracePeriodSeconds()=%v, want %v", got, want) } @@ -302,7 +298,6 @@ func TestUnstructuredSetters(t *testing.T) { "finalizer.1", "finalizer.2", }, - "clusterName": "cluster123", }, }, } @@ -338,7 +333,6 @@ func TestUnstructuredSetters(t *testing.T) { } unstruct.SetOwnerReferences(newOwnerReferences) unstruct.SetFinalizers([]string{"finalizer.1", "finalizer.2"}) - unstruct.SetClusterName("cluster123") unstruct.SetDeletionGracePeriodSeconds(&ten) unstruct.SetGeneration(ten) @@ -534,7 +528,6 @@ func TestAccessorMethods(t *testing.T) { {accessor: "Annotations", val: map[string]string{"foo": "bar"}}, {accessor: "Finalizers", val: []string{"foo"}}, {accessor: "OwnerReferences", val: []metav1.OwnerReference{{Name: "foo"}}}, - {accessor: "ClusterName", val: "foo"}, } for i, test := range tests { t.Logf("evaluating test %d (%s)", i, test.accessor) diff --git a/pkg/test/zz_generated.deepcopy.go b/pkg/test/zz_generated.deepcopy.go index f6a971a2c..b86d60921 100644 --- a/pkg/test/zz_generated.deepcopy.go +++ b/pkg/test/zz_generated.deepcopy.go @@ -1,3 +1,4 @@ +//go:build !ignore_autogenerated // +build !ignore_autogenerated /* diff --git a/pkg/types/nodename.go b/pkg/types/nodename.go index fee348d7e..cff9ca671 100644 --- a/pkg/types/nodename.go +++ b/pkg/types/nodename.go @@ -23,21 +23,21 @@ package types // // To clarify the various types: // -// * Node.Name is the Name field of the Node in the API. This should be stored in a NodeName. -// Unfortunately, because Name is part of ObjectMeta, we can't store it as a NodeName at the API level. +// - Node.Name is the Name field of the Node in the API. This should be stored in a NodeName. +// Unfortunately, because Name is part of ObjectMeta, we can't store it as a NodeName at the API level. // -// * Hostname is the hostname of the local machine (from uname -n). -// However, some components allow the user to pass in a --hostname-override flag, -// which will override this in most places. In the absence of anything more meaningful, -// kubelet will use Hostname as the Node.Name when it creates the Node. +// - Hostname is the hostname of the local machine (from uname -n). +// However, some components allow the user to pass in a --hostname-override flag, +// which will override this in most places. In the absence of anything more meaningful, +// kubelet will use Hostname as the Node.Name when it creates the Node. // // * The cloudproviders have the own names: GCE has InstanceName, AWS has InstanceId. // -// For GCE, InstanceName is the Name of an Instance object in the GCE API. On GCE, Instance.Name becomes the -// Hostname, and thus it makes sense also to use it as the Node.Name. But that is GCE specific, and it is up -// to the cloudprovider how to do this mapping. +// For GCE, InstanceName is the Name of an Instance object in the GCE API. On GCE, Instance.Name becomes the +// Hostname, and thus it makes sense also to use it as the Node.Name. But that is GCE specific, and it is up +// to the cloudprovider how to do this mapping. // -// For AWS, the InstanceID is not yet suitable for use as a Node.Name, so we actually use the -// PrivateDnsName for the Node.Name. And this is _not_ always the same as the hostname: if -// we are using a custom DHCP domain it won't be. +// For AWS, the InstanceID is not yet suitable for use as a Node.Name, so we actually use the +// PrivateDnsName for the Node.Name. And this is _not_ always the same as the hostname: if +// we are using a custom DHCP domain it won't be. type NodeName string diff --git a/pkg/util/cache/expiring.go b/pkg/util/cache/expiring.go index faf2c2645..0d2f153bf 100644 --- a/pkg/util/cache/expiring.go +++ b/pkg/util/cache/expiring.go @@ -21,17 +21,17 @@ import ( "sync" "time" - utilclock "k8s.io/apimachinery/pkg/util/clock" + "k8s.io/utils/clock" ) // NewExpiring returns an initialized expiring cache. func NewExpiring() *Expiring { - return NewExpiringWithClock(utilclock.RealClock{}) + return NewExpiringWithClock(clock.RealClock{}) } // NewExpiringWithClock is like NewExpiring but allows passing in a custom // clock for testing. -func NewExpiringWithClock(clock utilclock.Clock) *Expiring { +func NewExpiringWithClock(clock clock.Clock) *Expiring { return &Expiring{ clock: clock, cache: make(map[interface{}]entry), @@ -40,7 +40,7 @@ func NewExpiringWithClock(clock utilclock.Clock) *Expiring { // Expiring is a map whose entries expire after a per-entry timeout. type Expiring struct { - clock utilclock.Clock + clock clock.Clock // mu protects the below fields mu sync.RWMutex diff --git a/pkg/util/cache/expiring_test.go b/pkg/util/cache/expiring_test.go index 53f870765..6d45be636 100644 --- a/pkg/util/cache/expiring_test.go +++ b/pkg/util/cache/expiring_test.go @@ -25,7 +25,7 @@ import ( "github.com/google/uuid" - utilclock "k8s.io/apimachinery/pkg/util/clock" + testingclock "k8s.io/utils/clock/testing" ) func TestExpiringCache(t *testing.T) { @@ -58,7 +58,7 @@ func TestExpiringCache(t *testing.T) { } func TestExpiration(t *testing.T) { - fc := &utilclock.FakeClock{} + fc := &testingclock.FakeClock{} c := NewExpiringWithClock(fc) c.Set("a", "a", time.Second) @@ -104,7 +104,7 @@ func TestExpiration(t *testing.T) { } func TestGarbageCollection(t *testing.T) { - fc := &utilclock.FakeClock{} + fc := &testingclock.FakeClock{} type entry struct { key, val string diff --git a/pkg/util/cache/lruexpirecache_test.go b/pkg/util/cache/lruexpirecache_test.go index bfafe87b1..f1de853ef 100644 --- a/pkg/util/cache/lruexpirecache_test.go +++ b/pkg/util/cache/lruexpirecache_test.go @@ -21,7 +21,7 @@ import ( "time" "github.com/google/go-cmp/cmp" - "k8s.io/apimachinery/pkg/util/clock" + testingclock "k8s.io/utils/clock/testing" ) func expectEntry(t *testing.T, c *LRUExpireCache, key interface{}, value interface{}) { @@ -68,7 +68,7 @@ func TestSimpleRemove(t *testing.T) { } func TestExpiredGet(t *testing.T) { - fakeClock := clock.NewFakeClock(time.Now()) + fakeClock := testingclock.NewFakeClock(time.Now()) c := NewLRUExpireCacheWithClock(10, fakeClock) c.Add("short-lived", "12345", 1*time.Millisecond) // ensure the entry expired diff --git a/pkg/util/clock/clock.go b/pkg/util/clock/clock.go deleted file mode 100644 index 1a544d3b2..000000000 --- a/pkg/util/clock/clock.go +++ /dev/null @@ -1,445 +0,0 @@ -/* -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 clock - -import ( - "sync" - "time" -) - -// PassiveClock allows for injecting fake or real clocks into code -// that needs to read the current time but does not support scheduling -// activity in the future. -type PassiveClock interface { - Now() time.Time - Since(time.Time) time.Duration -} - -// Clock allows for injecting fake or real clocks into code that -// needs to do arbitrary things based on time. -type Clock interface { - PassiveClock - After(time.Duration) <-chan time.Time - AfterFunc(time.Duration, func()) Timer - NewTimer(time.Duration) Timer - Sleep(time.Duration) - NewTicker(time.Duration) Ticker -} - -// RealClock really calls time.Now() -type RealClock struct{} - -// Now returns the current time. -func (RealClock) Now() time.Time { - return time.Now() -} - -// Since returns time since the specified timestamp. -func (RealClock) Since(ts time.Time) time.Duration { - return time.Since(ts) -} - -// After is the same as time.After(d). -func (RealClock) After(d time.Duration) <-chan time.Time { - return time.After(d) -} - -// AfterFunc is the same as time.AfterFunc(d, f). -func (RealClock) AfterFunc(d time.Duration, f func()) Timer { - return &realTimer{ - timer: time.AfterFunc(d, f), - } -} - -// NewTimer returns a new Timer. -func (RealClock) NewTimer(d time.Duration) Timer { - return &realTimer{ - timer: time.NewTimer(d), - } -} - -// NewTicker returns a new Ticker. -func (RealClock) NewTicker(d time.Duration) Ticker { - return &realTicker{ - ticker: time.NewTicker(d), - } -} - -// Sleep pauses the RealClock for duration d. -func (RealClock) Sleep(d time.Duration) { - time.Sleep(d) -} - -// FakePassiveClock implements PassiveClock, but returns an arbitrary time. -type FakePassiveClock struct { - lock sync.RWMutex - time time.Time -} - -// FakeClock implements Clock, but returns an arbitrary time. -type FakeClock struct { - FakePassiveClock - - // waiters are waiting for the fake time to pass their specified time - waiters []fakeClockWaiter -} - -type fakeClockWaiter struct { - targetTime time.Time - stepInterval time.Duration - skipIfBlocked bool - destChan chan time.Time - afterFunc func() -} - -// NewFakePassiveClock returns a new FakePassiveClock. -func NewFakePassiveClock(t time.Time) *FakePassiveClock { - return &FakePassiveClock{ - time: t, - } -} - -// NewFakeClock returns a new FakeClock -func NewFakeClock(t time.Time) *FakeClock { - return &FakeClock{ - FakePassiveClock: *NewFakePassiveClock(t), - } -} - -// Now returns f's time. -func (f *FakePassiveClock) Now() time.Time { - f.lock.RLock() - defer f.lock.RUnlock() - return f.time -} - -// Since returns time since the time in f. -func (f *FakePassiveClock) Since(ts time.Time) time.Duration { - f.lock.RLock() - defer f.lock.RUnlock() - return f.time.Sub(ts) -} - -// SetTime sets the time on the FakePassiveClock. -func (f *FakePassiveClock) SetTime(t time.Time) { - f.lock.Lock() - defer f.lock.Unlock() - f.time = t -} - -// After is the Fake version of time.After(d). -func (f *FakeClock) After(d time.Duration) <-chan time.Time { - f.lock.Lock() - defer f.lock.Unlock() - stopTime := f.time.Add(d) - ch := make(chan time.Time, 1) // Don't block! - f.waiters = append(f.waiters, fakeClockWaiter{ - targetTime: stopTime, - destChan: ch, - }) - return ch -} - -// AfterFunc is the Fake version of time.AfterFunc(d, callback). -func (f *FakeClock) AfterFunc(d time.Duration, cb func()) Timer { - f.lock.Lock() - defer f.lock.Unlock() - stopTime := f.time.Add(d) - ch := make(chan time.Time, 1) // Don't block! - - timer := &fakeTimer{ - fakeClock: f, - waiter: fakeClockWaiter{ - targetTime: stopTime, - destChan: ch, - afterFunc: cb, - }, - } - f.waiters = append(f.waiters, timer.waiter) - return timer -} - -// NewTimer is the Fake version of time.NewTimer(d). -func (f *FakeClock) NewTimer(d time.Duration) Timer { - f.lock.Lock() - defer f.lock.Unlock() - stopTime := f.time.Add(d) - ch := make(chan time.Time, 1) // Don't block! - timer := &fakeTimer{ - fakeClock: f, - waiter: fakeClockWaiter{ - targetTime: stopTime, - destChan: ch, - }, - } - f.waiters = append(f.waiters, timer.waiter) - return timer -} - -// NewTicker returns a new Ticker. -func (f *FakeClock) NewTicker(d time.Duration) Ticker { - f.lock.Lock() - defer f.lock.Unlock() - tickTime := f.time.Add(d) - ch := make(chan time.Time, 1) // hold one tick - f.waiters = append(f.waiters, fakeClockWaiter{ - targetTime: tickTime, - stepInterval: d, - skipIfBlocked: true, - destChan: ch, - }) - - return &fakeTicker{ - c: ch, - } -} - -// Step moves clock by Duration, notifies anyone that's called After, Tick, or NewTimer -func (f *FakeClock) Step(d time.Duration) { - f.lock.Lock() - defer f.lock.Unlock() - f.setTimeLocked(f.time.Add(d)) -} - -// SetTime sets the time on a FakeClock. -func (f *FakeClock) SetTime(t time.Time) { - f.lock.Lock() - defer f.lock.Unlock() - f.setTimeLocked(t) -} - -// Actually changes the time and checks any waiters. f must be write-locked. -func (f *FakeClock) setTimeLocked(t time.Time) { - f.time = t - newWaiters := make([]fakeClockWaiter, 0, len(f.waiters)) - for i := range f.waiters { - w := &f.waiters[i] - if !w.targetTime.After(t) { - - if w.skipIfBlocked { - select { - case w.destChan <- t: - default: - } - } else { - w.destChan <- t - } - - if w.afterFunc != nil { - w.afterFunc() - } - - if w.stepInterval > 0 { - for !w.targetTime.After(t) { - w.targetTime = w.targetTime.Add(w.stepInterval) - } - newWaiters = append(newWaiters, *w) - } - - } else { - newWaiters = append(newWaiters, f.waiters[i]) - } - } - f.waiters = newWaiters -} - -// HasWaiters returns true if After or AfterFunc has been called on f but not yet satisfied -// (so you can write race-free tests). -func (f *FakeClock) HasWaiters() bool { - f.lock.RLock() - defer f.lock.RUnlock() - return len(f.waiters) > 0 -} - -// Sleep pauses the FakeClock for duration d. -func (f *FakeClock) Sleep(d time.Duration) { - f.Step(d) -} - -// IntervalClock implements Clock, but each invocation of Now steps the clock forward the specified duration -type IntervalClock struct { - Time time.Time - Duration time.Duration -} - -// Now returns i's time. -func (i *IntervalClock) Now() time.Time { - i.Time = i.Time.Add(i.Duration) - return i.Time -} - -// Since returns time since the time in i. -func (i *IntervalClock) Since(ts time.Time) time.Duration { - return i.Time.Sub(ts) -} - -// After is currently unimplemented, will panic. -// TODO: make interval clock use FakeClock so this can be implemented. -func (*IntervalClock) After(d time.Duration) <-chan time.Time { - panic("IntervalClock doesn't implement After") -} - -// AfterFunc is currently unimplemented, will panic. -// TODO: make interval clock use FakeClock so this can be implemented. -func (*IntervalClock) AfterFunc(d time.Duration, cb func()) Timer { - panic("IntervalClock doesn't implement AfterFunc") -} - -// NewTimer is currently unimplemented, will panic. -// TODO: make interval clock use FakeClock so this can be implemented. -func (*IntervalClock) NewTimer(d time.Duration) Timer { - panic("IntervalClock doesn't implement NewTimer") -} - -// NewTicker is currently unimplemented, will panic. -// TODO: make interval clock use FakeClock so this can be implemented. -func (*IntervalClock) NewTicker(d time.Duration) Ticker { - panic("IntervalClock doesn't implement NewTicker") -} - -// Sleep is currently unimplemented; will panic. -func (*IntervalClock) Sleep(d time.Duration) { - panic("IntervalClock doesn't implement Sleep") -} - -// Timer allows for injecting fake or real timers into code that -// needs to do arbitrary things based on time. -type Timer interface { - C() <-chan time.Time - Stop() bool - Reset(d time.Duration) bool -} - -// realTimer is backed by an actual time.Timer. -type realTimer struct { - timer *time.Timer -} - -// C returns the underlying timer's channel. -func (r *realTimer) C() <-chan time.Time { - return r.timer.C -} - -// Stop calls Stop() on the underlying timer. -func (r *realTimer) Stop() bool { - return r.timer.Stop() -} - -// Reset calls Reset() on the underlying timer. -func (r *realTimer) Reset(d time.Duration) bool { - return r.timer.Reset(d) -} - -// fakeTimer implements Timer based on a FakeClock. -type fakeTimer struct { - fakeClock *FakeClock - waiter fakeClockWaiter -} - -// C returns the channel that notifies when this timer has fired. -func (f *fakeTimer) C() <-chan time.Time { - return f.waiter.destChan -} - -// Stop conditionally stops the timer. If the timer has neither fired -// nor been stopped then this call stops the timer and returns true, -// otherwise this call returns false. This is like time.Timer::Stop. -func (f *fakeTimer) Stop() bool { - f.fakeClock.lock.Lock() - defer f.fakeClock.lock.Unlock() - // The timer has already fired or been stopped, unless it is found - // among the clock's waiters. - stopped := false - oldWaiters := f.fakeClock.waiters - newWaiters := make([]fakeClockWaiter, 0, len(oldWaiters)) - seekChan := f.waiter.destChan - for i := range oldWaiters { - // Identify the timer's fakeClockWaiter by the identity of the - // destination channel, nothing else is necessarily unique and - // constant since the timer's creation. - if oldWaiters[i].destChan == seekChan { - stopped = true - } else { - newWaiters = append(newWaiters, oldWaiters[i]) - } - } - - f.fakeClock.waiters = newWaiters - - return stopped -} - -// Reset conditionally updates the firing time of the timer. If the -// timer has neither fired nor been stopped then this call resets the -// timer to the fake clock's "now" + d and returns true, otherwise -// it creates a new waiter, adds it to the clock, and returns true. -// -// It is not possible to return false, because a fake timer can be reset -// from any state (waiting to fire, already fired, and stopped). -// -// See the GoDoc for time.Timer::Reset for more context on why -// the return value of Reset() is not useful. -func (f *fakeTimer) Reset(d time.Duration) bool { - f.fakeClock.lock.Lock() - defer f.fakeClock.lock.Unlock() - waiters := f.fakeClock.waiters - seekChan := f.waiter.destChan - for i := range waiters { - if waiters[i].destChan == seekChan { - waiters[i].targetTime = f.fakeClock.time.Add(d) - return true - } - } - // No existing waiter, timer has already fired or been reset. - // We should still enable Reset() to succeed by creating a - // new waiter and adding it to the clock's waiters. - newWaiter := fakeClockWaiter{ - targetTime: f.fakeClock.time.Add(d), - destChan: seekChan, - } - f.fakeClock.waiters = append(f.fakeClock.waiters, newWaiter) - return true -} - -// Ticker defines the Ticker interface -type Ticker interface { - C() <-chan time.Time - Stop() -} - -type realTicker struct { - ticker *time.Ticker -} - -func (t *realTicker) C() <-chan time.Time { - return t.ticker.C -} - -func (t *realTicker) Stop() { - t.ticker.Stop() -} - -type fakeTicker struct { - c <-chan time.Time -} - -func (t *fakeTicker) C() <-chan time.Time { - return t.c -} - -func (t *fakeTicker) Stop() { -} diff --git a/pkg/util/clock/clock_test.go b/pkg/util/clock/clock_test.go deleted file mode 100644 index 700d15ee5..000000000 --- a/pkg/util/clock/clock_test.go +++ /dev/null @@ -1,389 +0,0 @@ -/* -Copyright 2015 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 clock - -import ( - "testing" - "time" -) - -var ( - _ = Clock(RealClock{}) - _ = Clock(&FakeClock{}) - _ = Clock(&IntervalClock{}) - - _ = Timer(&realTimer{}) - _ = Timer(&fakeTimer{}) - - _ = Ticker(&realTicker{}) - _ = Ticker(&fakeTicker{}) -) - -type SettablePassiveClock interface { - PassiveClock - SetTime(time.Time) -} - -func exercisePassiveClock(t *testing.T, pc SettablePassiveClock) { - t1 := time.Now() - t2 := t1.Add(time.Hour) - pc.SetTime(t1) - tx := pc.Now() - if tx != t1 { - t.Errorf("SetTime(%#+v); Now() => %#+v", t1, tx) - } - dx := pc.Since(t1) - if dx != 0 { - t.Errorf("Since() => %v", dx) - } - pc.SetTime(t2) - dx = pc.Since(t1) - if dx != time.Hour { - t.Errorf("Since() => %v", dx) - } - tx = pc.Now() - if tx != t2 { - t.Errorf("Now() => %#+v", tx) - } -} - -func TestFakePassiveClock(t *testing.T) { - startTime := time.Now() - tc := NewFakePassiveClock(startTime) - exercisePassiveClock(t, tc) -} - -func TestFakeClock(t *testing.T) { - startTime := time.Now() - tc := NewFakeClock(startTime) - exercisePassiveClock(t, tc) - tc.SetTime(startTime) - tc.Step(time.Second) - now := tc.Now() - if now.Sub(startTime) != time.Second { - t.Errorf("input: %s now=%s gap=%s expected=%s", startTime, now, now.Sub(startTime), time.Second) - } -} - -func TestFakeClockSleep(t *testing.T) { - startTime := time.Now() - tc := NewFakeClock(startTime) - tc.Sleep(time.Duration(1) * time.Hour) - now := tc.Now() - if now.Sub(startTime) != time.Hour { - t.Errorf("Fake sleep failed, expected time to advance by one hour, instead, its %v", now.Sub(startTime)) - } -} - -func TestFakeAfter(t *testing.T) { - tc := NewFakeClock(time.Now()) - if tc.HasWaiters() { - t.Errorf("unexpected waiter?") - } - oneSec := tc.After(time.Second) - if !tc.HasWaiters() { - t.Errorf("unexpected lack of waiter?") - } - - oneOhOneSec := tc.After(time.Second + time.Millisecond) - twoSec := tc.After(2 * time.Second) - select { - case <-oneSec: - t.Errorf("unexpected channel read") - case <-oneOhOneSec: - t.Errorf("unexpected channel read") - case <-twoSec: - t.Errorf("unexpected channel read") - default: - } - - tc.Step(999 * time.Millisecond) - select { - case <-oneSec: - t.Errorf("unexpected channel read") - case <-oneOhOneSec: - t.Errorf("unexpected channel read") - case <-twoSec: - t.Errorf("unexpected channel read") - default: - } - - tc.Step(time.Millisecond) - select { - case <-oneSec: - // Expected! - case <-oneOhOneSec: - t.Errorf("unexpected channel read") - case <-twoSec: - t.Errorf("unexpected channel read") - default: - t.Errorf("unexpected non-channel read") - } - tc.Step(time.Millisecond) - select { - case <-oneSec: - // should not double-trigger! - t.Errorf("unexpected channel read") - case <-oneOhOneSec: - // Expected! - case <-twoSec: - t.Errorf("unexpected channel read") - default: - t.Errorf("unexpected non-channel read") - } -} - -func TestFakeAfterFunc(t *testing.T) { - tc := NewFakeClock(time.Now()) - if tc.HasWaiters() { - t.Errorf("unexpected waiter?") - } - expectOneSecTimerFire := false - oneSecTimerFire := 0 - tc.AfterFunc(time.Second, func() { - if !expectOneSecTimerFire { - t.Errorf("oneSecTimer func fired") - } else { - oneSecTimerFire++ - } - }) - if !tc.HasWaiters() { - t.Errorf("unexpected lack of waiter?") - } - - expectOneOhOneSecTimerFire := false - oneOhOneSecTimerFire := 0 - tc.AfterFunc(time.Second+time.Millisecond, func() { - if !expectOneOhOneSecTimerFire { - t.Errorf("oneOhOneSecTimer func fired") - } else { - oneOhOneSecTimerFire++ - } - }) - - expectTwoSecTimerFire := false - twoSecTimerFire := 0 - twoSecTimer := tc.AfterFunc(2*time.Second, func() { - if !expectTwoSecTimerFire { - t.Errorf("twoSecTimer func fired") - } else { - twoSecTimerFire++ - } - }) - - tc.Step(999 * time.Millisecond) - - expectOneSecTimerFire = true - tc.Step(time.Millisecond) - if oneSecTimerFire != 1 { - t.Errorf("expected oneSecTimerFire=1, got %d", oneSecTimerFire) - } - expectOneSecTimerFire = false - - expectOneOhOneSecTimerFire = true - tc.Step(time.Millisecond) - if oneOhOneSecTimerFire != 1 { - // should not double-trigger! - t.Errorf("expected oneOhOneSecTimerFire=1, got %d", oneOhOneSecTimerFire) - } - expectOneOhOneSecTimerFire = false - - // ensure a canceled timer doesn't fire - twoSecTimer.Stop() - tc.Step(time.Second) -} - -func TestFakeTimer(t *testing.T) { - tc := NewFakeClock(time.Now()) - if tc.HasWaiters() { - t.Errorf("unexpected waiter?") - } - oneSec := tc.NewTimer(time.Second) - twoSec := tc.NewTimer(time.Second * 2) - treSec := tc.NewTimer(time.Second * 3) - if !tc.HasWaiters() { - t.Errorf("unexpected lack of waiter?") - } - select { - case <-oneSec.C(): - t.Errorf("unexpected channel read") - case <-twoSec.C(): - t.Errorf("unexpected channel read") - case <-treSec.C(): - t.Errorf("unexpected channel read") - default: - } - tc.Step(999999999 * time.Nanosecond) // t=.999,999,999 - select { - case <-oneSec.C(): - t.Errorf("unexpected channel read") - case <-twoSec.C(): - t.Errorf("unexpected channel read") - case <-treSec.C(): - t.Errorf("unexpected channel read") - default: - } - tc.Step(time.Nanosecond) // t=1 - select { - case <-twoSec.C(): - t.Errorf("unexpected channel read") - case <-treSec.C(): - t.Errorf("unexpected channel read") - default: - } - select { - case <-oneSec.C(): - // Expected! - default: - t.Errorf("unexpected channel non-read") - } - tc.Step(time.Nanosecond) // t=1.000,000,001 - select { - case <-oneSec.C(): - t.Errorf("unexpected channel read") - case <-twoSec.C(): - t.Errorf("unexpected channel read") - case <-treSec.C(): - t.Errorf("unexpected channel read") - default: - } - if oneSec.Stop() { - t.Errorf("Expected oneSec.Stop() to return false") - } - if !twoSec.Stop() { - t.Errorf("Expected twoSec.Stop() to return true") - } - tc.Step(time.Second) // t=2.000,000,001 - select { - case <-oneSec.C(): - t.Errorf("unexpected channel read") - case <-twoSec.C(): - t.Errorf("unexpected channel read") - case <-treSec.C(): - t.Errorf("unexpected channel read") - default: - } - if !twoSec.Reset(time.Second) { - t.Errorf("Expected twoSec.Reset() to return true") - } - if !treSec.Reset(time.Second) { - t.Errorf("Expected treSec.Reset() to return true") - } - tc.Step(time.Nanosecond * 999999999) // t=3.0 - select { - case <-oneSec.C(): - t.Errorf("unexpected channel read") - case <-twoSec.C(): - t.Errorf("unexpected channel read") - case <-treSec.C(): - t.Errorf("unexpected channel read") - default: - } - tc.Step(time.Nanosecond) // t=3.000,000,001 - select { - case <-oneSec.C(): - t.Errorf("unexpected channel read") - case <-twoSec.C(): - // Expected! - default: - t.Errorf("unexpected channel non-read") - } - select { - case <-treSec.C(): - // Expected! - default: - t.Errorf("unexpected channel non-read") - } -} - -func TestFakeTick(t *testing.T) { - tc := NewFakeClock(time.Now()) - if tc.HasWaiters() { - t.Errorf("unexpected waiter?") - } - oneSec := tc.NewTicker(time.Second).C() - if !tc.HasWaiters() { - t.Errorf("unexpected lack of waiter?") - } - - oneOhOneSec := tc.NewTicker(time.Second + time.Millisecond).C() - twoSec := tc.NewTicker(2 * time.Second).C() - select { - case <-oneSec: - t.Errorf("unexpected channel read") - case <-oneOhOneSec: - t.Errorf("unexpected channel read") - case <-twoSec: - t.Errorf("unexpected channel read") - default: - } - - tc.Step(999 * time.Millisecond) // t=.999 - select { - case <-oneSec: - t.Errorf("unexpected channel read") - case <-oneOhOneSec: - t.Errorf("unexpected channel read") - case <-twoSec: - t.Errorf("unexpected channel read") - default: - } - - tc.Step(time.Millisecond) // t=1.000 - select { - case <-oneSec: - // Expected! - case <-oneOhOneSec: - t.Errorf("unexpected channel read") - case <-twoSec: - t.Errorf("unexpected channel read") - default: - t.Errorf("unexpected non-channel read") - } - tc.Step(time.Millisecond) // t=1.001 - select { - case <-oneSec: - // should not double-trigger! - t.Errorf("unexpected channel read") - case <-oneOhOneSec: - // Expected! - case <-twoSec: - t.Errorf("unexpected channel read") - default: - t.Errorf("unexpected non-channel read") - } - - tc.Step(time.Second) // t=2.001 - tc.Step(time.Second) // t=3.001 - tc.Step(time.Second) // t=4.001 - tc.Step(time.Second) // t=5.001 - - // The one second ticker should not accumulate ticks - accumulatedTicks := 0 - drained := false - for !drained { - select { - case <-oneSec: - accumulatedTicks++ - default: - drained = true - } - } - if accumulatedTicks != 1 { - t.Errorf("unexpected number of accumulated ticks: %d", accumulatedTicks) - } -} diff --git a/pkg/util/diff/diff.go b/pkg/util/diff/diff.go index fa9ffa51b..ec4002e38 100644 --- a/pkg/util/diff/diff.go +++ b/pkg/util/diff/diff.go @@ -135,7 +135,7 @@ func IgnoreUnset() cmp.Option { if v2.Len() == 0 { return true } - case reflect.Interface, reflect.Ptr: + case reflect.Interface, reflect.Pointer: if v2.IsNil() { return true } diff --git a/pkg/util/duration/duration.go b/pkg/util/duration/duration.go index 1bc1e3c81..c31b2a0cb 100644 --- a/pkg/util/duration/duration.go +++ b/pkg/util/duration/duration.go @@ -27,9 +27,9 @@ func ShortHumanDuration(d time.Duration) string { // Allow deviation no more than 2 seconds(excluded) to tolerate machine time // inconsistence, it can be considered as almost now. if seconds := int(d.Seconds()); seconds < -1 { - return fmt.Sprintf("") + return "" } else if seconds < 0 { - return fmt.Sprintf("0s") + return "0s" } else if seconds < 60 { return fmt.Sprintf("%ds", seconds) } else if minutes := int(d.Minutes()); minutes < 60 { @@ -49,9 +49,9 @@ func HumanDuration(d time.Duration) string { // Allow deviation no more than 2 seconds(excluded) to tolerate machine time // inconsistence, it can be considered as almost now. if seconds := int(d.Seconds()); seconds < -1 { - return fmt.Sprintf("") + return "" } else if seconds < 0 { - return fmt.Sprintf("0s") + return "0s" } else if seconds < 60*2 { return fmt.Sprintf("%ds", seconds) } diff --git a/pkg/util/framer/framer.go b/pkg/util/framer/framer.go index 45aa74bf5..ca08f8561 100644 --- a/pkg/util/framer/framer.go +++ b/pkg/util/framer/framer.go @@ -56,10 +56,10 @@ type lengthDelimitedFrameReader struct { // // The protocol is: // -// stream: message ... -// message: prefix body -// prefix: 4 byte uint32 in BigEndian order, denotes length of body -// body: bytes (0..prefix) +// stream: message ... +// message: prefix body +// prefix: 4 byte uint32 in BigEndian order, denotes length of body +// body: bytes (0..prefix) // // If the buffer passed to Read is not long enough to contain an entire frame, io.ErrShortRead // will be returned along with the number of bytes read. @@ -132,14 +132,14 @@ func (r *jsonFrameReader) Read(data []byte) (int, error) { // Return whatever remaining data exists from an in progress frame if n := len(r.remaining); n > 0 { if n <= len(data) { - //lint:ignore SA4006,SA4010 underlying array of data is modified here. + //nolint:staticcheck // SA4006,SA4010 underlying array of data is modified here. data = append(data[0:0], r.remaining...) r.remaining = nil return n, nil } n = len(data) - //lint:ignore SA4006,SA4010 underlying array of data is modified here. + //nolint:staticcheck // SA4006,SA4010 underlying array of data is modified here. data = append(data[0:0], r.remaining[:n]...) r.remaining = r.remaining[n:] return n, io.ErrShortBuffer @@ -157,7 +157,7 @@ func (r *jsonFrameReader) Read(data []byte) (int, error) { // and set m to it, which means we need to copy the partial result back into data and preserve // the remaining result for subsequent reads. if len(m) > n { - //lint:ignore SA4006,SA4010 underlying array of data is modified here. + //nolint:staticcheck // SA4006,SA4010 underlying array of data is modified here. data = append(data[0:0], m[:n]...) r.remaining = m[n:] return n, io.ErrShortBuffer diff --git a/pkg/util/httpstream/spdy/roundtripper.go b/pkg/util/httpstream/spdy/roundtripper.go index b8e329571..27c3d2d56 100644 --- a/pkg/util/httpstream/spdy/roundtripper.go +++ b/pkg/util/httpstream/spdy/roundtripper.go @@ -18,12 +18,11 @@ package spdy import ( "bufio" - "bytes" "context" "crypto/tls" "encoding/base64" + "errors" "fmt" - "io" "io/ioutil" "net" "net/http" @@ -32,6 +31,7 @@ import ( "strings" "time" + "golang.org/x/net/proxy" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -65,12 +65,6 @@ type SpdyRoundTripper struct { // Used primarily for mocking the proxy discovery in tests. proxier func(req *http.Request) (*url.URL, error) - // followRedirects indicates if the round tripper should examine responses for redirects and - // follow them. - followRedirects bool - // requireSameHostRedirects restricts redirect following to only follow redirects to the same host - // as the original request. - requireSameHostRedirects bool // pingPeriod is a period for sending Ping frames over established // connections. pingPeriod time.Duration @@ -82,37 +76,31 @@ var _ utilnet.Dialer = &SpdyRoundTripper{} // NewRoundTripper creates a new SpdyRoundTripper that will use the specified // tlsConfig. -func NewRoundTripper(tlsConfig *tls.Config, followRedirects, requireSameHostRedirects bool) *SpdyRoundTripper { +func NewRoundTripper(tlsConfig *tls.Config) *SpdyRoundTripper { return NewRoundTripperWithConfig(RoundTripperConfig{ - TLS: tlsConfig, - FollowRedirects: followRedirects, - RequireSameHostRedirects: requireSameHostRedirects, + TLS: tlsConfig, }) } // NewRoundTripperWithProxy creates a new SpdyRoundTripper that will use the // specified tlsConfig and proxy func. -func NewRoundTripperWithProxy(tlsConfig *tls.Config, followRedirects, requireSameHostRedirects bool, proxier func(*http.Request) (*url.URL, error)) *SpdyRoundTripper { +func NewRoundTripperWithProxy(tlsConfig *tls.Config, proxier func(*http.Request) (*url.URL, error)) *SpdyRoundTripper { return NewRoundTripperWithConfig(RoundTripperConfig{ - TLS: tlsConfig, - FollowRedirects: followRedirects, - RequireSameHostRedirects: requireSameHostRedirects, - Proxier: proxier, + TLS: tlsConfig, + Proxier: proxier, }) } -// NewRoundTripperWithProxy creates a new SpdyRoundTripper with the specified +// NewRoundTripperWithConfig creates a new SpdyRoundTripper with the specified // configuration. func NewRoundTripperWithConfig(cfg RoundTripperConfig) *SpdyRoundTripper { if cfg.Proxier == nil { cfg.Proxier = utilnet.NewProxierWithNoProxyCIDR(http.ProxyFromEnvironment) } return &SpdyRoundTripper{ - tlsConfig: cfg.TLS, - followRedirects: cfg.FollowRedirects, - requireSameHostRedirects: cfg.RequireSameHostRedirects, - proxier: cfg.Proxier, - pingPeriod: cfg.PingPeriod, + tlsConfig: cfg.TLS, + proxier: cfg.Proxier, + pingPeriod: cfg.PingPeriod, } } @@ -125,9 +113,6 @@ type RoundTripperConfig struct { // PingPeriod is a period for sending SPDY Pings on the connection. // Optional. PingPeriod time.Duration - - FollowRedirects bool - RequireSameHostRedirects bool } // TLSClientConfig implements pkg/util/net.TLSClientConfigHolder for proper TLS checking during @@ -163,6 +148,18 @@ func (s *SpdyRoundTripper) dial(req *http.Request) (net.Conn, error) { return s.dialWithoutProxy(req.Context(), req.URL) } + switch proxyURL.Scheme { + case "socks5": + return s.dialWithSocks5Proxy(req, proxyURL) + case "https", "http", "": + return s.dialWithHttpProxy(req, proxyURL) + } + + return nil, fmt.Errorf("proxy URL scheme not supported: %s", proxyURL.Scheme) +} + +// dialWithHttpProxy dials the host specified by url through an http or an https proxy. +func (s *SpdyRoundTripper) dialWithHttpProxy(req *http.Request, proxyURL *url.URL) (net.Conn, error) { // ensure we use a canonical host with proxyReq targetHost := netutil.CanonicalAddr(req.URL) @@ -173,30 +170,84 @@ func (s *SpdyRoundTripper) dial(req *http.Request) (net.Conn, error) { Host: targetHost, } + proxyReq = *proxyReq.WithContext(req.Context()) + if pa := s.proxyAuth(proxyURL); pa != "" { proxyReq.Header = http.Header{} proxyReq.Header.Set("Proxy-Authorization", pa) } - proxyDialConn, err := s.dialWithoutProxy(req.Context(), proxyURL) + proxyDialConn, err := s.dialWithoutProxy(proxyReq.Context(), proxyURL) if err != nil { return nil, err } - //lint:ignore SA1019 ignore deprecated httputil.NewProxyClientConn + //nolint:staticcheck // SA1019 ignore deprecated httputil.NewProxyClientConn proxyClientConn := httputil.NewProxyClientConn(proxyDialConn, nil) - _, err = proxyClientConn.Do(&proxyReq) - //lint:ignore SA1019 ignore deprecated httputil.ErrPersistEOF: it might be + response, err := proxyClientConn.Do(&proxyReq) + //nolint:staticcheck // SA1019 ignore deprecated httputil.ErrPersistEOF: it might be // returned from the invocation of proxyClientConn.Do if err != nil && err != httputil.ErrPersistEOF { return nil, err } + if response != nil && response.StatusCode >= 300 || response.StatusCode < 200 { + return nil, fmt.Errorf("CONNECT request to %s returned response: %s", proxyURL.Redacted(), response.Status) + } rwc, _ := proxyClientConn.Hijack() - if req.URL.Scheme != "https" { - return rwc, nil + if req.URL.Scheme == "https" { + return s.tlsConn(proxyReq.Context(), rwc, targetHost) } + return rwc, nil +} + +// dialWithSocks5Proxy dials the host specified by url through a socks5 proxy. +func (s *SpdyRoundTripper) dialWithSocks5Proxy(req *http.Request, proxyURL *url.URL) (net.Conn, error) { + // ensure we use a canonical host with proxyReq + targetHost := netutil.CanonicalAddr(req.URL) + proxyDialAddr := netutil.CanonicalAddr(proxyURL) + + var auth *proxy.Auth + if proxyURL.User != nil { + pass, _ := proxyURL.User.Password() + auth = &proxy.Auth{ + User: proxyURL.User.Username(), + Password: pass, + } + } + + dialer := s.Dialer + if dialer == nil { + dialer = &net.Dialer{ + Timeout: 30 * time.Second, + } + } + + proxyDialer, err := proxy.SOCKS5("tcp", proxyDialAddr, auth, dialer) + if err != nil { + return nil, err + } + + // According to the implementation of proxy.SOCKS5, the type assertion will always succeed + contextDialer, ok := proxyDialer.(proxy.ContextDialer) + if !ok { + return nil, errors.New("SOCKS5 Dialer must implement ContextDialer") + } + + proxyDialConn, err := contextDialer.DialContext(req.Context(), "tcp", targetHost) + if err != nil { + return nil, err + } + + if req.URL.Scheme == "https" { + return s.tlsConn(req.Context(), proxyDialConn, targetHost) + } + return proxyDialConn, nil +} + +// tlsConn returns a TLS client side connection using rwc as the underlying transport. +func (s *SpdyRoundTripper) tlsConn(ctx context.Context, rwc net.Conn, targetHost string) (net.Conn, error) { host, _, err := net.SplitHostPort(targetHost) if err != nil { @@ -214,17 +265,8 @@ func (s *SpdyRoundTripper) dial(req *http.Request) (net.Conn, error) { tlsConn := tls.Client(rwc, tlsConfig) - // need to manually call Handshake() so we can call VerifyHostname() below - if err := tlsConn.Handshake(); err != nil { - return nil, err - } - - // Return if we were configured to skip validation - if tlsConfig.InsecureSkipVerify { - return tlsConn, nil - } - - if err := tlsConn.VerifyHostname(tlsConfig.ServerName); err != nil { + if err := tlsConn.HandshakeContext(ctx); err != nil { + tlsConn.Close() return nil, err } @@ -234,46 +276,20 @@ func (s *SpdyRoundTripper) dial(req *http.Request) (net.Conn, error) { // dialWithoutProxy dials the host specified by url, using TLS if appropriate. func (s *SpdyRoundTripper) dialWithoutProxy(ctx context.Context, url *url.URL) (net.Conn, error) { dialAddr := netutil.CanonicalAddr(url) - - if url.Scheme == "http" { - if s.Dialer == nil { - var d net.Dialer - return d.DialContext(ctx, "tcp", dialAddr) - } else { - return s.Dialer.DialContext(ctx, "tcp", dialAddr) - } - } - - // TODO validate the TLSClientConfig is set up? - var conn *tls.Conn - var err error - if s.Dialer == nil { - conn, err = tls.Dial("tcp", dialAddr, s.tlsConfig) - } else { - conn, err = tls.DialWithDialer(s.Dialer, "tcp", dialAddr, s.tlsConfig) - } - if err != nil { - return nil, err + dialer := s.Dialer + if dialer == nil { + dialer = &net.Dialer{} } - // Return if we were configured to skip validation - if s.tlsConfig != nil && s.tlsConfig.InsecureSkipVerify { - return conn, nil + if url.Scheme == "http" { + return dialer.DialContext(ctx, "tcp", dialAddr) } - host, _, err := net.SplitHostPort(dialAddr) - if err != nil { - return nil, err - } - if s.tlsConfig != nil && len(s.tlsConfig.ServerName) > 0 { - host = s.tlsConfig.ServerName - } - err = conn.VerifyHostname(host) - if err != nil { - return nil, err + tlsDialer := tls.Dialer{ + NetDialer: dialer, + Config: s.tlsConfig, } - - return conn, nil + return tlsDialer.DialContext(ctx, "tcp", dialAddr) } // proxyAuth returns, for a given proxy URL, the value to be used for the Proxy-Authorization header @@ -281,48 +297,30 @@ func (s *SpdyRoundTripper) proxyAuth(proxyURL *url.URL) string { if proxyURL == nil || proxyURL.User == nil { return "" } - credentials := proxyURL.User.String() - encodedAuth := base64.StdEncoding.EncodeToString([]byte(credentials)) - return fmt.Sprintf("Basic %s", encodedAuth) + username := proxyURL.User.Username() + password, _ := proxyURL.User.Password() + auth := username + ":" + password + return "Basic " + base64.StdEncoding.EncodeToString([]byte(auth)) } // RoundTrip executes the Request and upgrades it. After a successful upgrade, // clients may call SpdyRoundTripper.Connection() to retrieve the upgraded // connection. func (s *SpdyRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - header := utilnet.CloneHeader(req.Header) - header.Add(httpstream.HeaderConnection, httpstream.HeaderUpgrade) - header.Add(httpstream.HeaderUpgrade, HeaderSpdy31) - - var ( - conn net.Conn - rawResponse []byte - err error - ) + req = utilnet.CloneRequest(req) + req.Header.Add(httpstream.HeaderConnection, httpstream.HeaderUpgrade) + req.Header.Add(httpstream.HeaderUpgrade, HeaderSpdy31) - if s.followRedirects { - conn, rawResponse, err = utilnet.ConnectWithRedirects(req.Method, req.URL, header, req.Body, s, s.requireSameHostRedirects) - } else { - clone := utilnet.CloneRequest(req) - clone.Header = header - conn, err = s.Dial(clone) - } + conn, err := s.Dial(req) if err != nil { return nil, err } - responseReader := bufio.NewReader( - io.MultiReader( - bytes.NewBuffer(rawResponse), - conn, - ), - ) + responseReader := bufio.NewReader(conn) resp, err := http.ReadResponse(responseReader, nil) if err != nil { - if conn != nil { - conn.Close() - } + conn.Close() return nil, err } diff --git a/pkg/util/httpstream/spdy/roundtripper_test.go b/pkg/util/httpstream/spdy/roundtripper_test.go index caca6670d..a0b7b169d 100644 --- a/pkg/util/httpstream/spdy/roundtripper_test.go +++ b/pkg/util/httpstream/spdy/roundtripper_test.go @@ -17,435 +17,612 @@ limitations under the License. package spdy import ( + "context" "crypto/tls" "crypto/x509" - "encoding/base64" - "fmt" "io" + "net" "net/http" "net/http/httptest" "net/url" - "strings" - "sync/atomic" + "strconv" "testing" + "github.com/armon/go-socks5" "github.com/elazarl/goproxy" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/util/httpstream" ) -// be sure to unset environment variable https_proxy (if exported) before testing, otherwise the testing will fail unexpectedly. -func TestRoundTripAndNewConnection(t *testing.T) { - for _, redirect := range []bool{false, true} { - t.Run(fmt.Sprintf("redirect = %t", redirect), func(t *testing.T) { - localhostPool := x509.NewCertPool() - if !localhostPool.AppendCertsFromPEM(localhostCert) { - t.Errorf("error setting up localhostCert pool") - } +type serverHandlerConfig struct { + shouldError bool + statusCode int + connectionHeader string + upgradeHeader string +} - httpsServerInvalidHostname := func(h http.Handler) *httptest.Server { - cert, err := tls.X509KeyPair(exampleCert, exampleKey) - if err != nil { - t.Errorf("https (invalid hostname): proxy_test: %v", err) - } - ts := httptest.NewUnstartedServer(h) - ts.TLS = &tls.Config{ - Certificates: []tls.Certificate{cert}, - } - ts.StartTLS() - return ts +func serverHandler(t *testing.T, config serverHandlerConfig) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + if config.shouldError { + if e, a := httpstream.HeaderUpgrade, req.Header.Get(httpstream.HeaderConnection); e != a { + t.Fatalf("expected connection=upgrade header, got '%s", a) } - httpsServerValidHostname := func(h http.Handler) *httptest.Server { - cert, err := tls.X509KeyPair(localhostCert, localhostKey) - if err != nil { - t.Errorf("https (valid hostname): proxy_test: %v", err) - } - ts := httptest.NewUnstartedServer(h) - ts.TLS = &tls.Config{ - Certificates: []tls.Certificate{cert}, - } - ts.StartTLS() - return ts - } - - testCases := map[string]struct { - serverFunc func(http.Handler) *httptest.Server - proxyServerFunc func(http.Handler) *httptest.Server - proxyAuth *url.Userinfo - clientTLS *tls.Config - serverConnectionHeader string - serverUpgradeHeader string - serverStatusCode int - shouldError bool - }{ - "no headers": { - serverFunc: httptest.NewServer, - serverConnectionHeader: "", - serverUpgradeHeader: "", - serverStatusCode: http.StatusSwitchingProtocols, - shouldError: true, - }, - "no upgrade header": { - serverFunc: httptest.NewServer, - serverConnectionHeader: "Upgrade", - serverUpgradeHeader: "", - serverStatusCode: http.StatusSwitchingProtocols, - shouldError: true, - }, - "no connection header": { - serverFunc: httptest.NewServer, - serverConnectionHeader: "", - serverUpgradeHeader: "SPDY/3.1", - serverStatusCode: http.StatusSwitchingProtocols, - shouldError: true, - }, - "no switching protocol status code": { - serverFunc: httptest.NewServer, - serverConnectionHeader: "Upgrade", - serverUpgradeHeader: "SPDY/3.1", - serverStatusCode: http.StatusForbidden, - shouldError: true, - }, - "http": { - serverFunc: httptest.NewServer, - serverConnectionHeader: "Upgrade", - serverUpgradeHeader: "SPDY/3.1", - serverStatusCode: http.StatusSwitchingProtocols, - shouldError: false, - }, - "https (invalid hostname + InsecureSkipVerify)": { - serverFunc: httpsServerInvalidHostname, - clientTLS: &tls.Config{InsecureSkipVerify: true}, - serverConnectionHeader: "Upgrade", - serverUpgradeHeader: "SPDY/3.1", - serverStatusCode: http.StatusSwitchingProtocols, - shouldError: false, - }, - "https (invalid hostname + hostname verification)": { - serverFunc: httpsServerInvalidHostname, - clientTLS: &tls.Config{InsecureSkipVerify: false}, - serverConnectionHeader: "Upgrade", - serverUpgradeHeader: "SPDY/3.1", - serverStatusCode: http.StatusSwitchingProtocols, - shouldError: true, - }, - "https (valid hostname + RootCAs)": { - serverFunc: httpsServerValidHostname, - clientTLS: &tls.Config{RootCAs: localhostPool}, - serverConnectionHeader: "Upgrade", - serverUpgradeHeader: "SPDY/3.1", - serverStatusCode: http.StatusSwitchingProtocols, - shouldError: false, - }, - "proxied http->http": { - serverFunc: httptest.NewServer, - proxyServerFunc: httptest.NewServer, - serverConnectionHeader: "Upgrade", - serverUpgradeHeader: "SPDY/3.1", - serverStatusCode: http.StatusSwitchingProtocols, - shouldError: false, - }, - "proxied https (invalid hostname + InsecureSkipVerify) -> http": { - serverFunc: httptest.NewServer, - proxyServerFunc: httpsServerInvalidHostname, - clientTLS: &tls.Config{InsecureSkipVerify: true}, - serverConnectionHeader: "Upgrade", - serverUpgradeHeader: "SPDY/3.1", - serverStatusCode: http.StatusSwitchingProtocols, - shouldError: false, - }, - "proxied https with auth (invalid hostname + InsecureSkipVerify) -> http": { - serverFunc: httptest.NewServer, - proxyServerFunc: httpsServerInvalidHostname, - proxyAuth: url.UserPassword("proxyuser", "proxypasswd"), - clientTLS: &tls.Config{InsecureSkipVerify: true}, - serverConnectionHeader: "Upgrade", - serverUpgradeHeader: "SPDY/3.1", - serverStatusCode: http.StatusSwitchingProtocols, - shouldError: false, - }, - "proxied https (invalid hostname + hostname verification) -> http": { - serverFunc: httptest.NewServer, - proxyServerFunc: httpsServerInvalidHostname, - clientTLS: &tls.Config{InsecureSkipVerify: false}, - serverConnectionHeader: "Upgrade", - serverUpgradeHeader: "SPDY/3.1", - serverStatusCode: http.StatusSwitchingProtocols, - shouldError: true, // fails because the client doesn't trust the proxy - }, - "proxied https (valid hostname + RootCAs) -> http": { - serverFunc: httptest.NewServer, - proxyServerFunc: httpsServerValidHostname, - clientTLS: &tls.Config{RootCAs: localhostPool}, - serverConnectionHeader: "Upgrade", - serverUpgradeHeader: "SPDY/3.1", - serverStatusCode: http.StatusSwitchingProtocols, - shouldError: false, - }, - "proxied https with auth (valid hostname + RootCAs) -> http": { - serverFunc: httptest.NewServer, - proxyServerFunc: httpsServerValidHostname, - proxyAuth: url.UserPassword("proxyuser", "proxypasswd"), - clientTLS: &tls.Config{RootCAs: localhostPool}, - serverConnectionHeader: "Upgrade", - serverUpgradeHeader: "SPDY/3.1", - serverStatusCode: http.StatusSwitchingProtocols, - shouldError: false, - }, - "proxied https (invalid hostname + InsecureSkipVerify) -> https (invalid hostname)": { - serverFunc: httpsServerInvalidHostname, - proxyServerFunc: httpsServerInvalidHostname, - clientTLS: &tls.Config{InsecureSkipVerify: true}, - serverConnectionHeader: "Upgrade", - serverUpgradeHeader: "SPDY/3.1", - serverStatusCode: http.StatusSwitchingProtocols, - shouldError: false, // works because the test proxy ignores TLS errors - }, - "proxied https with auth (invalid hostname + InsecureSkipVerify) -> https (invalid hostname)": { - serverFunc: httpsServerInvalidHostname, - proxyServerFunc: httpsServerInvalidHostname, - proxyAuth: url.UserPassword("proxyuser", "proxypasswd"), - clientTLS: &tls.Config{InsecureSkipVerify: true}, - serverConnectionHeader: "Upgrade", - serverUpgradeHeader: "SPDY/3.1", - serverStatusCode: http.StatusSwitchingProtocols, - shouldError: false, // works because the test proxy ignores TLS errors - }, - "proxied https (invalid hostname + hostname verification) -> https (invalid hostname)": { - serverFunc: httpsServerInvalidHostname, - proxyServerFunc: httpsServerInvalidHostname, - clientTLS: &tls.Config{InsecureSkipVerify: false}, - serverConnectionHeader: "Upgrade", - serverUpgradeHeader: "SPDY/3.1", - serverStatusCode: http.StatusSwitchingProtocols, - shouldError: true, // fails because the client doesn't trust the proxy - }, - "proxied https (valid hostname + RootCAs) -> https (valid hostname + RootCAs)": { - serverFunc: httpsServerValidHostname, - proxyServerFunc: httpsServerValidHostname, - clientTLS: &tls.Config{RootCAs: localhostPool}, - serverConnectionHeader: "Upgrade", - serverUpgradeHeader: "SPDY/3.1", - serverStatusCode: http.StatusSwitchingProtocols, - shouldError: false, - }, - "proxied https with auth (valid hostname + RootCAs) -> https (valid hostname + RootCAs)": { - serverFunc: httpsServerValidHostname, - proxyServerFunc: httpsServerValidHostname, - proxyAuth: url.UserPassword("proxyuser", "proxypasswd"), - clientTLS: &tls.Config{RootCAs: localhostPool}, - serverConnectionHeader: "Upgrade", - serverUpgradeHeader: "SPDY/3.1", - serverStatusCode: http.StatusSwitchingProtocols, - shouldError: false, - }, - } + w.Header().Set(httpstream.HeaderConnection, config.connectionHeader) + w.Header().Set(httpstream.HeaderUpgrade, config.upgradeHeader) + w.WriteHeader(config.statusCode) - for k, testCase := range testCases { - server := testCase.serverFunc(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - if testCase.shouldError { - if e, a := httpstream.HeaderUpgrade, req.Header.Get(httpstream.HeaderConnection); e != a { - t.Fatalf("%s: Expected connection=upgrade header, got '%s", k, a) - } + return + } - w.Header().Set(httpstream.HeaderConnection, testCase.serverConnectionHeader) - w.Header().Set(httpstream.HeaderUpgrade, testCase.serverUpgradeHeader) - w.WriteHeader(testCase.serverStatusCode) + streamCh := make(chan httpstream.Stream) - return - } + responseUpgrader := NewResponseUpgrader() + spdyConn := responseUpgrader.UpgradeResponse(w, req, func(s httpstream.Stream, replySent <-chan struct{}) error { + streamCh <- s + return nil + }) + if spdyConn == nil { + t.Fatal("unexpected nil spdyConn") + } + defer spdyConn.Close() - streamCh := make(chan httpstream.Stream) + stream := <-streamCh + io.Copy(stream, stream) + } +} - responseUpgrader := NewResponseUpgrader() - spdyConn := responseUpgrader.UpgradeResponse(w, req, func(s httpstream.Stream, replySent <-chan struct{}) error { - streamCh <- s - return nil - }) - if spdyConn == nil { - t.Fatalf("%s: unexpected nil spdyConn", k) - } - defer spdyConn.Close() +type serverFunc func(http.Handler) *httptest.Server + +func httpsServerInvalidHostname(t *testing.T) serverFunc { + return func(h http.Handler) *httptest.Server { + cert, err := tls.X509KeyPair(exampleCert, exampleKey) + if err != nil { + t.Errorf("https (invalid hostname): proxy_test: %v", err) + } + ts := httptest.NewUnstartedServer(h) + ts.TLS = &tls.Config{ + Certificates: []tls.Certificate{cert}, + } + ts.StartTLS() + return ts + } +} - stream := <-streamCh - io.Copy(stream, stream) - })) - defer server.Close() +func httpsServerValidHostname(t *testing.T) serverFunc { + return func(h http.Handler) *httptest.Server { + cert, err := tls.X509KeyPair(localhostCert, localhostKey) + if err != nil { + t.Errorf("https (valid hostname): proxy_test: %v", err) + } + ts := httptest.NewUnstartedServer(h) + ts.TLS = &tls.Config{ + Certificates: []tls.Certificate{cert}, + } + ts.StartTLS() + return ts + } +} - serverURL, err := url.Parse(server.URL) - if err != nil { - t.Fatalf("%s: Error creating request: %s", k, err) - } - req, err := http.NewRequest("GET", server.URL, nil) - if err != nil { - t.Fatalf("%s: Error creating request: %s", k, err) - } +func localhostCertPool(t *testing.T) *x509.CertPool { + localhostPool := x509.NewCertPool() - spdyTransport := NewRoundTripper(testCase.clientTLS, redirect, redirect) - - var proxierCalled bool - var proxyCalledWithHost string - var proxyCalledWithAuth bool - var proxyCalledWithAuthHeader string - if testCase.proxyServerFunc != nil { - proxyHandler := goproxy.NewProxyHttpServer() - - proxyHandler.OnRequest().HandleConnectFunc(func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) { - proxyCalledWithHost = host - - proxyAuthHeaderName := "Proxy-Authorization" - _, proxyCalledWithAuth = ctx.Req.Header[proxyAuthHeaderName] - proxyCalledWithAuthHeader = ctx.Req.Header.Get(proxyAuthHeaderName) - return goproxy.OkConnect, host - }) - - proxy := testCase.proxyServerFunc(proxyHandler) - - spdyTransport.proxier = func(proxierReq *http.Request) (*url.URL, error) { - proxierCalled = true - proxyURL, err := url.Parse(proxy.URL) - if err != nil { - return nil, err - } - proxyURL.User = testCase.proxyAuth - return proxyURL, nil - } - defer proxy.Close() - } + if !localhostPool.AppendCertsFromPEM(localhostCert) { + t.Errorf("error setting up localhostCert pool") + } + return localhostPool +} - client := &http.Client{Transport: spdyTransport} +// be sure to unset environment variable https_proxy (if exported) before testing, otherwise the testing will fail unexpectedly. +func TestRoundTripAndNewConnection(t *testing.T) { + localhostPool := localhostCertPool(t) + + testCases := map[string]struct { + serverFunc func(http.Handler) *httptest.Server + proxyServerFunc func(http.Handler) *httptest.Server + proxyAuth *url.Userinfo + clientTLS *tls.Config + serverConnectionHeader string + serverUpgradeHeader string + serverStatusCode int + shouldError bool + }{ + "no headers": { + serverFunc: httptest.NewServer, + serverConnectionHeader: "", + serverUpgradeHeader: "", + serverStatusCode: http.StatusSwitchingProtocols, + shouldError: true, + }, + "no upgrade header": { + serverFunc: httptest.NewServer, + serverConnectionHeader: "Upgrade", + serverUpgradeHeader: "", + serverStatusCode: http.StatusSwitchingProtocols, + shouldError: true, + }, + "no connection header": { + serverFunc: httptest.NewServer, + serverConnectionHeader: "", + serverUpgradeHeader: "SPDY/3.1", + serverStatusCode: http.StatusSwitchingProtocols, + shouldError: true, + }, + "no switching protocol status code": { + serverFunc: httptest.NewServer, + serverConnectionHeader: "Upgrade", + serverUpgradeHeader: "SPDY/3.1", + serverStatusCode: http.StatusForbidden, + shouldError: true, + }, + "http": { + serverFunc: httptest.NewServer, + serverConnectionHeader: "Upgrade", + serverUpgradeHeader: "SPDY/3.1", + serverStatusCode: http.StatusSwitchingProtocols, + shouldError: false, + }, + "https (invalid hostname + InsecureSkipVerify)": { + serverFunc: httpsServerInvalidHostname(t), + clientTLS: &tls.Config{InsecureSkipVerify: true}, + serverConnectionHeader: "Upgrade", + serverUpgradeHeader: "SPDY/3.1", + serverStatusCode: http.StatusSwitchingProtocols, + shouldError: false, + }, + "https (invalid hostname + hostname verification)": { + serverFunc: httpsServerInvalidHostname(t), + clientTLS: &tls.Config{InsecureSkipVerify: false}, + serverConnectionHeader: "Upgrade", + serverUpgradeHeader: "SPDY/3.1", + serverStatusCode: http.StatusSwitchingProtocols, + shouldError: true, + }, + "https (valid hostname + RootCAs)": { + serverFunc: httpsServerValidHostname(t), + clientTLS: &tls.Config{RootCAs: localhostPool}, + serverConnectionHeader: "Upgrade", + serverUpgradeHeader: "SPDY/3.1", + serverStatusCode: http.StatusSwitchingProtocols, + shouldError: false, + }, + "proxied http->http": { + serverFunc: httptest.NewServer, + proxyServerFunc: httptest.NewServer, + serverConnectionHeader: "Upgrade", + serverUpgradeHeader: "SPDY/3.1", + serverStatusCode: http.StatusSwitchingProtocols, + shouldError: false, + }, + "proxied https (invalid hostname + InsecureSkipVerify) -> http": { + serverFunc: httptest.NewServer, + proxyServerFunc: httpsServerInvalidHostname(t), + clientTLS: &tls.Config{InsecureSkipVerify: true}, + serverConnectionHeader: "Upgrade", + serverUpgradeHeader: "SPDY/3.1", + serverStatusCode: http.StatusSwitchingProtocols, + shouldError: false, + }, + "proxied https with auth (invalid hostname + InsecureSkipVerify) -> http": { + serverFunc: httptest.NewServer, + proxyServerFunc: httpsServerInvalidHostname(t), + proxyAuth: url.UserPassword("proxyuser", "proxypasswd"), + clientTLS: &tls.Config{InsecureSkipVerify: true}, + serverConnectionHeader: "Upgrade", + serverUpgradeHeader: "SPDY/3.1", + serverStatusCode: http.StatusSwitchingProtocols, + shouldError: false, + }, + "proxied https (invalid hostname + hostname verification) -> http": { + serverFunc: httptest.NewServer, + proxyServerFunc: httpsServerInvalidHostname(t), + clientTLS: &tls.Config{InsecureSkipVerify: false}, + serverConnectionHeader: "Upgrade", + serverUpgradeHeader: "SPDY/3.1", + serverStatusCode: http.StatusSwitchingProtocols, + shouldError: true, // fails because the client doesn't trust the proxy + }, + "proxied https (valid hostname + RootCAs) -> http": { + serverFunc: httptest.NewServer, + proxyServerFunc: httpsServerValidHostname(t), + clientTLS: &tls.Config{RootCAs: localhostPool}, + serverConnectionHeader: "Upgrade", + serverUpgradeHeader: "SPDY/3.1", + serverStatusCode: http.StatusSwitchingProtocols, + shouldError: false, + }, + "proxied https with auth (valid hostname + RootCAs) -> http": { + serverFunc: httptest.NewServer, + proxyServerFunc: httpsServerValidHostname(t), + proxyAuth: url.UserPassword("proxyuser", "proxypasswd"), + clientTLS: &tls.Config{RootCAs: localhostPool}, + serverConnectionHeader: "Upgrade", + serverUpgradeHeader: "SPDY/3.1", + serverStatusCode: http.StatusSwitchingProtocols, + shouldError: false, + }, + "proxied https (invalid hostname + InsecureSkipVerify) -> https (invalid hostname)": { + serverFunc: httpsServerInvalidHostname(t), + proxyServerFunc: httpsServerInvalidHostname(t), + clientTLS: &tls.Config{InsecureSkipVerify: true}, + serverConnectionHeader: "Upgrade", + serverUpgradeHeader: "SPDY/3.1", + serverStatusCode: http.StatusSwitchingProtocols, + shouldError: false, // works because the test proxy ignores TLS errors + }, + "proxied https with auth (invalid hostname + InsecureSkipVerify) -> https (invalid hostname)": { + serverFunc: httpsServerInvalidHostname(t), + proxyServerFunc: httpsServerInvalidHostname(t), + proxyAuth: url.UserPassword("proxyuser", "proxypasswd"), + clientTLS: &tls.Config{InsecureSkipVerify: true}, + serverConnectionHeader: "Upgrade", + serverUpgradeHeader: "SPDY/3.1", + serverStatusCode: http.StatusSwitchingProtocols, + shouldError: false, // works because the test proxy ignores TLS errors + }, + "proxied https (invalid hostname + hostname verification) -> https (invalid hostname)": { + serverFunc: httpsServerInvalidHostname(t), + proxyServerFunc: httpsServerInvalidHostname(t), + clientTLS: &tls.Config{InsecureSkipVerify: false}, + serverConnectionHeader: "Upgrade", + serverUpgradeHeader: "SPDY/3.1", + serverStatusCode: http.StatusSwitchingProtocols, + shouldError: true, // fails because the client doesn't trust the proxy + }, + "proxied https (valid hostname + RootCAs) -> https (valid hostname + RootCAs)": { + serverFunc: httpsServerValidHostname(t), + proxyServerFunc: httpsServerValidHostname(t), + clientTLS: &tls.Config{RootCAs: localhostPool}, + serverConnectionHeader: "Upgrade", + serverUpgradeHeader: "SPDY/3.1", + serverStatusCode: http.StatusSwitchingProtocols, + shouldError: false, + }, + "proxied https with auth (valid hostname + RootCAs) -> https (valid hostname + RootCAs)": { + serverFunc: httpsServerValidHostname(t), + proxyServerFunc: httpsServerValidHostname(t), + proxyAuth: url.UserPassword("proxyuser", "proxypasswd"), + clientTLS: &tls.Config{RootCAs: localhostPool}, + serverConnectionHeader: "Upgrade", + serverUpgradeHeader: "SPDY/3.1", + serverStatusCode: http.StatusSwitchingProtocols, + shouldError: false, + }, + "proxied valid https, proxy auth with chars that percent escape -> valid https": { + serverFunc: httpsServerValidHostname(t), + proxyServerFunc: httpsServerValidHostname(t), + proxyAuth: url.UserPassword("proxy user", "proxypasswd%"), + clientTLS: &tls.Config{RootCAs: localhostPool}, + serverConnectionHeader: "Upgrade", + serverUpgradeHeader: "SPDY/3.1", + serverStatusCode: http.StatusSwitchingProtocols, + shouldError: false, + }, + } - resp, err := client.Do(req) - var conn httpstream.Connection - if err == nil { - conn, err = spdyTransport.NewConnection(resp) - } - haveErr := err != nil - if e, a := testCase.shouldError, haveErr; e != a { - t.Fatalf("%s: shouldError=%t, got %t: %v", k, e, a, err) - } - if testCase.shouldError { - continue - } - defer conn.Close() + for k, testCase := range testCases { + t.Run(k, func(t *testing.T) { + server := testCase.serverFunc(serverHandler( + t, serverHandlerConfig{ + shouldError: testCase.shouldError, + statusCode: testCase.serverStatusCode, + connectionHeader: testCase.serverConnectionHeader, + upgradeHeader: testCase.serverUpgradeHeader, + }, + )) + defer server.Close() - if resp.StatusCode != http.StatusSwitchingProtocols { - t.Fatalf("%s: expected http 101 switching protocols, got %d", k, resp.StatusCode) - } + serverURL, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("error creating request: %s", err) + } + req, err := http.NewRequest("GET", server.URL, nil) + if err != nil { + t.Fatalf("error creating request: %s", err) + } - stream, err := conn.CreateStream(http.Header{}) - if err != nil { - t.Fatalf("%s: error creating client stream: %s", k, err) - } + spdyTransport := NewRoundTripper(testCase.clientTLS) - n, err := stream.Write([]byte("hello")) - if err != nil { - t.Fatalf("%s: error writing to stream: %s", k, err) - } - if n != 5 { - t.Fatalf("%s: Expected to write 5 bytes, but actually wrote %d", k, n) - } + var proxierCalled bool + var proxyCalledWithHost string + var proxyCalledWithAuth bool + var proxyCalledWithAuthHeader string + if testCase.proxyServerFunc != nil { + proxyHandler := goproxy.NewProxyHttpServer() - b := make([]byte, 5) - n, err = stream.Read(b) - if err != nil { - t.Fatalf("%s: error reading from stream: %s", k, err) - } - if n != 5 { - t.Fatalf("%s: Expected to read 5 bytes, but actually read %d", k, n) - } - if e, a := "hello", string(b[0:n]); e != a { - t.Fatalf("%s: expected '%s', got '%s'", k, e, a) - } + proxyHandler.OnRequest().HandleConnectFunc(func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) { + proxyCalledWithHost = host - if testCase.proxyServerFunc != nil { - if !proxierCalled { - t.Fatalf("%s: Expected to use a proxy but proxier in SpdyRoundTripper wasn't called", k) - } - if proxyCalledWithHost != serverURL.Host { - t.Fatalf("%s: Expected to see a call to the proxy for backend %q, got %q", k, serverURL.Host, proxyCalledWithHost) + proxyAuthHeaderName := "Proxy-Authorization" + _, proxyCalledWithAuth = ctx.Req.Header[proxyAuthHeaderName] + proxyCalledWithAuthHeader = ctx.Req.Header.Get(proxyAuthHeaderName) + return goproxy.OkConnect, host + }) + + proxy := testCase.proxyServerFunc(proxyHandler) + + spdyTransport.proxier = func(proxierReq *http.Request) (*url.URL, error) { + proxierCalled = true + proxyURL, err := url.Parse(proxy.URL) + if err != nil { + return nil, err } + proxyURL.User = testCase.proxyAuth + return proxyURL, nil } + defer proxy.Close() + } + + client := &http.Client{Transport: spdyTransport} + + resp, err := client.Do(req) + var conn httpstream.Connection + if err == nil { + conn, err = spdyTransport.NewConnection(resp) + } + haveErr := err != nil + if e, a := testCase.shouldError, haveErr; e != a { + t.Fatalf("shouldError=%t, got %t: %v", e, a, err) + } + if testCase.shouldError { + return + } + defer conn.Close() + + if resp.StatusCode != http.StatusSwitchingProtocols { + t.Fatalf("expected http 101 switching protocols, got %d", resp.StatusCode) + } + + stream, err := conn.CreateStream(http.Header{}) + if err != nil { + t.Fatalf("error creating client stream: %s", err) + } + + n, err := stream.Write([]byte("hello")) + if err != nil { + t.Fatalf("error writing to stream: %s", err) + } + if n != 5 { + t.Fatalf("expected to write 5 bytes, but actually wrote %d", n) + } - var expectedProxyAuth string - if testCase.proxyAuth != nil { - encodedCredentials := base64.StdEncoding.EncodeToString([]byte(testCase.proxyAuth.String())) - expectedProxyAuth = "Basic " + encodedCredentials + b := make([]byte, 5) + n, err = stream.Read(b) + if err != nil { + t.Fatalf("error reading from stream: %s", err) + } + if n != 5 { + t.Fatalf("expected to read 5 bytes, but actually read %d", n) + } + if e, a := "hello", string(b[0:n]); e != a { + t.Fatalf("expected '%s', got '%s'", e, a) + } + + if testCase.proxyServerFunc != nil { + if !proxierCalled { + t.Fatal("expected to use a proxy but proxier in SpdyRoundTripper wasn't called") + } + if proxyCalledWithHost != serverURL.Host { + t.Fatalf("expected to see a call to the proxy for backend %q, got %q", serverURL.Host, proxyCalledWithHost) } - if len(expectedProxyAuth) == 0 && proxyCalledWithAuth { - t.Fatalf("%s: Proxy authorization unexpected, got %q", k, proxyCalledWithAuthHeader) + } + + if testCase.proxyAuth != nil { + expectedUsername := testCase.proxyAuth.Username() + expectedPassword, _ := testCase.proxyAuth.Password() + username, password, ok := (&http.Request{Header: http.Header{"Authorization": []string{proxyCalledWithAuthHeader}}}).BasicAuth() + if !ok { + t.Fatalf("invalid proxy auth header %s", proxyCalledWithAuthHeader) } - if proxyCalledWithAuthHeader != expectedProxyAuth { - t.Fatalf("%s: Expected to see a call to the proxy with credentials %q, got %q", k, testCase.proxyAuth, proxyCalledWithAuthHeader) + if username != expectedUsername || password != expectedPassword { + t.Fatalf("expected proxy auth \"%s:%s\", got \"%s:%s\"", expectedUsername, expectedPassword, username, password) } + } else if proxyCalledWithAuth { + t.Fatalf("proxy authorization unexpected, got %q", proxyCalledWithAuthHeader) } }) } } -func TestRoundTripRedirects(t *testing.T) { - tests := []struct { - redirects int32 - expectSuccess bool - }{ - {0, true}, - {1, true}, - {9, true}, - {10, false}, +type Interceptor struct { + Authorization socks5.AuthContext + proxyCalledWithHost *string +} + +func (i *Interceptor) GetAuthContext() (int, map[string]string) { + return int(i.Authorization.Method), i.Authorization.Payload +} + +func (i *Interceptor) Rewrite(ctx context.Context, req *socks5.Request) (context.Context, *socks5.AddrSpec) { + *i.proxyCalledWithHost = req.DestAddr.Address() + i.Authorization = socks5.AuthContext(*req.AuthContext) + return ctx, req.DestAddr +} + +// be sure to unset environment variable https_proxy (if exported) before testing, otherwise the testing will fail unexpectedly. +func TestRoundTripSocks5AndNewConnection(t *testing.T) { + localhostPool := localhostCertPool(t) + + socks5Server := func(creds *socks5.StaticCredentials, interceptor *Interceptor) *socks5.Server { + var conf *socks5.Config + if creds != nil { + authenticator := socks5.UserPassAuthenticator{Credentials: creds} + conf = &socks5.Config{ + AuthMethods: []socks5.Authenticator{authenticator}, + Rewriter: interceptor, + } + } else { + conf = &socks5.Config{Rewriter: interceptor} + } + + ts, err := socks5.New(conf) + if err != nil { + t.Errorf("failed to create sock5 server: %v", err) + } + return ts } - for _, test := range tests { - t.Run(fmt.Sprintf("with %d redirects", test.redirects), func(t *testing.T) { - var redirects int32 = 0 - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - if redirects < test.redirects { - atomic.AddInt32(&redirects, 1) - http.Redirect(w, req, "redirect", http.StatusFound) - return - } - streamCh := make(chan httpstream.Stream) - responseUpgrader := NewResponseUpgrader() - spdyConn := responseUpgrader.UpgradeResponse(w, req, func(s httpstream.Stream, replySent <-chan struct{}) error { - streamCh <- s - return nil - }) - if spdyConn == nil { - t.Fatalf("unexpected nil spdyConn") - } - defer spdyConn.Close() + testCases := map[string]struct { + clientTLS *tls.Config + proxyAuth *url.Userinfo + serverConnectionHeader string + serverFunc serverFunc + serverStatusCode int + serverUpgradeHeader string + shouldError bool + }{ + "proxied without auth -> http": { + serverFunc: httptest.NewServer, + serverConnectionHeader: "Upgrade", + serverStatusCode: http.StatusSwitchingProtocols, + serverUpgradeHeader: "SPDY/3.1", + shouldError: false, + }, + "proxied with invalid auth -> http": { + serverFunc: httptest.NewServer, + proxyAuth: url.UserPassword("invalid", "auth"), + serverConnectionHeader: "Upgrade", + serverStatusCode: http.StatusSwitchingProtocols, + serverUpgradeHeader: "SPDY/3.1", + shouldError: true, + }, + "proxied with valid auth -> http": { + serverFunc: httptest.NewServer, + proxyAuth: url.UserPassword("proxyuser", "proxypasswd"), + serverConnectionHeader: "Upgrade", + serverStatusCode: http.StatusSwitchingProtocols, + serverUpgradeHeader: "SPDY/3.1", + shouldError: false, + }, + "proxied with valid auth -> https (invalid hostname + InsecureSkipVerify)": { + serverFunc: httpsServerInvalidHostname(t), + proxyAuth: url.UserPassword("proxyuser", "proxypasswd"), + clientTLS: &tls.Config{InsecureSkipVerify: true}, + serverConnectionHeader: "Upgrade", + serverUpgradeHeader: "SPDY/3.1", + serverStatusCode: http.StatusSwitchingProtocols, + shouldError: false, + }, + "proxied with valid auth -> https (invalid hostname + hostname verification)": { + serverFunc: httpsServerInvalidHostname(t), + proxyAuth: url.UserPassword("proxyuser", "proxypasswd"), + clientTLS: &tls.Config{InsecureSkipVerify: false}, + serverConnectionHeader: "Upgrade", + serverUpgradeHeader: "SPDY/3.1", + serverStatusCode: http.StatusSwitchingProtocols, + shouldError: true, + }, + "proxied with valid auth -> https (valid hostname + RootCAs)": { + serverFunc: httpsServerValidHostname(t), + proxyAuth: url.UserPassword("proxyuser", "proxypasswd"), + clientTLS: &tls.Config{RootCAs: localhostPool}, + serverConnectionHeader: "Upgrade", + serverUpgradeHeader: "SPDY/3.1", + serverStatusCode: http.StatusSwitchingProtocols, + shouldError: false, + }, + } - stream := <-streamCh - io.Copy(stream, stream) - })) + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + server := testCase.serverFunc(serverHandler( + t, serverHandlerConfig{ + shouldError: testCase.shouldError, + statusCode: testCase.serverStatusCode, + connectionHeader: testCase.serverConnectionHeader, + upgradeHeader: testCase.serverUpgradeHeader, + }, + )) defer server.Close() req, err := http.NewRequest("GET", server.URL, nil) if err != nil { - t.Fatalf("Error creating request: %s", err) + t.Fatalf("error creating request: %s", err) } - spdyTransport := NewRoundTripper(nil, true, true) - client := &http.Client{Transport: spdyTransport} + spdyTransport := NewRoundTripper(testCase.clientTLS) + var proxierCalled bool + var proxyCalledWithHost string - resp, err := client.Do(req) - if test.expectSuccess { + interceptor := &Interceptor{proxyCalledWithHost: &proxyCalledWithHost} + + proxyHandler := socks5Server(nil, interceptor) + + if testCase.proxyAuth != nil { + proxyHandler = socks5Server(&socks5.StaticCredentials{ + "proxyuser": "proxypasswd", // Socks5 server static credentials when client authentication is expected + }, interceptor) + } + + closed := make(chan struct{}) + isClosed := func() bool { + select { + case <-closed: + return true + default: + return false + } + } + + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("socks5Server: proxy_test: Listen: %v", err) + } + defer l.Close() + + go func(shoulderror bool) { + conn, err := l.Accept() if err != nil { - t.Fatalf("error calling Do: %v", err) + if isClosed() { + return + } + + t.Errorf("error accepting connection: %s", err) } - } else { - if err == nil { - t.Fatalf("expecting an error") - } else if !strings.Contains(err.Error(), "too many redirects") { - t.Fatalf("expecting too many redirects, got %v", err) + + if err := proxyHandler.ServeConn(conn); err != nil && !shoulderror { + // If the connection request is closed before the channel is closed + // the test will fail with a ServeConn error. Since the test only return + // early if expects shouldError=true, the channel is closed at the end of + // the test, just before all the deferred connections Close() are executed. + if isClosed() { + return + } + + t.Errorf("ServeConn error: %s", err) } + }(testCase.shouldError) + spdyTransport.proxier = func(proxierReq *http.Request) (*url.URL, error) { + proxierCalled = true + return &url.URL{ + Scheme: "socks5", + Host: net.JoinHostPort("127.0.0.1", strconv.Itoa(l.Addr().(*net.TCPAddr).Port)), + User: testCase.proxyAuth, + }, nil + } + + client := &http.Client{Transport: spdyTransport} + + resp, err := client.Do(req) + haveErr := err != nil + if e, a := testCase.shouldError, haveErr; e != a { + t.Fatalf("shouldError=%t, got %t: %v", e, a, err) + } + if testCase.shouldError { return } conn, err := spdyTransport.NewConnection(resp) - if err != nil { - t.Fatalf("error calling NewConnection: %v", err) + haveErr = err != nil + if e, a := testCase.shouldError, haveErr; e != a { + t.Fatalf("shouldError=%t, got %t: %v", e, a, err) + } + if testCase.shouldError { + return } + defer conn.Close() if resp.StatusCode != http.StatusSwitchingProtocols { @@ -462,7 +639,7 @@ func TestRoundTripRedirects(t *testing.T) { t.Fatalf("error writing to stream: %s", err) } if n != 5 { - t.Fatalf("Expected to write 5 bytes, but actually wrote %d", n) + t.Fatalf("expected to write 5 bytes, but actually wrote %d", n) } b := make([]byte, 5) @@ -471,17 +648,70 @@ func TestRoundTripRedirects(t *testing.T) { t.Fatalf("error reading from stream: %s", err) } if n != 5 { - t.Fatalf("Expected to read 5 bytes, but actually read %d", n) + t.Fatalf("expected to read 5 bytes, but actually read %d", n) } if e, a := "hello", string(b[0:n]); e != a { t.Fatalf("expected '%s', got '%s'", e, a) } + + if !proxierCalled { + t.Fatal("xpected to use a proxy but proxier in SpdyRoundTripper wasn't called") + } + + serverURL, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("error creating request: %s", err) + } + if proxyCalledWithHost != serverURL.Host { + t.Fatalf("expected to see a call to the proxy for backend %q, got %q", serverURL.Host, proxyCalledWithHost) + } + + authMethod, authUser := interceptor.GetAuthContext() + + if testCase.proxyAuth != nil { + expectedSocks5AuthMethod := 2 + expectedSocks5AuthUser := "proxyuser" + + if expectedSocks5AuthMethod != authMethod { + t.Fatalf("socks5 Proxy authorization unexpected, got %d, expected %d", authMethod, expectedSocks5AuthMethod) + } + + if expectedSocks5AuthUser != authUser["Username"] { + t.Fatalf("socks5 Proxy authorization user unexpected, got %q, expected %q", authUser["Username"], expectedSocks5AuthUser) + } + } else { + if authMethod != 0 { + t.Fatalf("proxy authentication method unexpected, got %d", authMethod) + } + if len(authUser) != 0 { + t.Fatalf("unexpected proxy user: %v", authUser) + } + } + + // The channel must be closed before any of the connections are closed + close(closed) + }) + } +} + +func TestRoundTripPassesContextToDialer(t *testing.T) { + urls := []string{"http://127.0.0.1:1233/", "https://127.0.0.1:1233/"} + for _, u := range urls { + t.Run(u, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + req, err := http.NewRequestWithContext(ctx, "GET", u, nil) + require.NoError(t, err) + spdyTransport := NewRoundTripper(&tls.Config{}) + _, err = spdyTransport.Dial(req) + assert.EqualError(t, err, "dial tcp 127.0.0.1:1233: operation was canceled") }) } } // exampleCert was generated from crypto/tls/generate_cert.go with the following command: -// go run generate_cert.go --rsa-bits 2048 --host example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h +// +// go run generate_cert.go --rsa-bits 2048 --host example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h var exampleCert = []byte(`-----BEGIN CERTIFICATE----- MIIDADCCAeigAwIBAgIQVHG3Fn9SdWayyLOZKCW1vzANBgkqhkiG9w0BAQsFADAS MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw @@ -531,7 +761,8 @@ LB4rdf46lV0mUkvd2/oofIbTrzukjQSnyfLawb/2uJGV1IkTcZcn9CI= -----END RSA PRIVATE KEY-----`) // localhostCert was generated from crypto/tls/generate_cert.go with the following command: -// go run generate_cert.go --rsa-bits 2048 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h +// +// go run generate_cert.go --rsa-bits 2048 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h var localhostCert = []byte(`-----BEGIN CERTIFICATE----- MIIDGTCCAgGgAwIBAgIRALL5AZcefF4kkYV1SEG6YrMwDQYJKoZIhvcNAQELBQAw EjEQMA4GA1UEChMHQWNtZSBDbzAgFw03MDAxMDEwMDAwMDBaGA8yMDg0MDEyOTE2 diff --git a/pkg/util/httpstream/spdy/upgrade.go b/pkg/util/httpstream/spdy/upgrade.go index f17eb09e9..d30ae2fa3 100644 --- a/pkg/util/httpstream/spdy/upgrade.go +++ b/pkg/util/httpstream/spdy/upgrade.go @@ -94,7 +94,7 @@ func (u responseUpgrader) UpgradeResponse(w http.ResponseWriter, req *http.Reque hijacker, ok := w.(http.Hijacker) if !ok { - errorMsg := fmt.Sprintf("unable to upgrade: unable to hijack response") + errorMsg := "unable to upgrade: unable to hijack response" http.Error(w, errorMsg, http.StatusInternalServerError) return nil } diff --git a/pkg/util/intstr/generated.pb.go b/pkg/util/intstr/generated.pb.go index a4792034a..8f9ced93f 100644 --- a/pkg/util/intstr/generated.pb.go +++ b/pkg/util/intstr/generated.pb.go @@ -78,25 +78,25 @@ func init() { var fileDescriptor_94e046ae3ce6121c = []byte{ // 292 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x4c, 0x8f, 0x31, 0x4b, 0x33, 0x31, - 0x1c, 0xc6, 0x93, 0xb7, 0x7d, 0x8b, 0x9e, 0xe0, 0x50, 0x1c, 0x8a, 0x43, 0x7a, 0x28, 0xc8, 0x0d, - 0x9a, 0xac, 0xe2, 0xd8, 0xad, 0x20, 0x08, 0x57, 0x71, 0x70, 0xbb, 0x6b, 0x63, 0x1a, 0xae, 0x4d, - 0x42, 0xee, 0x7f, 0xc2, 0x6d, 0xfd, 0x08, 0xba, 0x39, 0xfa, 0x71, 0x6e, 0xec, 0xd8, 0x41, 0x8a, - 0x17, 0xbf, 0x85, 0x93, 0x5c, 0xee, 0x40, 0xa7, 0xe4, 0x79, 0x9e, 0xdf, 0x2f, 0x90, 0xe0, 0x36, - 0xbb, 0xce, 0xa9, 0xd4, 0x2c, 0x2b, 0x52, 0x6e, 0x15, 0x07, 0x9e, 0xb3, 0x67, 0xae, 0x16, 0xda, - 0xb2, 0x6e, 0x48, 0x8c, 0x5c, 0x27, 0xf3, 0xa5, 0x54, 0xdc, 0x96, 0xcc, 0x64, 0x82, 0x15, 0x20, - 0x57, 0x4c, 0x2a, 0xc8, 0xc1, 0x32, 0xc1, 0x15, 0xb7, 0x09, 0xf0, 0x05, 0x35, 0x56, 0x83, 0x1e, - 0x9e, 0xb7, 0x12, 0xfd, 0x2b, 0x51, 0x93, 0x09, 0xda, 0x48, 0xb4, 0x95, 0x4e, 0xaf, 0x84, 0x84, - 0x65, 0x91, 0xd2, 0xb9, 0x5e, 0x33, 0xa1, 0x85, 0x66, 0xde, 0x4d, 0x8b, 0x27, 0x9f, 0x7c, 0xf0, - 0xb7, 0xf6, 0xcd, 0xb3, 0x57, 0x1c, 0x1c, 0x4d, 0x15, 0xdc, 0xd9, 0x19, 0x58, 0xa9, 0xc4, 0x30, - 0x0a, 0xfa, 0x50, 0x1a, 0x3e, 0xc2, 0x21, 0x8e, 0x7a, 0x93, 0x93, 0x6a, 0x3f, 0x46, 0x6e, 0x3f, - 0xee, 0xdf, 0x97, 0x86, 0x7f, 0x77, 0x67, 0xec, 0x89, 0xe1, 0x45, 0x30, 0x90, 0x0a, 0x1e, 0x92, - 0xd5, 0xe8, 0x5f, 0x88, 0xa3, 0xff, 0x93, 0xe3, 0x8e, 0x1d, 0x4c, 0x7d, 0x1b, 0x77, 0x6b, 0xc3, - 0xe5, 0x60, 0x1b, 0xae, 0x17, 0xe2, 0xe8, 0xf0, 0x97, 0x9b, 0xf9, 0x36, 0xee, 0xd6, 0x9b, 0x83, - 0xb7, 0xf7, 0x31, 0xda, 0x7c, 0x84, 0x68, 0x72, 0x59, 0xd5, 0x04, 0x6d, 0x6b, 0x82, 0x76, 0x35, - 0x41, 0x1b, 0x47, 0x70, 0xe5, 0x08, 0xde, 0x3a, 0x82, 0x77, 0x8e, 0xe0, 0x4f, 0x47, 0xf0, 0xcb, - 0x17, 0x41, 0x8f, 0x83, 0xf6, 0xc3, 0x3f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x52, 0xa0, 0xb5, 0xc9, - 0x64, 0x01, 0x00, 0x00, + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x90, 0xb1, 0x4a, 0x03, 0x31, + 0x1c, 0xc6, 0x13, 0x5b, 0x8b, 0x9e, 0xe0, 0x50, 0x1c, 0x8a, 0x43, 0x7a, 0x58, 0x90, 0x5b, 0x4c, + 0x56, 0x71, 0xec, 0x56, 0x10, 0x84, 0x56, 0x1c, 0xdc, 0xee, 0xda, 0x98, 0x86, 0x6b, 0x93, 0x90, + 0xfb, 0x9f, 0x70, 0x5b, 0x1f, 0x41, 0x37, 0x47, 0x1f, 0xe7, 0xc6, 0x8e, 0x1d, 0xa4, 0x78, 0xf1, + 0x2d, 0x9c, 0xe4, 0x72, 0x07, 0x3a, 0x3a, 0x25, 0xdf, 0xf7, 0xfd, 0x7e, 0x19, 0x12, 0xdc, 0xa6, + 0xd7, 0x19, 0x95, 0x9a, 0xa5, 0x79, 0xc2, 0xad, 0xe2, 0xc0, 0x33, 0xf6, 0xcc, 0xd5, 0x42, 0x5b, + 0xd6, 0x0e, 0xb1, 0x91, 0xeb, 0x78, 0xbe, 0x94, 0x8a, 0xdb, 0x82, 0x99, 0x54, 0xb0, 0x1c, 0xe4, + 0x8a, 0x49, 0x05, 0x19, 0x58, 0x26, 0xb8, 0xe2, 0x36, 0x06, 0xbe, 0xa0, 0xc6, 0x6a, 0xd0, 0xfd, + 0x51, 0x23, 0xd1, 0xbf, 0x12, 0x35, 0xa9, 0xa0, 0xb5, 0x44, 0x1b, 0xe9, 0xfc, 0x4a, 0x48, 0x58, + 0xe6, 0x09, 0x9d, 0xeb, 0x35, 0x13, 0x5a, 0x68, 0xe6, 0xdd, 0x24, 0x7f, 0xf2, 0xc9, 0x07, 0x7f, + 0x6b, 0xde, 0xbc, 0x78, 0xc5, 0xc1, 0xc9, 0x44, 0xc1, 0x9d, 0x9d, 0x81, 0x95, 0x4a, 0xf4, 0xa3, + 0xa0, 0x0b, 0x85, 0xe1, 0x03, 0x1c, 0xe2, 0xa8, 0x33, 0x3e, 0x2b, 0xf7, 0x43, 0xe4, 0xf6, 0xc3, + 0xee, 0x7d, 0x61, 0xf8, 0x77, 0x7b, 0x4e, 0x3d, 0xd1, 0xbf, 0x0c, 0x7a, 0x52, 0xc1, 0x43, 0xbc, + 0x1a, 0x1c, 0x84, 0x38, 0x3a, 0x1c, 0x9f, 0xb6, 0x6c, 0x6f, 0xe2, 0xdb, 0x69, 0xbb, 0xd6, 0x5c, + 0x06, 0xb6, 0xe6, 0x3a, 0x21, 0x8e, 0x8e, 0x7f, 0xb9, 0x99, 0x6f, 0xa7, 0xed, 0x7a, 0x73, 0xf4, + 0xf6, 0x3e, 0x44, 0x9b, 0x8f, 0x10, 0x8d, 0x27, 0x65, 0x45, 0xd0, 0xb6, 0x22, 0x68, 0x57, 0x11, + 0xb4, 0x71, 0x04, 0x97, 0x8e, 0xe0, 0xad, 0x23, 0x78, 0xe7, 0x08, 0xfe, 0x74, 0x04, 0xbf, 0x7c, + 0x11, 0xf4, 0x38, 0xfa, 0xc7, 0x17, 0xfe, 0x04, 0x00, 0x00, 0xff, 0xff, 0xdc, 0xc4, 0xf0, 0xa0, + 0x81, 0x01, 0x00, 0x00, } func (m *IntOrString) Marshal() (dAtA []byte, err error) { diff --git a/pkg/util/intstr/generated.proto b/pkg/util/intstr/generated.proto index a76f79851..7c63c5e45 100644 --- a/pkg/util/intstr/generated.proto +++ b/pkg/util/intstr/generated.proto @@ -22,7 +22,7 @@ syntax = "proto2"; package k8s.io.apimachinery.pkg.util.intstr; // Package-wide variables from generator "generated". -option go_package = "intstr"; +option go_package = "k8s.io/apimachinery/pkg/util/intstr"; // IntOrString is a type that can hold an int32 or a string. When used in // JSON or YAML marshalling and unmarshalling, it produces or consumes the diff --git a/pkg/util/intstr/instr_fuzz.go b/pkg/util/intstr/instr_fuzz.go index 2501d5516..a502b5adb 100644 --- a/pkg/util/intstr/instr_fuzz.go +++ b/pkg/util/intstr/instr_fuzz.go @@ -1,3 +1,4 @@ +//go:build !notest // +build !notest /* diff --git a/pkg/util/intstr/intstr.go b/pkg/util/intstr/intstr.go index c0e8927fe..5e8009704 100644 --- a/pkg/util/intstr/intstr.go +++ b/pkg/util/intstr/intstr.go @@ -131,6 +131,10 @@ func (IntOrString) OpenAPISchemaType() []string { return []string{"string"} } // the OpenAPI spec of this type. func (IntOrString) OpenAPISchemaFormat() string { return "int-or-string" } +// OpenAPIV3OneOfTypes is used by the kube-openapi generator when constructing +// the OpenAPI v3 spec of this type. +func (IntOrString) OpenAPIV3OneOfTypes() []string { return []string{"integer", "string"} } + func ValueOrDefault(intOrPercent *IntOrString, defaultValue IntOrString) *IntOrString { if intOrPercent == nil { return &defaultValue @@ -141,7 +145,7 @@ func ValueOrDefault(intOrPercent *IntOrString, defaultValue IntOrString) *IntOrS // GetScaledValueFromIntOrPercent is meant to replace GetValueFromIntOrPercent. // This method returns a scaled value from an IntOrString type. If the IntOrString // is a percentage string value it's treated as a percentage and scaled appropriately -// in accordance to the total, if it's an int value it's treated as a a simple value and +// in accordance to the total, if it's an int value it's treated as a simple value and // if it is a string value which is either non-numeric or numeric but lacking a trailing '%' it returns an error. func GetScaledValueFromIntOrPercent(intOrPercent *IntOrString, total int, roundUp bool) (int, error) { if intOrPercent == nil { diff --git a/pkg/util/json/json.go b/pkg/util/json/json.go index 778e58f70..55dba361c 100644 --- a/pkg/util/json/json.go +++ b/pkg/util/json/json.go @@ -17,10 +17,11 @@ limitations under the License. package json import ( - "bytes" "encoding/json" "fmt" "io" + + kjson "sigs.k8s.io/json" ) // NewEncoder delegates to json.NewEncoder @@ -38,50 +39,11 @@ func Marshal(v interface{}) ([]byte, error) { // limit recursive depth to prevent stack overflow errors const maxDepth = 10000 -// Unmarshal unmarshals the given data -// If v is a *map[string]interface{}, *[]interface{}, or *interface{} numbers -// are converted to int64 or float64 +// Unmarshal unmarshals the given data. +// Object keys are case-sensitive. +// Numbers decoded into interface{} fields are converted to int64 or float64. func Unmarshal(data []byte, v interface{}) error { - switch v := v.(type) { - case *map[string]interface{}: - // Build a decoder from the given data - decoder := json.NewDecoder(bytes.NewBuffer(data)) - // Preserve numbers, rather than casting to float64 automatically - decoder.UseNumber() - // Run the decode - if err := decoder.Decode(v); err != nil { - return err - } - // If the decode succeeds, post-process the map to convert json.Number objects to int64 or float64 - return ConvertMapNumbers(*v, 0) - - case *[]interface{}: - // Build a decoder from the given data - decoder := json.NewDecoder(bytes.NewBuffer(data)) - // Preserve numbers, rather than casting to float64 automatically - decoder.UseNumber() - // Run the decode - if err := decoder.Decode(v); err != nil { - return err - } - // If the decode succeeds, post-process the map to convert json.Number objects to int64 or float64 - return ConvertSliceNumbers(*v, 0) - - case *interface{}: - // Build a decoder from the given data - decoder := json.NewDecoder(bytes.NewBuffer(data)) - // Preserve numbers, rather than casting to float64 automatically - decoder.UseNumber() - // Run the decode - if err := decoder.Decode(v); err != nil { - return err - } - // If the decode succeeds, post-process the map to convert json.Number objects to int64 or float64 - return ConvertInterfaceNumbers(v, 0) - - default: - return json.Unmarshal(data, v) - } + return kjson.UnmarshalCaseSensitivePreserveInts(data, v) } // ConvertInterfaceNumbers converts any json.Number values to int64 or float64. diff --git a/pkg/util/json/json_test.go b/pkg/util/json/json_test.go index 1602feb00..9464eb7f3 100644 --- a/pkg/util/json/json_test.go +++ b/pkg/util/json/json_test.go @@ -1,3 +1,4 @@ +//go:build go1.8 // +build go1.8 /* @@ -121,7 +122,7 @@ func TestEvaluateTypes(t *testing.T) { }, { In: `-0.0`, - Data: float64(-0.0), + Data: float64(-0.0), //nolint:staticcheck // SA4026: in Go, the floating-point literal '-0.0' is the same as '0.0' Out: `-0`, }, { @@ -351,8 +352,8 @@ func TestEvaluateTypes(t *testing.T) { t.Run(fmt.Sprintf("%d_raw", i), func(t *testing.T) { // decode the input as a standalone object - inputJSON := fmt.Sprintf(`%s`, tc.In) - expectedJSON := fmt.Sprintf(`%s`, tc.Out) + inputJSON := tc.In + expectedJSON := tc.Out var m interface{} err := Unmarshal([]byte(inputJSON), &m) if tc.Err && err != nil { diff --git a/pkg/util/jsonmergepatch/patch.go b/pkg/util/jsonmergepatch/patch.go index e56e17734..c480ffacb 100644 --- a/pkg/util/jsonmergepatch/patch.go +++ b/pkg/util/jsonmergepatch/patch.go @@ -74,7 +74,7 @@ func CreateThreeWayJSONMergePatch(original, modified, current []byte, fns ...mer var patchMap map[string]interface{} err = json.Unmarshal(patch, &patchMap) if err != nil { - return nil, fmt.Errorf("Failed to unmarshal patch for precondition check: %s", patch) + return nil, fmt.Errorf("failed to unmarshal patch for precondition check: %s", patch) } meetPreconditions, err := meetPreconditions(patchMap, fns...) if err != nil { diff --git a/pkg/util/managedfields/extract.go b/pkg/util/managedfields/extract.go index 590c16867..d2ce66c1b 100644 --- a/pkg/util/managedfields/extract.go +++ b/pkg/util/managedfields/extract.go @@ -45,7 +45,7 @@ import ( // and their field paths and types are exactly the same, then ExtractInto can be // called with the root resource as the object and the subresource as the // applyConfiguration. This works for "status", obviously, because status is -// represented by the exact same object as the root resource. This this does NOT +// represented by the exact same object as the root resource. This does NOT // work, for example, with the "scale" subresources of Deployment, ReplicaSet and // StatefulSet. While the spec.replicas, status.replicas fields are in the same // exact field path locations as they are in autoscaling.Scale, the selector @@ -75,6 +75,13 @@ func ExtractInto(object runtime.Object, objectType typed.ParseableType, fieldMan if !ok { return fmt.Errorf("unable to convert managed fields for %s to unstructured, expected map, got %T", fieldManager, u) } + + // set the type meta manually if it doesn't exist to avoid missing kind errors + // when decoding from unstructured JSON + if _, ok := m["kind"]; !ok && object.GetObjectKind().GroupVersionKind().Kind != "" { + m["kind"] = object.GetObjectKind().GroupVersionKind().Kind + m["apiVersion"] = object.GetObjectKind().GroupVersionKind().GroupVersion().String() + } if err := runtime.DefaultUnstructuredConverter.FromUnstructured(m, applyConfiguration); err != nil { return fmt.Errorf("error extracting into obj from unstructured: %w", err) } diff --git a/pkg/util/managedfields/gvkparser.go b/pkg/util/managedfields/gvkparser.go new file mode 100644 index 000000000..408739c50 --- /dev/null +++ b/pkg/util/managedfields/gvkparser.go @@ -0,0 +1,128 @@ +/* +Copyright 2018 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 managedfields + +import ( + "fmt" + + "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" +) + +// groupVersionKindExtensionKey is the key used to lookup the +// GroupVersionKind value for an object definition from the +// definition's "extensions" map. +const groupVersionKindExtensionKey = "x-kubernetes-group-version-kind" + +// GvkParser contains a Parser that allows introspecting the schema. +type GvkParser struct { + gvks map[schema.GroupVersionKind]string + parser typed.Parser +} + +// Type returns a helper which can produce objects of the given type. Any +// errors are deferred until a further function is called. +func (p *GvkParser) Type(gvk schema.GroupVersionKind) *typed.ParseableType { + typeName, ok := p.gvks[gvk] + if !ok { + return nil + } + t := p.parser.Type(typeName) + return &t +} + +// NewGVKParser builds a GVKParser from a proto.Models. This +// will automatically find the proper version of the object, and the +// corresponding schema information. +func NewGVKParser(models proto.Models, preserveUnknownFields bool) (*GvkParser, error) { + typeSchema, err := schemaconv.ToSchemaWithPreserveUnknownFields(models, preserveUnknownFields) + if err != nil { + return nil, fmt.Errorf("failed to convert models to schema: %v", err) + } + parser := GvkParser{ + gvks: map[schema.GroupVersionKind]string{}, + } + parser.parser = typed.Parser{Schema: smdschema.Schema{Types: typeSchema.Types}} + for _, modelName := range models.ListModels() { + model := models.LookupModel(modelName) + if model == nil { + panic(fmt.Sprintf("ListModels returns a model that can't be looked-up for: %v", modelName)) + } + gvkList := parseGroupVersionKind(model) + for _, gvk := range gvkList { + if len(gvk.Kind) > 0 { + _, ok := parser.gvks[gvk] + if ok { + return nil, fmt.Errorf("duplicate entry for %v", gvk) + } + parser.gvks[gvk] = modelName + } + } + } + return &parser, nil +} + +// Get and parse GroupVersionKind from the extension. Returns empty if it doesn't have one. +func parseGroupVersionKind(s proto.Schema) []schema.GroupVersionKind { + extensions := s.GetExtensions() + + gvkListResult := []schema.GroupVersionKind{} + + // Get the extensions + gvkExtension, ok := extensions[groupVersionKindExtensionKey] + if !ok { + return []schema.GroupVersionKind{} + } + + // gvk extension must be a list of at least 1 element. + gvkList, ok := gvkExtension.([]interface{}) + if !ok { + return []schema.GroupVersionKind{} + } + + for _, gvk := range gvkList { + // gvk extension list must be a map with group, version, and + // kind fields + gvkMap, ok := gvk.(map[interface{}]interface{}) + if !ok { + continue + } + group, ok := gvkMap["group"].(string) + if !ok { + continue + } + version, ok := gvkMap["version"].(string) + if !ok { + continue + } + kind, ok := gvkMap["kind"].(string) + if !ok { + continue + } + + gvkListResult = append(gvkListResult, schema.GroupVersionKind{ + Group: group, + Version: version, + Kind: kind, + }) + } + + return gvkListResult +} diff --git a/pkg/util/mergepatch/OWNERS b/pkg/util/mergepatch/OWNERS index 3f72c69ba..349bc69d6 100644 --- a/pkg/util/mergepatch/OWNERS +++ b/pkg/util/mergepatch/OWNERS @@ -1,7 +1,6 @@ # See the OWNERS docs at https://go.k8s.io/owners approvers: -- pwittrock + - pwittrock reviewers: -- mengqiy -- apelisse + - apelisse diff --git a/pkg/util/mergepatch/util.go b/pkg/util/mergepatch/util.go index 990fa0d43..e39627568 100644 --- a/pkg/util/mergepatch/util.go +++ b/pkg/util/mergepatch/util.go @@ -88,7 +88,8 @@ func toYAML(v interface{}) (string, error) { // supports JSON merge patch semantics. // // NOTE: Numbers with different types (e.g. int(0) vs int64(0)) will be detected as conflicts. -// Make sure the unmarshaling of left and right are consistent (e.g. use the same library). +// +// Make sure the unmarshaling of left and right are consistent (e.g. use the same library). func HasConflicts(left, right interface{}) (bool, error) { switch typedLeft := left.(type) { case map[string]interface{}: diff --git a/pkg/util/net/http.go b/pkg/util/net/http.go index d75ac6efa..8cc1810af 100644 --- a/pkg/util/net/http.go +++ b/pkg/util/net/http.go @@ -17,7 +17,6 @@ limitations under the License. package net import ( - "bufio" "bytes" "context" "crypto/tls" @@ -39,6 +38,7 @@ import ( "golang.org/x/net/http2" "k8s.io/klog/v2" + netutils "k8s.io/utils/net" ) // JoinPreservingTrailingSlash does a path.Join of the specified elements, @@ -237,6 +237,29 @@ func DialerFor(transport http.RoundTripper) (DialFunc, error) { } } +// CloseIdleConnectionsFor close idles connections for the Transport. +// If the Transport is wrapped it iterates over the wrapped round trippers +// until it finds one that implements the CloseIdleConnections method. +// If the Transport does not have a CloseIdleConnections method +// then this function does nothing. +func CloseIdleConnectionsFor(transport http.RoundTripper) { + if transport == nil { + return + } + type closeIdler interface { + CloseIdleConnections() + } + + switch transport := transport.(type) { + case closeIdler: + transport.CloseIdleConnections() + case RoundTripperWrapper: + CloseIdleConnectionsFor(transport.WrappedRoundTripper()) + default: + klog.Warningf("unknown transport type: %T", transport) + } +} + type TLSClientConfigHolder interface { TLSClientConfig() *tls.Config } @@ -289,7 +312,7 @@ func SourceIPs(req *http.Request) []net.IP { // Use the first valid one. parts := strings.Split(hdrForwardedFor, ",") for _, part := range parts { - ip := net.ParseIP(strings.TrimSpace(part)) + ip := netutils.ParseIPSloppy(strings.TrimSpace(part)) if ip != nil { srcIPs = append(srcIPs, ip) } @@ -299,7 +322,7 @@ func SourceIPs(req *http.Request) []net.IP { // Try the X-Real-Ip header. hdrRealIp := hdr.Get("X-Real-Ip") if hdrRealIp != "" { - ip := net.ParseIP(hdrRealIp) + ip := netutils.ParseIPSloppy(hdrRealIp) // Only append the X-Real-Ip if it's not already contained in the X-Forwarded-For chain. if ip != nil && !containsIP(srcIPs, ip) { srcIPs = append(srcIPs, ip) @@ -311,11 +334,11 @@ func SourceIPs(req *http.Request) []net.IP { // Remote Address in Go's HTTP server is in the form host:port so we need to split that first. host, _, err := net.SplitHostPort(req.RemoteAddr) if err == nil { - remoteIP = net.ParseIP(host) + remoteIP = netutils.ParseIPSloppy(host) } // Fallback if Remote Address was just IP. if remoteIP == nil { - remoteIP = net.ParseIP(req.RemoteAddr) + remoteIP = netutils.ParseIPSloppy(req.RemoteAddr) } // Don't duplicate remote IP if it's already the last address in the chain. @@ -382,7 +405,7 @@ func NewProxierWithNoProxyCIDR(delegate func(req *http.Request) (*url.URL, error cidrs := []*net.IPNet{} for _, noProxyRule := range noProxyRules { - _, cidr, _ := net.ParseCIDR(noProxyRule) + _, cidr, _ := netutils.ParseCIDRSloppy(noProxyRule) if cidr != nil { cidrs = append(cidrs, cidr) } @@ -393,7 +416,7 @@ func NewProxierWithNoProxyCIDR(delegate func(req *http.Request) (*url.URL, error } return func(req *http.Request) (*url.URL, error) { - ip := net.ParseIP(req.URL.Hostname()) + ip := netutils.ParseIPSloppy(req.URL.Hostname()) if ip == nil { return delegate(req) } @@ -422,104 +445,6 @@ type Dialer interface { Dial(req *http.Request) (net.Conn, error) } -// ConnectWithRedirects uses dialer to send req, following up to 10 redirects (relative to -// originalLocation). It returns the opened net.Conn and the raw response bytes. -// If requireSameHostRedirects is true, only redirects to the same host are permitted. -func ConnectWithRedirects(originalMethod string, originalLocation *url.URL, header http.Header, originalBody io.Reader, dialer Dialer, requireSameHostRedirects bool) (net.Conn, []byte, error) { - const ( - maxRedirects = 9 // Fail on the 10th redirect - maxResponseSize = 16384 // play it safe to allow the potential for lots of / large headers - ) - - var ( - location = originalLocation - method = originalMethod - intermediateConn net.Conn - rawResponse = bytes.NewBuffer(make([]byte, 0, 256)) - body = originalBody - ) - - defer func() { - if intermediateConn != nil { - intermediateConn.Close() - } - }() - -redirectLoop: - for redirects := 0; ; redirects++ { - if redirects > maxRedirects { - return nil, nil, fmt.Errorf("too many redirects (%d)", redirects) - } - - req, err := http.NewRequest(method, location.String(), body) - if err != nil { - return nil, nil, err - } - - req.Header = header - - intermediateConn, err = dialer.Dial(req) - if err != nil { - return nil, nil, err - } - - // Peek at the backend response. - rawResponse.Reset() - respReader := bufio.NewReader(io.TeeReader( - io.LimitReader(intermediateConn, maxResponseSize), // Don't read more than maxResponseSize bytes. - rawResponse)) // Save the raw response. - resp, err := http.ReadResponse(respReader, nil) - if err != nil { - // Unable to read the backend response; let the client handle it. - klog.Warningf("Error reading backend response: %v", err) - break redirectLoop - } - - switch resp.StatusCode { - case http.StatusFound: - // Redirect, continue. - default: - // Don't redirect. - break redirectLoop - } - - // Redirected requests switch to "GET" according to the HTTP spec: - // https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3 - method = "GET" - // don't send a body when following redirects - body = nil - - resp.Body.Close() // not used - - // Prepare to follow the redirect. - redirectStr := resp.Header.Get("Location") - if redirectStr == "" { - return nil, nil, fmt.Errorf("%d response missing Location header", resp.StatusCode) - } - // We have to parse relative to the current location, NOT originalLocation. For example, - // if we request http://foo.com/a and get back "http://bar.com/b", the result should be - // http://bar.com/b. If we then make that request and get back a redirect to "/c", the result - // should be http://bar.com/c, not http://foo.com/c. - location, err = location.Parse(redirectStr) - if err != nil { - return nil, nil, fmt.Errorf("malformed Location header: %v", err) - } - - // Only follow redirects to the same host. Otherwise, propagate the redirect response back. - if requireSameHostRedirects && location.Hostname() != originalLocation.Hostname() { - return nil, nil, fmt.Errorf("hostname mismatch: expected %s, found %s", originalLocation.Hostname(), location.Hostname()) - } - - // Reset the connection. - intermediateConn.Close() - intermediateConn = nil - } - - connToReturn := intermediateConn - intermediateConn = nil // Don't close the connection when we return it. - return connToReturn, rawResponse.Bytes(), nil -} - // CloneRequest creates a shallow copy of the request along with a deep copy of the Headers. func CloneRequest(req *http.Request) *http.Request { r := new(http.Request) diff --git a/pkg/util/net/http_test.go b/pkg/util/net/http_test.go index 9411bfa7d..5d3588070 100644 --- a/pkg/util/net/http_test.go +++ b/pkg/util/net/http_test.go @@ -1,3 +1,4 @@ +//go:build go1.8 // +build go1.8 /* @@ -19,15 +20,11 @@ limitations under the License. package net import ( - "bufio" - "bytes" "crypto/tls" "fmt" "io" - "io/ioutil" "net" "net/http" - "net/http/httptest" "net/url" "os" "reflect" @@ -35,13 +32,12 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/util/wait" + netutils "k8s.io/utils/net" ) func TestGetClientIP(t *testing.T) { ipString := "10.0.0.1" - ip := net.ParseIP(ipString) + ip := netutils.ParseIPSloppy(ipString) invalidIPString := "invalidIPString" testCases := []struct { Request http.Request @@ -291,157 +287,6 @@ func TestJoinPreservingTrailingSlash(t *testing.T) { } } -func TestConnectWithRedirects(t *testing.T) { - tests := []struct { - desc string - redirects []string - method string // initial request method, empty == GET - expectError bool - expectedRedirects int - newPort bool // special case different port test - }{{ - desc: "relative redirects allowed", - redirects: []string{"/ok"}, - expectedRedirects: 1, - }, { - desc: "redirects to the same host are allowed", - redirects: []string{"http://HOST/ok"}, // HOST replaced with server address in test - expectedRedirects: 1, - }, { - desc: "POST redirects to GET", - method: http.MethodPost, - redirects: []string{"/ok"}, - expectedRedirects: 1, - }, { - desc: "PUT redirects to GET", - method: http.MethodPut, - redirects: []string{"/ok"}, - expectedRedirects: 1, - }, { - desc: "DELETE redirects to GET", - method: http.MethodDelete, - redirects: []string{"/ok"}, - expectedRedirects: 1, - }, { - desc: "9 redirects are allowed", - redirects: []string{"/1", "/2", "/3", "/4", "/5", "/6", "/7", "/8", "/9"}, - expectedRedirects: 9, - }, { - desc: "10 redirects are forbidden", - redirects: []string{"/1", "/2", "/3", "/4", "/5", "/6", "/7", "/8", "/9", "/10"}, - expectError: true, - }, { - desc: "redirect to different host are prevented", - redirects: []string{"http://example.com/foo"}, - expectError: true, - }, { - desc: "multiple redirect to different host forbidden", - redirects: []string{"/1", "/2", "/3", "http://example.com/foo"}, - expectError: true, - }, { - desc: "redirect to different port is allowed", - redirects: []string{"http://HOST/foo"}, - expectedRedirects: 1, - newPort: true, - }} - - const resultString = "Test output" - for _, test := range tests { - t.Run(test.desc, func(t *testing.T) { - redirectCount := 0 - s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - // Verify redirect request. - if redirectCount > 0 { - expectedURL, err := url.Parse(test.redirects[redirectCount-1]) - require.NoError(t, err, "test URL error") - assert.Equal(t, req.URL.Path, expectedURL.Path, "unknown redirect path") - assert.Equal(t, http.MethodGet, req.Method, "redirects must always be GET") - } - if redirectCount < len(test.redirects) { - http.Redirect(w, req, test.redirects[redirectCount], http.StatusFound) - redirectCount++ - } else if redirectCount == len(test.redirects) { - w.Write([]byte(resultString)) - } else { - t.Errorf("unexpected number of redirects %d to %s", redirectCount, req.URL.String()) - } - })) - defer s.Close() - - u, err := url.Parse(s.URL) - require.NoError(t, err, "Error parsing server URL") - host := u.Host - - // Special case new-port test with a secondary server. - if test.newPort { - s2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - w.Write([]byte(resultString)) - })) - defer s2.Close() - u2, err := url.Parse(s2.URL) - require.NoError(t, err, "Error parsing secondary server URL") - - // Sanity check: secondary server uses same hostname, different port. - require.Equal(t, u.Hostname(), u2.Hostname(), "sanity check: same hostname") - require.NotEqual(t, u.Port(), u2.Port(), "sanity check: different port") - - // Redirect to the secondary server. - host = u2.Host - - } - - // Update redirect URLs with actual host. - for i := range test.redirects { - test.redirects[i] = strings.Replace(test.redirects[i], "HOST", host, 1) - } - - method := test.method - if method == "" { - method = http.MethodGet - } - - netdialer := &net.Dialer{ - Timeout: wait.ForeverTestTimeout, - KeepAlive: wait.ForeverTestTimeout, - } - dialer := DialerFunc(func(req *http.Request) (net.Conn, error) { - conn, err := netdialer.Dial("tcp", req.URL.Host) - if err != nil { - return conn, err - } - if err = req.Write(conn); err != nil { - require.NoError(t, conn.Close()) - return nil, fmt.Errorf("error sending request: %v", err) - } - return conn, err - }) - conn, rawResponse, err := ConnectWithRedirects(method, u, http.Header{} /*body*/, nil, dialer, true) - if test.expectError { - require.Error(t, err, "expected request error") - return - } - - require.NoError(t, err, "unexpected request error") - assert.NoError(t, conn.Close(), "error closing connection") - - resp, err := http.ReadResponse(bufio.NewReader(bytes.NewReader(rawResponse)), nil) - require.NoError(t, err, "unexpected request error") - - result, err := ioutil.ReadAll(resp.Body) - assert.NoError(t, err) - require.NoError(t, resp.Body.Close()) - if test.expectedRedirects < len(test.redirects) { - // Expect the last redirect to be returned. - assert.Equal(t, http.StatusFound, resp.StatusCode, "Final response is not a redirect") - assert.Equal(t, test.redirects[len(test.redirects)-1], resp.Header.Get("Location")) - assert.NotEqual(t, resultString, string(result), "wrong content") - } else { - assert.Equal(t, resultString, string(result), "stream content does not match") - } - }) - } -} - func TestAllowsHTTP2(t *testing.T) { testcases := []struct { Name string diff --git a/pkg/util/net/interface.go b/pkg/util/net/interface.go index 9adf4cfe4..01d028e72 100644 --- a/pkg/util/net/interface.go +++ b/pkg/util/net/interface.go @@ -27,6 +27,7 @@ import ( "strings" "k8s.io/klog/v2" + netutils "k8s.io/utils/net" ) type AddressFamily uint @@ -221,7 +222,7 @@ func getMatchingGlobalIP(addrs []net.Addr, family AddressFamily) (net.IP, error) if len(addrs) > 0 { for i := range addrs { klog.V(4).Infof("Checking addr %s.", addrs[i].String()) - ip, _, err := net.ParseCIDR(addrs[i].String()) + ip, _, err := netutils.ParseCIDRSloppy(addrs[i].String()) if err != nil { return nil, err } @@ -336,9 +337,9 @@ func chooseIPFromHostInterfaces(nw networkInterfacer, addressFamilies AddressFam continue } for _, addr := range addrs { - ip, _, err := net.ParseCIDR(addr.String()) + ip, _, err := netutils.ParseCIDRSloppy(addr.String()) if err != nil { - return nil, fmt.Errorf("Unable to parse CIDR for interface %q: %s", intf.Name, err) + return nil, fmt.Errorf("unable to parse CIDR for interface %q: %s", intf.Name, err) } if !memberOf(ip, family) { klog.V(4).Infof("Skipping: no address family match for %q on interface %q.", ip, intf.Name) diff --git a/pkg/util/net/interface_test.go b/pkg/util/net/interface_test.go index fac078d20..c4d543cda 100644 --- a/pkg/util/net/interface_test.go +++ b/pkg/util/net/interface_test.go @@ -23,6 +23,8 @@ import ( "os" "strings" "testing" + + netutils "k8s.io/utils/net" ) const gatewayfirst = `Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT @@ -119,8 +121,8 @@ var ( ) var ( - ipv4Route = Route{Interface: "eth3", Destination: net.ParseIP("0.0.0.0"), Gateway: net.ParseIP("10.254.0.1"), Family: familyIPv4} - ipv6Route = Route{Interface: "eth3", Destination: net.ParseIP("::"), Gateway: net.ParseIP("2001:1::1"), Family: familyIPv6} + ipv4Route = Route{Interface: "eth3", Destination: netutils.ParseIPSloppy("0.0.0.0"), Gateway: netutils.ParseIPSloppy("10.254.0.1"), Family: familyIPv4} + ipv6Route = Route{Interface: "eth3", Destination: netutils.ParseIPSloppy("::"), Gateway: netutils.ParseIPSloppy("2001:1::1"), Family: familyIPv6} ) var ( @@ -282,8 +284,8 @@ func TestFinalIP(t *testing.T) { {"loopbackv6", []net.Addr{addrStruct{val: "::1/128"}}, familyIPv6, nil}, {"link local v4", []net.Addr{addrStruct{val: "169.254.1.10/16"}}, familyIPv4, nil}, {"link local v6", []net.Addr{addrStruct{val: "fe80::2f7:6fff:fe6e:2956/64"}}, familyIPv6, nil}, - {"ip4", []net.Addr{addrStruct{val: "10.254.12.132/17"}}, familyIPv4, net.ParseIP("10.254.12.132")}, - {"ip6", []net.Addr{addrStruct{val: "2001::5/64"}}, familyIPv6, net.ParseIP("2001::5")}, + {"ip4", []net.Addr{addrStruct{val: "10.254.12.132/17"}}, familyIPv4, netutils.ParseIPSloppy("10.254.12.132")}, + {"ip6", []net.Addr{addrStruct{val: "2001::5/64"}}, familyIPv6, netutils.ParseIPSloppy("2001::5")}, {"no addresses", []net.Addr{}, familyIPv4, nil}, } @@ -556,8 +558,8 @@ func TestGetIPFromInterface(t *testing.T) { expected net.IP errStrFrag string }{ - {"ipv4", "eth3", familyIPv4, validNetworkInterface{}, net.ParseIP("10.254.71.145"), ""}, - {"ipv6", "eth3", familyIPv6, ipv6NetworkInterface{}, net.ParseIP("2001::200"), ""}, + {"ipv4", "eth3", familyIPv4, validNetworkInterface{}, netutils.ParseIPSloppy("10.254.71.145"), ""}, + {"ipv6", "eth3", familyIPv6, ipv6NetworkInterface{}, netutils.ParseIPSloppy("2001::200"), ""}, {"no ipv4", "eth3", familyIPv4, ipv6NetworkInterface{}, nil, ""}, {"no ipv6", "eth3", familyIPv6, validNetworkInterface{}, nil, ""}, {"I/F down", "eth3", familyIPv4, downNetworkInterface{}, nil, ""}, @@ -587,8 +589,8 @@ func TestGetIPFromLoopbackInterface(t *testing.T) { expected net.IP errStrFrag string }{ - {"ipv4", familyIPv4, linkLocalLoopbackNetworkInterface{}, net.ParseIP("10.1.1.1"), ""}, - {"ipv6", familyIPv6, linkLocalLoopbackNetworkInterface{}, net.ParseIP("fd00:1:1::1"), ""}, + {"ipv4", familyIPv4, linkLocalLoopbackNetworkInterface{}, netutils.ParseIPSloppy("10.1.1.1"), ""}, + {"ipv6", familyIPv6, linkLocalLoopbackNetworkInterface{}, netutils.ParseIPSloppy("fd00:1:1::1"), ""}, {"no global ipv4", familyIPv4, loopbackNetworkInterface{}, nil, ""}, {"no global ipv6", familyIPv6, loopbackNetworkInterface{}, nil, ""}, } @@ -614,21 +616,21 @@ func TestChooseHostInterfaceFromRoute(t *testing.T) { order AddressFamilyPreference expected net.IP }{ - {"single-stack ipv4", routeV4, validNetworkInterface{}, preferIPv4, net.ParseIP("10.254.71.145")}, - {"single-stack ipv4, prefer v6", routeV4, validNetworkInterface{}, preferIPv6, net.ParseIP("10.254.71.145")}, - {"single-stack ipv6", routeV6, ipv6NetworkInterface{}, preferIPv4, net.ParseIP("2001::200")}, - {"single-stack ipv6, prefer v6", routeV6, ipv6NetworkInterface{}, preferIPv6, net.ParseIP("2001::200")}, - {"dual stack", bothRoutes, v4v6NetworkInterface{}, preferIPv4, net.ParseIP("10.254.71.145")}, - {"dual stack, prefer v6", bothRoutes, v4v6NetworkInterface{}, preferIPv6, net.ParseIP("2001::10")}, - {"LLA and loopback with global, IPv4", routeV4, linkLocalLoopbackNetworkInterface{}, preferIPv4, net.ParseIP("10.1.1.1")}, - {"LLA and loopback with global, IPv6", routeV6, linkLocalLoopbackNetworkInterface{}, preferIPv6, net.ParseIP("fd00:1:1::1")}, - {"LLA and loopback with global, dual stack prefer IPv4", bothRoutes, linkLocalLoopbackNetworkInterface{}, preferIPv4, net.ParseIP("10.1.1.1")}, - {"LLA and loopback with global, dual stack prefer IPv6", bothRoutes, linkLocalLoopbackNetworkInterface{}, preferIPv6, net.ParseIP("fd00:1:1::1")}, + {"single-stack ipv4", routeV4, validNetworkInterface{}, preferIPv4, netutils.ParseIPSloppy("10.254.71.145")}, + {"single-stack ipv4, prefer v6", routeV4, validNetworkInterface{}, preferIPv6, netutils.ParseIPSloppy("10.254.71.145")}, + {"single-stack ipv6", routeV6, ipv6NetworkInterface{}, preferIPv4, netutils.ParseIPSloppy("2001::200")}, + {"single-stack ipv6, prefer v6", routeV6, ipv6NetworkInterface{}, preferIPv6, netutils.ParseIPSloppy("2001::200")}, + {"dual stack", bothRoutes, v4v6NetworkInterface{}, preferIPv4, netutils.ParseIPSloppy("10.254.71.145")}, + {"dual stack, prefer v6", bothRoutes, v4v6NetworkInterface{}, preferIPv6, netutils.ParseIPSloppy("2001::10")}, + {"LLA and loopback with global, IPv4", routeV4, linkLocalLoopbackNetworkInterface{}, preferIPv4, netutils.ParseIPSloppy("10.1.1.1")}, + {"LLA and loopback with global, IPv6", routeV6, linkLocalLoopbackNetworkInterface{}, preferIPv6, netutils.ParseIPSloppy("fd00:1:1::1")}, + {"LLA and loopback with global, dual stack prefer IPv4", bothRoutes, linkLocalLoopbackNetworkInterface{}, preferIPv4, netutils.ParseIPSloppy("10.1.1.1")}, + {"LLA and loopback with global, dual stack prefer IPv6", bothRoutes, linkLocalLoopbackNetworkInterface{}, preferIPv6, netutils.ParseIPSloppy("fd00:1:1::1")}, {"LLA and loopback with global, no routes", noRoutes, linkLocalLoopbackNetworkInterface{}, preferIPv6, nil}, - {"interface and loopback with global, IPv4", routeV4, globalsNetworkInterface{}, preferIPv4, net.ParseIP("192.168.1.1")}, - {"interface and loopback with global, IPv6", routeV6, globalsNetworkInterface{}, preferIPv6, net.ParseIP("fd00::200")}, - {"interface and loopback with global, dual stack prefer IPv4", bothRoutes, globalsNetworkInterface{}, preferIPv4, net.ParseIP("192.168.1.1")}, - {"interface and loopback with global, dual stack prefer IPv6", bothRoutes, globalsNetworkInterface{}, preferIPv6, net.ParseIP("fd00::200")}, + {"interface and loopback with global, IPv4", routeV4, globalsNetworkInterface{}, preferIPv4, netutils.ParseIPSloppy("192.168.1.1")}, + {"interface and loopback with global, IPv6", routeV6, globalsNetworkInterface{}, preferIPv6, netutils.ParseIPSloppy("fd00::200")}, + {"interface and loopback with global, dual stack prefer IPv4", bothRoutes, globalsNetworkInterface{}, preferIPv4, netutils.ParseIPSloppy("192.168.1.1")}, + {"interface and loopback with global, dual stack prefer IPv6", bothRoutes, globalsNetworkInterface{}, preferIPv6, netutils.ParseIPSloppy("fd00::200")}, {"interface and loopback with global, no routes", noRoutes, globalsNetworkInterface{}, preferIPv6, nil}, {"all LLA", routeV4, networkInterfaceWithOnlyLinkLocals{}, preferIPv4, nil}, {"no routes", noRoutes, validNetworkInterface{}, preferIPv4, nil}, @@ -649,10 +651,10 @@ func TestMemberOf(t *testing.T) { family AddressFamily expected bool }{ - {"ipv4 is 4", net.ParseIP("10.20.30.40"), familyIPv4, true}, - {"ipv4 is 6", net.ParseIP("10.10.10.10"), familyIPv6, false}, - {"ipv6 is 4", net.ParseIP("2001::100"), familyIPv4, false}, - {"ipv6 is 6", net.ParseIP("2001::100"), familyIPv6, true}, + {"ipv4 is 4", netutils.ParseIPSloppy("10.20.30.40"), familyIPv4, true}, + {"ipv4 is 6", netutils.ParseIPSloppy("10.10.10.10"), familyIPv6, false}, + {"ipv6 is 4", netutils.ParseIPSloppy("2001::100"), familyIPv4, false}, + {"ipv6 is 6", netutils.ParseIPSloppy("2001::100"), familyIPv6, true}, } for _, tc := range testCases { if memberOf(tc.ip, tc.family) != tc.expected { @@ -678,12 +680,12 @@ func TestGetIPFromHostInterfaces(t *testing.T) { {"no addresses", networkInterfaceWithNoAddrs{}, preferIPv4, nil, "no acceptable"}, {"invalid addr", networkInterfaceWithInvalidAddr{}, preferIPv4, nil, "invalid CIDR"}, {"no matches", networkInterfaceWithOnlyLinkLocals{}, preferIPv4, nil, "no acceptable"}, - {"single-stack ipv4", validNetworkInterface{}, preferIPv4, net.ParseIP("10.254.71.145"), ""}, - {"single-stack ipv4, prefer ipv6", validNetworkInterface{}, preferIPv6, net.ParseIP("10.254.71.145"), ""}, - {"single-stack ipv6", ipv6NetworkInterface{}, preferIPv4, net.ParseIP("2001::200"), ""}, - {"single-stack ipv6, prefer ipv6", ipv6NetworkInterface{}, preferIPv6, net.ParseIP("2001::200"), ""}, - {"dual stack", v4v6NetworkInterface{}, preferIPv4, net.ParseIP("10.254.71.145"), ""}, - {"dual stack, prefer ipv6", v4v6NetworkInterface{}, preferIPv6, net.ParseIP("2001::10"), ""}, + {"single-stack ipv4", validNetworkInterface{}, preferIPv4, netutils.ParseIPSloppy("10.254.71.145"), ""}, + {"single-stack ipv4, prefer ipv6", validNetworkInterface{}, preferIPv6, netutils.ParseIPSloppy("10.254.71.145"), ""}, + {"single-stack ipv6", ipv6NetworkInterface{}, preferIPv4, netutils.ParseIPSloppy("2001::200"), ""}, + {"single-stack ipv6, prefer ipv6", ipv6NetworkInterface{}, preferIPv6, netutils.ParseIPSloppy("2001::200"), ""}, + {"dual stack", v4v6NetworkInterface{}, preferIPv4, netutils.ParseIPSloppy("10.254.71.145"), ""}, + {"dual stack, prefer ipv6", v4v6NetworkInterface{}, preferIPv6, netutils.ParseIPSloppy("2001::10"), ""}, } for _, tc := range testCases { diff --git a/pkg/util/net/port_split.go b/pkg/util/net/port_split.go index c0fd4e20f..f54bb1e71 100644 --- a/pkg/util/net/port_split.go +++ b/pkg/util/net/port_split.go @@ -25,9 +25,9 @@ import ( var validSchemes = sets.NewString("http", "https", "") // SplitSchemeNamePort takes a string of the following forms: -// * "", returns "", "","", true -// * ":", returns "", "","",true -// * "::", returns "","","",true +// - "", returns "", "","", true +// - ":", returns "", "","",true +// - "::", returns "","","",true // // Name must be non-empty or valid will be returned false. // Scheme must be "http" or "https" if specified @@ -57,9 +57,10 @@ func SplitSchemeNamePort(id string) (scheme, name, port string, valid bool) { } // JoinSchemeNamePort returns a string that specifies the scheme, name, and port: -// * "" -// * ":" -// * "::" +// - "" +// - ":" +// - "::" +// // None of the parameters may contain a ':' character // Name is required // Scheme must be "", "http", or "https" diff --git a/pkg/util/net/util.go b/pkg/util/net/util.go index 5950087e0..1c2aba55f 100644 --- a/pkg/util/net/util.go +++ b/pkg/util/net/util.go @@ -25,6 +25,7 @@ import ( // IPNetEqual checks if the two input IPNets are representing the same subnet. // For example, +// // 10.0.0.1/24 and 10.0.0.0/24 are the same subnet. // 10.0.0.1/24 and 10.0.0.0/25 are not the same subnet. func IPNetEqual(ipnet1, ipnet2 *net.IPNet) bool { diff --git a/pkg/util/net/util_test.go b/pkg/util/net/util_test.go index 9e175fcb3..029f2f711 100644 --- a/pkg/util/net/util_test.go +++ b/pkg/util/net/util_test.go @@ -22,10 +22,12 @@ import ( "os" "syscall" "testing" + + netutils "k8s.io/utils/net" ) func getIPNet(cidr string) *net.IPNet { - _, ipnet, _ := net.ParseCIDR(cidr) + _, ipnet, _ := netutils.ParseCIDRSloppy(cidr) return ipnet } diff --git a/pkg/util/proxy/dial.go b/pkg/util/proxy/dial.go index 5042981c5..4ceb2e06e 100644 --- a/pkg/util/proxy/dial.go +++ b/pkg/util/proxy/dial.go @@ -24,10 +24,9 @@ import ( "net/http" "net/url" - "k8s.io/klog/v2" - utilnet "k8s.io/apimachinery/pkg/util/net" "k8s.io/apimachinery/third_party/forked/golang/netutil" + "k8s.io/klog/v2" ) // dialURL will dial the specified URL using the underlying dialer held by the passed @@ -52,10 +51,7 @@ func dialURL(ctx context.Context, url *url.URL, transport http.RoundTripper) (ne return d.DialContext(ctx, "tcp", dialAddr) case "https": // Get the tls config from the transport if we recognize it - var tlsConfig *tls.Config - var tlsConn *tls.Conn - var err error - tlsConfig, err = utilnet.TLSClientConfig(transport) + tlsConfig, err := utilnet.TLSClientConfig(transport) if err != nil { klog.V(5).Infof("Unable to unwrap transport %T to get at TLS config: %v", transport, err) } @@ -75,7 +71,7 @@ func dialURL(ctx context.Context, url *url.URL, transport http.RoundTripper) (ne InsecureSkipVerify: true, } } else if len(tlsConfig.ServerName) == 0 && !tlsConfig.InsecureSkipVerify { - // tls.Handshake() requires ServerName or InsecureSkipVerify + // tls.HandshakeContext() requires ServerName or InsecureSkipVerify // infer the ServerName from the hostname we're connecting to. inferredHost := dialAddr if host, _, err := net.SplitHostPort(dialAddr); err == nil { @@ -87,7 +83,7 @@ func dialURL(ctx context.Context, url *url.URL, transport http.RoundTripper) (ne tlsConfig = tlsConfigCopy } - // Since this method is primary used within a "Connection: Upgrade" call we assume the caller is + // Since this method is primarily used within a "Connection: Upgrade" call we assume the caller is // going to write HTTP/1.1 request to the wire. http2 should not be allowed in the TLSConfig.NextProtos, // so we explicitly set that here. We only do this check if the TLSConfig support http/1.1. if supportsHTTP11(tlsConfig.NextProtos) { @@ -95,38 +91,21 @@ func dialURL(ctx context.Context, url *url.URL, transport http.RoundTripper) (ne tlsConfig.NextProtos = []string{"http/1.1"} } - tlsConn = tls.Client(netConn, tlsConfig) - if err := tlsConn.Handshake(); err != nil { + tlsConn := tls.Client(netConn, tlsConfig) + if err := tlsConn.HandshakeContext(ctx); err != nil { netConn.Close() return nil, err } - + return tlsConn, nil } else { - // Dial. This Dial method does not allow to pass a context unfortunately - tlsConn, err = tls.Dial("tcp", dialAddr, tlsConfig) - if err != nil { - return nil, err + // Dial. + tlsDialer := tls.Dialer{ + Config: tlsConfig, } + return tlsDialer.DialContext(ctx, "tcp", dialAddr) } - - // Return if we were configured to skip validation - if tlsConfig != nil && tlsConfig.InsecureSkipVerify { - return tlsConn, nil - } - - // Verify - host, _, _ := net.SplitHostPort(dialAddr) - if tlsConfig != nil && len(tlsConfig.ServerName) > 0 { - host = tlsConfig.ServerName - } - if err := tlsConn.VerifyHostname(host); err != nil { - tlsConn.Close() - return nil, err - } - - return tlsConn, nil default: - return nil, fmt.Errorf("Unknown scheme: %s", url.Scheme) + return nil, fmt.Errorf("unknown scheme: %s", url.Scheme) } } diff --git a/pkg/util/proxy/dial_test.go b/pkg/util/proxy/dial_test.go index 47c24c2a9..705aea8ff 100644 --- a/pkg/util/proxy/dial_test.go +++ b/pkg/util/proxy/dial_test.go @@ -26,7 +26,7 @@ import ( "net/http/httptest" "net/url" "reflect" - "strings" + "regexp" "testing" "k8s.io/apimachinery/pkg/util/diff" @@ -56,7 +56,7 @@ func TestDialURL(t *testing.T) { }, "secure, no roots": { TLSConfig: &tls.Config{InsecureSkipVerify: false}, - ExpectError: "unknown authority", + ExpectError: "unknown authority|not trusted", }, "secure with roots": { TLSConfig: &tls.Config{InsecureSkipVerify: false, RootCAs: roots}, @@ -76,7 +76,7 @@ func TestDialURL(t *testing.T) { "secure, no roots, custom dial": { TLSConfig: &tls.Config{InsecureSkipVerify: false}, Dial: d.DialContext, - ExpectError: "unknown authority", + ExpectError: "unknown authority|not trusted", }, "secure with roots, custom dial": { TLSConfig: &tls.Config{InsecureSkipVerify: false, RootCAs: roots}, @@ -154,7 +154,7 @@ func TestDialURL(t *testing.T) { if tc.ExpectError == "" { t.Errorf("%s: expected no error, got %q", k, err.Error()) } - if !strings.Contains(err.Error(), tc.ExpectError) { + if tc.ExpectError != "" && !regexp.MustCompile(tc.ExpectError).MatchString(err.Error()) { t.Errorf("%s: expected error containing %q, got %q", k, tc.ExpectError, err.Error()) } return @@ -177,7 +177,8 @@ func TestDialURL(t *testing.T) { } // localhostCert was generated from crypto/tls/generate_cert.go with the following command: -// go run generate_cert.go --rsa-bits 2048 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h +// +// go run generate_cert.go --rsa-bits 2048 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h var localhostCert = []byte(`-----BEGIN CERTIFICATE----- MIIDGTCCAgGgAwIBAgIRAKfNl1LEAt7nFPYvHBnpv2swDQYJKoZIhvcNAQELBQAw EjEQMA4GA1UEChMHQWNtZSBDbzAgFw03MDAxMDEwMDAwMDBaGA8yMDg0MDEyOTE2 diff --git a/pkg/util/proxy/transport.go b/pkg/util/proxy/transport.go index 28c654676..489d9b042 100644 --- a/pkg/util/proxy/transport.go +++ b/pkg/util/proxy/transport.go @@ -39,7 +39,8 @@ import ( // atomsToAttrs states which attributes of which tags require URL substitution. // Sources: http://www.w3.org/TR/REC-html40/index/attributes.html -// http://www.w3.org/html/wg/drafts/html/master/index.html#attributes-1 +// +// http://www.w3.org/html/wg/drafts/html/master/index.html#attributes-1 var atomsToAttrs = map[atom.Atom]sets.String{ atom.A: sets.NewString("href"), atom.Applet: sets.NewString("codebase"), @@ -83,7 +84,7 @@ type Transport struct { // RoundTrip implements the http.RoundTripper interface func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { // Add reverse proxy headers. - forwardedURI := path.Join(t.PathPrepend, req.URL.Path) + forwardedURI := path.Join(t.PathPrepend, req.URL.EscapedPath()) if strings.HasSuffix(req.URL.Path, "/") { forwardedURI = forwardedURI + "/" } @@ -106,7 +107,11 @@ func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { } if redirect := resp.Header.Get("Location"); redirect != "" { - resp.Header.Set("Location", t.rewriteURL(redirect, req.URL, req.Host)) + targetURL, err := url.Parse(redirect) + if err != nil { + return nil, errors.NewInternalError(fmt.Errorf("error trying to parse Location header: %v", err)) + } + resp.Header.Set("Location", t.rewriteURL(targetURL, req.URL, req.Host)) return resp, nil } @@ -128,14 +133,8 @@ func (rt *Transport) WrappedRoundTripper() http.RoundTripper { // rewriteURL rewrites a single URL to go through the proxy, if the URL refers // to the same host as sourceURL, which is the page on which the target URL -// occurred, or if the URL matches the sourceRequestHost. If any error occurs (e.g. -// parsing), it returns targetURL. -func (t *Transport) rewriteURL(targetURL string, sourceURL *url.URL, sourceRequestHost string) string { - url, err := url.Parse(targetURL) - if err != nil { - return targetURL - } - +// occurred, or if the URL matches the sourceRequestHost. +func (t *Transport) rewriteURL(url *url.URL, sourceURL *url.URL, sourceRequestHost string) string { // Example: // When API server processes a proxy request to a service (e.g. /api/v1/namespace/foo/service/bar/proxy/), // the sourceURL.Host (i.e. req.URL.Host) is the endpoint IP address of the service. The @@ -151,7 +150,7 @@ func (t *Transport) rewriteURL(targetURL string, sourceURL *url.URL, sourceReque isDifferentHost := url.Host != "" && url.Host != sourceURL.Host && url.Host != sourceRequestHost isRelative := !strings.HasPrefix(url.Path, "/") if isDifferentHost || isRelative { - return targetURL + return url.String() } // Do not rewrite scheme and host if the Transport has empty scheme and host @@ -178,7 +177,7 @@ func (t *Transport) rewriteURL(targetURL string, sourceURL *url.URL, sourceReque // rewriteHTML scans the HTML for tags with url-valued attributes, and updates // those values with the urlRewriter function. The updated HTML is output to the // writer. -func rewriteHTML(reader io.Reader, writer io.Writer, urlRewriter func(string) string) error { +func rewriteHTML(reader io.Reader, writer io.Writer, urlRewriter func(*url.URL) string) error { // Note: This assumes the content is UTF-8. tokenizer := html.NewTokenizer(reader) @@ -193,7 +192,14 @@ func rewriteHTML(reader io.Reader, writer io.Writer, urlRewriter func(string) st if urlAttrs, ok := atomsToAttrs[token.DataAtom]; ok { for i, attr := range token.Attr { if urlAttrs.Has(attr.Key) { - token.Attr[i].Val = urlRewriter(attr.Val) + url, err := url.Parse(attr.Val) + if err != nil { + // Do not rewrite the URL if it isn't valid. It is intended not + // to error here to prevent the inability to understand the + // content of the body to cause a fatal error. + continue + } + token.Attr[i].Val = urlRewriter(url) } } } @@ -248,7 +254,7 @@ func (t *Transport) rewriteResponse(req *http.Request, resp *http.Response) (*ht return resp, nil } - urlRewriter := func(targetUrl string) string { + urlRewriter := func(targetUrl *url.URL) string { return t.rewriteURL(targetUrl, req.URL, req.Host) } err := rewriteHTML(reader, writer, urlRewriter) diff --git a/pkg/util/proxy/transport_test.go b/pkg/util/proxy/transport_test.go index 90816ffa5..94c4b2e01 100644 --- a/pkg/util/proxy/transport_test.go +++ b/pkg/util/proxy/transport_test.go @@ -197,6 +197,43 @@ func TestProxyTransport(t *testing.T) { contentType: "text/html", forwardedURI: "/proxy/node/node1:10250/logs/log.log", }, + "forwarded URI must be escaped": { + input: "", + sourceURL: "http://mynode.com/logs/log.log%00", + transport: testTransport, + output: "", + contentType: "text/html", + forwardedURI: "/proxy/node/node1:10250/logs/log.log%00%3Cscript%3Ealert%281%29%3C/script%3E", + }, + "redirect rel must be escaped": { + sourceURL: "http://mynode.com/redirect", + transport: testTransport, + redirect: "/redirected/target/%00/", + redirectWant: "http://foo.com/proxy/node/node1:10250/redirected/target/%00%3Cscript%3Ealert%281%29%3C/script%3E/", + forwardedURI: "/proxy/node/node1:10250/redirect", + }, + "redirect abs same host must be escaped": { + sourceURL: "http://mynode.com/redirect", + transport: testTransport, + redirect: "http://mynode.com/redirected/target/%00/", + redirectWant: "http://foo.com/proxy/node/node1:10250/redirected/target/%00%3Cscript%3Ealert%281%29%3C/script%3E/", + forwardedURI: "/proxy/node/node1:10250/redirect", + }, + "redirect abs other host must be escaped": { + sourceURL: "http://mynode.com/redirect", + transport: testTransport, + redirect: "http://example.com/redirected/target/%00/", + redirectWant: "http://example.com/redirected/target/%00%3Cscript%3Ealert%281%29%3C/script%3E/", + forwardedURI: "/proxy/node/node1:10250/redirect", + }, + "redirect abs use reqHost no host no scheme must be escaped": { + sourceURL: "http://mynode.com/redirect", + transport: emptyHostAndSchemeTransport, + redirect: "http://10.0.0.1:8001/redirected/target/%00/", + redirectWant: "http://10.0.0.1:8001/proxy/node/node1:10250/redirected/target/%00%3Cscript%3Ealert%281%29%3C/script%3E/", + forwardedURI: "/proxy/node/node1:10250/redirect", + reqHost: "10.0.0.1:8001", + }, } testItem := func(name string, item *Item) { diff --git a/pkg/util/proxy/upgradeaware.go b/pkg/util/proxy/upgradeaware.go index 8ef16eeb6..a5bb58575 100644 --- a/pkg/util/proxy/upgradeaware.go +++ b/pkg/util/proxy/upgradeaware.go @@ -60,6 +60,8 @@ type UpgradeAwareHandler struct { // Location is the location of the upstream proxy. It is used as the location to Dial on the upstream server // for upgrade requests unless UseRequestLocationOnUpgrade is true. Location *url.URL + // AppendLocationPath determines if the original path of the Location should be appended to the upstream proxy request path + AppendLocationPath bool // Transport provides an optional round tripper to use to proxy. If nil, the default proxy transport is used Transport http.RoundTripper // UpgradeTransport, if specified, will be used as the backend transport when upgrade requests are provided. @@ -67,11 +69,6 @@ type UpgradeAwareHandler struct { UpgradeTransport UpgradeRequestRoundTripper // WrapTransport indicates whether the provided Transport should be wrapped with default proxy transport behavior (URL rewriting, X-Forwarded-* header setting) WrapTransport bool - // InterceptRedirects determines whether the proxy should sniff backend responses for redirects, - // following them as necessary. - InterceptRedirects bool - // RequireSameHostRedirects only allows redirects to the same host. It is only used if InterceptRedirects=true. - RequireSameHostRedirects bool // UseRequestLocation will use the incoming request URL when talking to the backend server. UseRequestLocation bool // UseLocationHost overrides the HTTP host header in requests to the backend server to use the Host from Location. @@ -86,6 +83,8 @@ type UpgradeAwareHandler struct { MaxBytesPerSec int64 // Responder is passed errors that occur while setting up proxying. Responder ErrorResponder + // Reject to forward redirect response + RejectForwardingRedirects bool } const defaultFlushInterval = 200 * time.Millisecond @@ -190,6 +189,26 @@ func NewUpgradeAwareHandler(location *url.URL, transport http.RoundTripper, wrap } } +func proxyRedirectsforRootPath(path string, w http.ResponseWriter, req *http.Request) bool { + redirect := false + method := req.Method + + // From pkg/genericapiserver/endpoints/handlers/proxy.go#ServeHTTP: + // Redirect requests with an empty path to a location that ends with a '/' + // This is essentially a hack for https://issue.k8s.io/4958. + // Note: Keep this code after tryUpgrade to not break that flow. + if len(path) == 0 && (method == http.MethodGet || method == http.MethodHead) { + var queryPart string + if len(req.URL.RawQuery) > 0 { + queryPart = "?" + req.URL.RawQuery + } + w.Header().Set("Location", req.URL.Path+"/"+queryPart) + w.WriteHeader(http.StatusMovedPermanently) + redirect = true + } + return redirect +} + // ServeHTTP handles the proxy request func (h *UpgradeAwareHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { if h.tryUpgrade(w, req) { @@ -209,17 +228,8 @@ func (h *UpgradeAwareHandler) ServeHTTP(w http.ResponseWriter, req *http.Request loc.Path += "/" } - // From pkg/genericapiserver/endpoints/handlers/proxy.go#ServeHTTP: - // Redirect requests with an empty path to a location that ends with a '/' - // This is essentially a hack for http://issue.k8s.io/4958. - // Note: Keep this code after tryUpgrade to not break that flow. - if len(loc.Path) == 0 { - var queryPart string - if len(req.URL.RawQuery) > 0 { - queryPart = "?" + req.URL.RawQuery - } - w.Header().Set("Location", req.URL.Path+"/"+queryPart) - w.WriteHeader(http.StatusMovedPermanently) + proxyRedirect := proxyRedirectsforRootPath(loc.Path, w, req) + if proxyRedirect { return } @@ -239,10 +249,41 @@ func (h *UpgradeAwareHandler) ServeHTTP(w http.ResponseWriter, req *http.Request newReq.Host = h.Location.Host } - proxy := httputil.NewSingleHostReverseProxy(&url.URL{Scheme: h.Location.Scheme, Host: h.Location.Host}) + // create the target location to use for the reverse proxy + reverseProxyLocation := &url.URL{Scheme: h.Location.Scheme, Host: h.Location.Host} + if h.AppendLocationPath { + reverseProxyLocation.Path = h.Location.Path + } + + proxy := httputil.NewSingleHostReverseProxy(reverseProxyLocation) proxy.Transport = h.Transport proxy.FlushInterval = h.FlushInterval proxy.ErrorLog = log.New(noSuppressPanicError{}, "", log.LstdFlags) + if h.RejectForwardingRedirects { + oldModifyResponse := proxy.ModifyResponse + proxy.ModifyResponse = func(response *http.Response) error { + code := response.StatusCode + if code >= 300 && code <= 399 && len(response.Header.Get("Location")) > 0 { + // close the original response + response.Body.Close() + msg := "the backend attempted to redirect this request, which is not permitted" + // replace the response + *response = http.Response{ + StatusCode: http.StatusBadGateway, + Status: fmt.Sprintf("%d %s", response.StatusCode, http.StatusText(response.StatusCode)), + Body: io.NopCloser(strings.NewReader(msg)), + ContentLength: int64(len(msg)), + } + } else { + if oldModifyResponse != nil { + if err := oldModifyResponse(response); err != nil { + return err + } + } + } + return nil + } + } if h.Responder != nil { // if an optional error interceptor/responder was provided wire it // the custom responder might be used for providing a unified error reporting @@ -282,23 +323,21 @@ func (h *UpgradeAwareHandler) tryUpgrade(w http.ResponseWriter, req *http.Reques location = *req.URL location.Scheme = h.Location.Scheme location.Host = h.Location.Host + if h.AppendLocationPath { + location.Path = singleJoiningSlash(h.Location.Path, location.Path) + } } clone := utilnet.CloneRequest(req) // Only append X-Forwarded-For in the upgrade path, since httputil.NewSingleHostReverseProxy // handles this in the non-upgrade path. utilnet.AppendForwardedForHeader(clone) - if h.InterceptRedirects { - klog.V(6).Infof("Connecting to backend proxy (intercepting redirects) %s\n Headers: %v", &location, clone.Header) - backendConn, rawResponse, err = utilnet.ConnectWithRedirects(req.Method, &location, clone.Header, req.Body, utilnet.DialerFunc(h.DialForUpgrade), h.RequireSameHostRedirects) - } else { - klog.V(6).Infof("Connecting to backend proxy (direct dial) %s\n Headers: %v", &location, clone.Header) - if h.UseLocationHost { - clone.Host = h.Location.Host - } - clone.URL = &location - backendConn, err = h.DialForUpgrade(clone) + klog.V(6).Infof("Connecting to backend proxy (direct dial) %s\n Headers: %v", &location, clone.Header) + if h.UseLocationHost { + clone.Host = h.Location.Host } + clone.URL = &location + backendConn, err = h.DialForUpgrade(clone) if err != nil { klog.V(6).Infof("Proxy connection error: %v", err) h.Responder.Error(w, req, err) @@ -414,6 +453,20 @@ func (h *UpgradeAwareHandler) tryUpgrade(w http.ResponseWriter, req *http.Reques return true } +// FIXME: Taken from net/http/httputil/reverseproxy.go as singleJoiningSlash is not exported to be re-used. +// See-also: https://github.com/golang/go/issues/44290 +func singleJoiningSlash(a, b string) string { + aslash := strings.HasSuffix(a, "/") + bslash := strings.HasPrefix(b, "/") + switch { + case aslash && bslash: + return a + b[1:] + case !aslash && !bslash: + return a + "/" + b + } + return a + b +} + func (h *UpgradeAwareHandler) DialForUpgrade(req *http.Request) (net.Conn, error) { if h.UpgradeTransport == nil { return dial(req, h.Transport) diff --git a/pkg/util/proxy/upgradeaware_test.go b/pkg/util/proxy/upgradeaware_test.go index 02fc02ec7..f4b98610e 100644 --- a/pkg/util/proxy/upgradeaware_test.go +++ b/pkg/util/proxy/upgradeaware_test.go @@ -163,6 +163,7 @@ func TestServeHTTP(t *testing.T) { expectedRespHeader map[string]string notExpectedRespHeader []string upgradeRequired bool + appendLocationPath bool expectError func(err error) bool useLocationHost bool }{ @@ -246,6 +247,27 @@ func TestServeHTTP(t *testing.T) { expectedPath: "/some/path", useLocationHost: true, }, + { + name: "append server path to request path", + method: "GET", + requestPath: "/base", + expectedPath: "/base/base", + appendLocationPath: true, + }, + { + name: "append server path to request path with ending slash", + method: "GET", + requestPath: "/base/", + expectedPath: "/base/base/", + appendLocationPath: true, + }, + { + name: "don't append server path to request path", + method: "GET", + requestPath: "/base", + expectedPath: "/base", + appendLocationPath: false, + }, } for i, test := range tests { @@ -269,6 +291,7 @@ func TestServeHTTP(t *testing.T) { backendURL.Path = test.requestPath proxyHandler := NewUpgradeAwareHandler(backendURL, nil, false, test.upgradeRequired, responder) proxyHandler.UseLocationHost = test.useLocationHost + proxyHandler.AppendLocationPath = test.appendLocationPath proxyServer := httptest.NewServer(proxyHandler) defer proxyServer.Close() proxyURL, _ := url.Parse(proxyServer.URL) @@ -478,61 +501,54 @@ func TestProxyUpgrade(t *testing.T) { } for k, tc := range testcases { - for _, redirect := range []bool{false, true} { - tcName := k - backendPath := "/hello" - if redirect { - tcName += " with redirect" - backendPath = "/redirect" - } - func() { // Cleanup after each test case. - backend := http.NewServeMux() - backend.Handle("/hello", websocket.Handler(func(ws *websocket.Conn) { - if ws.Request().Header.Get("Authorization") != tc.ExpectedAuth { - t.Errorf("%s: unexpected headers on request: %v", k, ws.Request().Header) - defer ws.Close() - ws.Write([]byte("you failed")) - return - } + tcName := k + backendPath := "/hello" + func() { // Cleanup after each test case. + backend := http.NewServeMux() + backend.Handle("/hello", websocket.Handler(func(ws *websocket.Conn) { + if ws.Request().Header.Get("Authorization") != tc.ExpectedAuth { + t.Errorf("%s: unexpected headers on request: %v", k, ws.Request().Header) defer ws.Close() - body := make([]byte, 5) - ws.Read(body) - ws.Write([]byte("hello " + string(body))) - })) - backend.Handle("/redirect", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, "/hello", http.StatusFound) - })) - backendServer := tc.ServerFunc(backend) - defer backendServer.Close() - - serverURL, _ := url.Parse(backendServer.URL) - serverURL.Path = backendPath - proxyHandler := NewUpgradeAwareHandler(serverURL, tc.ProxyTransport, false, false, &noErrorsAllowed{t: t}) - proxyHandler.UpgradeTransport = tc.UpgradeTransport - proxyHandler.InterceptRedirects = redirect - proxy := httptest.NewServer(proxyHandler) - defer proxy.Close() - - ws, err := websocket.Dial("ws://"+proxy.Listener.Addr().String()+"/some/path", "", "http://127.0.0.1/") - if err != nil { - t.Fatalf("%s: websocket dial err: %s", tcName, err) + ws.Write([]byte("you failed")) + return } defer ws.Close() + body := make([]byte, 5) + ws.Read(body) + ws.Write([]byte("hello " + string(body))) + })) + backend.Handle("/redirect", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "/hello", http.StatusFound) + })) + backendServer := tc.ServerFunc(backend) + defer backendServer.Close() - if _, err := ws.Write([]byte("world")); err != nil { - t.Fatalf("%s: write err: %s", tcName, err) - } + serverURL, _ := url.Parse(backendServer.URL) + serverURL.Path = backendPath + proxyHandler := NewUpgradeAwareHandler(serverURL, tc.ProxyTransport, false, false, &noErrorsAllowed{t: t}) + proxyHandler.UpgradeTransport = tc.UpgradeTransport + proxy := httptest.NewServer(proxyHandler) + defer proxy.Close() - response := make([]byte, 20) - n, err := ws.Read(response) - if err != nil { - t.Fatalf("%s: read err: %s", tcName, err) - } - if e, a := "hello world", string(response[0:n]); e != a { - t.Fatalf("%s: expected '%#v', got '%#v'", tcName, e, a) - } - }() - } + ws, err := websocket.Dial("ws://"+proxy.Listener.Addr().String()+"/some/path", "", "http://127.0.0.1/") + if err != nil { + t.Fatalf("%s: websocket dial err: %s", tcName, err) + } + defer ws.Close() + + if _, err := ws.Write([]byte("world")); err != nil { + t.Fatalf("%s: write err: %s", tcName, err) + } + + response := make([]byte, 20) + n, err := ws.Read(response) + if err != nil { + t.Fatalf("%s: read err: %s", tcName, err) + } + if e, a := "hello world", string(response[0:n]); e != a { + t.Fatalf("%s: expected '%#v', got '%#v'", tcName, e, a) + } + }() } } @@ -591,107 +607,198 @@ func TestProxyUpgradeConnectionErrorResponse(t *testing.T) { } func TestProxyUpgradeErrorResponseTerminates(t *testing.T) { - for _, intercept := range []bool{true, false} { - for _, code := range []int{400, 500} { - t.Run(fmt.Sprintf("intercept=%v,code=%v", intercept, code), func(t *testing.T) { - // Set up a backend server - backend := http.NewServeMux() - backend.Handle("/hello", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(code) - w.Write([]byte(`some data`)) - })) - backend.Handle("/there", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Error("request to /there") - })) - backendServer := httptest.NewServer(backend) - defer backendServer.Close() - backendServerURL, _ := url.Parse(backendServer.URL) - backendServerURL.Path = "/hello" - - // Set up a proxy pointing to a specific path on the backend - proxyHandler := NewUpgradeAwareHandler(backendServerURL, nil, false, false, &noErrorsAllowed{t: t}) - proxyHandler.InterceptRedirects = intercept - proxy := httptest.NewServer(proxyHandler) - defer proxy.Close() - proxyURL, _ := url.Parse(proxy.URL) - - conn, err := net.Dial("tcp", proxyURL.Host) - require.NoError(t, err) - bufferedReader := bufio.NewReader(conn) - - // Send upgrade request resulting in a non-101 response from the backend - req, _ := http.NewRequest("GET", "/", nil) - req.Header.Set(httpstream.HeaderConnection, httpstream.HeaderUpgrade) - require.NoError(t, req.Write(conn)) - // Verify we get the correct response and full message body content - resp, err := http.ReadResponse(bufferedReader, nil) - require.NoError(t, err) - data, err := ioutil.ReadAll(resp.Body) - require.NoError(t, err) - require.Equal(t, resp.StatusCode, code) - require.Equal(t, data, []byte(`some data`)) - resp.Body.Close() - - // try to read from the connection to verify it was closed - b := make([]byte, 1) - conn.SetReadDeadline(time.Now().Add(time.Second)) - if _, err := conn.Read(b); err != io.EOF { - t.Errorf("expected EOF, got %v", err) - } + for _, code := range []int{400, 500} { + t.Run(fmt.Sprintf("code=%v", code), func(t *testing.T) { + // Set up a backend server + backend := http.NewServeMux() + backend.Handle("/hello", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(code) + w.Write([]byte(`some data`)) + })) + backend.Handle("/there", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Error("request to /there") + })) + backendServer := httptest.NewServer(backend) + defer backendServer.Close() + backendServerURL, _ := url.Parse(backendServer.URL) + backendServerURL.Path = "/hello" + + // Set up a proxy pointing to a specific path on the backend + proxyHandler := NewUpgradeAwareHandler(backendServerURL, nil, false, false, &noErrorsAllowed{t: t}) + proxy := httptest.NewServer(proxyHandler) + defer proxy.Close() + proxyURL, _ := url.Parse(proxy.URL) + + conn, err := net.Dial("tcp", proxyURL.Host) + require.NoError(t, err) + bufferedReader := bufio.NewReader(conn) + + // Send upgrade request resulting in a non-101 response from the backend + req, _ := http.NewRequest("GET", "/", nil) + req.Header.Set(httpstream.HeaderConnection, httpstream.HeaderUpgrade) + require.NoError(t, req.Write(conn)) + // Verify we get the correct response and full message body content + resp, err := http.ReadResponse(bufferedReader, nil) + require.NoError(t, err) + data, err := ioutil.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, resp.StatusCode, code) + require.Equal(t, data, []byte(`some data`)) + resp.Body.Close() + + // try to read from the connection to verify it was closed + b := make([]byte, 1) + conn.SetReadDeadline(time.Now().Add(time.Second)) + if _, err := conn.Read(b); err != io.EOF { + t.Errorf("expected EOF, got %v", err) + } - // Send another request to another endpoint to verify it is not received - req, _ = http.NewRequest("GET", "/there", nil) - req.Write(conn) - // wait to ensure the handler does not receive the request - time.Sleep(time.Second) + // Send another request to another endpoint to verify it is not received + req, _ = http.NewRequest("GET", "/there", nil) + req.Write(conn) + // wait to ensure the handler does not receive the request + time.Sleep(time.Second) - // clean up - conn.Close() - }) - } + // clean up + conn.Close() + }) } } func TestProxyUpgradeErrorResponse(t *testing.T) { - for _, intercept := range []bool{true, false} { - for _, code := range []int{200, 300, 302, 307} { - t.Run(fmt.Sprintf("intercept=%v,code=%v", intercept, code), func(t *testing.T) { - // Set up a backend server - backend := http.NewServeMux() - backend.Handle("/hello", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, "https://example.com/there", code) - })) - backendServer := httptest.NewServer(backend) - defer backendServer.Close() - backendServerURL, _ := url.Parse(backendServer.URL) - backendServerURL.Path = "/hello" - - // Set up a proxy pointing to a specific path on the backend - proxyHandler := NewUpgradeAwareHandler(backendServerURL, nil, false, false, &fakeResponder{t: t}) - proxyHandler.InterceptRedirects = intercept - proxyHandler.RequireSameHostRedirects = true - proxy := httptest.NewServer(proxyHandler) - defer proxy.Close() - proxyURL, _ := url.Parse(proxy.URL) - - conn, err := net.Dial("tcp", proxyURL.Host) - require.NoError(t, err) - bufferedReader := bufio.NewReader(conn) - - // Send upgrade request resulting in a non-101 response from the backend - req, _ := http.NewRequest("GET", "/", nil) - req.Header.Set(httpstream.HeaderConnection, httpstream.HeaderUpgrade) - require.NoError(t, req.Write(conn)) - // Verify we get the correct response and full message body content - resp, err := http.ReadResponse(bufferedReader, nil) - require.NoError(t, err) - assert.Equal(t, fakeStatusCode, resp.StatusCode) - resp.Body.Close() - - // clean up - conn.Close() - }) - } + for _, code := range []int{200, 300, 302, 307} { + t.Run(fmt.Sprintf("code=%v", code), func(t *testing.T) { + // Set up a backend server + backend := http.NewServeMux() + backend.Handle("/hello", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "https://example.com/there", code) + })) + backendServer := httptest.NewServer(backend) + defer backendServer.Close() + backendServerURL, _ := url.Parse(backendServer.URL) + backendServerURL.Path = "/hello" + + // Set up a proxy pointing to a specific path on the backend + proxyHandler := NewUpgradeAwareHandler(backendServerURL, nil, false, false, &fakeResponder{t: t}) + proxy := httptest.NewServer(proxyHandler) + defer proxy.Close() + proxyURL, _ := url.Parse(proxy.URL) + + conn, err := net.Dial("tcp", proxyURL.Host) + require.NoError(t, err) + bufferedReader := bufio.NewReader(conn) + + // Send upgrade request resulting in a non-101 response from the backend + req, _ := http.NewRequest("GET", "/", nil) + req.Header.Set(httpstream.HeaderConnection, httpstream.HeaderUpgrade) + require.NoError(t, req.Write(conn)) + // Verify we get the correct response and full message body content + resp, err := http.ReadResponse(bufferedReader, nil) + require.NoError(t, err) + assert.Equal(t, fakeStatusCode, resp.StatusCode) + resp.Body.Close() + + // clean up + conn.Close() + }) + } +} + +func TestRejectForwardingRedirectsOption(t *testing.T) { + originalBody := []byte(`some data`) + testCases := []struct { + name string + rejectForwardingRedirects bool + serverStatusCode int + redirect string + expectStatusCode int + expectBody []byte + }{ + { + name: "reject redirection enabled in proxy, backend server sending 200 response", + rejectForwardingRedirects: true, + serverStatusCode: 200, + expectStatusCode: 200, + expectBody: originalBody, + }, + { + name: "reject redirection enabled in proxy, backend server sending 301 response", + rejectForwardingRedirects: true, + serverStatusCode: 301, + redirect: "/", + expectStatusCode: 502, + expectBody: []byte(`the backend attempted to redirect this request, which is not permitted`), + }, + { + name: "reject redirection enabled in proxy, backend server sending 304 response with a location header", + rejectForwardingRedirects: true, + serverStatusCode: 304, + redirect: "/", + expectStatusCode: 502, + expectBody: []byte(`the backend attempted to redirect this request, which is not permitted`), + }, + { + name: "reject redirection enabled in proxy, backend server sending 304 response with no location header", + rejectForwardingRedirects: true, + serverStatusCode: 304, + expectStatusCode: 304, + expectBody: []byte{}, // client doesn't read the body for 304 responses + }, + { + name: "reject redirection disabled in proxy, backend server sending 200 response", + rejectForwardingRedirects: false, + serverStatusCode: 200, + expectStatusCode: 200, + expectBody: originalBody, + }, + { + name: "reject redirection disabled in proxy, backend server sending 301 response", + rejectForwardingRedirects: false, + serverStatusCode: 301, + redirect: "/", + expectStatusCode: 301, + expectBody: originalBody, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Set up a backend server + backendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if tc.redirect != "" { + w.Header().Set("Location", tc.redirect) + } + w.WriteHeader(tc.serverStatusCode) + w.Write(originalBody) + })) + defer backendServer.Close() + backendServerURL, _ := url.Parse(backendServer.URL) + + // Set up a proxy pointing to the backend + proxyHandler := NewUpgradeAwareHandler(backendServerURL, nil, false, false, &fakeResponder{t: t}) + proxyHandler.RejectForwardingRedirects = tc.rejectForwardingRedirects + proxy := httptest.NewServer(proxyHandler) + defer proxy.Close() + proxyURL, _ := url.Parse(proxy.URL) + + conn, err := net.Dial("tcp", proxyURL.Host) + require.NoError(t, err) + bufferedReader := bufio.NewReader(conn) + + req, _ := http.NewRequest("GET", proxyURL.String(), nil) + require.NoError(t, req.Write(conn)) + // Verify we get the correct response and message body content + resp, err := http.ReadResponse(bufferedReader, nil) + require.NoError(t, err) + assert.Equal(t, tc.expectStatusCode, resp.StatusCode) + data, err := ioutil.ReadAll(resp.Body) + require.NoError(t, err) + assert.Equal(t, tc.expectBody, data) + assert.Equal(t, int64(len(tc.expectBody)), resp.ContentLength) + resp.Body.Close() + + // clean up + conn.Close() + }) } } @@ -1032,8 +1139,80 @@ func TestErrorPropagation(t *testing.T) { } } +func TestProxyRedirectsforRootPath(t *testing.T) { + + tests := []struct { + name string + method string + requestPath string + expectedHeader http.Header + expectedStatusCode int + redirect bool + }{ + { + name: "root path, simple get", + method: "GET", + requestPath: "", + redirect: true, + expectedStatusCode: 301, + expectedHeader: http.Header{ + "Location": []string{"/"}, + }, + }, + { + name: "root path, simple put", + method: "PUT", + requestPath: "", + redirect: false, + expectedStatusCode: 200, + }, + { + name: "root path, simple head", + method: "HEAD", + requestPath: "", + redirect: true, + expectedStatusCode: 301, + expectedHeader: http.Header{ + "Location": []string{"/"}, + }, + }, + { + name: "root path, simple delete with params", + method: "DELETE", + requestPath: "", + redirect: false, + expectedStatusCode: 200, + }, + } + + for _, test := range tests { + func() { + w := httptest.NewRecorder() + req, err := http.NewRequest(test.method, test.requestPath, nil) + if err != nil { + t.Fatal(err) + } + + redirect := proxyRedirectsforRootPath(test.requestPath, w, req) + if got, want := redirect, test.redirect; got != want { + t.Errorf("Expected redirect state %v; got %v", want, got) + } + + res := w.Result() + if got, want := res.StatusCode, test.expectedStatusCode; got != want { + t.Errorf("Expected status code %d; got %d", want, got) + } + + if res.StatusCode == 301 && !reflect.DeepEqual(res.Header, test.expectedHeader) { + t.Errorf("Expected location header to be %v, got %v", test.expectedHeader, res.Header) + } + }() + } +} + // exampleCert was generated from crypto/tls/generate_cert.go with the following command: -// go run generate_cert.go --rsa-bits 1024 --host example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h +// +// go run generate_cert.go --rsa-bits 1024 --host example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h var exampleCert = []byte(`-----BEGIN CERTIFICATE----- MIIDADCCAeigAwIBAgIQVHG3Fn9SdWayyLOZKCW1vzANBgkqhkiG9w0BAQsFADAS MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw diff --git a/pkg/util/rand/rand_test.go b/pkg/util/rand/rand_test.go index 517043a52..ea4bc66d5 100644 --- a/pkg/util/rand/rand_test.go +++ b/pkg/util/rand/rand_test.go @@ -28,7 +28,7 @@ const ( ) func TestString(t *testing.T) { - valid := "0123456789abcdefghijklmnopqrstuvwxyz" + valid := "bcdfghjklmnpqrstvwxz2456789" for _, l := range []int{0, 1, 2, 10, 123} { s := String(l) if len(s) != l { diff --git a/pkg/util/remotecommand/constants.go b/pkg/util/remotecommand/constants.go index acfeb827c..237ebaef4 100644 --- a/pkg/util/remotecommand/constants.go +++ b/pkg/util/remotecommand/constants.go @@ -27,8 +27,8 @@ const ( // The SPDY subprotocol "channel.k8s.io" is used for remote command // attachment/execution. This represents the initial unversioned subprotocol, - // which has the known bugs http://issues.k8s.io/13394 and - // http://issues.k8s.io/13395. + // which has the known bugs https://issues.k8s.io/13394 and + // https://issues.k8s.io/13395. StreamProtocolV1Name = "channel.k8s.io" // The SPDY subprotocol "v2.channel.k8s.io" is used for remote command diff --git a/pkg/util/runtime/runtime.go b/pkg/util/runtime/runtime.go index 035c52811..d738725ca 100644 --- a/pkg/util/runtime/runtime.go +++ b/pkg/util/runtime/runtime.go @@ -27,9 +27,10 @@ import ( ) var ( - // ReallyCrash controls the behavior of HandleCrash and now defaults - // true. It's still exposed so components can optionally set to false - // to restore prior behavior. + // ReallyCrash controls the behavior of HandleCrash and defaults to + // true. It's exposed so components can optionally set to false + // to restore prior behavior. This flag is mostly used for tests to validate + // crash conditions. ReallyCrash = true ) @@ -141,7 +142,7 @@ func GetCaller() string { runtime.Callers(3, pc[:]) f := runtime.FuncForPC(pc[0]) if f == nil { - return fmt.Sprintf("Unable to find caller") + return "Unable to find caller" } return f.Name() } diff --git a/pkg/util/sets/byte.go b/pkg/util/sets/byte.go index 9bfa85d43..4d7a17c3a 100644 --- a/pkg/util/sets/byte.go +++ b/pkg/util/sets/byte.go @@ -1,5 +1,5 @@ /* -Copyright The Kubernetes Authors. +Copyright 2022 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. @@ -14,93 +14,75 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Code generated by set-gen. DO NOT EDIT. - package sets -import ( - "reflect" - "sort" -) - -// sets.Byte is a set of bytes, implemented via map[byte]struct{} for minimal memory consumption. +// Byte is a set of bytes, implemented via map[byte]struct{} for minimal memory consumption. +// +// Deprecated: use generic Set instead. +// new ways: +// s1 := Set[byte]{} +// s2 := New[byte]() type Byte map[byte]Empty // NewByte creates a Byte from a list of values. func NewByte(items ...byte) Byte { - ss := Byte{} - ss.Insert(items...) - return ss + return Byte(New[byte](items...)) } // ByteKeySet creates a Byte from a keys of a map[byte](? extends interface{}). // If the value passed in is not actually a map, this will panic. -func ByteKeySet(theMap interface{}) Byte { - v := reflect.ValueOf(theMap) - ret := Byte{} - - for _, keyValue := range v.MapKeys() { - ret.Insert(keyValue.Interface().(byte)) - } - return ret +func ByteKeySet[T any](theMap map[byte]T) Byte { + return Byte(KeySet(theMap)) } // Insert adds items to the set. func (s Byte) Insert(items ...byte) Byte { - for _, item := range items { - s[item] = Empty{} - } - return s + return Byte(cast(s).Insert(items...)) } // Delete removes all items from the set. func (s Byte) Delete(items ...byte) Byte { - for _, item := range items { - delete(s, item) - } - return s + return Byte(cast(s).Delete(items...)) } // Has returns true if and only if item is contained in the set. func (s Byte) Has(item byte) bool { - _, contained := s[item] - return contained + return cast(s).Has(item) } // HasAll returns true if and only if all items are contained in the set. func (s Byte) HasAll(items ...byte) bool { - for _, item := range items { - if !s.Has(item) { - return false - } - } - return true + return cast(s).HasAll(items...) } // HasAny returns true if any items are contained in the set. func (s Byte) HasAny(items ...byte) bool { - for _, item := range items { - if s.Has(item) { - return true - } - } - return false + return cast(s).HasAny(items...) } -// Difference returns a set of objects that are not in s2 +// Clone returns a new set which is a copy of the current set. +func (s Byte) Clone() Byte { + return Byte(cast(s).Clone()) +} + +// Difference returns a set of objects that are not in s2. // For example: // s1 = {a1, a2, a3} // s2 = {a1, a2, a4, a5} // s1.Difference(s2) = {a3} // s2.Difference(s1) = {a4, a5} -func (s Byte) Difference(s2 Byte) Byte { - result := NewByte() - for key := range s { - if !s2.Has(key) { - result.Insert(key) - } - } - return result +func (s1 Byte) Difference(s2 Byte) Byte { + return Byte(cast(s1).Difference(cast(s2))) +} + +// SymmetricDifference returns a set of elements which are in either of the sets, but not in their intersection. +// For example: +// s1 = {a1, a2, a3} +// s2 = {a1, a2, a4, a5} +// s1.SymmetricDifference(s2) = {a3, a4, a5} +// s2.SymmetricDifference(s1) = {a3, a4, a5} +func (s1 Byte) SymmetricDifference(s2 Byte) Byte { + return Byte(cast(s1).SymmetricDifference(cast(s2))) } // Union returns a new set which includes items in either s1 or s2. @@ -110,14 +92,7 @@ func (s Byte) Difference(s2 Byte) Byte { // s1.Union(s2) = {a1, a2, a3, a4} // s2.Union(s1) = {a1, a2, a3, a4} func (s1 Byte) Union(s2 Byte) Byte { - result := NewByte() - for key := range s1 { - result.Insert(key) - } - for key := range s2 { - result.Insert(key) - } - return result + return Byte(cast(s1).Union(cast(s2))) } // Intersection returns a new set which includes the item in BOTH s1 and s2 @@ -126,80 +101,37 @@ func (s1 Byte) Union(s2 Byte) Byte { // s2 = {a2, a3} // s1.Intersection(s2) = {a2} func (s1 Byte) Intersection(s2 Byte) Byte { - var walk, other Byte - result := NewByte() - if s1.Len() < s2.Len() { - walk = s1 - other = s2 - } else { - walk = s2 - other = s1 - } - for key := range walk { - if other.Has(key) { - result.Insert(key) - } - } - return result + return Byte(cast(s1).Intersection(cast(s2))) } // IsSuperset returns true if and only if s1 is a superset of s2. func (s1 Byte) IsSuperset(s2 Byte) bool { - for item := range s2 { - if !s1.Has(item) { - return false - } - } - return true + return cast(s1).IsSuperset(cast(s2)) } // Equal returns true if and only if s1 is equal (as a set) to s2. // Two sets are equal if their membership is identical. // (In practice, this means same elements, order doesn't matter) func (s1 Byte) Equal(s2 Byte) bool { - return len(s1) == len(s2) && s1.IsSuperset(s2) + return cast(s1).Equal(cast(s2)) } -type sortableSliceOfByte []byte - -func (s sortableSliceOfByte) Len() int { return len(s) } -func (s sortableSliceOfByte) Less(i, j int) bool { return lessByte(s[i], s[j]) } -func (s sortableSliceOfByte) Swap(i, j int) { s[i], s[j] = s[j], s[i] } - // List returns the contents as a sorted byte slice. func (s Byte) List() []byte { - res := make(sortableSliceOfByte, 0, len(s)) - for key := range s { - res = append(res, key) - } - sort.Sort(res) - return []byte(res) + return List(cast(s)) } // UnsortedList returns the slice with contents in random order. func (s Byte) UnsortedList() []byte { - res := make([]byte, 0, len(s)) - for key := range s { - res = append(res, key) - } - return res + return cast(s).UnsortedList() } -// Returns a single element from the set. +// PopAny returns a single element from the set. func (s Byte) PopAny() (byte, bool) { - for key := range s { - s.Delete(key) - return key, true - } - var zeroValue byte - return zeroValue, false + return cast(s).PopAny() } // Len returns the size of the set. func (s Byte) Len() int { return len(s) } - -func lessByte(lhs, rhs byte) bool { - return lhs < rhs -} diff --git a/pkg/util/sets/doc.go b/pkg/util/sets/doc.go index b152a0bf0..194883390 100644 --- a/pkg/util/sets/doc.go +++ b/pkg/util/sets/doc.go @@ -1,5 +1,5 @@ /* -Copyright The Kubernetes Authors. +Copyright 2022 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. @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Code generated by set-gen. DO NOT EDIT. - -// Package sets has auto-generated set types. +// Package sets has generic set and specified sets. Generic set will +// replace specified ones over time. And specific ones are deprecated. package sets diff --git a/pkg/util/sets/empty.go b/pkg/util/sets/empty.go index e11e622c5..fbb1df06d 100644 --- a/pkg/util/sets/empty.go +++ b/pkg/util/sets/empty.go @@ -1,5 +1,5 @@ /* -Copyright The Kubernetes Authors. +Copyright 2022 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. @@ -14,8 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Code generated by set-gen. DO NOT EDIT. - package sets // Empty is public since it is used by some internal API objects for conversions between external diff --git a/pkg/util/sets/int.go b/pkg/util/sets/int.go index 88bd70967..5876fc9de 100644 --- a/pkg/util/sets/int.go +++ b/pkg/util/sets/int.go @@ -1,5 +1,5 @@ /* -Copyright The Kubernetes Authors. +Copyright 2022 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. @@ -14,93 +14,75 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Code generated by set-gen. DO NOT EDIT. - package sets -import ( - "reflect" - "sort" -) - -// sets.Int is a set of ints, implemented via map[int]struct{} for minimal memory consumption. +// Int is a set of ints, implemented via map[int]struct{} for minimal memory consumption. +// +// Deprecated: use generic Set instead. +// new ways: +// s1 := Set[int]{} +// s2 := New[int]() type Int map[int]Empty // NewInt creates a Int from a list of values. func NewInt(items ...int) Int { - ss := Int{} - ss.Insert(items...) - return ss + return Int(New[int](items...)) } // IntKeySet creates a Int from a keys of a map[int](? extends interface{}). // If the value passed in is not actually a map, this will panic. -func IntKeySet(theMap interface{}) Int { - v := reflect.ValueOf(theMap) - ret := Int{} - - for _, keyValue := range v.MapKeys() { - ret.Insert(keyValue.Interface().(int)) - } - return ret +func IntKeySet[T any](theMap map[int]T) Int { + return Int(KeySet(theMap)) } // Insert adds items to the set. func (s Int) Insert(items ...int) Int { - for _, item := range items { - s[item] = Empty{} - } - return s + return Int(cast(s).Insert(items...)) } // Delete removes all items from the set. func (s Int) Delete(items ...int) Int { - for _, item := range items { - delete(s, item) - } - return s + return Int(cast(s).Delete(items...)) } // Has returns true if and only if item is contained in the set. func (s Int) Has(item int) bool { - _, contained := s[item] - return contained + return cast(s).Has(item) } // HasAll returns true if and only if all items are contained in the set. func (s Int) HasAll(items ...int) bool { - for _, item := range items { - if !s.Has(item) { - return false - } - } - return true + return cast(s).HasAll(items...) } // HasAny returns true if any items are contained in the set. func (s Int) HasAny(items ...int) bool { - for _, item := range items { - if s.Has(item) { - return true - } - } - return false + return cast(s).HasAny(items...) } -// Difference returns a set of objects that are not in s2 +// Clone returns a new set which is a copy of the current set. +func (s Int) Clone() Int { + return Int(cast(s).Clone()) +} + +// Difference returns a set of objects that are not in s2. // For example: // s1 = {a1, a2, a3} // s2 = {a1, a2, a4, a5} // s1.Difference(s2) = {a3} // s2.Difference(s1) = {a4, a5} -func (s Int) Difference(s2 Int) Int { - result := NewInt() - for key := range s { - if !s2.Has(key) { - result.Insert(key) - } - } - return result +func (s1 Int) Difference(s2 Int) Int { + return Int(cast(s1).Difference(cast(s2))) +} + +// SymmetricDifference returns a set of elements which are in either of the sets, but not in their intersection. +// For example: +// s1 = {a1, a2, a3} +// s2 = {a1, a2, a4, a5} +// s1.SymmetricDifference(s2) = {a3, a4, a5} +// s2.SymmetricDifference(s1) = {a3, a4, a5} +func (s1 Int) SymmetricDifference(s2 Int) Int { + return Int(cast(s1).SymmetricDifference(cast(s2))) } // Union returns a new set which includes items in either s1 or s2. @@ -110,14 +92,7 @@ func (s Int) Difference(s2 Int) Int { // s1.Union(s2) = {a1, a2, a3, a4} // s2.Union(s1) = {a1, a2, a3, a4} func (s1 Int) Union(s2 Int) Int { - result := NewInt() - for key := range s1 { - result.Insert(key) - } - for key := range s2 { - result.Insert(key) - } - return result + return Int(cast(s1).Union(cast(s2))) } // Intersection returns a new set which includes the item in BOTH s1 and s2 @@ -126,80 +101,37 @@ func (s1 Int) Union(s2 Int) Int { // s2 = {a2, a3} // s1.Intersection(s2) = {a2} func (s1 Int) Intersection(s2 Int) Int { - var walk, other Int - result := NewInt() - if s1.Len() < s2.Len() { - walk = s1 - other = s2 - } else { - walk = s2 - other = s1 - } - for key := range walk { - if other.Has(key) { - result.Insert(key) - } - } - return result + return Int(cast(s1).Intersection(cast(s2))) } // IsSuperset returns true if and only if s1 is a superset of s2. func (s1 Int) IsSuperset(s2 Int) bool { - for item := range s2 { - if !s1.Has(item) { - return false - } - } - return true + return cast(s1).IsSuperset(cast(s2)) } // Equal returns true if and only if s1 is equal (as a set) to s2. // Two sets are equal if their membership is identical. // (In practice, this means same elements, order doesn't matter) func (s1 Int) Equal(s2 Int) bool { - return len(s1) == len(s2) && s1.IsSuperset(s2) + return cast(s1).Equal(cast(s2)) } -type sortableSliceOfInt []int - -func (s sortableSliceOfInt) Len() int { return len(s) } -func (s sortableSliceOfInt) Less(i, j int) bool { return lessInt(s[i], s[j]) } -func (s sortableSliceOfInt) Swap(i, j int) { s[i], s[j] = s[j], s[i] } - // List returns the contents as a sorted int slice. func (s Int) List() []int { - res := make(sortableSliceOfInt, 0, len(s)) - for key := range s { - res = append(res, key) - } - sort.Sort(res) - return []int(res) + return List(cast(s)) } // UnsortedList returns the slice with contents in random order. func (s Int) UnsortedList() []int { - res := make([]int, 0, len(s)) - for key := range s { - res = append(res, key) - } - return res + return cast(s).UnsortedList() } -// Returns a single element from the set. +// PopAny returns a single element from the set. func (s Int) PopAny() (int, bool) { - for key := range s { - s.Delete(key) - return key, true - } - var zeroValue int - return zeroValue, false + return cast(s).PopAny() } // Len returns the size of the set. func (s Int) Len() int { return len(s) } - -func lessInt(lhs, rhs int) bool { - return lhs < rhs -} diff --git a/pkg/util/sets/int32.go b/pkg/util/sets/int32.go index 96a485554..2c640c5d0 100644 --- a/pkg/util/sets/int32.go +++ b/pkg/util/sets/int32.go @@ -1,5 +1,5 @@ /* -Copyright The Kubernetes Authors. +Copyright 2022 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. @@ -14,93 +14,75 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Code generated by set-gen. DO NOT EDIT. - package sets -import ( - "reflect" - "sort" -) - -// sets.Int32 is a set of int32s, implemented via map[int32]struct{} for minimal memory consumption. +// Int32 is a set of int32s, implemented via map[int32]struct{} for minimal memory consumption. +// +// Deprecated: use generic Set instead. +// new ways: +// s1 := Set[int32]{} +// s2 := New[int32]() type Int32 map[int32]Empty // NewInt32 creates a Int32 from a list of values. func NewInt32(items ...int32) Int32 { - ss := Int32{} - ss.Insert(items...) - return ss + return Int32(New[int32](items...)) } // Int32KeySet creates a Int32 from a keys of a map[int32](? extends interface{}). // If the value passed in is not actually a map, this will panic. -func Int32KeySet(theMap interface{}) Int32 { - v := reflect.ValueOf(theMap) - ret := Int32{} - - for _, keyValue := range v.MapKeys() { - ret.Insert(keyValue.Interface().(int32)) - } - return ret +func Int32KeySet[T any](theMap map[int32]T) Int32 { + return Int32(KeySet(theMap)) } // Insert adds items to the set. func (s Int32) Insert(items ...int32) Int32 { - for _, item := range items { - s[item] = Empty{} - } - return s + return Int32(cast(s).Insert(items...)) } // Delete removes all items from the set. func (s Int32) Delete(items ...int32) Int32 { - for _, item := range items { - delete(s, item) - } - return s + return Int32(cast(s).Delete(items...)) } // Has returns true if and only if item is contained in the set. func (s Int32) Has(item int32) bool { - _, contained := s[item] - return contained + return cast(s).Has(item) } // HasAll returns true if and only if all items are contained in the set. func (s Int32) HasAll(items ...int32) bool { - for _, item := range items { - if !s.Has(item) { - return false - } - } - return true + return cast(s).HasAll(items...) } // HasAny returns true if any items are contained in the set. func (s Int32) HasAny(items ...int32) bool { - for _, item := range items { - if s.Has(item) { - return true - } - } - return false + return cast(s).HasAny(items...) } -// Difference returns a set of objects that are not in s2 +// Clone returns a new set which is a copy of the current set. +func (s Int32) Clone() Int32 { + return Int32(cast(s).Clone()) +} + +// Difference returns a set of objects that are not in s2. // For example: // s1 = {a1, a2, a3} // s2 = {a1, a2, a4, a5} // s1.Difference(s2) = {a3} // s2.Difference(s1) = {a4, a5} -func (s Int32) Difference(s2 Int32) Int32 { - result := NewInt32() - for key := range s { - if !s2.Has(key) { - result.Insert(key) - } - } - return result +func (s1 Int32) Difference(s2 Int32) Int32 { + return Int32(cast(s1).Difference(cast(s2))) +} + +// SymmetricDifference returns a set of elements which are in either of the sets, but not in their intersection. +// For example: +// s1 = {a1, a2, a3} +// s2 = {a1, a2, a4, a5} +// s1.SymmetricDifference(s2) = {a3, a4, a5} +// s2.SymmetricDifference(s1) = {a3, a4, a5} +func (s1 Int32) SymmetricDifference(s2 Int32) Int32 { + return Int32(cast(s1).SymmetricDifference(cast(s2))) } // Union returns a new set which includes items in either s1 or s2. @@ -110,14 +92,7 @@ func (s Int32) Difference(s2 Int32) Int32 { // s1.Union(s2) = {a1, a2, a3, a4} // s2.Union(s1) = {a1, a2, a3, a4} func (s1 Int32) Union(s2 Int32) Int32 { - result := NewInt32() - for key := range s1 { - result.Insert(key) - } - for key := range s2 { - result.Insert(key) - } - return result + return Int32(cast(s1).Union(cast(s2))) } // Intersection returns a new set which includes the item in BOTH s1 and s2 @@ -126,80 +101,37 @@ func (s1 Int32) Union(s2 Int32) Int32 { // s2 = {a2, a3} // s1.Intersection(s2) = {a2} func (s1 Int32) Intersection(s2 Int32) Int32 { - var walk, other Int32 - result := NewInt32() - if s1.Len() < s2.Len() { - walk = s1 - other = s2 - } else { - walk = s2 - other = s1 - } - for key := range walk { - if other.Has(key) { - result.Insert(key) - } - } - return result + return Int32(cast(s1).Intersection(cast(s2))) } // IsSuperset returns true if and only if s1 is a superset of s2. func (s1 Int32) IsSuperset(s2 Int32) bool { - for item := range s2 { - if !s1.Has(item) { - return false - } - } - return true + return cast(s1).IsSuperset(cast(s2)) } // Equal returns true if and only if s1 is equal (as a set) to s2. // Two sets are equal if their membership is identical. // (In practice, this means same elements, order doesn't matter) func (s1 Int32) Equal(s2 Int32) bool { - return len(s1) == len(s2) && s1.IsSuperset(s2) + return cast(s1).Equal(cast(s2)) } -type sortableSliceOfInt32 []int32 - -func (s sortableSliceOfInt32) Len() int { return len(s) } -func (s sortableSliceOfInt32) Less(i, j int) bool { return lessInt32(s[i], s[j]) } -func (s sortableSliceOfInt32) Swap(i, j int) { s[i], s[j] = s[j], s[i] } - // List returns the contents as a sorted int32 slice. func (s Int32) List() []int32 { - res := make(sortableSliceOfInt32, 0, len(s)) - for key := range s { - res = append(res, key) - } - sort.Sort(res) - return []int32(res) + return List(cast(s)) } // UnsortedList returns the slice with contents in random order. func (s Int32) UnsortedList() []int32 { - res := make([]int32, 0, len(s)) - for key := range s { - res = append(res, key) - } - return res + return cast(s).UnsortedList() } -// Returns a single element from the set. +// PopAny returns a single element from the set. func (s Int32) PopAny() (int32, bool) { - for key := range s { - s.Delete(key) - return key, true - } - var zeroValue int32 - return zeroValue, false + return cast(s).PopAny() } // Len returns the size of the set. func (s Int32) Len() int { return len(s) } - -func lessInt32(lhs, rhs int32) bool { - return lhs < rhs -} diff --git a/pkg/util/sets/int64.go b/pkg/util/sets/int64.go index b375a1b06..bf3eb3ffa 100644 --- a/pkg/util/sets/int64.go +++ b/pkg/util/sets/int64.go @@ -1,5 +1,5 @@ /* -Copyright The Kubernetes Authors. +Copyright 2022 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. @@ -14,93 +14,75 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Code generated by set-gen. DO NOT EDIT. - package sets -import ( - "reflect" - "sort" -) - -// sets.Int64 is a set of int64s, implemented via map[int64]struct{} for minimal memory consumption. +// Int64 is a set of int64s, implemented via map[int64]struct{} for minimal memory consumption. +// +// Deprecated: use generic Set instead. +// new ways: +// s1 := Set[int64]{} +// s2 := New[int64]() type Int64 map[int64]Empty // NewInt64 creates a Int64 from a list of values. func NewInt64(items ...int64) Int64 { - ss := Int64{} - ss.Insert(items...) - return ss + return Int64(New[int64](items...)) } // Int64KeySet creates a Int64 from a keys of a map[int64](? extends interface{}). // If the value passed in is not actually a map, this will panic. -func Int64KeySet(theMap interface{}) Int64 { - v := reflect.ValueOf(theMap) - ret := Int64{} - - for _, keyValue := range v.MapKeys() { - ret.Insert(keyValue.Interface().(int64)) - } - return ret +func Int64KeySet[T any](theMap map[int64]T) Int64 { + return Int64(KeySet(theMap)) } // Insert adds items to the set. func (s Int64) Insert(items ...int64) Int64 { - for _, item := range items { - s[item] = Empty{} - } - return s + return Int64(cast(s).Insert(items...)) } // Delete removes all items from the set. func (s Int64) Delete(items ...int64) Int64 { - for _, item := range items { - delete(s, item) - } - return s + return Int64(cast(s).Delete(items...)) } // Has returns true if and only if item is contained in the set. func (s Int64) Has(item int64) bool { - _, contained := s[item] - return contained + return cast(s).Has(item) } // HasAll returns true if and only if all items are contained in the set. func (s Int64) HasAll(items ...int64) bool { - for _, item := range items { - if !s.Has(item) { - return false - } - } - return true + return cast(s).HasAll(items...) } // HasAny returns true if any items are contained in the set. func (s Int64) HasAny(items ...int64) bool { - for _, item := range items { - if s.Has(item) { - return true - } - } - return false + return cast(s).HasAny(items...) } -// Difference returns a set of objects that are not in s2 +// Clone returns a new set which is a copy of the current set. +func (s Int64) Clone() Int64 { + return Int64(cast(s).Clone()) +} + +// Difference returns a set of objects that are not in s2. // For example: // s1 = {a1, a2, a3} // s2 = {a1, a2, a4, a5} // s1.Difference(s2) = {a3} // s2.Difference(s1) = {a4, a5} -func (s Int64) Difference(s2 Int64) Int64 { - result := NewInt64() - for key := range s { - if !s2.Has(key) { - result.Insert(key) - } - } - return result +func (s1 Int64) Difference(s2 Int64) Int64 { + return Int64(cast(s1).Difference(cast(s2))) +} + +// SymmetricDifference returns a set of elements which are in either of the sets, but not in their intersection. +// For example: +// s1 = {a1, a2, a3} +// s2 = {a1, a2, a4, a5} +// s1.SymmetricDifference(s2) = {a3, a4, a5} +// s2.SymmetricDifference(s1) = {a3, a4, a5} +func (s1 Int64) SymmetricDifference(s2 Int64) Int64 { + return Int64(cast(s1).SymmetricDifference(cast(s2))) } // Union returns a new set which includes items in either s1 or s2. @@ -110,14 +92,7 @@ func (s Int64) Difference(s2 Int64) Int64 { // s1.Union(s2) = {a1, a2, a3, a4} // s2.Union(s1) = {a1, a2, a3, a4} func (s1 Int64) Union(s2 Int64) Int64 { - result := NewInt64() - for key := range s1 { - result.Insert(key) - } - for key := range s2 { - result.Insert(key) - } - return result + return Int64(cast(s1).Union(cast(s2))) } // Intersection returns a new set which includes the item in BOTH s1 and s2 @@ -126,80 +101,37 @@ func (s1 Int64) Union(s2 Int64) Int64 { // s2 = {a2, a3} // s1.Intersection(s2) = {a2} func (s1 Int64) Intersection(s2 Int64) Int64 { - var walk, other Int64 - result := NewInt64() - if s1.Len() < s2.Len() { - walk = s1 - other = s2 - } else { - walk = s2 - other = s1 - } - for key := range walk { - if other.Has(key) { - result.Insert(key) - } - } - return result + return Int64(cast(s1).Intersection(cast(s2))) } // IsSuperset returns true if and only if s1 is a superset of s2. func (s1 Int64) IsSuperset(s2 Int64) bool { - for item := range s2 { - if !s1.Has(item) { - return false - } - } - return true + return cast(s1).IsSuperset(cast(s2)) } // Equal returns true if and only if s1 is equal (as a set) to s2. // Two sets are equal if their membership is identical. // (In practice, this means same elements, order doesn't matter) func (s1 Int64) Equal(s2 Int64) bool { - return len(s1) == len(s2) && s1.IsSuperset(s2) + return cast(s1).Equal(cast(s2)) } -type sortableSliceOfInt64 []int64 - -func (s sortableSliceOfInt64) Len() int { return len(s) } -func (s sortableSliceOfInt64) Less(i, j int) bool { return lessInt64(s[i], s[j]) } -func (s sortableSliceOfInt64) Swap(i, j int) { s[i], s[j] = s[j], s[i] } - // List returns the contents as a sorted int64 slice. func (s Int64) List() []int64 { - res := make(sortableSliceOfInt64, 0, len(s)) - for key := range s { - res = append(res, key) - } - sort.Sort(res) - return []int64(res) + return List(cast(s)) } // UnsortedList returns the slice with contents in random order. func (s Int64) UnsortedList() []int64 { - res := make([]int64, 0, len(s)) - for key := range s { - res = append(res, key) - } - return res + return cast(s).UnsortedList() } -// Returns a single element from the set. +// PopAny returns a single element from the set. func (s Int64) PopAny() (int64, bool) { - for key := range s { - s.Delete(key) - return key, true - } - var zeroValue int64 - return zeroValue, false + return cast(s).PopAny() } // Len returns the size of the set. func (s Int64) Len() int { return len(s) } - -func lessInt64(lhs, rhs int64) bool { - return lhs < rhs -} diff --git a/pkg/util/sets/ordered.go b/pkg/util/sets/ordered.go new file mode 100644 index 000000000..443dac62e --- /dev/null +++ b/pkg/util/sets/ordered.go @@ -0,0 +1,53 @@ +/* +Copyright 2022 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 sets + +// ordered is a constraint that permits any ordered type: any type +// that supports the operators < <= >= >. +// If future releases of Go add new ordered types, +// this constraint will be modified to include them. +type ordered interface { + integer | float | ~string +} + +// integer is a constraint that permits any integer type. +// If future releases of Go add new predeclared integer types, +// this constraint will be modified to include them. +type integer interface { + signed | unsigned +} + +// float is a constraint that permits any floating-point type. +// If future releases of Go add new predeclared floating-point types, +// this constraint will be modified to include them. +type float interface { + ~float32 | ~float64 +} + +// signed is a constraint that permits any signed integer type. +// If future releases of Go add new predeclared signed integer types, +// this constraint will be modified to include them. +type signed interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 +} + +// unsigned is a constraint that permits any unsigned integer type. +// If future releases of Go add new predeclared unsigned integer types, +// this constraint will be modified to include them. +type unsigned interface { + ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr +} diff --git a/pkg/util/sets/set.go b/pkg/util/sets/set.go new file mode 100644 index 000000000..99c292fed --- /dev/null +++ b/pkg/util/sets/set.go @@ -0,0 +1,227 @@ +/* +Copyright 2022 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 sets + +import ( + "sort" +) + +// Set is a set of the same type elements, implemented via map[comparable]struct{} for minimal memory consumption. +type Set[T comparable] map[T]Empty + +// cast transforms specified set to generic Set[T]. +func cast[T comparable](s map[T]Empty) Set[T] { return s } + +// New creates a Set from a list of values. +// NOTE: type param must be explicitly instantiated if given items are empty. +func New[T comparable](items ...T) Set[T] { + ss := make(Set[T], len(items)) + ss.Insert(items...) + return ss +} + +// KeySet creates a Set from a keys of a map[comparable](? extends interface{}). +// If the value passed in is not actually a map, this will panic. +func KeySet[T comparable, V any](theMap map[T]V) Set[T] { + ret := Set[T]{} + for keyValue := range theMap { + ret.Insert(keyValue) + } + return ret +} + +// Insert adds items to the set. +func (s Set[T]) Insert(items ...T) Set[T] { + for _, item := range items { + s[item] = Empty{} + } + return s +} + +func Insert[T comparable](set Set[T], items ...T) Set[T] { + return set.Insert(items...) +} + +// Delete removes all items from the set. +func (s Set[T]) Delete(items ...T) Set[T] { + for _, item := range items { + delete(s, item) + } + return s +} + +// Has returns true if and only if item is contained in the set. +func (s Set[T]) Has(item T) bool { + _, contained := s[item] + return contained +} + +// HasAll returns true if and only if all items are contained in the set. +func (s Set[T]) HasAll(items ...T) bool { + for _, item := range items { + if !s.Has(item) { + return false + } + } + return true +} + +// HasAny returns true if any items are contained in the set. +func (s Set[T]) HasAny(items ...T) bool { + for _, item := range items { + if s.Has(item) { + return true + } + } + return false +} + +// Clone returns a new set which is a copy of the current set. +func (s Set[T]) Clone() Set[T] { + result := make(Set[T], len(s)) + for key := range s { + result.Insert(key) + } + return result +} + +// Difference returns a set of objects that are not in s2. +// For example: +// s1 = {a1, a2, a3} +// s2 = {a1, a2, a4, a5} +// s1.Difference(s2) = {a3} +// s2.Difference(s1) = {a4, a5} +func (s1 Set[T]) Difference(s2 Set[T]) Set[T] { + result := New[T]() + for key := range s1 { + if !s2.Has(key) { + result.Insert(key) + } + } + return result +} + +// SymmetricDifference returns a set of elements which are in either of the sets, but not in their intersection. +// For example: +// s1 = {a1, a2, a3} +// s2 = {a1, a2, a4, a5} +// s1.SymmetricDifference(s2) = {a3, a4, a5} +// s2.SymmetricDifference(s1) = {a3, a4, a5} +func (s1 Set[T]) SymmetricDifference(s2 Set[T]) Set[T] { + return s1.Difference(s2).Union(s2.Difference(s1)) +} + +// Union returns a new set which includes items in either s1 or s2. +// For example: +// s1 = {a1, a2} +// s2 = {a3, a4} +// s1.Union(s2) = {a1, a2, a3, a4} +// s2.Union(s1) = {a1, a2, a3, a4} +func (s1 Set[T]) Union(s2 Set[T]) Set[T] { + result := s1.Clone() + for key := range s2 { + result.Insert(key) + } + return result +} + +// Intersection returns a new set which includes the item in BOTH s1 and s2 +// For example: +// s1 = {a1, a2} +// s2 = {a2, a3} +// s1.Intersection(s2) = {a2} +func (s1 Set[T]) Intersection(s2 Set[T]) Set[T] { + var walk, other Set[T] + result := New[T]() + if s1.Len() < s2.Len() { + walk = s1 + other = s2 + } else { + walk = s2 + other = s1 + } + for key := range walk { + if other.Has(key) { + result.Insert(key) + } + } + return result +} + +// IsSuperset returns true if and only if s1 is a superset of s2. +func (s1 Set[T]) IsSuperset(s2 Set[T]) bool { + for item := range s2 { + if !s1.Has(item) { + return false + } + } + return true +} + +// Equal returns true if and only if s1 is equal (as a set) to s2. +// Two sets are equal if their membership is identical. +// (In practice, this means same elements, order doesn't matter) +func (s1 Set[T]) Equal(s2 Set[T]) bool { + return len(s1) == len(s2) && s1.IsSuperset(s2) +} + +type sortableSliceOfGeneric[T ordered] []T + +func (g sortableSliceOfGeneric[T]) Len() int { return len(g) } +func (g sortableSliceOfGeneric[T]) Less(i, j int) bool { return less[T](g[i], g[j]) } +func (g sortableSliceOfGeneric[T]) Swap(i, j int) { g[i], g[j] = g[j], g[i] } + +// List returns the contents as a sorted T slice. +// +// This is a separate function and not a method because not all types supported +// by Generic are ordered and only those can be sorted. +func List[T ordered](s Set[T]) []T { + res := make(sortableSliceOfGeneric[T], 0, len(s)) + for key := range s { + res = append(res, key) + } + sort.Sort(res) + return res +} + +// UnsortedList returns the slice with contents in random order. +func (s Set[T]) UnsortedList() []T { + res := make([]T, 0, len(s)) + for key := range s { + res = append(res, key) + } + return res +} + +// PopAny returns a single element from the set. +func (s Set[T]) PopAny() (T, bool) { + for key := range s { + s.Delete(key) + return key, true + } + var zeroValue T + return zeroValue, false +} + +// Len returns the size of the set. +func (s Set[T]) Len() int { + return len(s) +} + +func less[T ordered](lhs, rhs T) bool { + return lhs < rhs +} diff --git a/pkg/util/sets/set_generic_test.go b/pkg/util/sets/set_generic_test.go new file mode 100644 index 000000000..a86cbb174 --- /dev/null +++ b/pkg/util/sets/set_generic_test.go @@ -0,0 +1,307 @@ +/* +Copyright 2022 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 sets_test + +import ( + "reflect" + "testing" + + "k8s.io/apimachinery/pkg/util/sets" +) + +func TestSet(t *testing.T) { + s := sets.Set[string]{} + s2 := sets.Set[string]{} + if len(s) != 0 { + t.Errorf("Expected len=0: %d", len(s)) + } + s.Insert("a", "b") + if len(s) != 2 { + t.Errorf("Expected len=2: %d", len(s)) + } + s.Insert("c") + if s.Has("d") { + t.Errorf("Unexpected contents: %#v", s) + } + if !s.Has("a") { + t.Errorf("Missing contents: %#v", s) + } + s.Delete("a") + if s.Has("a") { + t.Errorf("Unexpected contents: %#v", s) + } + s.Insert("a") + if s.HasAll("a", "b", "d") { + t.Errorf("Unexpected contents: %#v", s) + } + if !s.HasAll("a", "b") { + t.Errorf("Missing contents: %#v", s) + } + s2.Insert("a", "b", "d") + if s.IsSuperset(s2) { + t.Errorf("Unexpected contents: %#v", s) + } + s2.Delete("d") + if !s.IsSuperset(s2) { + t.Errorf("Missing contents: %#v", s) + } +} + +func TestSetDeleteMultiples(t *testing.T) { + s := sets.Set[string]{} + s.Insert("a", "b", "c") + if len(s) != 3 { + t.Errorf("Expected len=3: %d", len(s)) + } + + s.Delete("a", "c") + if len(s) != 1 { + t.Errorf("Expected len=1: %d", len(s)) + } + if s.Has("a") { + t.Errorf("Unexpected contents: %#v", s) + } + if s.Has("c") { + t.Errorf("Unexpected contents: %#v", s) + } + if !s.Has("b") { + t.Errorf("Missing contents: %#v", s) + } + +} + +func TestNewSet(t *testing.T) { + s := sets.New("a", "b", "c") + if len(s) != 3 { + t.Errorf("Expected len=3: %d", len(s)) + } + if !s.Has("a") || !s.Has("b") || !s.Has("c") { + t.Errorf("Unexpected contents: %#v", s) + } +} + +func TestKeySet(t *testing.T) { + m := map[string]int{"a": 1, "b": 2, "c": 3} + ss := sets.KeySet[string](m) + if !ss.Equal(sets.New("a", "b", "c")) { + t.Errorf("Unexpected contents: %#v", sets.List(ss)) + } +} + +func TestNewEmptySet(t *testing.T) { + s := sets.New[string]() + if len(s) != 0 { + t.Errorf("Expected len=0: %d", len(s)) + } + s.Insert("a", "b", "c") + if len(s) != 3 { + t.Errorf("Expected len=3: %d", len(s)) + } + if !s.Has("a") || !s.Has("b") || !s.Has("c") { + t.Errorf("Unexpected contents: %#v", s) + } +} + +func TestSortedList(t *testing.T) { + s := sets.New("z", "y", "x", "a") + if !reflect.DeepEqual(sets.List(s), []string{"a", "x", "y", "z"}) { + t.Errorf("List gave unexpected result: %#v", sets.List(s)) + } +} + +func TestSetDifference(t *testing.T) { + a := sets.New("1", "2", "3") + b := sets.New("1", "2", "4", "5") + c := a.Difference(b) + d := b.Difference(a) + if len(c) != 1 { + t.Errorf("Expected len=1: %d", len(c)) + } + if !c.Has("3") { + t.Errorf("Unexpected contents: %#v", sets.List(c)) + } + if len(d) != 2 { + t.Errorf("Expected len=2: %d", len(d)) + } + if !d.Has("4") || !d.Has("5") { + t.Errorf("Unexpected contents: %#v", sets.List(d)) + } +} + +func TestSetSymmetricDifference(t *testing.T) { + a := sets.New("1", "2", "3") + b := sets.New("1", "2", "4", "5") + c := a.SymmetricDifference(b) + d := b.SymmetricDifference(a) + if !c.Equal(sets.New("3", "4", "5")) { + t.Errorf("Unexpected contents: %#v", sets.List(c)) + } + if !d.Equal(sets.New("3", "4", "5")) { + t.Errorf("Unexpected contents: %#v", sets.List(d)) + } +} + +func TestSetHasAny(t *testing.T) { + a := sets.New("1", "2", "3") + + if !a.HasAny("1", "4") { + t.Errorf("expected true, got false") + } + + if a.HasAny("0", "4") { + t.Errorf("expected false, got true") + } +} + +func TestSetEquals(t *testing.T) { + // Simple case (order doesn't matter) + a := sets.New("1", "2") + b := sets.New("2", "1") + if !a.Equal(b) { + t.Errorf("Expected to be equal: %v vs %v", a, b) + } + + // It is a set; duplicates are ignored + b = sets.New("2", "2", "1") + if !a.Equal(b) { + t.Errorf("Expected to be equal: %v vs %v", a, b) + } + + // Edge cases around empty sets / empty strings + a = sets.New[string]() + b = sets.New[string]() + if !a.Equal(b) { + t.Errorf("Expected to be equal: %v vs %v", a, b) + } + + b = sets.New("1", "2", "3") + if a.Equal(b) { + t.Errorf("Expected to be not-equal: %v vs %v", a, b) + } + + b = sets.New("1", "2", "") + if a.Equal(b) { + t.Errorf("Expected to be not-equal: %v vs %v", a, b) + } + + // Check for equality after mutation + a = sets.New[string]() + a.Insert("1") + if a.Equal(b) { + t.Errorf("Expected to be not-equal: %v vs %v", a, b) + } + + a.Insert("2") + if a.Equal(b) { + t.Errorf("Expected to be not-equal: %v vs %v", a, b) + } + + a.Insert("") + if !a.Equal(b) { + t.Errorf("Expected to be equal: %v vs %v", a, b) + } + + a.Delete("") + if a.Equal(b) { + t.Errorf("Expected to be not-equal: %v vs %v", a, b) + } +} + +func TestUnion(t *testing.T) { + tests := []struct { + s1 sets.Set[string] + s2 sets.Set[string] + expected sets.Set[string] + }{ + { + sets.New("1", "2", "3", "4"), + sets.New("3", "4", "5", "6"), + sets.New("1", "2", "3", "4", "5", "6"), + }, + { + sets.New("1", "2", "3", "4"), + sets.New[string](), + sets.New("1", "2", "3", "4"), + }, + { + sets.New[string](), + sets.New("1", "2", "3", "4"), + sets.New("1", "2", "3", "4"), + }, + { + sets.New[string](), + sets.New[string](), + sets.New[string](), + }, + } + + for _, test := range tests { + union := test.s1.Union(test.s2) + if union.Len() != test.expected.Len() { + t.Errorf("Expected union.Len()=%d but got %d", test.expected.Len(), union.Len()) + } + + if !union.Equal(test.expected) { + t.Errorf("Expected union.Equal(expected) but not true. union:%v expected:%v", sets.List(union), sets.List(test.expected)) + } + } +} + +func TestIntersection(t *testing.T) { + tests := []struct { + s1 sets.Set[string] + s2 sets.Set[string] + expected sets.Set[string] + }{ + { + sets.New("1", "2", "3", "4"), + sets.New("3", "4", "5", "6"), + sets.New("3", "4"), + }, + { + sets.New("1", "2", "3", "4"), + sets.New("1", "2", "3", "4"), + sets.New("1", "2", "3", "4"), + }, + { + sets.New("1", "2", "3", "4"), + sets.New[string](), + sets.New[string](), + }, + { + sets.New[string](), + sets.New("1", "2", "3", "4"), + sets.New[string](), + }, + { + sets.New[string](), + sets.New[string](), + sets.New[string](), + }, + } + + for _, test := range tests { + intersection := test.s1.Intersection(test.s2) + if intersection.Len() != test.expected.Len() { + t.Errorf("Expected intersection.Len()=%d but got %d", test.expected.Len(), intersection.Len()) + } + + if !intersection.Equal(test.expected) { + t.Errorf("Expected intersection.Equal(expected) but not true. intersection:%v expected:%v", sets.List(intersection), sets.List(intersection)) + } + } +} diff --git a/pkg/util/sets/set_test.go b/pkg/util/sets/set_test.go index df722ec27..9f492c1f4 100644 --- a/pkg/util/sets/set_test.go +++ b/pkg/util/sets/set_test.go @@ -17,6 +17,8 @@ limitations under the License. package sets import ( + "fmt" + "math/rand" "reflect" "testing" ) @@ -118,6 +120,19 @@ func TestStringSetDifference(t *testing.T) { } } +func TestStringSetSymmetricDifference(t *testing.T) { + a := NewString("1", "2", "3") + b := NewString("1", "2", "4", "5") + c := a.SymmetricDifference(b) + d := b.SymmetricDifference(a) + if !c.Equal(NewString("3", "4", "5")) { + t.Errorf("Unexpected contents: %#v", c.List()) + } + if !d.Equal(NewString("3", "4", "5")) { + t.Errorf("Unexpected contents: %#v", d.List()) + } +} + func TestStringSetHasAny(t *testing.T) { a := NewString("1", "2", "3") @@ -268,3 +283,91 @@ func TestStringIntersection(t *testing.T) { } } } + +type randomStringAlphabet string + +func (a randomStringAlphabet) makeString(minLen, maxLen int) string { + n := minLen + if minLen < maxLen { + n += rand.Intn(maxLen - minLen) + } + var s string + for i := 0; i < n; i++ { + s += string(a[rand.Intn(len(a))]) + } + return s +} + +var randomStringMaker = randomStringAlphabet("abcdefghijklmnopqrstuvwxyz0123456789") + +func BenchmarkStringSet(b *testing.B) { + cases := []struct { + size int + minStringLen int + maxStringLen int + }{ + {20, 10, 20}, + {50, 10, 30}, + {100, 20, 40}, + {500, 20, 50}, + {1000, 20, 60}, + } + + for i := range cases { + here := cases[i] + makeSet := func() String { + s := NewString() + for j := 0; j < here.size; j++ { + s.Insert(randomStringMaker.makeString(here.minStringLen, here.maxStringLen)) + } + return s + } + operands := make([]String, 500) + for i := range operands { + operands[i] = makeSet() + } + randOperand := func() String { return operands[rand.Intn(len(operands))] } + + b.Run(fmt.Sprintf("insert-%v", here.size), func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + makeSet() + } + }) + + b.Run(fmt.Sprintf("key-set-%v", here.size), func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + StringKeySet(randOperand()) + } + }) + + b.Run(fmt.Sprintf("has-%v", here.size), func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + randOperand().Has(randomStringMaker.makeString(here.minStringLen, here.maxStringLen)) + } + }) + + b.Run(fmt.Sprintf("intersection-%v", here.size), func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + randOperand().Intersection(randOperand()) + } + }) + + b.Run(fmt.Sprintf("symmetric-difference-%v", here.size), func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + randOperand().SymmetricDifference(randOperand()) + } + }) + + b.Run(fmt.Sprintf("list-%v", here.size), func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + randOperand().List() + } + }) + } +} diff --git a/pkg/util/sets/string.go b/pkg/util/sets/string.go index e6f37db88..1dab6d13c 100644 --- a/pkg/util/sets/string.go +++ b/pkg/util/sets/string.go @@ -1,5 +1,5 @@ /* -Copyright The Kubernetes Authors. +Copyright 2022 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. @@ -14,93 +14,75 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Code generated by set-gen. DO NOT EDIT. - package sets -import ( - "reflect" - "sort" -) - -// sets.String is a set of strings, implemented via map[string]struct{} for minimal memory consumption. +// String is a set of strings, implemented via map[string]struct{} for minimal memory consumption. +// +// Deprecated: use generic Set instead. +// new ways: +// s1 := Set[string]{} +// s2 := New[string]() type String map[string]Empty // NewString creates a String from a list of values. func NewString(items ...string) String { - ss := String{} - ss.Insert(items...) - return ss + return String(New[string](items...)) } // StringKeySet creates a String from a keys of a map[string](? extends interface{}). // If the value passed in is not actually a map, this will panic. -func StringKeySet(theMap interface{}) String { - v := reflect.ValueOf(theMap) - ret := String{} - - for _, keyValue := range v.MapKeys() { - ret.Insert(keyValue.Interface().(string)) - } - return ret +func StringKeySet[T any](theMap map[string]T) String { + return String(KeySet(theMap)) } // Insert adds items to the set. func (s String) Insert(items ...string) String { - for _, item := range items { - s[item] = Empty{} - } - return s + return String(cast(s).Insert(items...)) } // Delete removes all items from the set. func (s String) Delete(items ...string) String { - for _, item := range items { - delete(s, item) - } - return s + return String(cast(s).Delete(items...)) } // Has returns true if and only if item is contained in the set. func (s String) Has(item string) bool { - _, contained := s[item] - return contained + return cast(s).Has(item) } // HasAll returns true if and only if all items are contained in the set. func (s String) HasAll(items ...string) bool { - for _, item := range items { - if !s.Has(item) { - return false - } - } - return true + return cast(s).HasAll(items...) } // HasAny returns true if any items are contained in the set. func (s String) HasAny(items ...string) bool { - for _, item := range items { - if s.Has(item) { - return true - } - } - return false + return cast(s).HasAny(items...) } -// Difference returns a set of objects that are not in s2 +// Clone returns a new set which is a copy of the current set. +func (s String) Clone() String { + return String(cast(s).Clone()) +} + +// Difference returns a set of objects that are not in s2. // For example: // s1 = {a1, a2, a3} // s2 = {a1, a2, a4, a5} // s1.Difference(s2) = {a3} // s2.Difference(s1) = {a4, a5} -func (s String) Difference(s2 String) String { - result := NewString() - for key := range s { - if !s2.Has(key) { - result.Insert(key) - } - } - return result +func (s1 String) Difference(s2 String) String { + return String(cast(s1).Difference(cast(s2))) +} + +// SymmetricDifference returns a set of elements which are in either of the sets, but not in their intersection. +// For example: +// s1 = {a1, a2, a3} +// s2 = {a1, a2, a4, a5} +// s1.SymmetricDifference(s2) = {a3, a4, a5} +// s2.SymmetricDifference(s1) = {a3, a4, a5} +func (s1 String) SymmetricDifference(s2 String) String { + return String(cast(s1).SymmetricDifference(cast(s2))) } // Union returns a new set which includes items in either s1 or s2. @@ -110,14 +92,7 @@ func (s String) Difference(s2 String) String { // s1.Union(s2) = {a1, a2, a3, a4} // s2.Union(s1) = {a1, a2, a3, a4} func (s1 String) Union(s2 String) String { - result := NewString() - for key := range s1 { - result.Insert(key) - } - for key := range s2 { - result.Insert(key) - } - return result + return String(cast(s1).Union(cast(s2))) } // Intersection returns a new set which includes the item in BOTH s1 and s2 @@ -126,80 +101,37 @@ func (s1 String) Union(s2 String) String { // s2 = {a2, a3} // s1.Intersection(s2) = {a2} func (s1 String) Intersection(s2 String) String { - var walk, other String - result := NewString() - if s1.Len() < s2.Len() { - walk = s1 - other = s2 - } else { - walk = s2 - other = s1 - } - for key := range walk { - if other.Has(key) { - result.Insert(key) - } - } - return result + return String(cast(s1).Intersection(cast(s2))) } // IsSuperset returns true if and only if s1 is a superset of s2. func (s1 String) IsSuperset(s2 String) bool { - for item := range s2 { - if !s1.Has(item) { - return false - } - } - return true + return cast(s1).IsSuperset(cast(s2)) } // Equal returns true if and only if s1 is equal (as a set) to s2. // Two sets are equal if their membership is identical. // (In practice, this means same elements, order doesn't matter) func (s1 String) Equal(s2 String) bool { - return len(s1) == len(s2) && s1.IsSuperset(s2) + return cast(s1).Equal(cast(s2)) } -type sortableSliceOfString []string - -func (s sortableSliceOfString) Len() int { return len(s) } -func (s sortableSliceOfString) Less(i, j int) bool { return lessString(s[i], s[j]) } -func (s sortableSliceOfString) Swap(i, j int) { s[i], s[j] = s[j], s[i] } - // List returns the contents as a sorted string slice. func (s String) List() []string { - res := make(sortableSliceOfString, 0, len(s)) - for key := range s { - res = append(res, key) - } - sort.Sort(res) - return []string(res) + return List(cast(s)) } // UnsortedList returns the slice with contents in random order. func (s String) UnsortedList() []string { - res := make([]string, 0, len(s)) - for key := range s { - res = append(res, key) - } - return res + return cast(s).UnsortedList() } -// Returns a single element from the set. +// PopAny returns a single element from the set. func (s String) PopAny() (string, bool) { - for key := range s { - s.Delete(key) - return key, true - } - var zeroValue string - return zeroValue, false + return cast(s).PopAny() } // Len returns the size of the set. func (s String) Len() int { return len(s) } - -func lessString(lhs, rhs string) bool { - return lhs < rhs -} diff --git a/pkg/util/sets/types/types.go b/pkg/util/sets/types/types.go deleted file mode 100644 index 2218dbc46..000000000 --- a/pkg/util/sets/types/types.go +++ /dev/null @@ -1,34 +0,0 @@ -/* -Copyright 2015 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 types just provides input types to the set generator. It also -// contains a "go generate" block. -// (You must first `go install k8s.io/code-generator/cmd/set-gen`) -package types - -//go:generate set-gen -i k8s.io/kubernetes/pkg/util/sets/types -//lint:file-ignore U1000 Ignore unused fields - -type ReferenceSetTypes struct { - // These types all cause files to be generated. - // These types should be reflected in the output of - // the "//pkg/util/sets:set-gen" genrule. - a int64 - b int - c byte - d string - e int32 -} diff --git a/pkg/util/strategicpatch/OWNERS b/pkg/util/strategicpatch/OWNERS index cfee199fa..4443bafd1 100644 --- a/pkg/util/strategicpatch/OWNERS +++ b/pkg/util/strategicpatch/OWNERS @@ -1,8 +1,8 @@ # See the OWNERS docs at https://go.k8s.io/owners approvers: -- pwittrock -- mengqiy + - pwittrock reviewers: -- mengqiy -- apelisse + - apelisse +emeritus_approvers: + - mengqiy diff --git a/pkg/util/strategicpatch/meta.go b/pkg/util/strategicpatch/meta.go index c31de15e7..df305b712 100644 --- a/pkg/util/strategicpatch/meta.go +++ b/pkg/util/strategicpatch/meta.go @@ -31,22 +31,22 @@ type PatchMeta struct { patchMergeKey string } -func (pm PatchMeta) GetPatchStrategies() []string { +func (pm *PatchMeta) GetPatchStrategies() []string { if pm.patchStrategies == nil { return []string{} } return pm.patchStrategies } -func (pm PatchMeta) SetPatchStrategies(ps []string) { +func (pm *PatchMeta) SetPatchStrategies(ps []string) { pm.patchStrategies = ps } -func (pm PatchMeta) GetPatchMergeKey() string { +func (pm *PatchMeta) GetPatchMergeKey() string { return pm.patchMergeKey } -func (pm PatchMeta) SetPatchMergeKey(pmk string) { +func (pm *PatchMeta) SetPatchMergeKey(pmk string) { pm.patchMergeKey = pmk } @@ -105,7 +105,7 @@ func (s PatchMetaFromStruct) LookupPatchMetadataForSlice(key string) (LookupPatc // If the underlying element is neither an array nor a slice, the pointer is pointing to a slice, // e.g. https://github.com/kubernetes/kubernetes/blob/bc22e206c79282487ea0bf5696d5ccec7e839a76/staging/src/k8s.io/apimachinery/pkg/util/strategicpatch/patch_test.go#L2782-L2822 // If the underlying element is either an array or a slice, return its element type. - case reflect.Ptr: + case reflect.Pointer: t = t.Elem() if t.Kind() == reflect.Array || t.Kind() == reflect.Slice { t = t.Elem() @@ -129,7 +129,7 @@ func getTagStructType(dataStruct interface{}) (reflect.Type, error) { t := reflect.TypeOf(dataStruct) // Get the underlying type for pointers - if t.Kind() == reflect.Ptr { + if t.Kind() == reflect.Pointer { t = t.Elem() } diff --git a/pkg/util/strategicpatch/patch.go b/pkg/util/strategicpatch/patch.go index fd2081a28..6fb369732 100644 --- a/pkg/util/strategicpatch/patch.go +++ b/pkg/util/strategicpatch/patch.go @@ -1328,15 +1328,25 @@ func mergeMap(original, patch map[string]interface{}, schema LookupPatchMeta, me _, ok := original[k] if !ok { - // If it's not in the original document, just take the patch value. - original[k] = patchV + if !isDeleteList { + // If it's not in the original document, just take the patch value. + if mergeOptions.IgnoreUnmatchedNulls { + discardNullValuesFromPatch(patchV) + } + original[k] = patchV + } continue } originalType := reflect.TypeOf(original[k]) patchType := reflect.TypeOf(patchV) if originalType != patchType { - original[k] = patchV + if !isDeleteList { + if mergeOptions.IgnoreUnmatchedNulls { + discardNullValuesFromPatch(patchV) + } + original[k] = patchV + } continue } // If they're both maps or lists, recurse into the value. @@ -1371,6 +1381,25 @@ func mergeMap(original, patch map[string]interface{}, schema LookupPatchMeta, me return original, nil } +// discardNullValuesFromPatch discards all null property values from patch. +// It traverses all slices and map types. +func discardNullValuesFromPatch(patchV interface{}) { + switch patchV := patchV.(type) { + case map[string]interface{}: + for k, v := range patchV { + if v == nil { + delete(patchV, k) + } else { + discardNullValuesFromPatch(v) + } + } + case []interface{}: + for _, v := range patchV { + discardNullValuesFromPatch(v) + } + } +} + // mergeMapHandler handles how to merge `patchV` whose key is `key` with `original` respecting // fieldPatchStrategy and mergeOptions. func mergeMapHandler(original, patch interface{}, schema LookupPatchMeta, diff --git a/pkg/util/strategicpatch/patch_test.go b/pkg/util/strategicpatch/patch_test.go index 06814273d..8ddcddc5b 100644 --- a/pkg/util/strategicpatch/patch_test.go +++ b/pkg/util/strategicpatch/patch_test.go @@ -669,6 +669,57 @@ mergingList: ExpectedError: "does not contain declared merge key", }, }, + { + Description: "$deleteFromPrimitiveList of nonexistent item in primitive list should not add the item to the list", + StrategicMergePatchRawTestCaseData: StrategicMergePatchRawTestCaseData{ + Original: []byte(` +mergingIntList: + - 1 + - 2 +`), + TwoWay: []byte(` +$deleteFromPrimitiveList/mergingIntList: + - 3 +`), + Modified: []byte(` +mergingIntList: + - 1 + - 2 +`), + }, + }, + { + Description: "$deleteFromPrimitiveList on empty primitive list should not add the item to the list", + StrategicMergePatchRawTestCaseData: StrategicMergePatchRawTestCaseData{ + Original: []byte(` +mergingIntList: +`), + TwoWay: []byte(` +$deleteFromPrimitiveList/mergingIntList: + - 3 +`), + Modified: []byte(` +mergingIntList: +`), + }, + }, + { + Description: "$deleteFromPrimitiveList on nonexistent primitive list should not add the primitive list", + StrategicMergePatchRawTestCaseData: StrategicMergePatchRawTestCaseData{ + Original: []byte(` +foo: + - bar +`), + TwoWay: []byte(` +$deleteFromPrimitiveList/mergingIntList: + - 3 +`), + Modified: []byte(` +foo: + - bar +`), + }, + }, } func TestCustomStrategicMergePatch(t *testing.T) { @@ -704,7 +755,6 @@ func TestCustomStrategicMergePatch(t *testing.T) { // yields the correct outcome. They are also test cases for CreateTwoWayMergePatch // and CreateThreeWayMergePatch, to assert that they both generate the correct patch // for the given set of input documents. -// var createStrategicMergePatchTestCaseData = []byte(` testCases: - description: nil original @@ -6662,6 +6712,106 @@ func TestUnknownField(t *testing.T) { ExpectedThreeWay: `{}`, ExpectedThreeWayResult: `{"array":[1,2,3],"complex":{"nested":true},"name":"foo","scalar":true}`, }, + "no diff even if modified null": { + Original: `{"array":[1,2,3],"complex":{"nested":true},"name":"foo","scalar":true}`, + Current: `{"array":[1,2,3],"complex":{"nested":true},"name":"foo","scalar":true}`, + Modified: `{"array":[1,2,3],"complex":{"nested":true},"complex_nullable":{"key":null},"name":"foo","scalar":true}`, + + ExpectedTwoWay: `{"complex_nullable":{"key":null}}`, + ExpectedTwoWayResult: `{"array":[1,2,3],"complex":{"nested":true},"complex_nullable":{},"name":"foo","scalar":true}`, + ExpectedThreeWay: `{"complex_nullable":{"key":null}}`, + ExpectedThreeWayResult: `{"array":[1,2,3],"complex":{"nested":true},"complex_nullable":{},"name":"foo","scalar":true}`, + }, + "discard nulls in nested and adds not nulls": { + Original: `{"array":[1,2,3],"complex":{"nested":true},"name":"foo","scalar":true}`, + Current: `{"array":[1,2,3],"complex":{"nested":true},"name":"foo","scalar":true}`, + Modified: `{"array":[1,2,3],"complex":{"nested":true},"complex_nullable":{"key":{"keynotnull":"value","keynull":null}},"name":"foo","scalar":true}`, + + ExpectedTwoWay: `{"complex_nullable":{"key":{"keynotnull":"value","keynull":null}}}`, + ExpectedTwoWayResult: `{"array":[1,2,3],"complex":{"nested":true},"complex_nullable":{"key":{"keynotnull":"value"}},"name":"foo","scalar":true}`, + ExpectedThreeWay: `{"complex_nullable":{"key":{"keynotnull":"value","keynull":null}}}`, + ExpectedThreeWayResult: `{"array":[1,2,3],"complex":{"nested":true},"complex_nullable":{"key":{"keynotnull":"value"}},"name":"foo","scalar":true}`, + }, + "discard if modified all nulls": { + Original: `{}`, + Current: `{}`, + Modified: `{"complex":{"nested":null}}`, + + ExpectedTwoWay: `{"complex":{"nested":null}}`, + ExpectedTwoWayResult: `{"complex":{}}`, + ExpectedThreeWay: `{"complex":{"nested":null}}`, + ExpectedThreeWayResult: `{"complex":{}}`, + }, + "add only not nulls": { + Original: `{}`, + Current: `{}`, + Modified: `{"complex":{"nested":null,"nested2":"foo"}}`, + + ExpectedTwoWay: `{"complex":{"nested":null,"nested2":"foo"}}`, + ExpectedTwoWayResult: `{"complex":{"nested2":"foo"}}`, + ExpectedThreeWay: `{"complex":{"nested":null,"nested2":"foo"}}`, + ExpectedThreeWayResult: `{"complex":{"nested2":"foo"}}`, + }, + "null values in original are preserved": { + Original: `{"thing":null}`, + Current: `{"thing":null}`, + Modified: `{"nested":{"value":5},"thing":null}`, + + ExpectedTwoWay: `{"nested":{"value":5}}`, + ExpectedTwoWayResult: `{"nested":{"value":5},"thing":null}`, + ExpectedThreeWay: `{"nested":{"value":5}}`, + ExpectedThreeWayResult: `{"nested":{"value":5},"thing":null}`, + }, + "nested null values in original are preserved": { + Original: `{"complex":{"key":null},"thing":null}`, + Current: `{"complex":{"key":null},"thing":null}`, + Modified: `{"complex":{"key":null},"nested":{"value":5},"thing":null}`, + + ExpectedTwoWay: `{"nested":{"value":5}}`, + ExpectedTwoWayResult: `{"complex":{"key":null},"nested":{"value":5},"thing":null}`, + ExpectedThreeWay: `{"nested":{"value":5}}`, + ExpectedThreeWayResult: `{"complex":{"key":null},"nested":{"value":5},"thing":null}`, + }, + "add empty slices": { + Original: `{"array":[1,2,3],"complex":{"nested":true},"name":"foo","scalar":true}`, + Current: `{"array":[1,2,3],"complex":{"nested":true},"name":"foo","scalar":true}`, + Modified: `{"array":[1,2,3],"complex":{"nested":true},"complex_nullable":[],"name":"foo","scalar":true}`, + + ExpectedTwoWay: `{"complex_nullable":[]}`, + ExpectedTwoWayResult: `{"array":[1,2,3],"complex":{"nested":true},"complex_nullable":[],"name":"foo","scalar":true}`, + ExpectedThreeWay: `{"complex_nullable":[]}`, + ExpectedThreeWayResult: `{"array":[1,2,3],"complex":{"nested":true},"complex_nullable":[],"name":"foo","scalar":true}`, + }, + "filter nulls from nested slices": { + Original: `{}`, + Current: `{}`, + Modified: `{"complex_nullable":[{"inner_one":{"key_one":"foo","key_two":null}}]}`, + + ExpectedTwoWay: `{"complex_nullable":[{"inner_one":{"key_one":"foo","key_two":null}}]}`, + ExpectedTwoWayResult: `{"complex_nullable":[{"inner_one":{"key_one":"foo"}}]}`, + ExpectedThreeWay: `{"complex_nullable":[{"inner_one":{"key_one":"foo","key_two":null}}]}`, + ExpectedThreeWayResult: `{"complex_nullable":[{"inner_one":{"key_one":"foo"}}]}`, + }, + "filter if slice is all empty": { + Original: `{}`, + Current: `{}`, + Modified: `{"complex_nullable":[{"inner_one":{"key_one":null,"key_two":null}}]}`, + + ExpectedTwoWay: `{"complex_nullable":[{"inner_one":{"key_one":null,"key_two":null}}]}`, + ExpectedTwoWayResult: `{"complex_nullable":[{"inner_one":{}}]}`, + ExpectedThreeWay: `{"complex_nullable":[{"inner_one":{"key_one":null,"key_two":null}}]}`, + ExpectedThreeWayResult: `{"complex_nullable":[{"inner_one":{}}]}`, + }, + "not filter nulls from non-associative slice": { + Original: `{}`, + Current: `{}`, + Modified: `{"complex_nullable":["key1",null,"key2"]}`, + + ExpectedTwoWay: `{"complex_nullable":["key1",null,"key2"]}`, + ExpectedTwoWayResult: `{"complex_nullable":["key1",null,"key2"]}`, + ExpectedThreeWay: `{"complex_nullable":["key1",null,"key2"]}`, + ExpectedThreeWayResult: `{"complex_nullable":["key1",null,"key2"]}`, + }, "added only": { Original: `{"name":"foo"}`, Current: `{"name":"foo"}`, diff --git a/pkg/util/strategicpatch/testing/openapi.go b/pkg/util/strategicpatch/testing/openapi.go index ab964557a..be7aa074b 100644 --- a/pkg/util/strategicpatch/testing/openapi.go +++ b/pkg/util/strategicpatch/testing/openapi.go @@ -21,7 +21,7 @@ import ( "os" "sync" - openapi_v2 "github.com/googleapis/gnostic/openapiv2" + openapi_v2 "github.com/google/gnostic/openapiv2" openapi "k8s.io/kube-openapi/pkg/util/proto" ) diff --git a/pkg/util/validation/field/errors.go b/pkg/util/validation/field/errors.go index c283ad189..ae73bda96 100644 --- a/pkg/util/validation/field/errors.go +++ b/pkg/util/validation/field/errors.go @@ -42,19 +42,31 @@ func (v *Error) Error() string { return fmt.Sprintf("%s: %s", v.Field, v.ErrorBody()) } +type OmitValueType struct{} + +var omitValue = OmitValueType{} + // ErrorBody returns the error message without the field name. This is useful // for building nice-looking higher-level error reporting. func (v *Error) ErrorBody() string { var s string - switch v.Type { - case ErrorTypeRequired, ErrorTypeForbidden, ErrorTypeTooLong, ErrorTypeInternal: + switch { + case v.Type == ErrorTypeRequired: + s = v.Type.String() + case v.Type == ErrorTypeForbidden: + s = v.Type.String() + case v.Type == ErrorTypeTooLong: + s = v.Type.String() + case v.Type == ErrorTypeInternal: + s = v.Type.String() + case v.BadValue == omitValue: s = v.Type.String() default: value := v.BadValue valueType := reflect.TypeOf(value) if value == nil || valueType == nil { value = "null" - } else if valueType.Kind() == reflect.Ptr { + } else if valueType.Kind() == reflect.Pointer { if reflectValue := reflect.ValueOf(value); reflectValue.IsNil() { value = "null" } else { @@ -123,6 +135,8 @@ const ( // ErrorTypeInternal is used to report other errors that are not related // to user input. See InternalError(). ErrorTypeInternal ErrorType = "InternalError" + // ErrorTypeTypeInvalid is for the value did not match the schema type for that field + ErrorTypeTypeInvalid ErrorType = "FieldValueTypeInvalid" ) // String converts a ErrorType into its corresponding canonical error message. @@ -146,11 +160,18 @@ func (t ErrorType) String() string { return "Too many" case ErrorTypeInternal: return "Internal error" + case ErrorTypeTypeInvalid: + return "Invalid value" default: panic(fmt.Sprintf("unrecognized validation error: %q", string(t))) } } +// TypeInvalid returns a *Error indicating "type is invalid" +func TypeInvalid(field *Path, value interface{}, detail string) *Error { + return &Error{ErrorTypeTypeInvalid, field.String(), value, detail} +} + // NotFound returns a *Error indicating "value not found". This is // used to report failure to find a requested value (e.g. looking up an ID). func NotFound(field *Path, value interface{}) *Error { @@ -207,11 +228,40 @@ func TooLong(field *Path, value interface{}, maxLength int) *Error { return &Error{ErrorTypeTooLong, field.String(), value, fmt.Sprintf("must have at most %d bytes", maxLength)} } +// TooLongMaxLength returns a *Error indicating "too long". This is used to +// report that the given value is too long. This is similar to +// Invalid, but the returned error will not include the too-long +// value. If maxLength is negative, no max length will be included in the message. +func TooLongMaxLength(field *Path, value interface{}, maxLength int) *Error { + var msg string + if maxLength >= 0 { + msg = fmt.Sprintf("may not be longer than %d", maxLength) + } else { + msg = "value is too long" + } + return &Error{ErrorTypeTooLong, field.String(), value, msg} +} + // TooMany returns a *Error indicating "too many". This is used to // report that a given list has too many items. This is similar to TooLong, // but the returned error indicates quantity instead of length. func TooMany(field *Path, actualQuantity, maxQuantity int) *Error { - return &Error{ErrorTypeTooMany, field.String(), actualQuantity, fmt.Sprintf("must have at most %d items", maxQuantity)} + var msg string + + if maxQuantity >= 0 { + msg = fmt.Sprintf("must have at most %d items", maxQuantity) + } else { + msg = "has too many items" + } + + var actual interface{} + if actualQuantity >= 0 { + actual = actualQuantity + } else { + actual = omitValue + } + + return &Error{ErrorTypeTooMany, field.String(), actual, msg} } // InternalError returns a *Error indicating "internal error". This is used @@ -239,6 +289,9 @@ func NewErrorTypeMatcher(t ErrorType) utilerrors.Matcher { // ToAggregate converts the ErrorList into an errors.Aggregate. func (list ErrorList) ToAggregate() utilerrors.Aggregate { + if len(list) == 0 { + return nil + } errs := make([]error, 0, len(list)) errorMsgs := sets.NewString() for _, err := range list { diff --git a/pkg/util/validation/validation.go b/pkg/util/validation/validation.go index c8b419984..e767092dd 100644 --- a/pkg/util/validation/validation.go +++ b/pkg/util/validation/validation.go @@ -25,6 +25,7 @@ import ( "strings" "k8s.io/apimachinery/pkg/util/validation/field" + netutils "k8s.io/utils/net" ) const qnameCharFmt string = "[A-Za-z0-9]" @@ -333,7 +334,7 @@ func IsValidPortName(port string) []string { errs = append(errs, "must contain only alpha-numeric characters (a-z, 0-9), and hyphens (-)") } if !portNameOneLetterRegexp.MatchString(port) { - errs = append(errs, "must contain at least one letter or number (a-z, 0-9)") + errs = append(errs, "must contain at least one letter (a-z)") } if strings.Contains(port, "--") { errs = append(errs, "must not contain consecutive hyphens") @@ -346,7 +347,7 @@ func IsValidPortName(port string) []string { // IsValidIP tests that the argument is a valid IP address. func IsValidIP(value string) []string { - if net.ParseIP(value) == nil { + if netutils.ParseIPSloppy(value) == nil { return []string{"must be a valid IP address, (e.g. 10.9.8.7 or 2001:db8::ffff)"} } return nil @@ -355,7 +356,7 @@ func IsValidIP(value string) []string { // IsValidIPv4Address tests that the argument is a valid IPv4 address. func IsValidIPv4Address(fldPath *field.Path, value string) field.ErrorList { var allErrors field.ErrorList - ip := net.ParseIP(value) + ip := netutils.ParseIPSloppy(value) if ip == nil || ip.To4() == nil { allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid IPv4 address")) } @@ -365,7 +366,7 @@ func IsValidIPv4Address(fldPath *field.Path, value string) field.ErrorList { // IsValidIPv6Address tests that the argument is a valid IPv6 address. func IsValidIPv6Address(fldPath *field.Path, value string) field.ErrorList { var allErrors field.ErrorList - ip := net.ParseIP(value) + ip := netutils.ParseIPSloppy(value) if ip == nil || ip.To4() != nil { allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid IPv6 address")) } diff --git a/pkg/util/wait/wait.go b/pkg/util/wait/wait.go index afb24876a..137627b40 100644 --- a/pkg/util/wait/wait.go +++ b/pkg/util/wait/wait.go @@ -24,14 +24,16 @@ import ( "sync" "time" - "k8s.io/apimachinery/pkg/util/clock" "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/utils/clock" ) // For any test of the style: -// ... -// <- time.After(timeout): -// t.Errorf("Timed out") +// +// ... +// <- time.After(timeout): +// t.Errorf("Timed out") +// // The value for timeout should effectively be "forever." Obviously we don't want our tests to truly lock up forever, but 30s // is long enough that it is effectively forever for the things that can slow down a run on a heavily contended machine // (GC, seeks, etc), but not so long as to make a developer ctrl-c a test run if they do happen to break that test. @@ -166,6 +168,9 @@ func BackoffUntil(f func(), backoff BackoffManager, sliding bool, stopCh <-chan // of every loop to prevent extra executions of f(). select { case <-stopCh: + if !t.Stop() { + <-t.C() + } return case <-t.C(): } @@ -285,13 +290,13 @@ func (b *Backoff) Step() time.Duration { return duration } -// contextForChannel derives a child context from a parent channel. +// ContextForChannel derives a child context from a parent channel. // // The derived context's Done channel is closed when the returned cancel function // is called or when the parent channel is closed, whichever happens first. // // Note the caller must *always* call the CancelFunc, otherwise resources may be leaked. -func contextForChannel(parentCh <-chan struct{}) (context.Context, context.CancelFunc) { +func ContextForChannel(parentCh <-chan struct{}) (context.Context, context.CancelFunc) { ctx, cancel := context.WithCancel(context.Background()) go func() { @@ -461,7 +466,7 @@ func PollWithContext(ctx context.Context, interval, timeout time.Duration, condi // PollUntil always waits interval before the first run of 'condition'. // 'condition' will always be invoked at least once. func PollUntil(interval time.Duration, condition ConditionFunc, stopCh <-chan struct{}) error { - ctx, cancel := contextForChannel(stopCh) + ctx, cancel := ContextForChannel(stopCh) defer cancel() return PollUntilWithContext(ctx, interval, condition.WithContext()) } @@ -528,7 +533,7 @@ func PollImmediateWithContext(ctx context.Context, interval, timeout time.Durati // PollImmediateUntil runs the 'condition' before waiting for the interval. // 'condition' will always be invoked at least once. func PollImmediateUntil(interval time.Duration, condition ConditionFunc, stopCh <-chan struct{}) error { - ctx, cancel := contextForChannel(stopCh) + ctx, cancel := ContextForChannel(stopCh) defer cancel() return PollImmediateUntilWithContext(ctx, interval, condition.WithContext()) } @@ -563,7 +568,7 @@ func PollImmediateInfiniteWithContext(ctx context.Context, interval time.Duratio return poll(ctx, true, poller(interval, 0), condition) } -// Internally used, each of the the public 'Poll*' function defined in this +// Internally used, each of the public 'Poll*' function defined in this // package should invoke this internal function with appropriate parameters. // ctx: the context specified by the caller, for infinite polling pass // a context that never gets cancelled or expired. @@ -612,7 +617,7 @@ type WaitWithContextFunc func(ctx context.Context) <-chan struct{} // WaitFor continually checks 'fn' as driven by 'wait'. // -// WaitFor gets a channel from 'wait()'', and then invokes 'fn' once for every value +// WaitFor gets a channel from 'wait()”, and then invokes 'fn' once for every value // placed on the channel and once more when the channel is closed. If the channel is closed // and 'fn' returns false without error, WaitFor returns ErrWaitTimeout. // @@ -626,14 +631,14 @@ type WaitWithContextFunc func(ctx context.Context) <-chan struct{} // "uniform pseudo-random", the `fn` might still run one or multiple time, // though eventually `WaitFor` will return. func WaitFor(wait WaitFunc, fn ConditionFunc, done <-chan struct{}) error { - ctx, cancel := contextForChannel(done) + ctx, cancel := ContextForChannel(done) defer cancel() return WaitForWithContext(ctx, wait.WithContext(), fn.WithContext()) } // WaitForWithContext continually checks 'fn' as driven by 'wait'. // -// WaitForWithContext gets a channel from 'wait()'', and then invokes 'fn' +// WaitForWithContext gets a channel from 'wait()”, and then invokes 'fn' // once for every value placed on the channel and once more when the // channel is closed. If the channel is closed and 'fn' // returns false without error, WaitForWithContext returns ErrWaitTimeout. diff --git a/pkg/util/wait/wait_test.go b/pkg/util/wait/wait_test.go index dbd3dfbda..f42c22083 100644 --- a/pkg/util/wait/wait_test.go +++ b/pkg/util/wait/wait_test.go @@ -26,8 +26,9 @@ import ( "testing" "time" - "k8s.io/apimachinery/pkg/util/clock" "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/utils/clock" + testingclock "k8s.io/utils/clock/testing" ) func TestUntil(t *testing.T) { @@ -691,7 +692,7 @@ func TestContextForChannel(t *testing.T) { wg.Add(1) go func() { defer wg.Done() - ctx, cancel := contextForChannel(parentCh) + ctx, cancel := ContextForChannel(parentCh) defer cancel() <-ctx.Done() }() @@ -713,7 +714,7 @@ func TestContextForChannel(t *testing.T) { } func TestExponentialBackoffManagerGetNextBackoff(t *testing.T) { - fc := clock.NewFakeClock(time.Now()) + fc := testingclock.NewFakeClock(time.Now()) backoff := NewExponentialBackoffManager(1, 10, 10, 2.0, 0.0, fc) durations := []time.Duration{1, 2, 4, 8, 10, 10, 10} for i := 0; i < len(durations); i++ { @@ -732,7 +733,7 @@ func TestExponentialBackoffManagerGetNextBackoff(t *testing.T) { func TestJitteredBackoffManagerGetNextBackoff(t *testing.T) { // positive jitter - backoffMgr := NewJitteredBackoffManager(1, 1, clock.NewFakeClock(time.Now())) + backoffMgr := NewJitteredBackoffManager(1, 1, testingclock.NewFakeClock(time.Now())) for i := 0; i < 5; i++ { backoff := backoffMgr.(*jitteredBackoffManagerImpl).getNextBackoff() if backoff < 1 || backoff > 2 { @@ -741,7 +742,7 @@ func TestJitteredBackoffManagerGetNextBackoff(t *testing.T) { } // negative jitter, shall be a fixed backoff - backoffMgr = NewJitteredBackoffManager(1, -1, clock.NewFakeClock(time.Now())) + backoffMgr = NewJitteredBackoffManager(1, -1, testingclock.NewFakeClock(time.Now())) backoff := backoffMgr.(*jitteredBackoffManagerImpl).getNextBackoff() if backoff != 1 { t.Errorf("backoff should be 1, but got %d", backoff) @@ -753,7 +754,7 @@ func TestJitterBackoffManagerWithRealClock(t *testing.T) { for i := 0; i < 5; i++ { start := time.Now() <-backoffMgr.Backoff().C() - passed := time.Now().Sub(start) + passed := time.Since(start) if passed < 1*time.Millisecond { t.Errorf("backoff should be at least 1ms, but got %s", passed.String()) } @@ -768,7 +769,7 @@ func TestExponentialBackoffManagerWithRealClock(t *testing.T) { for i := range durationFactors { start := time.Now() <-backoffMgr.Backoff().C() - passed := time.Now().Sub(start) + passed := time.Since(start) if passed < durationFactors[i]*time.Millisecond { t.Errorf("backoff should be at least %d ms, but got %s", durationFactors[i], passed.String()) } diff --git a/pkg/util/yaml/decoder.go b/pkg/util/yaml/decoder.go index 56f4262ac..9837b3df2 100644 --- a/pkg/util/yaml/decoder.go +++ b/pkg/util/yaml/decoder.go @@ -59,6 +59,34 @@ func Unmarshal(data []byte, v interface{}) error { } } +// UnmarshalStrict unmarshals the given data +// strictly (erroring when there are duplicate fields). +func UnmarshalStrict(data []byte, v interface{}) error { + preserveIntFloat := func(d *json.Decoder) *json.Decoder { + d.UseNumber() + return d + } + switch v := v.(type) { + case *map[string]interface{}: + if err := yaml.UnmarshalStrict(data, v, preserveIntFloat); err != nil { + return err + } + return jsonutil.ConvertMapNumbers(*v, 0) + case *[]interface{}: + if err := yaml.UnmarshalStrict(data, v, preserveIntFloat); err != nil { + return err + } + return jsonutil.ConvertSliceNumbers(*v, 0) + case *interface{}: + if err := yaml.UnmarshalStrict(data, v, preserveIntFloat); err != nil { + return err + } + return jsonutil.ConvertInterfaceNumbers(v, 0) + default: + return yaml.UnmarshalStrict(data, v) + } +} + // ToJSON converts a single YAML document into a JSON document // or returns an error. If the document appears to be JSON the // YAML decoding path is not used (so that error messages are diff --git a/pkg/version/helpers_test.go b/pkg/version/helpers_test.go index 863a53697..4ce40c989 100644 --- a/pkg/version/helpers_test.go +++ b/pkg/version/helpers_test.go @@ -45,7 +45,7 @@ func TestCompareKubeAwareVersionStrings(t *testing.T) { if e { t.Errorf("expected %s to be greater than %s", tc.v1, tc.v2) } else { - t.Errorf("expected %s to be less than than %s", tc.v1, tc.v2) + t.Errorf("expected %s to be less than %s", tc.v1, tc.v2) } } } diff --git a/pkg/watch/filter.go b/pkg/watch/filter.go index 22c9449f5..a5735a0b4 100644 --- a/pkg/watch/filter.go +++ b/pkg/watch/filter.go @@ -32,7 +32,6 @@ type FilterFunc func(in Event) (out Event, keep bool) // WARNING: filter has a fatal flaw, in that it can't properly update the // Type field (Add/Modified/Deleted) to reflect items beginning to pass the // filter when they previously didn't. -// func Filter(w Interface, f FilterFunc) Interface { fw := &filteredWatch{ incoming: w, diff --git a/pkg/watch/filter_test.go b/pkg/watch/filter_test.go index 4b5ae898a..106e06b46 100644 --- a/pkg/watch/filter_test.go +++ b/pkg/watch/filter_test.go @@ -82,3 +82,33 @@ func TestFilterStop(t *testing.T) { t.Errorf("got %v, wanted %v", e, a) } } + +func TestRecorder(t *testing.T) { + events := []Event{ + {Type: Added, Object: testType("foo")}, + {Type: Added, Object: testType("bar")}, + {Type: Added, Object: testType("baz")}, + {Type: Added, Object: testType("qux")}, + {Type: Added, Object: testType("zoo")}, + } + + source := NewFake() + go func() { + for _, item := range events { + source.Action(item.Type, item.Object) + } + source.Stop() + }() + + recorder := NewRecorder(source) + for { + _, ok := <-recorder.Interface.ResultChan() + if !ok { + break + } + } + recordedEvents := recorder.Events() + if !reflect.DeepEqual(recordedEvents, events) { + t.Errorf("got %v, expected %v", recordedEvents, events) + } +} diff --git a/pkg/watch/mux.go b/pkg/watch/mux.go index e01d51906..d51f9567e 100644 --- a/pkg/watch/mux.go +++ b/pkg/watch/mux.go @@ -17,6 +17,7 @@ limitations under the License. package watch import ( + "fmt" "sync" "k8s.io/apimachinery/pkg/runtime" @@ -44,8 +45,11 @@ type Broadcaster struct { nextWatcher int64 distributing sync.WaitGroup - incoming chan Event - stopped chan struct{} + // incomingBlock allows us to ensure we don't race and end up sending events + // to a closed channel following a broadcaster shutdown. + incomingBlock sync.Mutex + incoming chan Event + stopped chan struct{} // How large to make watcher's channel. watchQueueLength int @@ -111,6 +115,8 @@ func (obj functionFakeRuntimeObject) DeepCopyObject() runtime.Object { // won't ever see that event, and will always see any event after they are // added. func (m *Broadcaster) blockQueue(f func()) { + m.incomingBlock.Lock() + defer m.incomingBlock.Unlock() select { case <-m.stopped: return @@ -132,7 +138,7 @@ func (m *Broadcaster) blockQueue(f func()) { // Note: new watchers will only receive new events. They won't get an entire history // of previous events. It will block until the watcher is actually added to the // broadcaster. -func (m *Broadcaster) Watch() Interface { +func (m *Broadcaster) Watch() (Interface, error) { var w *broadcasterWatcher m.blockQueue(func() { id := m.nextWatcher @@ -146,11 +152,9 @@ func (m *Broadcaster) Watch() Interface { m.watchers[id] = w }) if w == nil { - // The panic here is to be consistent with the previous interface behavior - // we are willing to re-evaluate in the future. - panic("broadcaster already stopped") + return nil, fmt.Errorf("broadcaster already stopped") } - return w + return w, nil } // WatchWithPrefix adds a new watcher to the list and returns an Interface for it. It sends @@ -158,7 +162,7 @@ func (m *Broadcaster) Watch() Interface { // The returned watch will have a queue length that is at least large enough to accommodate // all of the items in queuedEvents. It will block until the watcher is actually added to // the broadcaster. -func (m *Broadcaster) WatchWithPrefix(queuedEvents []Event) Interface { +func (m *Broadcaster) WatchWithPrefix(queuedEvents []Event) (Interface, error) { var w *broadcasterWatcher m.blockQueue(func() { id := m.nextWatcher @@ -179,11 +183,9 @@ func (m *Broadcaster) WatchWithPrefix(queuedEvents []Event) Interface { } }) if w == nil { - // The panic here is to be consistent with the previous interface behavior - // we are willing to re-evaluate in the future. - panic("broadcaster already stopped") + return nil, fmt.Errorf("broadcaster already stopped") } - return w + return w, nil } // stopWatching stops the given watcher and removes it from the list. @@ -210,19 +212,38 @@ func (m *Broadcaster) closeAll() { } // Action distributes the given event among all watchers. -func (m *Broadcaster) Action(action EventType, obj runtime.Object) { +func (m *Broadcaster) Action(action EventType, obj runtime.Object) error { + m.incomingBlock.Lock() + defer m.incomingBlock.Unlock() + select { + case <-m.stopped: + return fmt.Errorf("broadcaster already stopped") + default: + } + m.incoming <- Event{action, obj} + return nil } // Action distributes the given event among all watchers, or drops it on the floor // if too many incoming actions are queued up. Returns true if the action was sent, // false if dropped. -func (m *Broadcaster) ActionOrDrop(action EventType, obj runtime.Object) bool { +func (m *Broadcaster) ActionOrDrop(action EventType, obj runtime.Object) (bool, error) { + m.incomingBlock.Lock() + defer m.incomingBlock.Unlock() + + // Ensure that if the broadcaster is stopped we do not send events to it. + select { + case <-m.stopped: + return false, fmt.Errorf("broadcaster already stopped") + default: + } + select { case m.incoming <- Event{action, obj}: - return true + return true, nil default: - return false + return false, nil } } diff --git a/pkg/watch/mux_test.go b/pkg/watch/mux_test.go index 544e57136..aa3f3b7bb 100644 --- a/pkg/watch/mux_test.go +++ b/pkg/watch/mux_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package watch_test +package watch import ( "reflect" @@ -22,10 +22,10 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/wait" - . "k8s.io/apimachinery/pkg/watch" ) type myType struct { @@ -58,6 +58,10 @@ func TestBroadcaster(t *testing.T) { wg := sync.WaitGroup{} wg.Add(testWatchers) for i := 0; i < testWatchers; i++ { + w, err := m.Watch() + if err != nil { + t.Fatalf("Unable start event watcher: '%v' (will not retry!)", err) + } // Verify that each watcher gets the events in the correct order go func(watcher int, w Interface) { tableLine := 0 @@ -75,7 +79,7 @@ func TestBroadcaster(t *testing.T) { tableLine++ } wg.Done() - }(i, m.Watch()) + }(i, w) } for i, item := range table { @@ -90,8 +94,14 @@ func TestBroadcaster(t *testing.T) { func TestBroadcasterWatcherClose(t *testing.T) { m := NewBroadcaster(0, WaitIfChannelFull) - w := m.Watch() - w2 := m.Watch() + w, err := m.Watch() + if err != nil { + t.Fatalf("Unable start event watcher: '%v' (will not retry!)", err) + } + w2, err := m.Watch() + if err != nil { + t.Fatalf("Unable start event watcher: '%v' (will not retry!)", err) + } w.Stop() m.Shutdown() if _, open := <-w.ResultChan(); open { @@ -108,6 +118,14 @@ func TestBroadcasterWatcherClose(t *testing.T) { func TestBroadcasterWatcherStopDeadlock(t *testing.T) { done := make(chan bool) m := NewBroadcaster(0, WaitIfChannelFull) + w, err := m.Watch() + if err != nil { + t.Fatalf("Unable start event watcher: '%v' (will not retry!)", err) + } + w2, err := m.Watch() + if err != nil { + t.Fatalf("Unable start event watcher: '%v' (will not retry!)", err) + } go func(w0, w1 Interface) { // We know Broadcaster is in the distribute loop once one watcher receives // an event. Stop the other watcher while distribute is trying to @@ -119,7 +137,7 @@ func TestBroadcasterWatcherStopDeadlock(t *testing.T) { w0.Stop() } close(done) - }(m.Watch(), m.Watch()) + }(w, w2) m.Action(Added, &myType{}) select { case <-time.After(wait.ForeverTestTimeout): @@ -137,8 +155,12 @@ func TestBroadcasterDropIfChannelFull(t *testing.T) { // Add a couple watchers watches := make([]Interface, 2) + var err error for i := range watches { - watches[i] = m.Watch() + watches[i], err = m.Watch() + if err != nil { + t.Fatalf("Unable start event watcher: '%v' (will not retry!)", err) + } } // Send a couple events before closing the broadcast channel. @@ -194,33 +216,76 @@ func TestBroadcasterWatchAfterShutdown(t *testing.T) { m := NewBroadcaster(0, WaitIfChannelFull) m.Shutdown() - watch := func() { - defer func() { - if err := recover(); err == nil { - t.Error("should cause panic") - } - }() - m.Watch() + _, err := m.Watch() + assert.EqualError(t, err, "broadcaster already stopped", "Watch should report error id broadcaster is shutdown") + + _, err = m.WatchWithPrefix([]Event{event1, event2}) + assert.EqualError(t, err, "broadcaster already stopped", "WatchWithPrefix should report error id broadcaster is shutdown") +} + +func TestBroadcasterSendEventAfterShutdown(t *testing.T) { + m := NewBroadcaster(1, DropIfChannelFull) + + event := Event{Type: Added, Object: &myType{"foo", "hello world"}} + + // Add a couple watchers + watches := make([]Interface, 2) + for i := range watches { + watches[i], _ = m.Watch() } - watch() + m.Shutdown() - watchWithPrefix := func() { - defer func() { - if err := recover(); err == nil { - t.Error("should cause panic") - } + // Send a couple events after closing the broadcast channel. + t.Log("Sending event") + + err := m.Action(event.Type, event.Object) + assert.EqualError(t, err, "broadcaster already stopped", "ActionOrDrop should report error id broadcaster is shutdown") + + sendOnClosed, err := m.ActionOrDrop(event.Type, event.Object) + assert.Equal(t, sendOnClosed, false, "ActionOrDrop should return false if broadcaster is already shutdown") + assert.EqualError(t, err, "broadcaster already stopped", "ActionOrDrop should report error id broadcaster is shutdown") +} + +// Test this since we see usage patterns where the broadcaster and watchers are +// stopped simultaneously leading to races. +func TestBroadcasterShutdownRace(t *testing.T) { + m := NewBroadcaster(1, WaitIfChannelFull) + stopCh := make(chan struct{}) + + // Add a bunch of watchers + const testWatchers = 2 + for i := 0; i < testWatchers; i++ { + i := i + + _, err := m.Watch() + if err != nil { + t.Fatalf("Unable start event watcher: '%v' (will not retry!)", err) + } + // This is how we force the watchers to close down independently of the + // eventbroadcaster, see real usage pattern in startRecordingEvents() + go func() { + <-stopCh + t.Log("Stopping Watchers") + m.stopWatching(int64(i)) }() - m.WatchWithPrefix([]Event{event1, event2}) } - watchWithPrefix() - action := func() { - defer func() { - if err := recover(); err == nil { - t.Error("should cause panic") - } - }() - m.Action(event1.Type, event1.Object) + event := Event{Type: Added, Object: &myType{"foo", "hello world"}} + err := m.Action(event.Type, event.Object) + if err != nil { + t.Fatalf("error sending event: %v", err) } - action() + + // Manually simulate m.Shutdown() but change it to force a race scenario + // 1. Close watcher stopchannel, so watchers are closed independently of the + // eventBroadcaster + // 2. Shutdown the m.incoming slightly Before m.stopped so that the watcher's + // call of Blockqueue can pass the m.stopped check. + m.blockQueue(func() { + close(stopCh) + close(m.incoming) + time.Sleep(1 * time.Millisecond) + close(m.stopped) + }) + m.distributing.Wait() } diff --git a/pkg/watch/watch.go b/pkg/watch/watch.go index fd0550e4a..b6c7bbfa8 100644 --- a/pkg/watch/watch.go +++ b/pkg/watch/watch.go @@ -27,11 +27,11 @@ import ( // Interface can be implemented by anything that knows how to watch and report changes. type Interface interface { - // Stops watching. Will close the channel returned by ResultChan(). Releases + // Stop stops watching. Will close the channel returned by ResultChan(). Releases // any resources used by the watch. Stop() - // Returns a chan which will receive all the events. If an error occurs + // ResultChan returns a chan which will receive all the events. If an error occurs // or Stop() is called, the implementation will close this channel and // release any resources used by the watch. ResultChan() <-chan Event diff --git a/pkg/watch/zz_generated.deepcopy.go b/pkg/watch/zz_generated.deepcopy.go index 71ef4da33..dd27d4526 100644 --- a/pkg/watch/zz_generated.deepcopy.go +++ b/pkg/watch/zz_generated.deepcopy.go @@ -1,3 +1,4 @@ +//go:build !ignore_autogenerated // +build !ignore_autogenerated /* diff --git a/third_party/forked/golang/LICENSE b/third_party/forked/golang/LICENSE new file mode 100644 index 000000000..6a66aea5e --- /dev/null +++ b/third_party/forked/golang/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/third_party/forked/golang/PATENTS b/third_party/forked/golang/PATENTS new file mode 100644 index 000000000..733099041 --- /dev/null +++ b/third_party/forked/golang/PATENTS @@ -0,0 +1,22 @@ +Additional IP Rights Grant (Patents) + +"This implementation" means the copyrightable works distributed by +Google as part of the Go project. + +Google hereby grants to You a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable (except as stated in this section) +patent license to make, have made, use, offer to sell, sell, import, +transfer and otherwise run, modify and propagate the contents of this +implementation of Go, where such license applies only to those patent +claims, both currently owned or controlled by Google and acquired in +the future, licensable by Google that are necessarily infringed by this +implementation of Go. This grant does not include claims that would be +infringed only as a consequence of further modification of this +implementation. If you or your agent or exclusive licensee institute or +order or agree to the institution of patent litigation against any +entity (including a cross-claim or counterclaim in a lawsuit) alleging +that this implementation of Go or any code incorporated within this +implementation of Go constitutes direct or contributory patent +infringement, or inducement of patent infringement, then any patent +rights granted to you under this License for this implementation of Go +shall terminate as of the date such litigation is filed. diff --git a/third_party/forked/golang/json/OWNERS b/third_party/forked/golang/json/OWNERS index 3f72c69ba..349bc69d6 100644 --- a/third_party/forked/golang/json/OWNERS +++ b/third_party/forked/golang/json/OWNERS @@ -1,7 +1,6 @@ # See the OWNERS docs at https://go.k8s.io/owners approvers: -- pwittrock + - pwittrock reviewers: -- mengqiy -- apelisse + - apelisse diff --git a/third_party/forked/golang/json/fields.go b/third_party/forked/golang/json/fields.go index 8205a4dd1..5b8514b3f 100644 --- a/third_party/forked/golang/json/fields.go +++ b/third_party/forked/golang/json/fields.go @@ -28,7 +28,7 @@ const ( // TODO: fix the returned errors to be introspectable. func LookupPatchMetadataForStruct(t reflect.Type, jsonField string) ( elemType reflect.Type, patchStrategies []string, patchMergeKey string, e error) { - if t.Kind() == reflect.Ptr { + if t.Kind() == reflect.Pointer { t = t.Elem() } @@ -183,7 +183,7 @@ func typeFields(t reflect.Type) []field { index[len(f.index)] = i ft := sf.Type - if ft.Name() == "" && ft.Kind() == reflect.Ptr { + if ft.Name() == "" && ft.Kind() == reflect.Pointer { // Follow pointer. ft = ft.Elem() } diff --git a/third_party/forked/golang/netutil/addr.go b/third_party/forked/golang/netutil/addr.go index c70f431c2..bd26f427e 100644 --- a/third_party/forked/golang/netutil/addr.go +++ b/third_party/forked/golang/netutil/addr.go @@ -12,8 +12,9 @@ func hasPort(s string) bool { return strings.LastIndex(s, ":") > strings.LastInd // FROM: http://golang.org/src/net/http/transport.go var portMap = map[string]string{ - "http": "80", - "https": "443", + "http": "80", + "https": "443", + "socks5": "1080", } // FROM: http://golang.org/src/net/http/transport.go diff --git a/third_party/forked/golang/reflect/deep_equal.go b/third_party/forked/golang/reflect/deep_equal.go index 6be80349a..511e625b6 100644 --- a/third_party/forked/golang/reflect/deep_equal.go +++ b/third_party/forked/golang/reflect/deep_equal.go @@ -100,7 +100,8 @@ func makeUsefulPanic(v reflect.Value) { // Tests for deep equality using reflected types. The map argument tracks // comparisons that have already been seen, which allows short circuiting on // recursive types. -func (e Equalities) deepValueEqual(v1, v2 reflect.Value, visited map[visit]bool, depth int) bool { +// equateNilAndEmpty controls whether empty maps/slices are equivalent to nil +func (e Equalities) deepValueEqual(v1, v2 reflect.Value, visited map[visit]bool, equateNilAndEmpty bool, depth int) bool { defer makeUsefulPanic(v1) if !v1.IsValid() || !v2.IsValid() { @@ -150,17 +151,36 @@ func (e Equalities) deepValueEqual(v1, v2 reflect.Value, visited map[visit]bool, // We don't need to check length here because length is part of // an array's type, which has already been filtered for. for i := 0; i < v1.Len(); i++ { - if !e.deepValueEqual(v1.Index(i), v2.Index(i), visited, depth+1) { + if !e.deepValueEqual(v1.Index(i), v2.Index(i), visited, equateNilAndEmpty, depth+1) { return false } } return true case reflect.Slice: - if (v1.IsNil() || v1.Len() == 0) != (v2.IsNil() || v2.Len() == 0) { - return false - } - if v1.IsNil() || v1.Len() == 0 { - return true + if equateNilAndEmpty { + if (v1.IsNil() || v1.Len() == 0) != (v2.IsNil() || v2.Len() == 0) { + return false + } + + if v1.IsNil() || v1.Len() == 0 { + return true + } + } else { + if v1.IsNil() != v2.IsNil() { + return false + } + + // Optimize nil and empty cases + // Two lists that are BOTH nil are equal + // No need to check v2 is nil since v1.IsNil == v2.IsNil from above + if v1.IsNil() { + return true + } + + // Two lists that are both empty and both non nil are equal + if v1.Len() == 0 || v2.Len() == 0 { + return true + } } if v1.Len() != v2.Len() { return false @@ -169,7 +189,7 @@ func (e Equalities) deepValueEqual(v1, v2 reflect.Value, visited map[visit]bool, return true } for i := 0; i < v1.Len(); i++ { - if !e.deepValueEqual(v1.Index(i), v2.Index(i), visited, depth+1) { + if !e.deepValueEqual(v1.Index(i), v2.Index(i), visited, equateNilAndEmpty, depth+1) { return false } } @@ -178,22 +198,40 @@ func (e Equalities) deepValueEqual(v1, v2 reflect.Value, visited map[visit]bool, if v1.IsNil() || v2.IsNil() { return v1.IsNil() == v2.IsNil() } - return e.deepValueEqual(v1.Elem(), v2.Elem(), visited, depth+1) + return e.deepValueEqual(v1.Elem(), v2.Elem(), visited, equateNilAndEmpty, depth+1) case reflect.Ptr: - return e.deepValueEqual(v1.Elem(), v2.Elem(), visited, depth+1) + return e.deepValueEqual(v1.Elem(), v2.Elem(), visited, equateNilAndEmpty, depth+1) case reflect.Struct: for i, n := 0, v1.NumField(); i < n; i++ { - if !e.deepValueEqual(v1.Field(i), v2.Field(i), visited, depth+1) { + if !e.deepValueEqual(v1.Field(i), v2.Field(i), visited, equateNilAndEmpty, depth+1) { return false } } return true case reflect.Map: - if (v1.IsNil() || v1.Len() == 0) != (v2.IsNil() || v2.Len() == 0) { - return false - } - if v1.IsNil() || v1.Len() == 0 { - return true + if equateNilAndEmpty { + if (v1.IsNil() || v1.Len() == 0) != (v2.IsNil() || v2.Len() == 0) { + return false + } + if v1.IsNil() || v1.Len() == 0 { + return true + } + } else { + if v1.IsNil() != v2.IsNil() { + return false + } + + // Optimize nil and empty cases + // Two maps that are BOTH nil are equal + // No need to check v2 is nil since v1.IsNil == v2.IsNil from above + if v1.IsNil() { + return true + } + + // Two maps that are both empty and both non nil are equal + if v1.Len() == 0 || v2.Len() == 0 { + return true + } } if v1.Len() != v2.Len() { return false @@ -202,7 +240,7 @@ func (e Equalities) deepValueEqual(v1, v2 reflect.Value, visited map[visit]bool, return true } for _, k := range v1.MapKeys() { - if !e.deepValueEqual(v1.MapIndex(k), v2.MapIndex(k), visited, depth+1) { + if !e.deepValueEqual(v1.MapIndex(k), v2.MapIndex(k), visited, equateNilAndEmpty, depth+1) { return false } } @@ -232,6 +270,14 @@ func (e Equalities) deepValueEqual(v1, v2 reflect.Value, visited map[visit]bool, // Unexported field members cannot be compared and will cause an informative panic; you must add an Equality // function for these types. func (e Equalities) DeepEqual(a1, a2 interface{}) bool { + return e.deepEqual(a1, a2, true) +} + +func (e Equalities) DeepEqualWithNilDifferentFromEmpty(a1, a2 interface{}) bool { + return e.deepEqual(a1, a2, false) +} + +func (e Equalities) deepEqual(a1, a2 interface{}, equateNilAndEmpty bool) bool { if a1 == nil || a2 == nil { return a1 == a2 } @@ -240,7 +286,7 @@ func (e Equalities) DeepEqual(a1, a2 interface{}) bool { if v1.Type() != v2.Type() { return false } - return e.deepValueEqual(v1, v2, make(map[visit]bool), 0) + return e.deepValueEqual(v1, v2, make(map[visit]bool), equateNilAndEmpty, 0) } func (e Equalities) deepValueDerive(v1, v2 reflect.Value, visited map[visit]bool, depth int) bool { @@ -327,7 +373,7 @@ func (e Equalities) deepValueDerive(v1, v2 reflect.Value, visited map[visit]bool return true } return e.deepValueDerive(v1.Elem(), v2.Elem(), visited, depth+1) - case reflect.Ptr: + case reflect.Pointer: if v1.IsNil() { return true } diff --git a/third_party/forked/golang/reflect/deep_equal_test.go b/third_party/forked/golang/reflect/deep_equal_test.go index bcb08b27e..622281761 100644 --- a/third_party/forked/golang/reflect/deep_equal_test.go +++ b/third_party/forked/golang/reflect/deep_equal_test.go @@ -16,6 +16,10 @@ func TestEqualities(t *testing.T) { type Baz struct { Y Bar } + type Zap struct { + A []int + B map[string][]int + } err := e.AddFuncs( func(a, b int) bool { return a+1 == b @@ -32,10 +36,12 @@ func TestEqualities(t *testing.T) { X int } - table := []struct { + type Case struct { a, b interface{} equal bool - }{ + } + + table := []Case{ {1, 2, true}, {2, 1, false}, {"foo", "fo", false}, @@ -70,6 +76,26 @@ func TestEqualities(t *testing.T) { t.Errorf("Expected (%+v == %+v) == %v, but got %v", item.a, item.b, e, a) } } + + // Cases which hinge upon implicit nil/empty map/slice equality + implicitTable := []Case{ + {map[string][]int{}, map[string][]int(nil), true}, + {[]int{}, []int(nil), true}, + {map[string][]int{"foo": nil}, map[string][]int{"foo": {}}, true}, + {Zap{A: nil, B: map[string][]int{"foo": nil}}, Zap{A: []int{}, B: map[string][]int{"foo": {}}}, true}, + } + + for _, item := range implicitTable { + if e, a := item.equal, e.DeepEqual(item.a, item.b); e != a { + t.Errorf("Expected (%+v == %+v) == %v, but got %v", item.a, item.b, e, a) + } + } + + for _, item := range implicitTable { + if e, a := !item.equal, e.DeepEqualWithNilDifferentFromEmpty(item.a, item.b); e != a { + t.Errorf("Expected (%+v == %+v) == %v, but got %v", item.a, item.b, e, a) + } + } } func TestDerivatives(t *testing.T) {