Bump sigs.k8s.io/controller-runtime to v0.17.5

Signed-off-by: wei-chenglai <qazwsx0939059006@gmail.com>
This commit is contained in:
wei-chenglai 2024-05-08 21:16:15 -04:00
parent 81b8c4c811
commit 409c1c703b
53 changed files with 6012 additions and 356 deletions

6
go.mod
View File

@ -6,7 +6,7 @@ require (
github.com/adhocore/gronx v1.6.3 github.com/adhocore/gronx v1.6.3
github.com/distribution/reference v0.5.0 github.com/distribution/reference v0.5.0
github.com/emirpasic/gods v1.18.1 github.com/emirpasic/gods v1.18.1
github.com/evanphx/json-patch/v5 v5.6.0 github.com/evanphx/json-patch/v5 v5.8.0
github.com/go-co-op/gocron v1.30.1 github.com/go-co-op/gocron v1.30.1
github.com/gogo/protobuf v1.3.2 github.com/gogo/protobuf v1.3.2
github.com/google/go-cmp v0.6.0 github.com/google/go-cmp v0.6.0
@ -53,7 +53,7 @@ require (
k8s.io/utils v0.0.0-20231127182322-b307cd553661 k8s.io/utils v0.0.0-20231127182322-b307cd553661
layeh.com/gopher-json v0.0.0-20201124131017-552bb3c4c3bf layeh.com/gopher-json v0.0.0-20201124131017-552bb3c4c3bf
sigs.k8s.io/cluster-api v1.5.0 sigs.k8s.io/cluster-api v1.5.0
sigs.k8s.io/controller-runtime v0.16.3 sigs.k8s.io/controller-runtime v0.17.5
sigs.k8s.io/custom-metrics-apiserver v1.29.0 sigs.k8s.io/custom-metrics-apiserver v1.29.0
sigs.k8s.io/kind v0.22.0 sigs.k8s.io/kind v0.22.0
sigs.k8s.io/mcs-api v0.1.0 sigs.k8s.io/mcs-api v0.1.0
@ -90,7 +90,7 @@ require (
github.com/go-errors/errors v1.4.2 // indirect github.com/go-errors/errors v1.4.2 // indirect
github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-logr/zapr v1.2.4 // indirect github.com/go-logr/zapr v1.3.0 // indirect
github.com/go-openapi/jsonpointer v0.20.2 // indirect github.com/go-openapi/jsonpointer v0.20.2 // indirect
github.com/go-openapi/jsonreference v0.20.4 // indirect github.com/go-openapi/jsonreference v0.20.4 // indirect
github.com/go-openapi/swag v0.22.7 // indirect github.com/go-openapi/swag v0.22.7 // indirect

15
go.sum
View File

@ -107,7 +107,6 @@ github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:l
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/aws/aws-sdk-go v1.42.27/go.mod h1:OGr6lGMAKGlG9CVrYnWYDKIyb829c6EVBRjxqjmPepc= github.com/aws/aws-sdk-go v1.42.27/go.mod h1:OGr6lGMAKGlG9CVrYnWYDKIyb829c6EVBRjxqjmPepc=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@ -215,8 +214,9 @@ github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLi
github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI= github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI=
github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch/v5 v5.0.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/evanphx/json-patch/v5 v5.0.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww=
github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
github.com/evanphx/json-patch/v5 v5.8.0 h1:lRj6N9Nci7MvzrXuX6HFzU8XjmhPiXPlsKEy1u0KQro=
github.com/evanphx/json-patch/v5 v5.8.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM= github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM=
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4=
github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8=
@ -253,14 +253,13 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-logr/zapr v0.1.0/go.mod h1:tabnROwaDl0UNxkVeFRbY8bwB37GwRv0P8lg6aAiEnk= github.com/go-logr/zapr v0.1.0/go.mod h1:tabnROwaDl0UNxkVeFRbY8bwB37GwRv0P8lg6aAiEnk=
github.com/go-logr/zapr v1.2.4 h1:QHVo+6stLbfJmYGkQ7uGHUCu5hnAFAj6mDe6Ea0SeOo= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
github.com/go-logr/zapr v1.2.4/go.mod h1:FyHWQIzQORZ0QVE1BtVHv3cKtNLuXsbNLtpuhNapBOA= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI=
github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
@ -856,7 +855,6 @@ go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
@ -867,7 +865,6 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@ -1501,8 +1498,8 @@ sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.29.0/go.mod h1:z7+wmG
sigs.k8s.io/cluster-api v1.5.0 h1:pwXvzScbAwnrB7EWHTApzW+VQfrj2OSrWAQDC9+bcbU= sigs.k8s.io/cluster-api v1.5.0 h1:pwXvzScbAwnrB7EWHTApzW+VQfrj2OSrWAQDC9+bcbU=
sigs.k8s.io/cluster-api v1.5.0/go.mod h1:ZSEP01t8oT6104gB4ljsOwwp5uJcI8SWy8IFp2HUvrc= sigs.k8s.io/cluster-api v1.5.0/go.mod h1:ZSEP01t8oT6104gB4ljsOwwp5uJcI8SWy8IFp2HUvrc=
sigs.k8s.io/controller-runtime v0.6.1/go.mod h1:XRYBPdbf5XJu9kpS84VJiZ7h/u1hF3gEORz0efEja7A= sigs.k8s.io/controller-runtime v0.6.1/go.mod h1:XRYBPdbf5XJu9kpS84VJiZ7h/u1hF3gEORz0efEja7A=
sigs.k8s.io/controller-runtime v0.16.3 h1:2TuvuokmfXvDUamSx1SuAOO3eTyye+47mJCigwG62c4= sigs.k8s.io/controller-runtime v0.17.5 h1:1FI9Lm7NiOOmBsgTV36/s2XrEFXnO2C4sbg/Zme72Rw=
sigs.k8s.io/controller-runtime v0.16.3/go.mod h1:j7bialYoSn142nv9sCOJmQgDXQXxnroFU4VnX/brVJ0= sigs.k8s.io/controller-runtime v0.17.5/go.mod h1:N0jpP5Lo7lMTF9aL56Z/B2oWBJjey6StQM0jRbKQXtY=
sigs.k8s.io/controller-tools v0.3.0/go.mod h1:enhtKGfxZD1GFEoMgP8Fdbu+uKQ/cq1/WGJhdVChfvI= sigs.k8s.io/controller-tools v0.3.0/go.mod h1:enhtKGfxZD1GFEoMgP8Fdbu+uKQ/cq1/WGJhdVChfvI=
sigs.k8s.io/custom-metrics-apiserver v1.29.0 h1:uUoUjbPrE6nVBE82bo8siIkUDMsfbaSTBB6jAx/LJ9M= sigs.k8s.io/custom-metrics-apiserver v1.29.0 h1:uUoUjbPrE6nVBE82bo8siIkUDMsfbaSTBB6jAx/LJ9M=
sigs.k8s.io/custom-metrics-apiserver v1.29.0/go.mod h1:4XXz92s/SEmP3L2nlUu6lMWorxEQXAD39AdL22IQkDA= sigs.k8s.io/custom-metrics-apiserver v1.29.0/go.mod h1:4XXz92s/SEmP3L2nlUu6lMWorxEQXAD39AdL22IQkDA=

View File

@ -51,7 +51,7 @@ func TestSetLeaseOwnerFunc(t *testing.T) {
}, },
wantErr: false, wantErr: false,
want: &coordinationv1.Lease{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{ want: &coordinationv1.Lease{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{
{APIVersion: "cluster.karmada.io/v1alpha1", Kind: "Cluster", Name: "test"}}}}, {Name: "test"}}}},
}, },
{ {
name: "cluster not found", name: "cluster not found",

View File

@ -61,7 +61,6 @@ func TestCreateOrUpdateEndpointSlice(t *testing.T) {
}, },
}, },
want: &discoveryv1.EndpointSlice{ want: &discoveryv1.EndpointSlice{
TypeMeta: metav1.TypeMeta{APIVersion: "discovery.k8s.io/v1", Kind: "EndpointSlice"},
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: "eps", Namespace: "ns", Name: "eps", Namespace: "ns",
Labels: map[string]string{"foo": "foo1"}, Labels: map[string]string{"foo": "foo1"},
@ -97,7 +96,6 @@ func TestCreateOrUpdateEndpointSlice(t *testing.T) {
}, },
}, },
want: &discoveryv1.EndpointSlice{ want: &discoveryv1.EndpointSlice{
TypeMeta: metav1.TypeMeta{APIVersion: "discovery.k8s.io/v1", Kind: "EndpointSlice"},
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: "eps", Namespace: "ns", Name: "eps", Namespace: "ns",
Labels: map[string]string{"foo": "foo1"}, Labels: map[string]string{"foo": "foo1"},
@ -145,7 +143,6 @@ func TestCreateOrUpdateEndpointSlice(t *testing.T) {
}, },
}, },
want: &discoveryv1.EndpointSlice{ want: &discoveryv1.EndpointSlice{
TypeMeta: metav1.TypeMeta{APIVersion: "discovery.k8s.io/v1", Kind: "EndpointSlice"},
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: "eps", Namespace: "ns", Name: "eps", Namespace: "ns",
Labels: map[string]string{"foo": "foo1"}, Labels: map[string]string{"foo": "foo1"},

View File

@ -85,7 +85,7 @@ func NewClusterScaleClientSet(clusterName string, client client.Client) (*Cluste
if err != nil { if err != nil {
return nil, err return nil, err
} }
mapper, err := apiutil.NewDiscoveryRESTMapper(clusterConfig, httpClient) mapper, err := apiutil.NewDynamicRESTMapper(clusterConfig, httpClient)
if err != nil { if err != nil {
return nil, err return nil, err
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,141 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package json
import (
"bytes"
"unicode/utf8"
)
const (
caseMask = ^byte(0x20) // Mask to ignore case in ASCII.
kelvin = '\u212a'
smallLongEss = '\u017f'
)
// foldFunc returns one of four different case folding equivalence
// functions, from most general (and slow) to fastest:
//
// 1) bytes.EqualFold, if the key s contains any non-ASCII UTF-8
// 2) equalFoldRight, if s contains special folding ASCII ('k', 'K', 's', 'S')
// 3) asciiEqualFold, no special, but includes non-letters (including _)
// 4) simpleLetterEqualFold, no specials, no non-letters.
//
// The letters S and K are special because they map to 3 runes, not just 2:
// - S maps to s and to U+017F 'ſ' Latin small letter long s
// - k maps to K and to U+212A '' Kelvin sign
//
// See https://play.golang.org/p/tTxjOc0OGo
//
// The returned function is specialized for matching against s and
// should only be given s. It's not curried for performance reasons.
func foldFunc(s []byte) func(s, t []byte) bool {
nonLetter := false
special := false // special letter
for _, b := range s {
if b >= utf8.RuneSelf {
return bytes.EqualFold
}
upper := b & caseMask
if upper < 'A' || upper > 'Z' {
nonLetter = true
} else if upper == 'K' || upper == 'S' {
// See above for why these letters are special.
special = true
}
}
if special {
return equalFoldRight
}
if nonLetter {
return asciiEqualFold
}
return simpleLetterEqualFold
}
// equalFoldRight is a specialization of bytes.EqualFold when s is
// known to be all ASCII (including punctuation), but contains an 's',
// 'S', 'k', or 'K', requiring a Unicode fold on the bytes in t.
// See comments on foldFunc.
func equalFoldRight(s, t []byte) bool {
for _, sb := range s {
if len(t) == 0 {
return false
}
tb := t[0]
if tb < utf8.RuneSelf {
if sb != tb {
sbUpper := sb & caseMask
if 'A' <= sbUpper && sbUpper <= 'Z' {
if sbUpper != tb&caseMask {
return false
}
} else {
return false
}
}
t = t[1:]
continue
}
// sb is ASCII and t is not. t must be either kelvin
// sign or long s; sb must be s, S, k, or K.
tr, size := utf8.DecodeRune(t)
switch sb {
case 's', 'S':
if tr != smallLongEss {
return false
}
case 'k', 'K':
if tr != kelvin {
return false
}
default:
return false
}
t = t[size:]
}
return len(t) == 0
}
// asciiEqualFold is a specialization of bytes.EqualFold for use when
// s is all ASCII (but may contain non-letters) and contains no
// special-folding letters.
// See comments on foldFunc.
func asciiEqualFold(s, t []byte) bool {
if len(s) != len(t) {
return false
}
for i, sb := range s {
tb := t[i]
if sb == tb {
continue
}
if ('a' <= sb && sb <= 'z') || ('A' <= sb && sb <= 'Z') {
if sb&caseMask != tb&caseMask {
return false
}
} else {
return false
}
}
return true
}
// simpleLetterEqualFold is a specialization of bytes.EqualFold for
// use when s is all ASCII letters (no underscores, etc) and also
// doesn't contain 'k', 'K', 's', or 'S'.
// See comments on foldFunc.
func simpleLetterEqualFold(s, t []byte) bool {
if len(s) != len(t) {
return false
}
for i, b := range s {
if b&caseMask != t[i]&caseMask {
return false
}
}
return true
}

View File

@ -0,0 +1,42 @@
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build gofuzz
package json
import (
"fmt"
)
func Fuzz(data []byte) (score int) {
for _, ctor := range []func() any{
func() any { return new(any) },
func() any { return new(map[string]any) },
func() any { return new([]any) },
} {
v := ctor()
err := Unmarshal(data, v)
if err != nil {
continue
}
score = 1
m, err := Marshal(v)
if err != nil {
fmt.Printf("v=%#v\n", v)
panic(err)
}
u := ctor()
err = Unmarshal(m, u)
if err != nil {
fmt.Printf("v=%#v\n", v)
fmt.Printf("m=%s\n", m)
panic(err)
}
}
return
}

View File

@ -0,0 +1,143 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package json
import (
"bytes"
)
// Compact appends to dst the JSON-encoded src with
// insignificant space characters elided.
func Compact(dst *bytes.Buffer, src []byte) error {
return compact(dst, src, false)
}
func compact(dst *bytes.Buffer, src []byte, escape bool) error {
origLen := dst.Len()
scan := newScanner()
defer freeScanner(scan)
start := 0
for i, c := range src {
if escape && (c == '<' || c == '>' || c == '&') {
if start < i {
dst.Write(src[start:i])
}
dst.WriteString(`\u00`)
dst.WriteByte(hex[c>>4])
dst.WriteByte(hex[c&0xF])
start = i + 1
}
// Convert U+2028 and U+2029 (E2 80 A8 and E2 80 A9).
if escape && c == 0xE2 && i+2 < len(src) && src[i+1] == 0x80 && src[i+2]&^1 == 0xA8 {
if start < i {
dst.Write(src[start:i])
}
dst.WriteString(`\u202`)
dst.WriteByte(hex[src[i+2]&0xF])
start = i + 3
}
v := scan.step(scan, c)
if v >= scanSkipSpace {
if v == scanError {
break
}
if start < i {
dst.Write(src[start:i])
}
start = i + 1
}
}
if scan.eof() == scanError {
dst.Truncate(origLen)
return scan.err
}
if start < len(src) {
dst.Write(src[start:])
}
return nil
}
func newline(dst *bytes.Buffer, prefix, indent string, depth int) {
dst.WriteByte('\n')
dst.WriteString(prefix)
for i := 0; i < depth; i++ {
dst.WriteString(indent)
}
}
// Indent appends to dst an indented form of the JSON-encoded src.
// Each element in a JSON object or array begins on a new,
// indented line beginning with prefix followed by one or more
// copies of indent according to the indentation nesting.
// The data appended to dst does not begin with the prefix nor
// any indentation, to make it easier to embed inside other formatted JSON data.
// Although leading space characters (space, tab, carriage return, newline)
// at the beginning of src are dropped, trailing space characters
// at the end of src are preserved and copied to dst.
// For example, if src has no trailing spaces, neither will dst;
// if src ends in a trailing newline, so will dst.
func Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error {
origLen := dst.Len()
scan := newScanner()
defer freeScanner(scan)
needIndent := false
depth := 0
for _, c := range src {
scan.bytes++
v := scan.step(scan, c)
if v == scanSkipSpace {
continue
}
if v == scanError {
break
}
if needIndent && v != scanEndObject && v != scanEndArray {
needIndent = false
depth++
newline(dst, prefix, indent, depth)
}
// Emit semantically uninteresting bytes
// (in particular, punctuation in strings) unmodified.
if v == scanContinue {
dst.WriteByte(c)
continue
}
// Add spacing around real punctuation.
switch c {
case '{', '[':
// delay indent so that empty object and array are formatted as {} and [].
needIndent = true
dst.WriteByte(c)
case ',':
dst.WriteByte(c)
newline(dst, prefix, indent, depth)
case ':':
dst.WriteByte(c)
dst.WriteByte(' ')
case '}', ']':
if needIndent {
// suppress indent in empty object/array
needIndent = false
} else {
depth--
newline(dst, prefix, indent, depth)
}
dst.WriteByte(c)
default:
dst.WriteByte(c)
}
}
if scan.eof() == scanError {
dst.Truncate(origLen)
return scan.err
}
return nil
}

View File

@ -0,0 +1,610 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package json
// JSON value parser state machine.
// Just about at the limit of what is reasonable to write by hand.
// Some parts are a bit tedious, but overall it nicely factors out the
// otherwise common code from the multiple scanning functions
// in this package (Compact, Indent, checkValid, etc).
//
// This file starts with two simple examples using the scanner
// before diving into the scanner itself.
import (
"strconv"
"sync"
)
// Valid reports whether data is a valid JSON encoding.
func Valid(data []byte) bool {
scan := newScanner()
defer freeScanner(scan)
return checkValid(data, scan) == nil
}
// checkValid verifies that data is valid JSON-encoded data.
// scan is passed in for use by checkValid to avoid an allocation.
// checkValid returns nil or a SyntaxError.
func checkValid(data []byte, scan *scanner) error {
scan.reset()
for _, c := range data {
scan.bytes++
if scan.step(scan, c) == scanError {
return scan.err
}
}
if scan.eof() == scanError {
return scan.err
}
return nil
}
// A SyntaxError is a description of a JSON syntax error.
// Unmarshal will return a SyntaxError if the JSON can't be parsed.
type SyntaxError struct {
msg string // description of error
Offset int64 // error occurred after reading Offset bytes
}
func (e *SyntaxError) Error() string { return e.msg }
// A scanner is a JSON scanning state machine.
// Callers call scan.reset and then pass bytes in one at a time
// by calling scan.step(&scan, c) for each byte.
// The return value, referred to as an opcode, tells the
// caller about significant parsing events like beginning
// and ending literals, objects, and arrays, so that the
// caller can follow along if it wishes.
// The return value scanEnd indicates that a single top-level
// JSON value has been completed, *before* the byte that
// just got passed in. (The indication must be delayed in order
// to recognize the end of numbers: is 123 a whole value or
// the beginning of 12345e+6?).
type scanner struct {
// The step is a func to be called to execute the next transition.
// Also tried using an integer constant and a single func
// with a switch, but using the func directly was 10% faster
// on a 64-bit Mac Mini, and it's nicer to read.
step func(*scanner, byte) int
// Reached end of top-level value.
endTop bool
// Stack of what we're in the middle of - array values, object keys, object values.
parseState []int
// Error that happened, if any.
err error
// total bytes consumed, updated by decoder.Decode (and deliberately
// not set to zero by scan.reset)
bytes int64
}
var scannerPool = sync.Pool{
New: func() any {
return &scanner{}
},
}
func newScanner() *scanner {
scan := scannerPool.Get().(*scanner)
// scan.reset by design doesn't set bytes to zero
scan.bytes = 0
scan.reset()
return scan
}
func freeScanner(scan *scanner) {
// Avoid hanging on to too much memory in extreme cases.
if len(scan.parseState) > 1024 {
scan.parseState = nil
}
scannerPool.Put(scan)
}
// These values are returned by the state transition functions
// assigned to scanner.state and the method scanner.eof.
// They give details about the current state of the scan that
// callers might be interested to know about.
// It is okay to ignore the return value of any particular
// call to scanner.state: if one call returns scanError,
// every subsequent call will return scanError too.
const (
// Continue.
scanContinue = iota // uninteresting byte
scanBeginLiteral // end implied by next result != scanContinue
scanBeginObject // begin object
scanObjectKey // just finished object key (string)
scanObjectValue // just finished non-last object value
scanEndObject // end object (implies scanObjectValue if possible)
scanBeginArray // begin array
scanArrayValue // just finished array value
scanEndArray // end array (implies scanArrayValue if possible)
scanSkipSpace // space byte; can skip; known to be last "continue" result
// Stop.
scanEnd // top-level value ended *before* this byte; known to be first "stop" result
scanError // hit an error, scanner.err.
)
// These values are stored in the parseState stack.
// They give the current state of a composite value
// being scanned. If the parser is inside a nested value
// the parseState describes the nested state, outermost at entry 0.
const (
parseObjectKey = iota // parsing object key (before colon)
parseObjectValue // parsing object value (after colon)
parseArrayValue // parsing array value
)
// This limits the max nesting depth to prevent stack overflow.
// This is permitted by https://tools.ietf.org/html/rfc7159#section-9
const maxNestingDepth = 10000
// reset prepares the scanner for use.
// It must be called before calling s.step.
func (s *scanner) reset() {
s.step = stateBeginValue
s.parseState = s.parseState[0:0]
s.err = nil
s.endTop = false
}
// eof tells the scanner that the end of input has been reached.
// It returns a scan status just as s.step does.
func (s *scanner) eof() int {
if s.err != nil {
return scanError
}
if s.endTop {
return scanEnd
}
s.step(s, ' ')
if s.endTop {
return scanEnd
}
if s.err == nil {
s.err = &SyntaxError{"unexpected end of JSON input", s.bytes}
}
return scanError
}
// pushParseState pushes a new parse state p onto the parse stack.
// an error state is returned if maxNestingDepth was exceeded, otherwise successState is returned.
func (s *scanner) pushParseState(c byte, newParseState int, successState int) int {
s.parseState = append(s.parseState, newParseState)
if len(s.parseState) <= maxNestingDepth {
return successState
}
return s.error(c, "exceeded max depth")
}
// popParseState pops a parse state (already obtained) off the stack
// and updates s.step accordingly.
func (s *scanner) popParseState() {
n := len(s.parseState) - 1
s.parseState = s.parseState[0:n]
if n == 0 {
s.step = stateEndTop
s.endTop = true
} else {
s.step = stateEndValue
}
}
func isSpace(c byte) bool {
return c <= ' ' && (c == ' ' || c == '\t' || c == '\r' || c == '\n')
}
// stateBeginValueOrEmpty is the state after reading `[`.
func stateBeginValueOrEmpty(s *scanner, c byte) int {
if isSpace(c) {
return scanSkipSpace
}
if c == ']' {
return stateEndValue(s, c)
}
return stateBeginValue(s, c)
}
// stateBeginValue is the state at the beginning of the input.
func stateBeginValue(s *scanner, c byte) int {
if isSpace(c) {
return scanSkipSpace
}
switch c {
case '{':
s.step = stateBeginStringOrEmpty
return s.pushParseState(c, parseObjectKey, scanBeginObject)
case '[':
s.step = stateBeginValueOrEmpty
return s.pushParseState(c, parseArrayValue, scanBeginArray)
case '"':
s.step = stateInString
return scanBeginLiteral
case '-':
s.step = stateNeg
return scanBeginLiteral
case '0': // beginning of 0.123
s.step = state0
return scanBeginLiteral
case 't': // beginning of true
s.step = stateT
return scanBeginLiteral
case 'f': // beginning of false
s.step = stateF
return scanBeginLiteral
case 'n': // beginning of null
s.step = stateN
return scanBeginLiteral
}
if '1' <= c && c <= '9' { // beginning of 1234.5
s.step = state1
return scanBeginLiteral
}
return s.error(c, "looking for beginning of value")
}
// stateBeginStringOrEmpty is the state after reading `{`.
func stateBeginStringOrEmpty(s *scanner, c byte) int {
if isSpace(c) {
return scanSkipSpace
}
if c == '}' {
n := len(s.parseState)
s.parseState[n-1] = parseObjectValue
return stateEndValue(s, c)
}
return stateBeginString(s, c)
}
// stateBeginString is the state after reading `{"key": value,`.
func stateBeginString(s *scanner, c byte) int {
if isSpace(c) {
return scanSkipSpace
}
if c == '"' {
s.step = stateInString
return scanBeginLiteral
}
return s.error(c, "looking for beginning of object key string")
}
// stateEndValue is the state after completing a value,
// such as after reading `{}` or `true` or `["x"`.
func stateEndValue(s *scanner, c byte) int {
n := len(s.parseState)
if n == 0 {
// Completed top-level before the current byte.
s.step = stateEndTop
s.endTop = true
return stateEndTop(s, c)
}
if isSpace(c) {
s.step = stateEndValue
return scanSkipSpace
}
ps := s.parseState[n-1]
switch ps {
case parseObjectKey:
if c == ':' {
s.parseState[n-1] = parseObjectValue
s.step = stateBeginValue
return scanObjectKey
}
return s.error(c, "after object key")
case parseObjectValue:
if c == ',' {
s.parseState[n-1] = parseObjectKey
s.step = stateBeginString
return scanObjectValue
}
if c == '}' {
s.popParseState()
return scanEndObject
}
return s.error(c, "after object key:value pair")
case parseArrayValue:
if c == ',' {
s.step = stateBeginValue
return scanArrayValue
}
if c == ']' {
s.popParseState()
return scanEndArray
}
return s.error(c, "after array element")
}
return s.error(c, "")
}
// stateEndTop is the state after finishing the top-level value,
// such as after reading `{}` or `[1,2,3]`.
// Only space characters should be seen now.
func stateEndTop(s *scanner, c byte) int {
if !isSpace(c) {
// Complain about non-space byte on next call.
s.error(c, "after top-level value")
}
return scanEnd
}
// stateInString is the state after reading `"`.
func stateInString(s *scanner, c byte) int {
if c == '"' {
s.step = stateEndValue
return scanContinue
}
if c == '\\' {
s.step = stateInStringEsc
return scanContinue
}
if c < 0x20 {
return s.error(c, "in string literal")
}
return scanContinue
}
// stateInStringEsc is the state after reading `"\` during a quoted string.
func stateInStringEsc(s *scanner, c byte) int {
switch c {
case 'b', 'f', 'n', 'r', 't', '\\', '/', '"':
s.step = stateInString
return scanContinue
case 'u':
s.step = stateInStringEscU
return scanContinue
}
return s.error(c, "in string escape code")
}
// stateInStringEscU is the state after reading `"\u` during a quoted string.
func stateInStringEscU(s *scanner, c byte) int {
if '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F' {
s.step = stateInStringEscU1
return scanContinue
}
// numbers
return s.error(c, "in \\u hexadecimal character escape")
}
// stateInStringEscU1 is the state after reading `"\u1` during a quoted string.
func stateInStringEscU1(s *scanner, c byte) int {
if '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F' {
s.step = stateInStringEscU12
return scanContinue
}
// numbers
return s.error(c, "in \\u hexadecimal character escape")
}
// stateInStringEscU12 is the state after reading `"\u12` during a quoted string.
func stateInStringEscU12(s *scanner, c byte) int {
if '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F' {
s.step = stateInStringEscU123
return scanContinue
}
// numbers
return s.error(c, "in \\u hexadecimal character escape")
}
// stateInStringEscU123 is the state after reading `"\u123` during a quoted string.
func stateInStringEscU123(s *scanner, c byte) int {
if '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F' {
s.step = stateInString
return scanContinue
}
// numbers
return s.error(c, "in \\u hexadecimal character escape")
}
// stateNeg is the state after reading `-` during a number.
func stateNeg(s *scanner, c byte) int {
if c == '0' {
s.step = state0
return scanContinue
}
if '1' <= c && c <= '9' {
s.step = state1
return scanContinue
}
return s.error(c, "in numeric literal")
}
// state1 is the state after reading a non-zero integer during a number,
// such as after reading `1` or `100` but not `0`.
func state1(s *scanner, c byte) int {
if '0' <= c && c <= '9' {
s.step = state1
return scanContinue
}
return state0(s, c)
}
// state0 is the state after reading `0` during a number.
func state0(s *scanner, c byte) int {
if c == '.' {
s.step = stateDot
return scanContinue
}
if c == 'e' || c == 'E' {
s.step = stateE
return scanContinue
}
return stateEndValue(s, c)
}
// stateDot is the state after reading the integer and decimal point in a number,
// such as after reading `1.`.
func stateDot(s *scanner, c byte) int {
if '0' <= c && c <= '9' {
s.step = stateDot0
return scanContinue
}
return s.error(c, "after decimal point in numeric literal")
}
// stateDot0 is the state after reading the integer, decimal point, and subsequent
// digits of a number, such as after reading `3.14`.
func stateDot0(s *scanner, c byte) int {
if '0' <= c && c <= '9' {
return scanContinue
}
if c == 'e' || c == 'E' {
s.step = stateE
return scanContinue
}
return stateEndValue(s, c)
}
// stateE is the state after reading the mantissa and e in a number,
// such as after reading `314e` or `0.314e`.
func stateE(s *scanner, c byte) int {
if c == '+' || c == '-' {
s.step = stateESign
return scanContinue
}
return stateESign(s, c)
}
// stateESign is the state after reading the mantissa, e, and sign in a number,
// such as after reading `314e-` or `0.314e+`.
func stateESign(s *scanner, c byte) int {
if '0' <= c && c <= '9' {
s.step = stateE0
return scanContinue
}
return s.error(c, "in exponent of numeric literal")
}
// stateE0 is the state after reading the mantissa, e, optional sign,
// and at least one digit of the exponent in a number,
// such as after reading `314e-2` or `0.314e+1` or `3.14e0`.
func stateE0(s *scanner, c byte) int {
if '0' <= c && c <= '9' {
return scanContinue
}
return stateEndValue(s, c)
}
// stateT is the state after reading `t`.
func stateT(s *scanner, c byte) int {
if c == 'r' {
s.step = stateTr
return scanContinue
}
return s.error(c, "in literal true (expecting 'r')")
}
// stateTr is the state after reading `tr`.
func stateTr(s *scanner, c byte) int {
if c == 'u' {
s.step = stateTru
return scanContinue
}
return s.error(c, "in literal true (expecting 'u')")
}
// stateTru is the state after reading `tru`.
func stateTru(s *scanner, c byte) int {
if c == 'e' {
s.step = stateEndValue
return scanContinue
}
return s.error(c, "in literal true (expecting 'e')")
}
// stateF is the state after reading `f`.
func stateF(s *scanner, c byte) int {
if c == 'a' {
s.step = stateFa
return scanContinue
}
return s.error(c, "in literal false (expecting 'a')")
}
// stateFa is the state after reading `fa`.
func stateFa(s *scanner, c byte) int {
if c == 'l' {
s.step = stateFal
return scanContinue
}
return s.error(c, "in literal false (expecting 'l')")
}
// stateFal is the state after reading `fal`.
func stateFal(s *scanner, c byte) int {
if c == 's' {
s.step = stateFals
return scanContinue
}
return s.error(c, "in literal false (expecting 's')")
}
// stateFals is the state after reading `fals`.
func stateFals(s *scanner, c byte) int {
if c == 'e' {
s.step = stateEndValue
return scanContinue
}
return s.error(c, "in literal false (expecting 'e')")
}
// stateN is the state after reading `n`.
func stateN(s *scanner, c byte) int {
if c == 'u' {
s.step = stateNu
return scanContinue
}
return s.error(c, "in literal null (expecting 'u')")
}
// stateNu is the state after reading `nu`.
func stateNu(s *scanner, c byte) int {
if c == 'l' {
s.step = stateNul
return scanContinue
}
return s.error(c, "in literal null (expecting 'l')")
}
// stateNul is the state after reading `nul`.
func stateNul(s *scanner, c byte) int {
if c == 'l' {
s.step = stateEndValue
return scanContinue
}
return s.error(c, "in literal null (expecting 'l')")
}
// stateError is the state after reaching a syntax error,
// such as after reading `[1}` or `5.1.2`.
func stateError(s *scanner, c byte) int {
return scanError
}
// error records an error and switches to the error state.
func (s *scanner) error(c byte, context string) int {
s.step = stateError
s.err = &SyntaxError{"invalid character " + quoteChar(c) + " " + context, s.bytes}
return scanError
}
// quoteChar formats c as a quoted character literal.
func quoteChar(c byte) string {
// special cases - different from quoted strings
if c == '\'' {
return `'\''`
}
if c == '"' {
return `'"'`
}
// use quoted string with different quotation marks
s := strconv.Quote(string(c))
return "'" + s[1:len(s)-1] + "'"
}

View File

@ -0,0 +1,515 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package json
import (
"bytes"
"errors"
"io"
)
// A Decoder reads and decodes JSON values from an input stream.
type Decoder struct {
r io.Reader
buf []byte
d decodeState
scanp int // start of unread data in buf
scanned int64 // amount of data already scanned
scan scanner
err error
tokenState int
tokenStack []int
}
// NewDecoder returns a new decoder that reads from r.
//
// The decoder introduces its own buffering and may
// read data from r beyond the JSON values requested.
func NewDecoder(r io.Reader) *Decoder {
return &Decoder{r: r}
}
// UseNumber causes the Decoder to unmarshal a number into an interface{} as a
// Number instead of as a float64.
func (dec *Decoder) UseNumber() { dec.d.useNumber = true }
// DisallowUnknownFields causes the Decoder to return an error when the destination
// is a struct and the input contains object keys which do not match any
// non-ignored, exported fields in the destination.
func (dec *Decoder) DisallowUnknownFields() { dec.d.disallowUnknownFields = true }
// Decode reads the next JSON-encoded value from its
// input and stores it in the value pointed to by v.
//
// See the documentation for Unmarshal for details about
// the conversion of JSON into a Go value.
func (dec *Decoder) Decode(v any) error {
if dec.err != nil {
return dec.err
}
if err := dec.tokenPrepareForDecode(); err != nil {
return err
}
if !dec.tokenValueAllowed() {
return &SyntaxError{msg: "not at beginning of value", Offset: dec.InputOffset()}
}
// Read whole value into buffer.
n, err := dec.readValue()
if err != nil {
return err
}
dec.d.init(dec.buf[dec.scanp : dec.scanp+n])
dec.scanp += n
// Don't save err from unmarshal into dec.err:
// the connection is still usable since we read a complete JSON
// object from it before the error happened.
err = dec.d.unmarshal(v)
// fixup token streaming state
dec.tokenValueEnd()
return err
}
// Buffered returns a reader of the data remaining in the Decoder's
// buffer. The reader is valid until the next call to Decode.
func (dec *Decoder) Buffered() io.Reader {
return bytes.NewReader(dec.buf[dec.scanp:])
}
// readValue reads a JSON value into dec.buf.
// It returns the length of the encoding.
func (dec *Decoder) readValue() (int, error) {
dec.scan.reset()
scanp := dec.scanp
var err error
Input:
// help the compiler see that scanp is never negative, so it can remove
// some bounds checks below.
for scanp >= 0 {
// Look in the buffer for a new value.
for ; scanp < len(dec.buf); scanp++ {
c := dec.buf[scanp]
dec.scan.bytes++
switch dec.scan.step(&dec.scan, c) {
case scanEnd:
// scanEnd is delayed one byte so we decrement
// the scanner bytes count by 1 to ensure that
// this value is correct in the next call of Decode.
dec.scan.bytes--
break Input
case scanEndObject, scanEndArray:
// scanEnd is delayed one byte.
// We might block trying to get that byte from src,
// so instead invent a space byte.
if stateEndValue(&dec.scan, ' ') == scanEnd {
scanp++
break Input
}
case scanError:
dec.err = dec.scan.err
return 0, dec.scan.err
}
}
// Did the last read have an error?
// Delayed until now to allow buffer scan.
if err != nil {
if err == io.EOF {
if dec.scan.step(&dec.scan, ' ') == scanEnd {
break Input
}
if nonSpace(dec.buf) {
err = io.ErrUnexpectedEOF
}
}
dec.err = err
return 0, err
}
n := scanp - dec.scanp
err = dec.refill()
scanp = dec.scanp + n
}
return scanp - dec.scanp, nil
}
func (dec *Decoder) refill() error {
// Make room to read more into the buffer.
// First slide down data already consumed.
if dec.scanp > 0 {
dec.scanned += int64(dec.scanp)
n := copy(dec.buf, dec.buf[dec.scanp:])
dec.buf = dec.buf[:n]
dec.scanp = 0
}
// Grow buffer if not large enough.
const minRead = 512
if cap(dec.buf)-len(dec.buf) < minRead {
newBuf := make([]byte, len(dec.buf), 2*cap(dec.buf)+minRead)
copy(newBuf, dec.buf)
dec.buf = newBuf
}
// Read. Delay error for next iteration (after scan).
n, err := dec.r.Read(dec.buf[len(dec.buf):cap(dec.buf)])
dec.buf = dec.buf[0 : len(dec.buf)+n]
return err
}
func nonSpace(b []byte) bool {
for _, c := range b {
if !isSpace(c) {
return true
}
}
return false
}
// An Encoder writes JSON values to an output stream.
type Encoder struct {
w io.Writer
err error
escapeHTML bool
indentBuf *bytes.Buffer
indentPrefix string
indentValue string
}
// NewEncoder returns a new encoder that writes to w.
func NewEncoder(w io.Writer) *Encoder {
return &Encoder{w: w, escapeHTML: true}
}
// Encode writes the JSON encoding of v to the stream,
// followed by a newline character.
//
// See the documentation for Marshal for details about the
// conversion of Go values to JSON.
func (enc *Encoder) Encode(v any) error {
if enc.err != nil {
return enc.err
}
e := newEncodeState()
defer encodeStatePool.Put(e)
err := e.marshal(v, encOpts{escapeHTML: enc.escapeHTML})
if err != nil {
return err
}
// Terminate each value with a newline.
// This makes the output look a little nicer
// when debugging, and some kind of space
// is required if the encoded value was a number,
// so that the reader knows there aren't more
// digits coming.
e.WriteByte('\n')
b := e.Bytes()
if enc.indentPrefix != "" || enc.indentValue != "" {
if enc.indentBuf == nil {
enc.indentBuf = new(bytes.Buffer)
}
enc.indentBuf.Reset()
err = Indent(enc.indentBuf, b, enc.indentPrefix, enc.indentValue)
if err != nil {
return err
}
b = enc.indentBuf.Bytes()
}
if _, err = enc.w.Write(b); err != nil {
enc.err = err
}
return err
}
// SetIndent instructs the encoder to format each subsequent encoded
// value as if indented by the package-level function Indent(dst, src, prefix, indent).
// Calling SetIndent("", "") disables indentation.
func (enc *Encoder) SetIndent(prefix, indent string) {
enc.indentPrefix = prefix
enc.indentValue = indent
}
// SetEscapeHTML specifies whether problematic HTML characters
// should be escaped inside JSON quoted strings.
// The default behavior is to escape &, <, and > to \u0026, \u003c, and \u003e
// to avoid certain safety problems that can arise when embedding JSON in HTML.
//
// In non-HTML settings where the escaping interferes with the readability
// of the output, SetEscapeHTML(false) disables this behavior.
func (enc *Encoder) SetEscapeHTML(on bool) {
enc.escapeHTML = on
}
// RawMessage is a raw encoded JSON value.
// It implements Marshaler and Unmarshaler and can
// be used to delay JSON decoding or precompute a JSON encoding.
type RawMessage []byte
// MarshalJSON returns m as the JSON encoding of m.
func (m RawMessage) MarshalJSON() ([]byte, error) {
if m == nil {
return []byte("null"), nil
}
return m, nil
}
// UnmarshalJSON sets *m to a copy of data.
func (m *RawMessage) UnmarshalJSON(data []byte) error {
if m == nil {
return errors.New("json.RawMessage: UnmarshalJSON on nil pointer")
}
*m = append((*m)[0:0], data...)
return nil
}
var _ Marshaler = (*RawMessage)(nil)
var _ Unmarshaler = (*RawMessage)(nil)
// A Token holds a value of one of these types:
//
// Delim, for the four JSON delimiters [ ] { }
// bool, for JSON booleans
// float64, for JSON numbers
// Number, for JSON numbers
// string, for JSON string literals
// nil, for JSON null
type Token any
const (
tokenTopValue = iota
tokenArrayStart
tokenArrayValue
tokenArrayComma
tokenObjectStart
tokenObjectKey
tokenObjectColon
tokenObjectValue
tokenObjectComma
)
// advance tokenstate from a separator state to a value state
func (dec *Decoder) tokenPrepareForDecode() error {
// Note: Not calling peek before switch, to avoid
// putting peek into the standard Decode path.
// peek is only called when using the Token API.
switch dec.tokenState {
case tokenArrayComma:
c, err := dec.peek()
if err != nil {
return err
}
if c != ',' {
return &SyntaxError{"expected comma after array element", dec.InputOffset()}
}
dec.scanp++
dec.tokenState = tokenArrayValue
case tokenObjectColon:
c, err := dec.peek()
if err != nil {
return err
}
if c != ':' {
return &SyntaxError{"expected colon after object key", dec.InputOffset()}
}
dec.scanp++
dec.tokenState = tokenObjectValue
}
return nil
}
func (dec *Decoder) tokenValueAllowed() bool {
switch dec.tokenState {
case tokenTopValue, tokenArrayStart, tokenArrayValue, tokenObjectValue:
return true
}
return false
}
func (dec *Decoder) tokenValueEnd() {
switch dec.tokenState {
case tokenArrayStart, tokenArrayValue:
dec.tokenState = tokenArrayComma
case tokenObjectValue:
dec.tokenState = tokenObjectComma
}
}
// A Delim is a JSON array or object delimiter, one of [ ] { or }.
type Delim rune
func (d Delim) String() string {
return string(d)
}
// Token returns the next JSON token in the input stream.
// At the end of the input stream, Token returns nil, io.EOF.
//
// Token guarantees that the delimiters [ ] { } it returns are
// properly nested and matched: if Token encounters an unexpected
// delimiter in the input, it will return an error.
//
// The input stream consists of basic JSON values—bool, string,
// number, and null—along with delimiters [ ] { } of type Delim
// to mark the start and end of arrays and objects.
// Commas and colons are elided.
func (dec *Decoder) Token() (Token, error) {
for {
c, err := dec.peek()
if err != nil {
return nil, err
}
switch c {
case '[':
if !dec.tokenValueAllowed() {
return dec.tokenError(c)
}
dec.scanp++
dec.tokenStack = append(dec.tokenStack, dec.tokenState)
dec.tokenState = tokenArrayStart
return Delim('['), nil
case ']':
if dec.tokenState != tokenArrayStart && dec.tokenState != tokenArrayComma {
return dec.tokenError(c)
}
dec.scanp++
dec.tokenState = dec.tokenStack[len(dec.tokenStack)-1]
dec.tokenStack = dec.tokenStack[:len(dec.tokenStack)-1]
dec.tokenValueEnd()
return Delim(']'), nil
case '{':
if !dec.tokenValueAllowed() {
return dec.tokenError(c)
}
dec.scanp++
dec.tokenStack = append(dec.tokenStack, dec.tokenState)
dec.tokenState = tokenObjectStart
return Delim('{'), nil
case '}':
if dec.tokenState != tokenObjectStart && dec.tokenState != tokenObjectComma {
return dec.tokenError(c)
}
dec.scanp++
dec.tokenState = dec.tokenStack[len(dec.tokenStack)-1]
dec.tokenStack = dec.tokenStack[:len(dec.tokenStack)-1]
dec.tokenValueEnd()
return Delim('}'), nil
case ':':
if dec.tokenState != tokenObjectColon {
return dec.tokenError(c)
}
dec.scanp++
dec.tokenState = tokenObjectValue
continue
case ',':
if dec.tokenState == tokenArrayComma {
dec.scanp++
dec.tokenState = tokenArrayValue
continue
}
if dec.tokenState == tokenObjectComma {
dec.scanp++
dec.tokenState = tokenObjectKey
continue
}
return dec.tokenError(c)
case '"':
if dec.tokenState == tokenObjectStart || dec.tokenState == tokenObjectKey {
var x string
old := dec.tokenState
dec.tokenState = tokenTopValue
err := dec.Decode(&x)
dec.tokenState = old
if err != nil {
return nil, err
}
dec.tokenState = tokenObjectColon
return x, nil
}
fallthrough
default:
if !dec.tokenValueAllowed() {
return dec.tokenError(c)
}
var x any
if err := dec.Decode(&x); err != nil {
return nil, err
}
return x, nil
}
}
}
func (dec *Decoder) tokenError(c byte) (Token, error) {
var context string
switch dec.tokenState {
case tokenTopValue:
context = " looking for beginning of value"
case tokenArrayStart, tokenArrayValue, tokenObjectValue:
context = " looking for beginning of value"
case tokenArrayComma:
context = " after array element"
case tokenObjectKey:
context = " looking for beginning of object key string"
case tokenObjectColon:
context = " after object key"
case tokenObjectComma:
context = " after object key:value pair"
}
return nil, &SyntaxError{"invalid character " + quoteChar(c) + context, dec.InputOffset()}
}
// More reports whether there is another element in the
// current array or object being parsed.
func (dec *Decoder) More() bool {
c, err := dec.peek()
return err == nil && c != ']' && c != '}'
}
func (dec *Decoder) peek() (byte, error) {
var err error
for {
for i := dec.scanp; i < len(dec.buf); i++ {
c := dec.buf[i]
if isSpace(c) {
continue
}
dec.scanp = i
return c, nil
}
// buffer has been scanned, now report any error
if err != nil {
return 0, err
}
err = dec.refill()
}
}
// InputOffset returns the input stream byte offset of the current decoder position.
// The offset gives the location of the end of the most recently returned token
// and the beginning of the next token.
func (dec *Decoder) InputOffset() int64 {
return dec.scanned + int64(dec.scanp)
}

View File

@ -0,0 +1,218 @@
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package json
import "unicode/utf8"
// safeSet holds the value true if the ASCII character with the given array
// position can be represented inside a JSON string without any further
// escaping.
//
// All values are true except for the ASCII control characters (0-31), the
// double quote ("), and the backslash character ("\").
var safeSet = [utf8.RuneSelf]bool{
' ': true,
'!': true,
'"': false,
'#': true,
'$': true,
'%': true,
'&': true,
'\'': true,
'(': true,
')': true,
'*': true,
'+': true,
',': true,
'-': true,
'.': true,
'/': true,
'0': true,
'1': true,
'2': true,
'3': true,
'4': true,
'5': true,
'6': true,
'7': true,
'8': true,
'9': true,
':': true,
';': true,
'<': true,
'=': true,
'>': true,
'?': true,
'@': true,
'A': true,
'B': true,
'C': true,
'D': true,
'E': true,
'F': true,
'G': true,
'H': true,
'I': true,
'J': true,
'K': true,
'L': true,
'M': true,
'N': true,
'O': true,
'P': true,
'Q': true,
'R': true,
'S': true,
'T': true,
'U': true,
'V': true,
'W': true,
'X': true,
'Y': true,
'Z': true,
'[': true,
'\\': false,
']': true,
'^': true,
'_': true,
'`': true,
'a': true,
'b': true,
'c': true,
'd': true,
'e': true,
'f': true,
'g': true,
'h': true,
'i': true,
'j': true,
'k': true,
'l': true,
'm': true,
'n': true,
'o': true,
'p': true,
'q': true,
'r': true,
's': true,
't': true,
'u': true,
'v': true,
'w': true,
'x': true,
'y': true,
'z': true,
'{': true,
'|': true,
'}': true,
'~': true,
'\u007f': true,
}
// htmlSafeSet holds the value true if the ASCII character with the given
// array position can be safely represented inside a JSON string, embedded
// inside of HTML <script> tags, without any additional escaping.
//
// All values are true except for the ASCII control characters (0-31), the
// double quote ("), the backslash character ("\"), HTML opening and closing
// tags ("<" and ">"), and the ampersand ("&").
var htmlSafeSet = [utf8.RuneSelf]bool{
' ': true,
'!': true,
'"': false,
'#': true,
'$': true,
'%': true,
'&': false,
'\'': true,
'(': true,
')': true,
'*': true,
'+': true,
',': true,
'-': true,
'.': true,
'/': true,
'0': true,
'1': true,
'2': true,
'3': true,
'4': true,
'5': true,
'6': true,
'7': true,
'8': true,
'9': true,
':': true,
';': true,
'<': false,
'=': true,
'>': false,
'?': true,
'@': true,
'A': true,
'B': true,
'C': true,
'D': true,
'E': true,
'F': true,
'G': true,
'H': true,
'I': true,
'J': true,
'K': true,
'L': true,
'M': true,
'N': true,
'O': true,
'P': true,
'Q': true,
'R': true,
'S': true,
'T': true,
'U': true,
'V': true,
'W': true,
'X': true,
'Y': true,
'Z': true,
'[': true,
'\\': false,
']': true,
'^': true,
'_': true,
'`': true,
'a': true,
'b': true,
'c': true,
'd': true,
'e': true,
'f': true,
'g': true,
'h': true,
'i': true,
'j': true,
'k': true,
'l': true,
'm': true,
'n': true,
'o': true,
'p': true,
'q': true,
'r': true,
's': true,
't': true,
'u': true,
'v': true,
'w': true,
'x': true,
'y': true,
'z': true,
'{': true,
'|': true,
'}': true,
'~': true,
'\u007f': true,
}

View File

@ -0,0 +1,38 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package json
import (
"strings"
)
// tagOptions is the string following a comma in a struct field's "json"
// tag, or the empty string. It does not include the leading comma.
type tagOptions string
// parseTag splits a struct field's json tag into its name and
// comma-separated options.
func parseTag(tag string) (string, tagOptions) {
tag, opt, _ := strings.Cut(tag, ",")
return tag, tagOptions(opt)
}
// Contains reports whether a comma-separated list of options
// contains a particular substr flag. substr must be surrounded by a
// string boundary or commas.
func (o tagOptions) Contains(optionName string) bool {
if len(o) == 0 {
return false
}
s := string(o)
for s != "" {
var name string
name, s, _ = strings.Cut(s, ",")
if name == optionName {
return true
}
}
return false
}

View File

@ -2,9 +2,12 @@ package jsonpatch
import ( import (
"bytes" "bytes"
"encoding/json" "errors"
"fmt" "fmt"
"io"
"reflect" "reflect"
"github.com/evanphx/json-patch/v5/internal/json"
) )
func merge(cur, patch *lazyNode, mergeMerge bool) *lazyNode { func merge(cur, patch *lazyNode, mergeMerge bool) *lazyNode {
@ -88,14 +91,14 @@ func pruneDocNulls(doc *partialDoc) *partialDoc {
func pruneAryNulls(ary *partialArray) *partialArray { func pruneAryNulls(ary *partialArray) *partialArray {
newAry := []*lazyNode{} newAry := []*lazyNode{}
for _, v := range *ary { for _, v := range ary.nodes {
if v != nil { if v != nil {
pruneNulls(v) pruneNulls(v)
} }
newAry = append(newAry, v) newAry = append(newAry, v)
} }
*ary = newAry ary.nodes = newAry
return ary return ary
} }
@ -117,20 +120,28 @@ func MergePatch(docData, patchData []byte) ([]byte, error) {
} }
func doMergePatch(docData, patchData []byte, mergeMerge bool) ([]byte, error) { func doMergePatch(docData, patchData []byte, mergeMerge bool) ([]byte, error) {
if !json.Valid(docData) {
return nil, errBadJSONDoc
}
if !json.Valid(patchData) {
return nil, errBadJSONPatch
}
doc := &partialDoc{} doc := &partialDoc{}
docErr := json.Unmarshal(docData, doc) docErr := doc.UnmarshalJSON(docData)
patch := &partialDoc{} patch := &partialDoc{}
patchErr := json.Unmarshal(patchData, patch) patchErr := patch.UnmarshalJSON(patchData)
if isSyntaxError(docErr) { if isSyntaxError(docErr) {
return nil, errBadJSONDoc return nil, errBadJSONDoc
} }
if isSyntaxError(patchErr) { if isSyntaxError(patchErr) {
return nil, errBadJSONPatch return patchData, nil
} }
if docErr == nil && doc.obj == nil { if docErr == nil && doc.obj == nil {
@ -138,7 +149,7 @@ func doMergePatch(docData, patchData []byte, mergeMerge bool) ([]byte, error) {
} }
if patchErr == nil && patch.obj == nil { if patchErr == nil && patch.obj == nil {
return nil, errBadJSONPatch return patchData, nil
} }
if docErr != nil || patchErr != nil { if docErr != nil || patchErr != nil {
@ -151,15 +162,19 @@ func doMergePatch(docData, patchData []byte, mergeMerge bool) ([]byte, error) {
} }
} else { } else {
patchAry := &partialArray{} patchAry := &partialArray{}
patchErr = json.Unmarshal(patchData, patchAry) patchErr = unmarshal(patchData, &patchAry.nodes)
if patchErr != nil { if patchErr != nil {
// Not an array either, a literal is the result directly.
if json.Valid(patchData) {
return patchData, nil
}
return nil, errBadJSONPatch return nil, errBadJSONPatch
} }
pruneAryNulls(patchAry) pruneAryNulls(patchAry)
out, patchErr := json.Marshal(patchAry) out, patchErr := json.Marshal(patchAry.nodes)
if patchErr != nil { if patchErr != nil {
return nil, errBadJSONPatch return nil, errBadJSONPatch
@ -175,6 +190,12 @@ func doMergePatch(docData, patchData []byte, mergeMerge bool) ([]byte, error) {
} }
func isSyntaxError(err error) bool { func isSyntaxError(err error) bool {
if errors.Is(err, io.EOF) {
return true
}
if errors.Is(err, io.ErrUnexpectedEOF) {
return true
}
if _, ok := err.(*json.SyntaxError); ok { if _, ok := err.(*json.SyntaxError); ok {
return true return true
} }
@ -227,12 +248,12 @@ func createObjectMergePatch(originalJSON, modifiedJSON []byte) ([]byte, error) {
originalDoc := map[string]interface{}{} originalDoc := map[string]interface{}{}
modifiedDoc := map[string]interface{}{} modifiedDoc := map[string]interface{}{}
err := json.Unmarshal(originalJSON, &originalDoc) err := unmarshal(originalJSON, &originalDoc)
if err != nil { if err != nil {
return nil, errBadJSONDoc return nil, errBadJSONDoc
} }
err = json.Unmarshal(modifiedJSON, &modifiedDoc) err = unmarshal(modifiedJSON, &modifiedDoc)
if err != nil { if err != nil {
return nil, errBadJSONDoc return nil, errBadJSONDoc
} }
@ -245,6 +266,10 @@ func createObjectMergePatch(originalJSON, modifiedJSON []byte) ([]byte, error) {
return json.Marshal(dest) return json.Marshal(dest)
} }
func unmarshal(data []byte, into interface{}) error {
return json.UnmarshalValid(data, into)
}
// createArrayMergePatch will return an array of merge-patch documents capable // createArrayMergePatch will return an array of merge-patch documents capable
// of converting the original document to the modified document for each // of converting the original document to the modified document for each
// pair of JSON documents provided in the arrays. // pair of JSON documents provided in the arrays.
@ -253,12 +278,12 @@ func createArrayMergePatch(originalJSON, modifiedJSON []byte) ([]byte, error) {
originalDocs := []json.RawMessage{} originalDocs := []json.RawMessage{}
modifiedDocs := []json.RawMessage{} modifiedDocs := []json.RawMessage{}
err := json.Unmarshal(originalJSON, &originalDocs) err := unmarshal(originalJSON, &originalDocs)
if err != nil { if err != nil {
return nil, errBadJSONDoc return nil, errBadJSONDoc
} }
err = json.Unmarshal(modifiedJSON, &modifiedDocs) err = unmarshal(modifiedJSON, &modifiedDocs)
if err != nil { if err != nil {
return nil, errBadJSONDoc return nil, errBadJSONDoc
} }
@ -314,6 +339,11 @@ func matchesValue(av, bv interface{}) bool {
if bt == at { if bt == at {
return true return true
} }
case json.Number:
bt := bv.(json.Number)
if bt == at {
return true
}
case float64: case float64:
bt := bv.(float64) bt := bv.(float64)
if bt == at { if bt == at {
@ -377,7 +407,7 @@ func getDiff(a, b map[string]interface{}) (map[string]interface{}, error) {
if len(dst) > 0 { if len(dst) > 0 {
into[key] = dst into[key] = dst
} }
case string, float64, bool: case string, float64, bool, json.Number:
if !matchesValue(av, bv) { if !matchesValue(av, bv) {
into[key] = bv into[key] = bv
} }

View File

@ -2,11 +2,12 @@ package jsonpatch
import ( import (
"bytes" "bytes"
"encoding/json"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"unicode"
"github.com/evanphx/json-patch/v5/internal/json"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -45,7 +46,7 @@ var (
type lazyNode struct { type lazyNode struct {
raw *json.RawMessage raw *json.RawMessage
doc *partialDoc doc *partialDoc
ary partialArray ary *partialArray
which int which int
} }
@ -56,11 +57,15 @@ type Operation map[string]*json.RawMessage
type Patch []Operation type Patch []Operation
type partialDoc struct { type partialDoc struct {
self *lazyNode
keys []string keys []string
obj map[string]*lazyNode obj map[string]*lazyNode
} }
type partialArray []*lazyNode type partialArray struct {
self *lazyNode
nodes []*lazyNode
}
type container interface { type container interface {
get(key string, options *ApplyOptions) (*lazyNode, error) get(key string, options *ApplyOptions) (*lazyNode, error)
@ -107,14 +112,14 @@ func newRawMessage(buf []byte) *json.RawMessage {
return &ra return &ra
} }
func (n *lazyNode) MarshalJSON() ([]byte, error) { func (n *lazyNode) RedirectMarshalJSON() (any, error) {
switch n.which { switch n.which {
case eRaw: case eRaw:
return json.Marshal(n.raw) return n.raw, nil
case eDoc: case eDoc:
return json.Marshal(n.doc) return n.doc, nil
case eAry: case eAry:
return json.Marshal(n.ary) return n.ary.nodes, nil
default: default:
return nil, ErrUnknownType return nil, ErrUnknownType
} }
@ -128,39 +133,38 @@ func (n *lazyNode) UnmarshalJSON(data []byte) error {
return nil return nil
} }
func (n *partialDoc) MarshalJSON() ([]byte, error) { func (n *partialDoc) TrustMarshalJSON(buf *bytes.Buffer) error {
var buf bytes.Buffer if err := buf.WriteByte('{'); err != nil {
if _, err := buf.WriteString("{"); err != nil { return err
return nil, err
} }
for i, k := range n.keys { for i, k := range n.keys {
if i > 0 { if i > 0 {
if _, err := buf.WriteString(", "); err != nil { if err := buf.WriteByte(','); err != nil {
return nil, err return err
} }
} }
key, err := json.Marshal(k) key, err := json.Marshal(k)
if err != nil { if err != nil {
return nil, err return err
} }
if _, err := buf.Write(key); err != nil { if _, err := buf.Write(key); err != nil {
return nil, err return err
} }
if _, err := buf.WriteString(": "); err != nil { if err := buf.WriteByte(':'); err != nil {
return nil, err return err
} }
value, err := json.Marshal(n.obj[k]) value, err := json.Marshal(n.obj[k])
if err != nil { if err != nil {
return nil, err return err
} }
if _, err := buf.Write(value); err != nil { if _, err := buf.Write(value); err != nil {
return nil, err return err
} }
} }
if _, err := buf.WriteString("}"); err != nil { if err := buf.WriteByte('}'); err != nil {
return nil, err return err
} }
return buf.Bytes(), nil return nil
} }
type syntaxError struct { type syntaxError struct {
@ -172,70 +176,29 @@ func (err *syntaxError) Error() string {
} }
func (n *partialDoc) UnmarshalJSON(data []byte) error { func (n *partialDoc) UnmarshalJSON(data []byte) error {
if err := json.Unmarshal(data, &n.obj); err != nil { keys, err := json.UnmarshalValidWithKeys(data, &n.obj)
if err != nil {
return err return err
} }
buffer := bytes.NewBuffer(data)
d := json.NewDecoder(buffer) n.keys = keys
if t, err := d.Token(); err != nil {
return err
} else if t != startObject {
return &syntaxError{fmt.Sprintf("unexpected JSON token in document node: %s", t)}
}
for d.More() {
k, err := d.Token()
if err != nil {
return err
}
key, ok := k.(string)
if !ok {
return &syntaxError{fmt.Sprintf("unexpected JSON token as document node key: %s", k)}
}
if err := skipValue(d); err != nil {
return err
}
n.keys = append(n.keys, key)
}
return nil return nil
} }
func skipValue(d *json.Decoder) error { func (n *partialArray) UnmarshalJSON(data []byte) error {
t, err := d.Token() return json.UnmarshalValid(data, &n.nodes)
if err != nil { }
return err
} func (n *partialArray) RedirectMarshalJSON() (interface{}, error) {
if t != startObject && t != startArray { return n.nodes, nil
return nil
}
for d.More() {
if t == startObject {
// consume key token
if _, err := d.Token(); err != nil {
return err
}
}
if err := skipValue(d); err != nil {
return err
}
}
end, err := d.Token()
if err != nil {
return err
}
if t == startObject && end != endObject {
return &syntaxError{msg: "expected close object token"}
}
if t == startArray && end != endArray {
return &syntaxError{msg: "expected close object token"}
}
return nil
} }
func deepCopy(src *lazyNode) (*lazyNode, int, error) { func deepCopy(src *lazyNode) (*lazyNode, int, error) {
if src == nil { if src == nil {
return nil, 0, nil return nil, 0, nil
} }
a, err := src.MarshalJSON() a, err := json.Marshal(src)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
@ -243,6 +206,16 @@ func deepCopy(src *lazyNode) (*lazyNode, int, error) {
return newLazyNode(newRawMessage(a)), sz, nil return newLazyNode(newRawMessage(a)), sz, nil
} }
func (n *lazyNode) nextByte() byte {
s := []byte(*n.raw)
for unicode.IsSpace(rune(s[0])) {
s = s[1:]
}
return s[0]
}
func (n *lazyNode) intoDoc() (*partialDoc, error) { func (n *lazyNode) intoDoc() (*partialDoc, error) {
if n.which == eDoc { if n.which == eDoc {
return n.doc, nil return n.doc, nil
@ -252,7 +225,15 @@ func (n *lazyNode) intoDoc() (*partialDoc, error) {
return nil, ErrInvalid return nil, ErrInvalid
} }
err := json.Unmarshal(*n.raw, &n.doc) if n.nextByte() != '{' {
return nil, ErrInvalid
}
err := unmarshal(*n.raw, &n.doc)
if n.doc == nil {
return nil, ErrInvalid
}
if err != nil { if err != nil {
return nil, err return nil, err
@ -264,21 +245,21 @@ func (n *lazyNode) intoDoc() (*partialDoc, error) {
func (n *lazyNode) intoAry() (*partialArray, error) { func (n *lazyNode) intoAry() (*partialArray, error) {
if n.which == eAry { if n.which == eAry {
return &n.ary, nil return n.ary, nil
} }
if n.raw == nil { if n.raw == nil {
return nil, ErrInvalid return nil, ErrInvalid
} }
err := json.Unmarshal(*n.raw, &n.ary) err := unmarshal(*n.raw, &n.ary)
if err != nil { if err != nil {
return nil, err return nil, err
} }
n.which = eAry n.which = eAry
return &n.ary, nil return n.ary, nil
} }
func (n *lazyNode) compact() []byte { func (n *lazyNode) compact() []byte {
@ -302,12 +283,16 @@ func (n *lazyNode) tryDoc() bool {
return false return false
} }
err := json.Unmarshal(*n.raw, &n.doc) err := unmarshal(*n.raw, &n.doc)
if err != nil { if err != nil {
return false return false
} }
if n.doc == nil {
return false
}
n.which = eDoc n.which = eDoc
return true return true
} }
@ -317,7 +302,7 @@ func (n *lazyNode) tryAry() bool {
return false return false
} }
err := json.Unmarshal(*n.raw, &n.ary) err := unmarshal(*n.raw, &n.ary)
if err != nil { if err != nil {
return false return false
@ -327,6 +312,18 @@ func (n *lazyNode) tryAry() bool {
return true return true
} }
func (n *lazyNode) isNull() bool {
if n == nil {
return true
}
if n.raw == nil {
return true
}
return bytes.Equal(n.compact(), rawJSONNull)
}
func (n *lazyNode) equal(o *lazyNode) bool { func (n *lazyNode) equal(o *lazyNode) bool {
if n.which == eRaw { if n.which == eRaw {
if !n.tryDoc() && !n.tryAry() { if !n.tryDoc() && !n.tryAry() {
@ -334,7 +331,27 @@ func (n *lazyNode) equal(o *lazyNode) bool {
return false return false
} }
return bytes.Equal(n.compact(), o.compact()) nc := n.compact()
oc := o.compact()
if nc[0] == '"' && oc[0] == '"' {
// ok, 2 strings
var ns, os string
err := json.UnmarshalValid(nc, &ns)
if err != nil {
return false
}
err = json.UnmarshalValid(oc, &os)
if err != nil {
return false
}
return ns == os
}
return bytes.Equal(nc, oc)
} }
} }
@ -380,12 +397,12 @@ func (n *lazyNode) equal(o *lazyNode) bool {
return false return false
} }
if len(n.ary) != len(o.ary) { if len(n.ary.nodes) != len(o.ary.nodes) {
return false return false
} }
for idx, val := range n.ary { for idx, val := range n.ary.nodes {
if !val.equal(o.ary[idx]) { if !val.equal(o.ary.nodes[idx]) {
return false return false
} }
} }
@ -398,7 +415,7 @@ func (o Operation) Kind() string {
if obj, ok := o["op"]; ok && obj != nil { if obj, ok := o["op"]; ok && obj != nil {
var op string var op string
err := json.Unmarshal(*obj, &op) err := unmarshal(*obj, &op)
if err != nil { if err != nil {
return "unknown" return "unknown"
@ -415,7 +432,7 @@ func (o Operation) Path() (string, error) {
if obj, ok := o["path"]; ok && obj != nil { if obj, ok := o["path"]; ok && obj != nil {
var op string var op string
err := json.Unmarshal(*obj, &op) err := unmarshal(*obj, &op)
if err != nil { if err != nil {
return "unknown", err return "unknown", err
@ -432,7 +449,7 @@ func (o Operation) From() (string, error) {
if obj, ok := o["from"]; ok && obj != nil { if obj, ok := o["from"]; ok && obj != nil {
var op string var op string
err := json.Unmarshal(*obj, &op) err := unmarshal(*obj, &op)
if err != nil { if err != nil {
return "unknown", err return "unknown", err
@ -446,6 +463,10 @@ func (o Operation) From() (string, error) {
func (o Operation) value() *lazyNode { func (o Operation) value() *lazyNode {
if obj, ok := o["value"]; ok { if obj, ok := o["value"]; ok {
// A `null` gets decoded as a nil RawMessage, so let's fix it up here.
if obj == nil {
return newLazyNode(newRawMessage(rawJSONNull))
}
return newLazyNode(obj) return newLazyNode(obj)
} }
@ -454,10 +475,14 @@ func (o Operation) value() *lazyNode {
// ValueInterface decodes the operation value into an interface. // ValueInterface decodes the operation value into an interface.
func (o Operation) ValueInterface() (interface{}, error) { func (o Operation) ValueInterface() (interface{}, error) {
if obj, ok := o["value"]; ok && obj != nil { if obj, ok := o["value"]; ok {
if obj == nil {
return nil, nil
}
var v interface{} var v interface{}
err := json.Unmarshal(*obj, &v) err := unmarshal(*obj, &v)
if err != nil { if err != nil {
return nil, err return nil, err
@ -493,6 +518,9 @@ func findObject(pd *container, path string, options *ApplyOptions) (container, s
split := strings.Split(path, "/") split := strings.Split(path, "/")
if len(split) < 2 { if len(split) < 2 {
if path == "" {
return doc, ""
}
return nil, "" return nil, ""
} }
@ -548,6 +576,9 @@ func (d *partialDoc) add(key string, val *lazyNode, options *ApplyOptions) error
} }
func (d *partialDoc) get(key string, options *ApplyOptions) (*lazyNode, error) { func (d *partialDoc) get(key string, options *ApplyOptions) (*lazyNode, error) {
if key == "" {
return d.self, nil
}
v, ok := d.obj[key] v, ok := d.obj[key]
if !ok { if !ok {
return v, errors.Wrapf(ErrMissing, "unable to get nonexistent key: %s", key) return v, errors.Wrapf(ErrMissing, "unable to get nonexistent key: %s", key)
@ -587,19 +618,19 @@ func (d *partialArray) set(key string, val *lazyNode, options *ApplyOptions) err
if !options.SupportNegativeIndices { if !options.SupportNegativeIndices {
return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx) return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
} }
if idx < -len(*d) { if idx < -len(d.nodes) {
return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx) return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
} }
idx += len(*d) idx += len(d.nodes)
} }
(*d)[idx] = val d.nodes[idx] = val
return nil return nil
} }
func (d *partialArray) add(key string, val *lazyNode, options *ApplyOptions) error { func (d *partialArray) add(key string, val *lazyNode, options *ApplyOptions) error {
if key == "-" { if key == "-" {
*d = append(*d, val) d.nodes = append(d.nodes, val)
return nil return nil
} }
@ -608,11 +639,11 @@ func (d *partialArray) add(key string, val *lazyNode, options *ApplyOptions) err
return errors.Wrapf(err, "value was not a proper array index: '%s'", key) return errors.Wrapf(err, "value was not a proper array index: '%s'", key)
} }
sz := len(*d) + 1 sz := len(d.nodes) + 1
ary := make([]*lazyNode, sz) ary := make([]*lazyNode, sz)
cur := *d cur := d
if idx >= len(ary) { if idx >= len(ary) {
return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx) return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
@ -628,15 +659,19 @@ func (d *partialArray) add(key string, val *lazyNode, options *ApplyOptions) err
idx += len(ary) idx += len(ary)
} }
copy(ary[0:idx], cur[0:idx]) copy(ary[0:idx], cur.nodes[0:idx])
ary[idx] = val ary[idx] = val
copy(ary[idx+1:], cur[idx:]) copy(ary[idx+1:], cur.nodes[idx:])
*d = ary d.nodes = ary
return nil return nil
} }
func (d *partialArray) get(key string, options *ApplyOptions) (*lazyNode, error) { func (d *partialArray) get(key string, options *ApplyOptions) (*lazyNode, error) {
if key == "" {
return d.self, nil
}
idx, err := strconv.Atoi(key) idx, err := strconv.Atoi(key)
if err != nil { if err != nil {
@ -647,17 +682,17 @@ func (d *partialArray) get(key string, options *ApplyOptions) (*lazyNode, error)
if !options.SupportNegativeIndices { if !options.SupportNegativeIndices {
return nil, errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx) return nil, errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
} }
if idx < -len(*d) { if idx < -len(d.nodes) {
return nil, errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx) return nil, errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
} }
idx += len(*d) idx += len(d.nodes)
} }
if idx >= len(*d) { if idx >= len(d.nodes) {
return nil, errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx) return nil, errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
} }
return (*d)[idx], nil return d.nodes[idx], nil
} }
func (d *partialArray) remove(key string, options *ApplyOptions) error { func (d *partialArray) remove(key string, options *ApplyOptions) error {
@ -666,9 +701,9 @@ func (d *partialArray) remove(key string, options *ApplyOptions) error {
return err return err
} }
cur := *d cur := d
if idx >= len(cur) { if idx >= len(cur.nodes) {
if options.AllowMissingPathOnRemove { if options.AllowMissingPathOnRemove {
return nil return nil
} }
@ -679,21 +714,21 @@ func (d *partialArray) remove(key string, options *ApplyOptions) error {
if !options.SupportNegativeIndices { if !options.SupportNegativeIndices {
return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx) return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
} }
if idx < -len(cur) { if idx < -len(cur.nodes) {
if options.AllowMissingPathOnRemove { if options.AllowMissingPathOnRemove {
return nil return nil
} }
return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx) return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
} }
idx += len(cur) idx += len(cur.nodes)
} }
ary := make([]*lazyNode, len(cur)-1) ary := make([]*lazyNode, len(cur.nodes)-1)
copy(ary[0:idx], cur[0:idx]) copy(ary[0:idx], cur.nodes[0:idx])
copy(ary[idx:], cur[idx+1:]) copy(ary[idx:], cur.nodes[idx+1:])
*d = ary d.nodes = ary
return nil return nil
} }
@ -703,6 +738,32 @@ func (p Patch) add(doc *container, op Operation, options *ApplyOptions) error {
return errors.Wrapf(ErrMissing, "add operation failed to decode path") return errors.Wrapf(ErrMissing, "add operation failed to decode path")
} }
// special case, adding to empty means replacing the container with the value given
if path == "" {
val := op.value()
var pd container
if (*val.raw)[0] == '[' {
pd = &partialArray{
self: val,
}
} else {
pd = &partialDoc{
self: val,
}
}
err := json.UnmarshalValid(*val.raw, pd)
if err != nil {
return err
}
*doc = pd
return nil
}
if options.EnsurePathExistsOnAdd { if options.EnsurePathExistsOnAdd {
err = ensurePathExists(doc, path, options) err = ensurePathExists(doc, path, options)
@ -758,9 +819,9 @@ func ensurePathExists(pd *container, path string, options *ApplyOptions) error {
if arrIndex, err = strconv.Atoi(part); err == nil { if arrIndex, err = strconv.Atoi(part); err == nil {
pa, ok := doc.(*partialArray) pa, ok := doc.(*partialArray)
if ok && arrIndex >= len(*pa)+1 { if ok && arrIndex >= len(pa.nodes)+1 {
// Pad the array with null values up to the required index. // Pad the array with null values up to the required index.
for i := len(*pa); i <= arrIndex-1; i++ { for i := len(pa.nodes); i <= arrIndex-1; i++ {
doc.add(strconv.Itoa(i), newLazyNode(newRawMessage(rawJSONNull)), options) doc.add(strconv.Itoa(i), newLazyNode(newRawMessage(rawJSONNull)), options)
} }
} }
@ -794,7 +855,10 @@ func ensurePathExists(pd *container, path string, options *ApplyOptions) error {
newNode := newLazyNode(newRawMessage(rawJSONObject)) newNode := newLazyNode(newRawMessage(rawJSONObject))
doc.add(part, newNode, options) doc.add(part, newNode, options)
doc, _ = newNode.intoDoc() doc, err = newNode.intoDoc()
if err != nil {
return err
}
} }
} else { } else {
if isArray(*target.raw) { if isArray(*target.raw) {
@ -816,6 +880,43 @@ func ensurePathExists(pd *container, path string, options *ApplyOptions) error {
return nil return nil
} }
func validateOperation(op Operation) error {
switch op.Kind() {
case "add", "replace":
if _, err := op.ValueInterface(); err != nil {
return errors.Wrapf(err, "failed to decode 'value'")
}
case "move", "copy":
if _, err := op.From(); err != nil {
return errors.Wrapf(err, "failed to decode 'from'")
}
case "remove", "test":
default:
return fmt.Errorf("unsupported operation")
}
if _, err := op.Path(); err != nil {
return errors.Wrapf(err, "failed to decode 'path'")
}
return nil
}
func validatePatch(p Patch) error {
for _, op := range p {
if err := validateOperation(op); err != nil {
opData, infoErr := json.Marshal(op)
if infoErr != nil {
return errors.Wrapf(err, "invalid operation")
}
return errors.Wrapf(err, "invalid operation %s", opData)
}
}
return nil
}
func (p Patch) remove(doc *container, op Operation, options *ApplyOptions) error { func (p Patch) remove(doc *container, op Operation, options *ApplyOptions) error {
path, err := op.Path() path, err := op.Path()
if err != nil { if err != nil {
@ -858,7 +959,7 @@ func (p Patch) replace(doc *container, op Operation, options *ApplyOptions) erro
switch val.which { switch val.which {
case eAry: case eAry:
*doc = &val.ary *doc = val.ary
case eDoc: case eDoc:
*doc = val.doc *doc = val.doc
case eRaw: case eRaw:
@ -893,6 +994,10 @@ func (p Patch) move(doc *container, op Operation, options *ApplyOptions) error {
return errors.Wrapf(err, "move operation failed to decode from") return errors.Wrapf(err, "move operation failed to decode from")
} }
if from == "" {
return errors.Wrapf(ErrInvalid, "unable to move entire document to another path")
}
con, key := findObject(doc, from, options) con, key := findObject(doc, from, options)
if con == nil { if con == nil {
@ -942,7 +1047,7 @@ func (p Patch) test(doc *container, op Operation, options *ApplyOptions) error {
self.doc = sv self.doc = sv
self.which = eDoc self.which = eDoc
case *partialArray: case *partialArray:
self.ary = *sv self.ary = sv
self.which = eAry self.which = eAry
} }
@ -964,12 +1069,14 @@ func (p Patch) test(doc *container, op Operation, options *ApplyOptions) error {
return errors.Wrapf(err, "error in test for path: '%s'", path) return errors.Wrapf(err, "error in test for path: '%s'", path)
} }
ov := op.value()
if val == nil { if val == nil {
if op.value().raw == nil { if ov.isNull() {
return nil return nil
} }
return errors.Wrapf(ErrTestFailed, "testing value %s failed", path) return errors.Wrapf(ErrTestFailed, "testing value %s failed", path)
} else if op.value() == nil { } else if ov.isNull() {
return errors.Wrapf(ErrTestFailed, "testing value %s failed", path) return errors.Wrapf(ErrTestFailed, "testing value %s failed", path)
} }
@ -989,7 +1096,7 @@ func (p Patch) copy(doc *container, op Operation, accumulatedCopySize *int64, op
con, key := findObject(doc, from, options) con, key := findObject(doc, from, options)
if con == nil { if con == nil {
return errors.Wrapf(ErrMissing, "copy operation does not apply: doc is missing from path: %s", from) return errors.Wrapf(ErrMissing, "copy operation does not apply: doc is missing from path: \"%s\"", from)
} }
val, err := con.get(key, options) val, err := con.get(key, options)
@ -1036,14 +1143,22 @@ func Equal(a, b []byte) bool {
// DecodePatch decodes the passed JSON document as an RFC 6902 patch. // DecodePatch decodes the passed JSON document as an RFC 6902 patch.
func DecodePatch(buf []byte) (Patch, error) { func DecodePatch(buf []byte) (Patch, error) {
if !json.Valid(buf) {
return nil, ErrInvalid
}
var p Patch var p Patch
err := json.Unmarshal(buf, &p) err := unmarshal(buf, &p)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err := validatePatch(p); err != nil {
return nil, err
}
return p, nil return p, nil
} }
@ -1072,14 +1187,25 @@ func (p Patch) ApplyIndentWithOptions(doc []byte, indent string, options *ApplyO
return doc, nil return doc, nil
} }
var pd container if !json.Valid(doc) {
if doc[0] == '[' { return nil, ErrInvalid
pd = &partialArray{}
} else {
pd = &partialDoc{}
} }
err := json.Unmarshal(doc, pd) raw := json.RawMessage(doc)
self := newLazyNode(&raw)
var pd container
if doc[0] == '[' {
pd = &partialArray{
self: self,
}
} else {
pd = &partialDoc{
self: self,
}
}
err := unmarshal(doc, pd)
if err != nil { if err != nil {
return nil, err return nil, err

61
vendor/github.com/go-logr/logr/slogr/slogr.go generated vendored Normal file
View File

@ -0,0 +1,61 @@
//go:build go1.21
// +build go1.21
/*
Copyright 2023 The logr 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 slogr enables usage of a slog.Handler with logr.Logger as front-end
// API and of a logr.LogSink through the slog.Handler and thus slog.Logger
// APIs.
//
// See the README in the top-level [./logr] package for a discussion of
// interoperability.
//
// Deprecated: use the main logr package instead.
package slogr
import (
"log/slog"
"github.com/go-logr/logr"
)
// NewLogr returns a logr.Logger which writes to the slog.Handler.
//
// Deprecated: use [logr.FromSlogHandler] instead.
func NewLogr(handler slog.Handler) logr.Logger {
return logr.FromSlogHandler(handler)
}
// NewSlogHandler returns a slog.Handler which writes to the same sink as the logr.Logger.
//
// Deprecated: use [logr.ToSlogHandler] instead.
func NewSlogHandler(logger logr.Logger) slog.Handler {
return logr.ToSlogHandler(logger)
}
// ToSlogHandler returns a slog.Handler which writes to the same sink as the logr.Logger.
//
// Deprecated: use [logr.ToSlogHandler] instead.
func ToSlogHandler(logger logr.Logger) slog.Handler {
return logr.ToSlogHandler(logger)
}
// SlogSink is an optional interface that a LogSink can implement to support
// logging through the slog.Logger or slog.Handler APIs better.
//
// Deprecated: use [logr.SlogSink] instead.
type SlogSink = logr.SlogSink

20
vendor/github.com/go-logr/zapr/.golangci.yaml generated vendored Normal file
View File

@ -0,0 +1,20 @@
issues:
exclude-use-default: false
linters:
disable-all: true
enable:
- asciicheck
- errcheck
- forcetypeassert
- gocritic
- gofmt
- goimports
- gosimple
- govet
- ineffassign
- misspell
- revive
- staticcheck
- typecheck
- unused

View File

@ -2,12 +2,17 @@ Zapr :zap:
========== ==========
A [logr](https://github.com/go-logr/logr) implementation using A [logr](https://github.com/go-logr/logr) implementation using
[Zap](https://github.com/uber-go/zap). [Zap](https://github.com/uber-go/zap). Can also be used as
[slog](https://pkg.go.dev/log/slog) handler.
Usage Usage
----- -----
Via logr:
```go ```go
package main
import ( import (
"fmt" "fmt"
@ -29,6 +34,33 @@ func main() {
} }
``` ```
Via slog:
```
package main
import (
"fmt"
"log/slog"
"github.com/go-logr/logr/slogr"
"github.com/go-logr/zapr"
"go.uber.org/zap"
)
func main() {
var log *slog.Logger
zapLog, err := zap.NewDevelopment()
if err != nil {
panic(fmt.Sprintf("who watches the watchmen (%v)?", err))
}
log = slog.New(slogr.NewSlogHandler(zapr.NewLogger(zapLog)))
log.Info("Logr in action!", "the answer", 42)
}
```
Increasing Verbosity Increasing Verbosity
-------------------- --------------------
@ -68,3 +100,8 @@ For the most part, concepts in Zap correspond directly with those in logr.
Unlike Zap, all fields *must* be in the form of sugared fields -- Unlike Zap, all fields *must* be in the form of sugared fields --
it's illegal to pass a strongly-typed Zap field in a key position to any it's illegal to pass a strongly-typed Zap field in a key position to any
of the logging methods (`Log`, `Error`). of the logging methods (`Log`, `Error`).
The zapr `logr.LogSink` implementation also implements `logr.SlogHandler`. That
enables `slogr.NewSlogHandler` to provide a `slog.Handler` which just passes
parameters through to zapr. zapr handles special slog values (Group,
LogValuer), regardless of which front-end API is used.

183
vendor/github.com/go-logr/zapr/slogzapr.go generated vendored Normal file
View File

@ -0,0 +1,183 @@
//go:build go1.21
// +build go1.21
/*
Copyright 2023 The logr 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 zapr
import (
"context"
"log/slog"
"runtime"
"github.com/go-logr/logr/slogr"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
var _ slogr.SlogSink = &zapLogger{}
func (zl *zapLogger) Handle(_ context.Context, record slog.Record) error {
zapLevel := zap.InfoLevel
intLevel := 0
isError := false
switch {
case record.Level >= slog.LevelError:
zapLevel = zap.ErrorLevel
isError = true
case record.Level >= slog.LevelWarn:
zapLevel = zap.WarnLevel
case record.Level >= 0:
// Already set above -> info.
default:
zapLevel = zapcore.Level(record.Level)
intLevel = int(-zapLevel)
}
if checkedEntry := zl.l.Check(zapLevel, record.Message); checkedEntry != nil {
checkedEntry.Time = record.Time
checkedEntry.Caller = pcToCallerEntry(record.PC)
var fieldsBuffer [2]zap.Field
fields := fieldsBuffer[:0]
if !isError && zl.numericLevelKey != "" {
// Record verbosity for info entries.
fields = append(fields, zap.Int(zl.numericLevelKey, intLevel))
}
// Inline all attributes.
fields = append(fields, zap.Inline(zapcore.ObjectMarshalerFunc(func(enc zapcore.ObjectEncoder) error {
record.Attrs(func(attr slog.Attr) bool {
encodeSlog(enc, attr)
return true
})
return nil
})))
checkedEntry.Write(fields...)
}
return nil
}
func encodeSlog(enc zapcore.ObjectEncoder, attr slog.Attr) {
if attr.Equal(slog.Attr{}) {
// Ignore empty attribute.
return
}
// Check in order of expected frequency, most common ones first.
//
// Usage statistics for parameters from Kubernetes 152876a3e,
// calculated with k/k/test/integration/logs/benchmark:
//
// kube-controller-manager -v10:
// strings: 10043 (85%)
// with API objects: 2 (0% of all arguments)
// types and their number of usage: NodeStatus:2
// numbers: 792 (6%)
// ObjectRef: 292 (2%)
// others: 595 (5%)
//
// kube-scheduler -v10:
// strings: 1325 (40%)
// with API objects: 109 (3% of all arguments)
// types and their number of usage: PersistentVolume:50 PersistentVolumeClaim:59
// numbers: 473 (14%)
// ObjectRef: 1305 (39%)
// others: 176 (5%)
kind := attr.Value.Kind()
switch kind {
case slog.KindString:
enc.AddString(attr.Key, attr.Value.String())
case slog.KindLogValuer:
// This includes klog.KObj.
encodeSlog(enc, slog.Attr{
Key: attr.Key,
Value: attr.Value.Resolve(),
})
case slog.KindInt64:
enc.AddInt64(attr.Key, attr.Value.Int64())
case slog.KindUint64:
enc.AddUint64(attr.Key, attr.Value.Uint64())
case slog.KindFloat64:
enc.AddFloat64(attr.Key, attr.Value.Float64())
case slog.KindBool:
enc.AddBool(attr.Key, attr.Value.Bool())
case slog.KindDuration:
enc.AddDuration(attr.Key, attr.Value.Duration())
case slog.KindTime:
enc.AddTime(attr.Key, attr.Value.Time())
case slog.KindGroup:
attrs := attr.Value.Group()
if attr.Key == "" {
// Inline group.
for _, attr := range attrs {
encodeSlog(enc, attr)
}
return
}
if len(attrs) == 0 {
// Ignore empty group.
return
}
_ = enc.AddObject(attr.Key, marshalAttrs(attrs))
default:
// We have to go through reflection in zap.Any to get support
// for e.g. fmt.Stringer.
zap.Any(attr.Key, attr.Value.Any()).AddTo(enc)
}
}
type marshalAttrs []slog.Attr
func (attrs marshalAttrs) MarshalLogObject(enc zapcore.ObjectEncoder) error {
for _, attr := range attrs {
encodeSlog(enc, attr)
}
return nil
}
var _ zapcore.ObjectMarshaler = marshalAttrs(nil)
func pcToCallerEntry(pc uintptr) zapcore.EntryCaller {
if pc == 0 {
return zapcore.EntryCaller{}
}
// Same as https://cs.opensource.google/go/x/exp/+/642cacee:slog/record.go;drc=642cacee5cc05231f45555a333d07f1005ffc287;l=70
fs := runtime.CallersFrames([]uintptr{pc})
f, _ := fs.Next()
if f.File == "" {
return zapcore.EntryCaller{}
}
return zapcore.EntryCaller{
Defined: true,
PC: pc,
File: f.File,
Line: f.Line,
Function: f.Function,
}
}
func (zl *zapLogger) WithAttrs(attrs []slog.Attr) slogr.SlogSink {
newLogger := *zl
newLogger.l = newLogger.l.With(zap.Inline(marshalAttrs(attrs)))
return &newLogger
}
func (zl *zapLogger) WithGroup(name string) slogr.SlogSink {
newLogger := *zl
newLogger.l = newLogger.l.With(zap.Namespace(name))
return &newLogger
}

View File

@ -31,14 +31,14 @@ limitations under the License.
// Package zapr defines an implementation of the github.com/go-logr/logr // Package zapr defines an implementation of the github.com/go-logr/logr
// interfaces built on top of Zap (go.uber.org/zap). // interfaces built on top of Zap (go.uber.org/zap).
// //
// Usage // # Usage
// //
// A new logr.Logger can be constructed from an existing zap.Logger using // A new logr.Logger can be constructed from an existing zap.Logger using
// the NewLogger function: // the NewLogger function:
// //
// log := zapr.NewLogger(someZapLogger) // log := zapr.NewLogger(someZapLogger)
// //
// Implementation Details // # Implementation Details
// //
// For the most part, concepts in Zap correspond directly with those in // For the most part, concepts in Zap correspond directly with those in
// logr. // logr.
@ -168,15 +168,6 @@ func (zl *zapLogger) handleFields(lvl int, args []interface{}, additional ...zap
return append(fields, additional...) return append(fields, additional...)
} }
func zapIt(field string, val interface{}) zap.Field {
// Handle types that implement logr.Marshaler: log the replacement
// object instead of the original one.
if marshaler, ok := val.(logr.Marshaler); ok {
field, val = invokeMarshaler(field, marshaler)
}
return zap.Any(field, val)
}
func invokeMarshaler(field string, m logr.Marshaler) (f string, ret interface{}) { func invokeMarshaler(field string, m logr.Marshaler) (f string, ret interface{}) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {

34
vendor/github.com/go-logr/zapr/zapr_noslog.go generated vendored Normal file
View File

@ -0,0 +1,34 @@
//go:build !go1.21
// +build !go1.21
/*
Copyright 2023 The logr 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 zapr
import (
"github.com/go-logr/logr"
"go.uber.org/zap"
)
func zapIt(field string, val interface{}) zap.Field {
// Handle types that implement logr.Marshaler: log the replacement
// object instead of the original one.
if marshaler, ok := val.(logr.Marshaler); ok {
field, val = invokeMarshaler(field, marshaler)
}
return zap.Any(field, val)
}

48
vendor/github.com/go-logr/zapr/zapr_slog.go generated vendored Normal file
View File

@ -0,0 +1,48 @@
//go:build go1.21
// +build go1.21
/*
Copyright 2023 The logr 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 zapr
import (
"log/slog"
"github.com/go-logr/logr"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func zapIt(field string, val interface{}) zap.Field {
switch valTyped := val.(type) {
case logr.Marshaler:
// Handle types that implement logr.Marshaler: log the replacement
// object instead of the original one.
field, val = invokeMarshaler(field, valTyped)
case slog.LogValuer:
// The same for slog.LogValuer. We let slog.Value handle
// potential panics and recursion.
val = slog.AnyValue(val).Resolve()
}
if slogValue, ok := val.(slog.Value); ok {
return zap.Inline(zapcore.ObjectMarshalerFunc(func(enc zapcore.ObjectEncoder) error {
encodeSlog(enc, slog.Attr{Key: field, Value: slogValue})
return nil
}))
}
return zap.Any(field, val)
}

15
vendor/modules.txt vendored
View File

@ -79,9 +79,10 @@ github.com/emirpasic/gods/utils
# github.com/evanphx/json-patch v5.7.0+incompatible # github.com/evanphx/json-patch v5.7.0+incompatible
## explicit ## explicit
github.com/evanphx/json-patch github.com/evanphx/json-patch
# github.com/evanphx/json-patch/v5 v5.6.0 # github.com/evanphx/json-patch/v5 v5.8.0
## explicit; go 1.12 ## explicit; go 1.18
github.com/evanphx/json-patch/v5 github.com/evanphx/json-patch/v5
github.com/evanphx/json-patch/v5/internal/json
# github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d # github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d
## explicit ## explicit
github.com/exponent-io/jsonpath github.com/exponent-io/jsonpath
@ -107,11 +108,12 @@ github.com/go-errors/errors
## explicit; go 1.18 ## explicit; go 1.18
github.com/go-logr/logr github.com/go-logr/logr
github.com/go-logr/logr/funcr github.com/go-logr/logr/funcr
github.com/go-logr/logr/slogr
# github.com/go-logr/stdr v1.2.2 # github.com/go-logr/stdr v1.2.2
## explicit; go 1.16 ## explicit; go 1.16
github.com/go-logr/stdr github.com/go-logr/stdr
# github.com/go-logr/zapr v1.2.4 # github.com/go-logr/zapr v1.3.0
## explicit; go 1.16 ## explicit; go 1.18
github.com/go-logr/zapr github.com/go-logr/zapr
# github.com/go-openapi/jsonpointer v0.20.2 # github.com/go-openapi/jsonpointer v0.20.2
## explicit; go 1.19 ## explicit; go 1.19
@ -1753,8 +1755,8 @@ sigs.k8s.io/cluster-api/util/certs
sigs.k8s.io/cluster-api/util/labels/format sigs.k8s.io/cluster-api/util/labels/format
sigs.k8s.io/cluster-api/util/secret sigs.k8s.io/cluster-api/util/secret
sigs.k8s.io/cluster-api/util/version sigs.k8s.io/cluster-api/util/version
# sigs.k8s.io/controller-runtime v0.16.3 # sigs.k8s.io/controller-runtime v0.17.5
## explicit; go 1.20 ## explicit; go 1.21
sigs.k8s.io/controller-runtime sigs.k8s.io/controller-runtime
sigs.k8s.io/controller-runtime/pkg/builder sigs.k8s.io/controller-runtime/pkg/builder
sigs.k8s.io/controller-runtime/pkg/cache sigs.k8s.io/controller-runtime/pkg/cache
@ -1784,6 +1786,7 @@ sigs.k8s.io/controller-runtime/pkg/internal/log
sigs.k8s.io/controller-runtime/pkg/internal/objectutil sigs.k8s.io/controller-runtime/pkg/internal/objectutil
sigs.k8s.io/controller-runtime/pkg/internal/recorder sigs.k8s.io/controller-runtime/pkg/internal/recorder
sigs.k8s.io/controller-runtime/pkg/internal/source sigs.k8s.io/controller-runtime/pkg/internal/source
sigs.k8s.io/controller-runtime/pkg/internal/syncs
sigs.k8s.io/controller-runtime/pkg/leaderelection sigs.k8s.io/controller-runtime/pkg/leaderelection
sigs.k8s.io/controller-runtime/pkg/log sigs.k8s.io/controller-runtime/pkg/log
sigs.k8s.io/controller-runtime/pkg/manager sigs.k8s.io/controller-runtime/pkg/manager

View File

@ -59,9 +59,9 @@ linters-settings:
- pkg: sigs.k8s.io/controller-runtime - pkg: sigs.k8s.io/controller-runtime
alias: ctrl alias: ctrl
staticcheck: staticcheck:
go: "1.20" go: "1.21"
stylecheck: stylecheck:
go: "1.20" go: "1.21"
revive: revive:
rules: rules:
# The following rules are recommended https://github.com/mgechev/revive#recommended-configuration # The following rules are recommended https://github.com/mgechev/revive#recommended-configuration

View File

@ -4,9 +4,9 @@ The Kubernetes controller-runtime Project is released on an as-needed basis. The
**Note:** Releases are done from the `release-MAJOR.MINOR` branches. For PATCH releases is not required **Note:** Releases are done from the `release-MAJOR.MINOR` branches. For PATCH releases is not required
to create a new branch you will just need to ensure that all big fixes are cherry-picked into the respective to create a new branch you will just need to ensure that all big fixes are cherry-picked into the respective
`release-MAJOR.MINOR` branch. To know more about versioning check https://semver.org/. `release-MAJOR.MINOR` branch. To know more about versioning check https://semver.org/.
## How to do a release ## How to do a release
### Create the new branch and the release tag ### Create the new branch and the release tag
@ -15,7 +15,7 @@ to create a new branch you will just need to ensure that all big fixes are cherr
### Now, let's generate the changelog ### Now, let's generate the changelog
1. Create the changelog from the new branch `release-<MAJOR.MINOR>` (`git checkout release-<MAJOR.MINOR>`). 1. Create the changelog from the new branch `release-<MAJOR.MINOR>` (`git checkout release-<MAJOR.MINOR>`).
You will need to use the [kubebuilder-release-tools][kubebuilder-release-tools] to generate the notes. See [here][release-notes-generation] You will need to use the [kubebuilder-release-tools][kubebuilder-release-tools] to generate the notes. See [here][release-notes-generation]
> **Note** > **Note**
@ -24,12 +24,12 @@ You will need to use the [kubebuilder-release-tools][kubebuilder-release-tools]
### Draft a new release from GitHub ### Draft a new release from GitHub
1. Create a new tag with the correct version from the new `release-<MAJOR.MINOR>` branch 1. Create a new tag with the correct version from the new `release-<MAJOR.MINOR>` branch
2. Add the changelog on it and publish. Now, the code source is released ! 2. Add the changelog on it and publish. Now, the code source is released !
### Add a new Prow test the for the new branch release ### Add a new Prow test the for the new branch release
1. Create a new prow test under [github.com/kubernetes/test-infra/tree/master/config/jobs/kubernetes-sigs/controller-runtime](https://github.com/kubernetes/test-infra/tree/master/config/jobs/kubernetes-sigs/controller-runtime) 1. Create a new prow test under [github.com/kubernetes/test-infra/tree/master/config/jobs/kubernetes-sigs/controller-runtime](https://github.com/kubernetes/test-infra/tree/master/config/jobs/kubernetes-sigs/controller-runtime)
for the new `release-<MAJOR.MINOR>` branch. (i.e. for the `0.11.0` release see the PR: https://github.com/kubernetes/test-infra/pull/25205) for the new `release-<MAJOR.MINOR>` branch. (i.e. for the `0.11.0` release see the PR: https://github.com/kubernetes/test-infra/pull/25205)
2. Ping the infra PR in the controller-runtime slack channel for reviews. 2. Ping the infra PR in the controller-runtime slack channel for reviews.
@ -45,3 +45,7 @@ For more info, see the release page: https://github.com/kubernetes-sigs/controll
```` ````
2. An announcement email is sent to `kubebuilder@googlegroups.com` with the subject `[ANNOUNCE] Controller-Runtime $VERSION is released` 2. An announcement email is sent to `kubebuilder@googlegroups.com` with the subject `[ANNOUNCE] Controller-Runtime $VERSION is released`
[kubebuilder-release-tools]: https://github.com/kubernetes-sigs/kubebuilder-release-tools
[release-notes-generation]: https://github.com/kubernetes-sigs/kubebuilder-release-tools/blob/master/README.md#release-notes-generation
[release-process]: https://github.com/kubernetes-sigs/kubebuilder/blob/master/VERSIONING.md#releasing

View File

@ -20,6 +20,7 @@ import (
"context" "context"
"fmt" "fmt"
"net/http" "net/http"
"sort"
"time" "time"
"golang.org/x/exp/maps" "golang.org/x/exp/maps"
@ -33,7 +34,7 @@ import (
"k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
toolscache "k8s.io/client-go/tools/cache" toolscache "k8s.io/client-go/tools/cache"
"k8s.io/utils/pointer" "k8s.io/utils/ptr"
"sigs.k8s.io/controller-runtime/pkg/cache/internal" "sigs.k8s.io/controller-runtime/pkg/cache/internal"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
@ -83,6 +84,9 @@ type Informers interface {
// of the underlying object. // of the underlying object.
GetInformerForKind(ctx context.Context, gvk schema.GroupVersionKind, opts ...InformerGetOption) (Informer, error) GetInformerForKind(ctx context.Context, gvk schema.GroupVersionKind, opts ...InformerGetOption) (Informer, error)
// RemoveInformer removes an informer entry and stops it if it was running.
RemoveInformer(ctx context.Context, obj client.Object) error
// Start runs all the informers known to this cache until the context is closed. // Start runs all the informers known to this cache until the context is closed.
// It blocks. // It blocks.
Start(ctx context.Context) error Start(ctx context.Context) error
@ -121,6 +125,8 @@ type Informer interface {
// HasSynced return true if the informers underlying store has synced. // HasSynced return true if the informers underlying store has synced.
HasSynced() bool HasSynced() bool
// IsStopped returns true if the informer has been stopped.
IsStopped() bool
} }
// AllNamespaces should be used as the map key to deliminate namespace settings // AllNamespaces should be used as the map key to deliminate namespace settings
@ -199,6 +205,12 @@ type Options struct {
// unless there is already one set in ByObject or DefaultNamespaces. // unless there is already one set in ByObject or DefaultNamespaces.
DefaultTransform toolscache.TransformFunc DefaultTransform toolscache.TransformFunc
// DefaultWatchErrorHandler will be used to the WatchErrorHandler which is called
// whenever ListAndWatch drops the connection with an error.
//
// After calling this handler, the informer will backoff and retry.
DefaultWatchErrorHandler toolscache.WatchErrorHandler
// DefaultUnsafeDisableDeepCopy is the default for UnsafeDisableDeepCopy // DefaultUnsafeDisableDeepCopy is the default for UnsafeDisableDeepCopy
// for everything that doesn't specify this. // for everything that doesn't specify this.
// //
@ -369,7 +381,8 @@ func newCache(restConfig *rest.Config, opts Options) newCacheFunc {
Field: config.FieldSelector, Field: config.FieldSelector,
}, },
Transform: config.Transform, Transform: config.Transform,
UnsafeDisableDeepCopy: pointer.BoolDeref(config.UnsafeDisableDeepCopy, false), WatchErrorHandler: opts.DefaultWatchErrorHandler,
UnsafeDisableDeepCopy: ptr.Deref(config.UnsafeDisableDeepCopy, false),
NewInformer: opts.newInformer, NewInformer: opts.newInformer,
}), }),
readerFailOnMissingInformer: opts.ReaderFailOnMissingInformer, readerFailOnMissingInformer: opts.ReaderFailOnMissingInformer,
@ -400,20 +413,12 @@ func defaultOpts(config *rest.Config, opts Options) (Options, error) {
// Construct a new Mapper if unset // Construct a new Mapper if unset
if opts.Mapper == nil { if opts.Mapper == nil {
var err error var err error
opts.Mapper, err = apiutil.NewDiscoveryRESTMapper(config, opts.HTTPClient) opts.Mapper, err = apiutil.NewDynamicRESTMapper(config, opts.HTTPClient)
if err != nil { if err != nil {
return Options{}, fmt.Errorf("could not create RESTMapper from config: %w", err) return Options{}, fmt.Errorf("could not create RESTMapper from config: %w", err)
} }
} }
for namespace, cfg := range opts.DefaultNamespaces {
cfg = defaultConfig(cfg, optionDefaultsToConfig(&opts))
if namespace == metav1.NamespaceAll {
cfg.FieldSelector = fields.AndSelectors(appendIfNotNil(namespaceAllSelector(maps.Keys(opts.DefaultNamespaces)), cfg.FieldSelector)...)
}
opts.DefaultNamespaces[namespace] = cfg
}
for obj, byObject := range opts.ByObject { for obj, byObject := range opts.ByObject {
isNamespaced, err := apiutil.IsObjectNamespaced(obj, opts.Scheme, opts.Mapper) isNamespaced, err := apiutil.IsObjectNamespaced(obj, opts.Scheme, opts.Mapper)
if err != nil { if err != nil {
@ -423,7 +428,12 @@ func defaultOpts(config *rest.Config, opts Options) (Options, error) {
return opts, fmt.Errorf("type %T is not namespaced, but its ByObject.Namespaces setting is not nil", obj) return opts, fmt.Errorf("type %T is not namespaced, but its ByObject.Namespaces setting is not nil", obj)
} }
// Default the namespace-level configs first, because they need to use the undefaulted type-level config. if isNamespaced && byObject.Namespaces == nil {
byObject.Namespaces = maps.Clone(opts.DefaultNamespaces)
}
// Default the namespace-level configs first, because they need to use the undefaulted type-level config
// to be able to potentially fall through to settings from DefaultNamespaces.
for namespace, config := range byObject.Namespaces { for namespace, config := range byObject.Namespaces {
// 1. Default from the undefaulted type-level config // 1. Default from the undefaulted type-level config
config = defaultConfig(config, byObjectToConfig(byObject)) config = defaultConfig(config, byObjectToConfig(byObject))
@ -449,19 +459,35 @@ func defaultOpts(config *rest.Config, opts Options) (Options, error) {
byObject.Namespaces[namespace] = config byObject.Namespaces[namespace] = config
} }
defaultedConfig := defaultConfig(byObjectToConfig(byObject), optionDefaultsToConfig(&opts)) // Only default ByObject iself if it isn't namespaced or has no namespaces configured, as only
byObject.Label = defaultedConfig.LabelSelector // then any of this will be honored.
byObject.Field = defaultedConfig.FieldSelector if !isNamespaced || len(byObject.Namespaces) == 0 {
byObject.Transform = defaultedConfig.Transform defaultedConfig := defaultConfig(byObjectToConfig(byObject), optionDefaultsToConfig(&opts))
byObject.UnsafeDisableDeepCopy = defaultedConfig.UnsafeDisableDeepCopy byObject.Label = defaultedConfig.LabelSelector
byObject.Field = defaultedConfig.FieldSelector
if isNamespaced && byObject.Namespaces == nil { byObject.Transform = defaultedConfig.Transform
byObject.Namespaces = opts.DefaultNamespaces byObject.UnsafeDisableDeepCopy = defaultedConfig.UnsafeDisableDeepCopy
} }
opts.ByObject[obj] = byObject opts.ByObject[obj] = byObject
} }
// Default namespaces after byObject has been defaulted, otherwise a namespace without selectors
// will get the `Default` selectors, then get copied to byObject and then not get defaulted from
// byObject, as it already has selectors.
for namespace, cfg := range opts.DefaultNamespaces {
cfg = defaultConfig(cfg, optionDefaultsToConfig(&opts))
if namespace == metav1.NamespaceAll {
cfg.FieldSelector = fields.AndSelectors(
appendIfNotNil(
namespaceAllSelector(maps.Keys(opts.DefaultNamespaces)),
cfg.FieldSelector,
)...,
)
}
opts.DefaultNamespaces[namespace] = cfg
}
// Default the resync period to 10 hours if unset // Default the resync period to 10 hours if unset
if opts.SyncPeriod == nil { if opts.SyncPeriod == nil {
opts.SyncPeriod = &defaultSyncPeriod opts.SyncPeriod = &defaultSyncPeriod
@ -486,20 +512,21 @@ func defaultConfig(toDefault, defaultFrom Config) Config {
return toDefault return toDefault
} }
func namespaceAllSelector(namespaces []string) fields.Selector { func namespaceAllSelector(namespaces []string) []fields.Selector {
selectors := make([]fields.Selector, 0, len(namespaces)-1) selectors := make([]fields.Selector, 0, len(namespaces)-1)
sort.Strings(namespaces)
for _, namespace := range namespaces { for _, namespace := range namespaces {
if namespace != metav1.NamespaceAll { if namespace != metav1.NamespaceAll {
selectors = append(selectors, fields.OneTermNotEqualSelector("metadata.namespace", namespace)) selectors = append(selectors, fields.OneTermNotEqualSelector("metadata.namespace", namespace))
} }
} }
return fields.AndSelectors(selectors...) return selectors
} }
func appendIfNotNil[T comparable](a, b T) []T { func appendIfNotNil[T comparable](a []T, b T) []T {
if b != *new(T) { if b != *new(T) {
return []T{a, b} return append(a, b)
} }
return []T{a} return a
} }

View File

@ -52,6 +52,14 @@ func (dbt *delegatingByGVKCache) List(ctx context.Context, list client.ObjectLis
return cache.List(ctx, list, opts...) return cache.List(ctx, list, opts...)
} }
func (dbt *delegatingByGVKCache) RemoveInformer(ctx context.Context, obj client.Object) error {
cache, err := dbt.cacheForObject(obj)
if err != nil {
return err
}
return cache.RemoveInformer(ctx, obj)
}
func (dbt *delegatingByGVKCache) GetInformer(ctx context.Context, obj client.Object, opts ...InformerGetOption) (Informer, error) { func (dbt *delegatingByGVKCache) GetInformer(ctx context.Context, obj client.Object, opts ...InformerGetOption) (Informer, error) {
cache, err := dbt.cacheForObject(obj) cache, err := dbt.cacheForObject(obj)
if err != nil { if err != nil {

View File

@ -190,6 +190,17 @@ func (ic *informerCache) getInformerForKind(ctx context.Context, gvk schema.Grou
return ic.Informers.Get(ctx, gvk, obj, &internal.GetOptions{}) return ic.Informers.Get(ctx, gvk, obj, &internal.GetOptions{})
} }
// RemoveInformer deactivates and removes the informer from the cache.
func (ic *informerCache) RemoveInformer(_ context.Context, obj client.Object) error {
gvk, err := apiutil.GVKForObject(obj, ic.scheme)
if err != nil {
return err
}
ic.Informers.Remove(gvk, obj)
return nil
}
// NeedLeaderElection implements the LeaderElectionRunnable interface // NeedLeaderElection implements the LeaderElectionRunnable interface
// to indicate that this can be started without requiring the leader lock. // to indicate that this can be started without requiring the leader lock.
func (ic *informerCache) NeedLeaderElection() bool { func (ic *informerCache) NeedLeaderElection() bool {

View File

@ -23,6 +23,7 @@ import (
apierrors "k8s.io/apimachinery/pkg/api/errors" apierrors "k8s.io/apimachinery/pkg/api/errors"
apimeta "k8s.io/apimachinery/pkg/api/meta" apimeta "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
@ -117,16 +118,14 @@ func (c *CacheReader) List(_ context.Context, out client.ObjectList, opts ...cli
switch { switch {
case listOpts.FieldSelector != nil: case listOpts.FieldSelector != nil:
// TODO(directxman12): support more complicated field selectors by requiresExact := selector.RequiresExactMatch(listOpts.FieldSelector)
// combining multiple indices, GetIndexers, etc
field, val, requiresExact := selector.RequiresExactMatch(listOpts.FieldSelector)
if !requiresExact { if !requiresExact {
return fmt.Errorf("non-exact field matches are not supported by the cache") return fmt.Errorf("non-exact field matches are not supported by the cache")
} }
// list all objects by the field selector. If this is namespaced and we have one, ask for the // list all objects by the field selector. If this is namespaced and we have one, ask for the
// namespaced index key. Otherwise, ask for the non-namespaced variant by using the fake "all namespaces" // namespaced index key. Otherwise, ask for the non-namespaced variant by using the fake "all namespaces"
// namespace. // namespace.
objs, err = c.indexer.ByIndex(FieldIndexName(field), KeyToNamespacedKey(listOpts.Namespace, val)) objs, err = byIndexes(c.indexer, listOpts.FieldSelector.Requirements(), listOpts.Namespace)
case listOpts.Namespace != "": case listOpts.Namespace != "":
objs, err = c.indexer.ByIndex(cache.NamespaceIndex, listOpts.Namespace) objs, err = c.indexer.ByIndex(cache.NamespaceIndex, listOpts.Namespace)
default: default:
@ -178,6 +177,54 @@ func (c *CacheReader) List(_ context.Context, out client.ObjectList, opts ...cli
return apimeta.SetList(out, runtimeObjs) return apimeta.SetList(out, runtimeObjs)
} }
func byIndexes(indexer cache.Indexer, requires fields.Requirements, namespace string) ([]interface{}, error) {
var (
err error
objs []interface{}
vals []string
)
indexers := indexer.GetIndexers()
for idx, req := range requires {
indexName := FieldIndexName(req.Field)
indexedValue := KeyToNamespacedKey(namespace, req.Value)
if idx == 0 {
// we use first require to get snapshot data
// TODO(halfcrazy): use complicated index when client-go provides byIndexes
// https://github.com/kubernetes/kubernetes/issues/109329
objs, err = indexer.ByIndex(indexName, indexedValue)
if err != nil {
return nil, err
}
if len(objs) == 0 {
return nil, nil
}
continue
}
fn, exist := indexers[indexName]
if !exist {
return nil, fmt.Errorf("index with name %s does not exist", indexName)
}
filteredObjects := make([]interface{}, 0, len(objs))
for _, obj := range objs {
vals, err = fn(obj)
if err != nil {
return nil, err
}
for _, val := range vals {
if val == indexedValue {
filteredObjects = append(filteredObjects, obj)
break
}
}
}
if len(filteredObjects) == 0 {
return nil, nil
}
objs = filteredObjects
}
return objs, nil
}
// objectKeyToStorageKey converts an object key to store key. // objectKeyToStorageKey converts an object key to store key.
// It's akin to MetaNamespaceKeyFunc. It's separate from // It's akin to MetaNamespaceKeyFunc. It's separate from
// String to allow keeping the key format easily in sync with // String to allow keeping the key format easily in sync with

View File

@ -36,6 +36,7 @@ import (
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
"k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/cache"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil" "sigs.k8s.io/controller-runtime/pkg/client/apiutil"
"sigs.k8s.io/controller-runtime/pkg/internal/syncs"
) )
// InformersOpts configures an InformerMap. // InformersOpts configures an InformerMap.
@ -49,6 +50,7 @@ type InformersOpts struct {
Selector Selector Selector Selector
Transform cache.TransformFunc Transform cache.TransformFunc
UnsafeDisableDeepCopy bool UnsafeDisableDeepCopy bool
WatchErrorHandler cache.WatchErrorHandler
} }
// NewInformers creates a new InformersMap that can create informers under the hood. // NewInformers creates a new InformersMap that can create informers under the hood.
@ -76,6 +78,7 @@ func NewInformers(config *rest.Config, options *InformersOpts) *Informers {
transform: options.Transform, transform: options.Transform,
unsafeDisableDeepCopy: options.UnsafeDisableDeepCopy, unsafeDisableDeepCopy: options.UnsafeDisableDeepCopy,
newInformer: newInformer, newInformer: newInformer,
watchErrorHandler: options.WatchErrorHandler,
} }
} }
@ -86,6 +89,20 @@ type Cache struct {
// CacheReader wraps Informer and implements the CacheReader interface for a single type // CacheReader wraps Informer and implements the CacheReader interface for a single type
Reader CacheReader Reader CacheReader
// Stop can be used to stop this individual informer.
stop chan struct{}
}
// Start starts the informer managed by a MapEntry.
// Blocks until the informer stops. The informer can be stopped
// either individually (via the entry's stop channel) or globally
// via the provided stop argument.
func (c *Cache) Start(stop <-chan struct{}) {
// Stop on either the whole map stopping or just this informer being removed.
internalStop, cancel := syncs.MergeChans(stop, c.stop)
defer cancel()
c.Informer.Run(internalStop)
} }
type tracker struct { type tracker struct {
@ -159,6 +176,11 @@ type Informers struct {
// NewInformer allows overriding of the shared index informer constructor for testing. // NewInformer allows overriding of the shared index informer constructor for testing.
newInformer func(cache.ListerWatcher, runtime.Object, time.Duration, cache.Indexers) cache.SharedIndexInformer newInformer func(cache.ListerWatcher, runtime.Object, time.Duration, cache.Indexers) cache.SharedIndexInformer
// WatchErrorHandler allows the shared index informer's
// watchErrorHandler to be set by overriding the options
// or to use the default watchErrorHandler
watchErrorHandler cache.WatchErrorHandler
} }
// Start calls Run on each of the informers and sets started to true. Blocks on the context. // Start calls Run on each of the informers and sets started to true. Blocks on the context.
@ -173,13 +195,13 @@ func (ip *Informers) Start(ctx context.Context) error {
// Start each informer // Start each informer
for _, i := range ip.tracker.Structured { for _, i := range ip.tracker.Structured {
ip.startInformerLocked(i.Informer) ip.startInformerLocked(i)
} }
for _, i := range ip.tracker.Unstructured { for _, i := range ip.tracker.Unstructured {
ip.startInformerLocked(i.Informer) ip.startInformerLocked(i)
} }
for _, i := range ip.tracker.Metadata { for _, i := range ip.tracker.Metadata {
ip.startInformerLocked(i.Informer) ip.startInformerLocked(i)
} }
// Set started to true so we immediately start any informers added later. // Set started to true so we immediately start any informers added later.
@ -194,7 +216,7 @@ func (ip *Informers) Start(ctx context.Context) error {
return nil return nil
} }
func (ip *Informers) startInformerLocked(informer cache.SharedIndexInformer) { func (ip *Informers) startInformerLocked(cacheEntry *Cache) {
// Don't start the informer in case we are already waiting for the items in // Don't start the informer in case we are already waiting for the items in
// the waitGroup to finish, since waitGroups don't support waiting and adding // the waitGroup to finish, since waitGroups don't support waiting and adding
// at the same time. // at the same time.
@ -205,7 +227,7 @@ func (ip *Informers) startInformerLocked(informer cache.SharedIndexInformer) {
ip.waitGroup.Add(1) ip.waitGroup.Add(1)
go func() { go func() {
defer ip.waitGroup.Done() defer ip.waitGroup.Done()
informer.Run(ip.ctx.Done()) cacheEntry.Start(ip.ctx.Done())
}() }()
} }
@ -281,6 +303,21 @@ func (ip *Informers) Get(ctx context.Context, gvk schema.GroupVersionKind, obj r
return started, i, nil return started, i, nil
} }
// Remove removes an informer entry and stops it if it was running.
func (ip *Informers) Remove(gvk schema.GroupVersionKind, obj runtime.Object) {
ip.mu.Lock()
defer ip.mu.Unlock()
informerMap := ip.informersByType(obj)
entry, ok := informerMap[gvk]
if !ok {
return
}
close(entry.stop)
delete(informerMap, gvk)
}
func (ip *Informers) informersByType(obj runtime.Object) map[schema.GroupVersionKind]*Cache { func (ip *Informers) informersByType(obj runtime.Object) map[schema.GroupVersionKind]*Cache {
switch obj.(type) { switch obj.(type) {
case runtime.Unstructured: case runtime.Unstructured:
@ -323,6 +360,13 @@ func (ip *Informers) addInformerToMap(gvk schema.GroupVersionKind, obj runtime.O
cache.NamespaceIndex: cache.MetaNamespaceIndexFunc, cache.NamespaceIndex: cache.MetaNamespaceIndexFunc,
}) })
// Set WatchErrorHandler on SharedIndexInformer if set
if ip.watchErrorHandler != nil {
if err := sharedIndexInformer.SetWatchErrorHandler(ip.watchErrorHandler); err != nil {
return nil, false, err
}
}
// Check to see if there is a transformer for this gvk // Check to see if there is a transformer for this gvk
if err := sharedIndexInformer.SetTransform(ip.transform); err != nil { if err := sharedIndexInformer.SetTransform(ip.transform); err != nil {
return nil, false, err return nil, false, err
@ -342,13 +386,14 @@ func (ip *Informers) addInformerToMap(gvk schema.GroupVersionKind, obj runtime.O
scopeName: mapping.Scope.Name(), scopeName: mapping.Scope.Name(),
disableDeepCopy: ip.unsafeDisableDeepCopy, disableDeepCopy: ip.unsafeDisableDeepCopy,
}, },
stop: make(chan struct{}),
} }
ip.informersByType(obj)[gvk] = i ip.informersByType(obj)[gvk] = i
// Start the informer in case the InformersMap has started, otherwise it will be // Start the informer in case the InformersMap has started, otherwise it will be
// started when the InformersMap starts. // started when the InformersMap starts.
if ip.started { if ip.started {
ip.startInformerLocked(i.Informer) ip.startInformerLocked(i)
} }
return i, ip.started, nil return i, ip.started, nil
} }

View File

@ -109,6 +109,27 @@ func (c *multiNamespaceCache) GetInformer(ctx context.Context, obj client.Object
return &multiNamespaceInformer{namespaceToInformer: namespaceToInformer}, nil return &multiNamespaceInformer{namespaceToInformer: namespaceToInformer}, nil
} }
func (c *multiNamespaceCache) RemoveInformer(ctx context.Context, obj client.Object) error {
// If the object is clusterscoped, get the informer from clusterCache,
// if not use the namespaced caches.
isNamespaced, err := apiutil.IsObjectNamespaced(obj, c.Scheme, c.RESTMapper)
if err != nil {
return err
}
if !isNamespaced {
return c.clusterCache.RemoveInformer(ctx, obj)
}
for _, cache := range c.namespaceToCache {
err := cache.RemoveInformer(ctx, obj)
if err != nil {
return err
}
}
return nil
}
func (c *multiNamespaceCache) GetInformerForKind(ctx context.Context, gvk schema.GroupVersionKind, opts ...InformerGetOption) (Informer, error) { func (c *multiNamespaceCache) GetInformerForKind(ctx context.Context, gvk schema.GroupVersionKind, opts ...InformerGetOption) (Informer, error) {
// If the object is cluster scoped, get the informer from clusterCache, // If the object is cluster scoped, get the informer from clusterCache,
// if not use the namespaced caches. // if not use the namespaced caches.
@ -391,3 +412,13 @@ func (i *multiNamespaceInformer) HasSynced() bool {
} }
return true return true
} }
// IsStopped checks if each namespaced informer has stopped, returns false if any are still running.
func (i *multiNamespaceInformer) IsStopped() bool {
for _, informer := range i.namespaceToInformer {
if stopped := informer.IsStopped(); !stopped {
return false
}
}
return true
}

View File

@ -31,11 +31,9 @@ import (
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/client-go/discovery"
"k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic"
clientgoscheme "k8s.io/client-go/kubernetes/scheme" clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
"k8s.io/client-go/restmapper"
) )
var ( var (
@ -60,25 +58,6 @@ func AddToProtobufScheme(addToScheme func(*runtime.Scheme) error) error {
return addToScheme(protobufScheme) return addToScheme(protobufScheme)
} }
// NewDiscoveryRESTMapper constructs a new RESTMapper based on discovery
// information fetched by a new client with the given config.
func NewDiscoveryRESTMapper(c *rest.Config, httpClient *http.Client) (meta.RESTMapper, error) {
if httpClient == nil {
return nil, fmt.Errorf("httpClient must not be nil, consider using rest.HTTPClientFor(c) to create a client")
}
// Get a mapper
dc, err := discovery.NewDiscoveryClientForConfigAndClient(c, httpClient)
if err != nil {
return nil, err
}
gr, err := restmapper.GetAPIGroupResources(dc)
if err != nil {
return nil, err
}
return restmapper.NewDiscoveryRESTMapper(gr), nil
}
// IsObjectNamespaced returns true if the object is namespace scoped. // IsObjectNamespaced returns true if the object is namespace scoped.
// For unstructured objects the gvk is found from the object itself. // For unstructured objects the gvk is found from the object itself.
func IsObjectNamespaced(obj runtime.Object, scheme *runtime.Scheme, restmapper meta.RESTMapper) (bool, error) { func IsObjectNamespaced(obj runtime.Object, scheme *runtime.Scheme, restmapper meta.RESTMapper) (bool, error) {

View File

@ -21,6 +21,7 @@ import (
"net/http" "net/http"
"sync" "sync"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
@ -52,7 +53,7 @@ func NewDynamicRESTMapper(cfg *rest.Config, httpClient *http.Client) (meta.RESTM
// client for discovery information to do REST mappings. // client for discovery information to do REST mappings.
type mapper struct { type mapper struct {
mapper meta.RESTMapper mapper meta.RESTMapper
client *discovery.DiscoveryClient client discovery.DiscoveryInterface
knownGroups map[string]*restmapper.APIGroupResources knownGroups map[string]*restmapper.APIGroupResources
apiGroups map[string]*metav1.APIGroup apiGroups map[string]*metav1.APIGroup
@ -166,8 +167,10 @@ func (m *mapper) addKnownGroupAndReload(groupName string, versions ...string) er
if err != nil { if err != nil {
return err return err
} }
for _, version := range apiGroup.Versions { if apiGroup != nil {
versions = append(versions, version.Version) for _, version := range apiGroup.Versions {
versions = append(versions, version.Version)
}
} }
} }
@ -179,23 +182,28 @@ func (m *mapper) addKnownGroupAndReload(groupName string, versions ...string) er
Group: metav1.APIGroup{Name: groupName}, Group: metav1.APIGroup{Name: groupName},
VersionedResources: make(map[string][]metav1.APIResource), VersionedResources: make(map[string][]metav1.APIResource),
} }
// Update information for group resources about versioned resources.
// The number of API calls is equal to the number of versions: /apis/<group>/<version>.
// If we encounter a missing API version (NotFound error), we will remove the group from
// the m.apiGroups and m.knownGroups caches.
// If this happens, in the next call the group will be added back to apiGroups
// and only the existing versions will be loaded in knownGroups.
groupVersionResources, err := m.fetchGroupVersionResourcesLocked(groupName, versions...)
if err != nil {
return fmt.Errorf("failed to get API group resources: %w", err)
}
if _, ok := m.knownGroups[groupName]; ok { if _, ok := m.knownGroups[groupName]; ok {
groupResources = m.knownGroups[groupName] groupResources = m.knownGroups[groupName]
} }
// Update information for group resources about versioned resources.
// The number of API calls is equal to the number of versions: /apis/<group>/<version>.
groupVersionResources, err := m.fetchGroupVersionResources(groupName, versions...)
if err != nil {
return fmt.Errorf("failed to get API group resources: %w", err)
}
for version, resources := range groupVersionResources {
groupResources.VersionedResources[version.Version] = resources.APIResources
}
// Update information for group resources about the API group by adding new versions. // Update information for group resources about the API group by adding new versions.
// Ignore the versions that are already registered. // Ignore the versions that are already registered.
for _, version := range versions { for groupVersion, resources := range groupVersionResources {
version := groupVersion.Version
groupResources.VersionedResources[version] = resources.APIResources
found := false found := false
for _, v := range groupResources.Group.Versions { for _, v := range groupResources.Group.Versions {
if v.Version == version { if v.Version == version {
@ -254,21 +262,17 @@ func (m *mapper) findAPIGroupByName(groupName string) (*metav1.APIGroup, error)
m.mu.Unlock() m.mu.Unlock()
// Looking in the cache again. // Looking in the cache again.
{ m.mu.RLock()
m.mu.RLock() defer m.mu.RUnlock()
group, ok := m.apiGroups[groupName]
m.mu.RUnlock()
if ok {
return group, nil
}
}
// If there is still nothing, return an error. // Don't return an error here if the API group is not present.
return nil, fmt.Errorf("failed to find API group %q", groupName) // The reloaded RESTMapper will take care of returning a NoMatchError.
return m.apiGroups[groupName], nil
} }
// fetchGroupVersionResources fetches the resources for the specified group and its versions. // fetchGroupVersionResourcesLocked fetches the resources for the specified group and its versions.
func (m *mapper) fetchGroupVersionResources(groupName string, versions ...string) (map[schema.GroupVersion]*metav1.APIResourceList, error) { // This method might modify the cache so it needs to be called under the lock.
func (m *mapper) fetchGroupVersionResourcesLocked(groupName string, versions ...string) (map[schema.GroupVersion]*metav1.APIResourceList, error) {
groupVersionResources := make(map[schema.GroupVersion]*metav1.APIResourceList) groupVersionResources := make(map[schema.GroupVersion]*metav1.APIResourceList)
failedGroups := make(map[schema.GroupVersion]error) failedGroups := make(map[schema.GroupVersion]error)
@ -276,9 +280,20 @@ func (m *mapper) fetchGroupVersionResources(groupName string, versions ...string
groupVersion := schema.GroupVersion{Group: groupName, Version: version} groupVersion := schema.GroupVersion{Group: groupName, Version: version}
apiResourceList, err := m.client.ServerResourcesForGroupVersion(groupVersion.String()) apiResourceList, err := m.client.ServerResourcesForGroupVersion(groupVersion.String())
if err != nil { if apierrors.IsNotFound(err) {
// If the version is not found, we remove the group from the cache
// so it gets refreshed on the next call.
if m.isAPIGroupCached(groupVersion) {
delete(m.apiGroups, groupName)
}
if m.isGroupVersionCached(groupVersion) {
delete(m.knownGroups, groupName)
}
continue
} else if err != nil {
failedGroups[groupVersion] = err failedGroups[groupVersion] = err
} }
if apiResourceList != nil { if apiResourceList != nil {
// even in case of error, some fallback might have been returned. // even in case of error, some fallback might have been returned.
groupVersionResources[groupVersion] = apiResourceList groupVersionResources[groupVersion] = apiResourceList
@ -292,3 +307,29 @@ func (m *mapper) fetchGroupVersionResources(groupName string, versions ...string
return groupVersionResources, nil return groupVersionResources, nil
} }
// isGroupVersionCached checks if a version for a group is cached in the known groups cache.
func (m *mapper) isGroupVersionCached(gv schema.GroupVersion) bool {
if cachedGroup, ok := m.knownGroups[gv.Group]; ok {
_, cached := cachedGroup.VersionedResources[gv.Version]
return cached
}
return false
}
// isAPIGroupCached checks if a version for a group is cached in the api groups cache.
func (m *mapper) isAPIGroupCached(gv schema.GroupVersion) bool {
cachedGroup, ok := m.apiGroups[gv.Group]
if !ok {
return false
}
for _, version := range cachedGroup.Versions {
if version.Version == gv.Version {
return true
}
}
return false
}

View File

@ -90,11 +90,18 @@ type CacheOptions struct {
type NewClientFunc func(config *rest.Config, options Options) (Client, error) type NewClientFunc func(config *rest.Config, options Options) (Client, error)
// New returns a new Client using the provided config and Options. // New returns a new Client using the provided config and Options.
// The returned client reads *and* writes directly from the server
// (it doesn't use object caches). It understands how to work with
// normal types (both custom resources and aggregated/built-in resources),
// as well as unstructured types.
// //
// The client's read behavior is determined by Options.Cache.
// If either Options.Cache or Options.Cache.Reader is nil,
// the client reads directly from the API server.
// If both Options.Cache and Options.Cache.Reader are non-nil,
// the client reads from a local cache. However, specific
// resources can still be configured to bypass the cache based
// on Options.Cache.Unstructured and Options.Cache.DisableFor.
// Write operations are always performed directly on the API server.
//
// The client understands how to work with normal types (both custom resources
// and aggregated/built-in resources), as well as unstructured types.
// In the case of normal types, the scheme will be used to look up the // In the case of normal types, the scheme will be used to look up the
// corresponding group, version, and kind for the given type. In the // corresponding group, version, and kind for the given type. In the
// case of unstructured types, the group, version, and kind will be extracted // case of unstructured types, the group, version, and kind will be extracted
@ -210,7 +217,8 @@ func newClient(config *rest.Config, options Options) (*client, error) {
var _ Client = &client{} var _ Client = &client{}
// client is a client.Client that reads and writes directly from/to an API server. // client is a client.Client configured to either read from a local cache or directly from the API server.
// Write operations are always performed directly on the API server.
// It lazily initializes new clients at the time they are used. // It lazily initializes new clients at the time they are used.
type client struct { type client struct {
typedClient typedClient typedClient typedClient

View File

@ -334,10 +334,12 @@ func (t versionedTracker) Create(gvr schema.GroupVersionResource, obj runtime.Ob
// tries to assign whatever it finds into a ListType it gets from schema.New() - Thus we have to ensure // tries to assign whatever it finds into a ListType it gets from schema.New() - Thus we have to ensure
// we save as the very same type, otherwise subsequent List requests will fail. // we save as the very same type, otherwise subsequent List requests will fail.
func convertFromUnstructuredIfNecessary(s *runtime.Scheme, o runtime.Object) (runtime.Object, error) { func convertFromUnstructuredIfNecessary(s *runtime.Scheme, o runtime.Object) (runtime.Object, error) {
gvk := o.GetObjectKind().GroupVersionKind()
u, isUnstructured := o.(runtime.Unstructured) u, isUnstructured := o.(runtime.Unstructured)
if !isUnstructured || !s.Recognizes(gvk) { if !isUnstructured {
return o, nil
}
gvk := o.GetObjectKind().GroupVersionKind()
if !s.Recognizes(gvk) {
return o, nil return o, nil
} }
@ -380,12 +382,9 @@ func (t versionedTracker) update(gvr schema.GroupVersionResource, obj runtime.Ob
field.ErrorList{field.Required(field.NewPath("metadata.name"), "name is required")}) field.ErrorList{field.Required(field.NewPath("metadata.name"), "name is required")})
} }
gvk := obj.GetObjectKind().GroupVersionKind() gvk, err := apiutil.GVKForObject(obj, t.scheme)
if gvk.Empty() { if err != nil {
gvk, err = apiutil.GVKForObject(obj, t.scheme) return err
if err != nil {
return err
}
} }
oldObject, err := t.ObjectTracker.Get(gvr, ns, accessor.GetName()) oldObject, err := t.ObjectTracker.Get(gvr, ns, accessor.GetName())
@ -464,25 +463,25 @@ func (c *fakeClient) Get(ctx context.Context, key client.ObjectKey, obj client.O
return err return err
} }
gvk, err := apiutil.GVKForObject(obj, c.scheme) if _, isUnstructured := obj.(runtime.Unstructured); isUnstructured {
if err != nil { gvk, err := apiutil.GVKForObject(obj, c.scheme)
return err if err != nil {
return err
}
ta, err := meta.TypeAccessor(o)
if err != nil {
return err
}
ta.SetKind(gvk.Kind)
ta.SetAPIVersion(gvk.GroupVersion().String())
} }
ta, err := meta.TypeAccessor(o)
if err != nil {
return err
}
ta.SetKind(gvk.Kind)
ta.SetAPIVersion(gvk.GroupVersion().String())
j, err := json.Marshal(o) j, err := json.Marshal(o)
if err != nil { if err != nil {
return err return err
} }
decoder := scheme.Codecs.UniversalDecoder()
zero(obj) zero(obj)
_, _, err = decoder.Decode(j, nil, obj) return json.Unmarshal(j, obj)
return err
} }
func (c *fakeClient) Watch(ctx context.Context, list client.ObjectList, opts ...client.ListOption) (watch.Interface, error) { func (c *fakeClient) Watch(ctx context.Context, list client.ObjectList, opts ...client.ListOption) (watch.Interface, error) {
@ -527,21 +526,21 @@ func (c *fakeClient) List(ctx context.Context, obj client.ObjectList, opts ...cl
return err return err
} }
ta, err := meta.TypeAccessor(o) if _, isUnstructured := obj.(runtime.Unstructured); isUnstructured {
if err != nil { ta, err := meta.TypeAccessor(o)
return err if err != nil {
return err
}
ta.SetKind(originalKind)
ta.SetAPIVersion(gvk.GroupVersion().String())
} }
ta.SetKind(originalKind)
ta.SetAPIVersion(gvk.GroupVersion().String())
j, err := json.Marshal(o) j, err := json.Marshal(o)
if err != nil { if err != nil {
return err return err
} }
decoder := scheme.Codecs.UniversalDecoder()
zero(obj) zero(obj)
_, _, err = decoder.Decode(j, nil, obj) if err := json.Unmarshal(j, obj); err != nil {
if err != nil {
return err return err
} }
@ -588,9 +587,7 @@ func (c *fakeClient) filterList(list []runtime.Object, gvk schema.GroupVersionKi
} }
func (c *fakeClient) filterWithFields(list []runtime.Object, gvk schema.GroupVersionKind, fs fields.Selector) ([]runtime.Object, error) { func (c *fakeClient) filterWithFields(list []runtime.Object, gvk schema.GroupVersionKind, fs fields.Selector) ([]runtime.Object, error) {
// We only allow filtering on the basis of a single field to ensure consistency with the requiresExact := selector.RequiresExactMatch(fs)
// behavior of the cache reader (which we're faking here).
fieldKey, fieldVal, requiresExact := selector.RequiresExactMatch(fs)
if !requiresExact { if !requiresExact {
return nil, fmt.Errorf("field selector %s is not in one of the two supported forms \"key==val\" or \"key=val\"", return nil, fmt.Errorf("field selector %s is not in one of the two supported forms \"key==val\" or \"key=val\"",
fs) fs)
@ -599,15 +596,24 @@ func (c *fakeClient) filterWithFields(list []runtime.Object, gvk schema.GroupVer
// Field selection is mimicked via indexes, so there's no sane answer this function can give // Field selection is mimicked via indexes, so there's no sane answer this function can give
// if there are no indexes registered for the GroupVersionKind of the objects in the list. // if there are no indexes registered for the GroupVersionKind of the objects in the list.
indexes := c.indexes[gvk] indexes := c.indexes[gvk]
if len(indexes) == 0 || indexes[fieldKey] == nil { for _, req := range fs.Requirements() {
return nil, fmt.Errorf("List on GroupVersionKind %v specifies selector on field %s, but no "+ if len(indexes) == 0 || indexes[req.Field] == nil {
"index with name %s has been registered for GroupVersionKind %v", gvk, fieldKey, fieldKey, gvk) return nil, fmt.Errorf("List on GroupVersionKind %v specifies selector on field %s, but no "+
"index with name %s has been registered for GroupVersionKind %v", gvk, req.Field, req.Field, gvk)
}
} }
indexExtractor := indexes[fieldKey]
filteredList := make([]runtime.Object, 0, len(list)) filteredList := make([]runtime.Object, 0, len(list))
for _, obj := range list { for _, obj := range list {
if c.objMatchesFieldSelector(obj, indexExtractor, fieldVal) { matches := true
for _, req := range fs.Requirements() {
indexExtractor := indexes[req.Field]
if !c.objMatchesFieldSelector(obj, indexExtractor, req.Value) {
matches = false
break
}
}
if matches {
filteredList = append(filteredList, obj) filteredList = append(filteredList, obj)
} }
} }
@ -862,21 +868,22 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client
if !handled { if !handled {
panic("tracker could not handle patch method") panic("tracker could not handle patch method")
} }
ta, err := meta.TypeAccessor(o)
if err != nil { if _, isUnstructured := obj.(runtime.Unstructured); isUnstructured {
return err ta, err := meta.TypeAccessor(o)
if err != nil {
return err
}
ta.SetKind(gvk.Kind)
ta.SetAPIVersion(gvk.GroupVersion().String())
} }
ta.SetKind(gvk.Kind)
ta.SetAPIVersion(gvk.GroupVersion().String())
j, err := json.Marshal(o) j, err := json.Marshal(o)
if err != nil { if err != nil {
return err return err
} }
decoder := scheme.Codecs.UniversalDecoder()
zero(obj) zero(obj)
_, _, err = decoder.Decode(j, nil, obj) return json.Unmarshal(j, obj)
return err
} }
// Applying a patch results in a deletionTimestamp that is truncated to the nearest second. // Applying a patch results in a deletionTimestamp that is truncated to the nearest second.
@ -940,7 +947,7 @@ func dryPatch(action testing.PatchActionImpl, tracker testing.ObjectTracker) (ru
if err := json.Unmarshal(modified, obj); err != nil { if err := json.Unmarshal(modified, obj); err != nil {
return nil, err return nil, err
} }
case types.StrategicMergePatchType, types.ApplyPatchType: case types.StrategicMergePatchType:
mergedByte, err := strategicpatch.StrategicMergePatch(old, action.GetPatch(), obj) mergedByte, err := strategicpatch.StrategicMergePatch(old, action.GetPatch(), obj)
if err != nil { if err != nil {
return nil, err return nil, err
@ -948,8 +955,10 @@ func dryPatch(action testing.PatchActionImpl, tracker testing.ObjectTracker) (ru
if err = json.Unmarshal(mergedByte, obj); err != nil { if err = json.Unmarshal(mergedByte, obj); err != nil {
return nil, err return nil, err
} }
case types.ApplyPatchType:
return nil, errors.New("apply patches are not supported in the fake client. Follow https://github.com/kubernetes/kubernetes/issues/115598 for the current status")
default: default:
return nil, fmt.Errorf("PatchType is not supported") return nil, fmt.Errorf("%s PatchType is not supported", action.GetPatchType())
} }
return obj, nil return obj, nil
} }
@ -1247,6 +1256,8 @@ func inTreeResourcesWithStatus() []schema.GroupVersionKind {
{Group: "flowcontrol.apiserver.k8s.io", Version: "v1beta2", Kind: "FlowSchema"}, {Group: "flowcontrol.apiserver.k8s.io", Version: "v1beta2", Kind: "FlowSchema"},
{Group: "flowcontrol.apiserver.k8s.io", Version: "v1beta2", Kind: "PriorityLevelConfiguration"}, {Group: "flowcontrol.apiserver.k8s.io", Version: "v1beta2", Kind: "PriorityLevelConfiguration"},
{Group: "flowcontrol.apiserver.k8s.io", Version: "v1", Kind: "FlowSchema"},
{Group: "flowcontrol.apiserver.k8s.io", Version: "v1", Kind: "PriorityLevelConfiguration"},
} }
} }

View File

@ -0,0 +1,106 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package client
import (
"context"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// WithFieldOwner wraps a Client and adds the fieldOwner as the field
// manager to all write requests from this client. If additional [FieldOwner]
// options are specified on methods of this client, the value specified here
// will be overridden.
func WithFieldOwner(c Client, fieldOwner string) Client {
return &clientWithFieldManager{
manager: fieldOwner,
c: c,
Reader: c,
}
}
type clientWithFieldManager struct {
manager string
c Client
Reader
}
func (f *clientWithFieldManager) Create(ctx context.Context, obj Object, opts ...CreateOption) error {
return f.c.Create(ctx, obj, append([]CreateOption{FieldOwner(f.manager)}, opts...)...)
}
func (f *clientWithFieldManager) Update(ctx context.Context, obj Object, opts ...UpdateOption) error {
return f.c.Update(ctx, obj, append([]UpdateOption{FieldOwner(f.manager)}, opts...)...)
}
func (f *clientWithFieldManager) Patch(ctx context.Context, obj Object, patch Patch, opts ...PatchOption) error {
return f.c.Patch(ctx, obj, patch, append([]PatchOption{FieldOwner(f.manager)}, opts...)...)
}
func (f *clientWithFieldManager) Delete(ctx context.Context, obj Object, opts ...DeleteOption) error {
return f.c.Delete(ctx, obj, opts...)
}
func (f *clientWithFieldManager) DeleteAllOf(ctx context.Context, obj Object, opts ...DeleteAllOfOption) error {
return f.c.DeleteAllOf(ctx, obj, opts...)
}
func (f *clientWithFieldManager) Scheme() *runtime.Scheme { return f.c.Scheme() }
func (f *clientWithFieldManager) RESTMapper() meta.RESTMapper { return f.c.RESTMapper() }
func (f *clientWithFieldManager) GroupVersionKindFor(obj runtime.Object) (schema.GroupVersionKind, error) {
return f.c.GroupVersionKindFor(obj)
}
func (f *clientWithFieldManager) IsObjectNamespaced(obj runtime.Object) (bool, error) {
return f.c.IsObjectNamespaced(obj)
}
func (f *clientWithFieldManager) Status() StatusWriter {
return &subresourceClientWithFieldOwner{
owner: f.manager,
subresourceWriter: f.c.Status(),
}
}
func (f *clientWithFieldManager) SubResource(subresource string) SubResourceClient {
c := f.c.SubResource(subresource)
return &subresourceClientWithFieldOwner{
owner: f.manager,
subresourceWriter: c,
SubResourceReader: c,
}
}
type subresourceClientWithFieldOwner struct {
owner string
subresourceWriter SubResourceWriter
SubResourceReader
}
func (f *subresourceClientWithFieldOwner) Create(ctx context.Context, obj Object, subresource Object, opts ...SubResourceCreateOption) error {
return f.subresourceWriter.Create(ctx, obj, subresource, append([]SubResourceCreateOption{FieldOwner(f.owner)}, opts...)...)
}
func (f *subresourceClientWithFieldOwner) Update(ctx context.Context, obj Object, opts ...SubResourceUpdateOption) error {
return f.subresourceWriter.Update(ctx, obj, append([]SubResourceUpdateOption{FieldOwner(f.owner)}, opts...)...)
}
func (f *subresourceClientWithFieldOwner) Patch(ctx context.Context, obj Object, patch Patch, opts ...SubResourcePatchOption) error {
return f.subresourceWriter.Patch(ctx, obj, patch, append([]SubResourcePatchOption{FieldOwner(f.owner)}, opts...)...)
}

View File

@ -419,7 +419,7 @@ type ListOptions struct {
LabelSelector labels.Selector LabelSelector labels.Selector
// FieldSelector filters results by a particular field. In order // FieldSelector filters results by a particular field. In order
// to use this with cache-based implementations, restrict usage to // to use this with cache-based implementations, restrict usage to
// a single field-value pair that's been added to the indexers. // exact match field-value pair that's been added to the indexers.
FieldSelector fields.Selector FieldSelector fields.Selector
// Namespace represents the namespace to list for, or empty for // Namespace represents the namespace to list for, or empty for
@ -514,7 +514,8 @@ type MatchingLabels map[string]string
func (m MatchingLabels) ApplyToList(opts *ListOptions) { func (m MatchingLabels) ApplyToList(opts *ListOptions) {
// TODO(directxman12): can we avoid reserializing this over and over? // TODO(directxman12): can we avoid reserializing this over and over?
if opts.LabelSelector == nil { if opts.LabelSelector == nil {
opts.LabelSelector = labels.NewSelector() opts.LabelSelector = labels.SelectorFromValidatedSet(map[string]string(m))
return
} }
// If there's already a selector, we need to AND the two together. // If there's already a selector, we need to AND the two together.
noValidSel := labels.SelectorFromValidatedSet(map[string]string(m)) noValidSel := labels.SelectorFromValidatedSet(map[string]string(m))

View File

@ -27,7 +27,7 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/utils/pointer" "k8s.io/utils/ptr"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil" "sigs.k8s.io/controller-runtime/pkg/client/apiutil"
@ -77,8 +77,8 @@ func SetControllerReference(owner, controlled metav1.Object, scheme *runtime.Sch
Kind: gvk.Kind, Kind: gvk.Kind,
Name: owner.GetName(), Name: owner.GetName(),
UID: owner.GetUID(), UID: owner.GetUID(),
BlockOwnerDeletion: pointer.Bool(true), BlockOwnerDeletion: ptr.To(true),
Controller: pointer.Bool(true), Controller: ptr.To(true),
} }
// Return early with an error if the object is already controlled. // Return early with an error if the object is already controlled.
@ -121,6 +121,84 @@ func SetOwnerReference(owner, object metav1.Object, scheme *runtime.Scheme) erro
return nil return nil
} }
// RemoveOwnerReference is a helper method to make sure the given object removes an owner reference to the object provided.
// This allows you to remove the owner to establish a new owner of the object in a subsequent call.
func RemoveOwnerReference(owner, object metav1.Object, scheme *runtime.Scheme) error {
owners := object.GetOwnerReferences()
length := len(owners)
if length < 1 {
return fmt.Errorf("%T does not have any owner references", object)
}
ro, ok := owner.(runtime.Object)
if !ok {
return fmt.Errorf("%T is not a runtime.Object, cannot call RemoveOwnerReference", owner)
}
gvk, err := apiutil.GVKForObject(ro, scheme)
if err != nil {
return err
}
index := indexOwnerRef(owners, metav1.OwnerReference{
APIVersion: gvk.GroupVersion().String(),
Name: owner.GetName(),
Kind: gvk.Kind,
})
if index == -1 {
return fmt.Errorf("%T does not have an owner reference for %T", object, owner)
}
owners = append(owners[:index], owners[index+1:]...)
object.SetOwnerReferences(owners)
return nil
}
// HasControllerReference returns true if the object
// has an owner ref with controller equal to true
func HasControllerReference(object metav1.Object) bool {
owners := object.GetOwnerReferences()
for _, owner := range owners {
isTrue := owner.Controller
if owner.Controller != nil && *isTrue {
return true
}
}
return false
}
// RemoveControllerReference removes an owner reference where the controller
// equals true
func RemoveControllerReference(owner, object metav1.Object, scheme *runtime.Scheme) error {
if ok := HasControllerReference(object); !ok {
return fmt.Errorf("%T does not have a owner reference with controller equals true", object)
}
ro, ok := owner.(runtime.Object)
if !ok {
return fmt.Errorf("%T is not a runtime.Object, cannot call RemoveControllerReference", owner)
}
gvk, err := apiutil.GVKForObject(ro, scheme)
if err != nil {
return err
}
ownerRefs := object.GetOwnerReferences()
index := indexOwnerRef(ownerRefs, metav1.OwnerReference{
APIVersion: gvk.GroupVersion().String(),
Name: owner.GetName(),
Kind: gvk.Kind,
})
if index == -1 {
return fmt.Errorf("%T does not have an controller reference for %T", object, owner)
}
if ownerRefs[index].Controller == nil || !*ownerRefs[index].Controller {
return fmt.Errorf("%T owner is not the controller reference for %T", owner, object)
}
ownerRefs = append(ownerRefs[:index], ownerRefs[index+1:]...)
object.SetOwnerReferences(ownerRefs)
return nil
}
func upsertOwnerRef(ref metav1.OwnerReference, object metav1.Object) { func upsertOwnerRef(ref metav1.OwnerReference, object metav1.Object) {
owners := object.GetOwnerReferences() owners := object.GetOwnerReferences()
if idx := indexOwnerRef(owners, ref); idx == -1 { if idx := indexOwnerRef(owners, ref); idx == -1 {
@ -166,7 +244,6 @@ func referSameObject(a, b metav1.OwnerReference) bool {
if err != nil { if err != nil {
return false return false
} }
return aGV.Group == bGV.Group && a.Kind == b.Kind && a.Name == b.Name return aGV.Group == bGV.Group && a.Kind == b.Kind && a.Name == b.Name
} }
@ -193,6 +270,9 @@ const ( // They should complete the sentence "Deployment default/foo has been ..
// The MutateFn is called regardless of creating or updating an object. // The MutateFn is called regardless of creating or updating an object.
// //
// It returns the executed operation and an error. // It returns the executed operation and an error.
//
// Note: changes made by MutateFn to any sub-resource (status...), will be
// discarded.
func CreateOrUpdate(ctx context.Context, c client.Client, obj client.Object, f MutateFn) (OperationResult, error) { func CreateOrUpdate(ctx context.Context, c client.Client, obj client.Object, f MutateFn) (OperationResult, error) {
key := client.ObjectKeyFromObject(obj) key := client.ObjectKeyFromObject(obj)
if err := c.Get(ctx, key, obj); err != nil { if err := c.Get(ctx, key, obj); err != nil {
@ -230,6 +310,12 @@ func CreateOrUpdate(ctx context.Context, c client.Client, obj client.Object, f M
// The MutateFn is called regardless of creating or updating an object. // The MutateFn is called regardless of creating or updating an object.
// //
// It returns the executed operation and an error. // It returns the executed operation and an error.
//
// Note: changes to any sub-resource other than status will be ignored.
// Changes to the status sub-resource will only be applied if the object
// already exist. To change the status on object creation, the easiest
// way is to requeue the object in the controller if OperationResult is
// OperationResultCreated
func CreateOrPatch(ctx context.Context, c client.Client, obj client.Object, f MutateFn) (OperationResult, error) { func CreateOrPatch(ctx context.Context, c client.Client, obj client.Object, f MutateFn) (OperationResult, error) {
key := client.ObjectKeyFromObject(obj) key := client.ObjectKeyFromObject(obj)
if err := c.Get(ctx, key, obj); err != nil { if err := c.Get(ctx, key, obj); err != nil {

View File

@ -42,7 +42,7 @@ import (
// Unless you are implementing your own EventHandler, you can ignore the functions on the EventHandler interface. // Unless you are implementing your own EventHandler, you can ignore the functions on the EventHandler interface.
// Most users shouldn't need to implement their own EventHandler. // Most users shouldn't need to implement their own EventHandler.
type EventHandler interface { type EventHandler interface {
// Create is called in response to an create event - e.g. Pod Creation. // Create is called in response to a create event - e.g. Pod Creation.
Create(context.Context, event.CreateEvent, workqueue.RateLimitingInterface) Create(context.Context, event.CreateEvent, workqueue.RateLimitingInterface)
// Update is called in response to an update event - e.g. Pod Updated. // Update is called in response to an update event - e.g. Pod Updated.

View File

@ -22,14 +22,16 @@ import (
) )
// RequiresExactMatch checks if the given field selector is of the form `k=v` or `k==v`. // RequiresExactMatch checks if the given field selector is of the form `k=v` or `k==v`.
func RequiresExactMatch(sel fields.Selector) (field, val string, required bool) { func RequiresExactMatch(sel fields.Selector) bool {
reqs := sel.Requirements() reqs := sel.Requirements()
if len(reqs) != 1 { if len(reqs) == 0 {
return "", "", false return false
} }
req := reqs[0]
if req.Operator != selection.Equals && req.Operator != selection.DoubleEquals { for _, req := range reqs {
return "", "", false if req.Operator != selection.Equals && req.Operator != selection.DoubleEquals {
return false
}
} }
return req.Field, req.Value, true return true
} }

View File

@ -0,0 +1,38 @@
package syncs
import (
"context"
"reflect"
"sync"
)
// MergeChans returns a channel that is closed when any of the input channels are signaled.
// The caller must call the returned CancelFunc to ensure no resources are leaked.
func MergeChans[T any](chans ...<-chan T) (<-chan T, context.CancelFunc) {
var once sync.Once
out := make(chan T)
cancel := make(chan T)
cancelFunc := func() {
once.Do(func() {
close(cancel)
})
<-out
}
cases := make([]reflect.SelectCase, len(chans)+1)
for i := range chans {
cases[i] = reflect.SelectCase{
Dir: reflect.SelectRecv,
Chan: reflect.ValueOf(chans[i]),
}
}
cases[len(cases)-1] = reflect.SelectCase{
Dir: reflect.SelectRecv,
Chan: reflect.ValueOf(cancel),
}
go func() {
defer close(out)
_, _, _ = reflect.Select(cases)
}()
return out, cancelFunc
}

View File

@ -179,6 +179,24 @@ func (cm *controllerManager) add(r Runnable) error {
return cm.runnables.Add(r) return cm.runnables.Add(r)
} }
// AddMetricsServerExtraHandler adds extra handler served on path to the http server that serves metrics.
func (cm *controllerManager) AddMetricsServerExtraHandler(path string, handler http.Handler) error {
cm.Lock()
defer cm.Unlock()
if cm.started {
return fmt.Errorf("unable to add new metrics handler because metrics endpoint has already been created")
}
if cm.metricsServer == nil {
cm.GetLogger().Info("warn: metrics server is currently disabled, registering extra handler %q will be ignored", path)
return nil
}
if err := cm.metricsServer.AddExtraHandler(path, handler); err != nil {
return err
}
cm.logger.V(2).Info("Registering metrics http server extra handler", "path", path)
return nil
}
// AddHealthzCheck allows you to add Healthz checker. // AddHealthzCheck allows you to add Healthz checker.
func (cm *controllerManager) AddHealthzCheck(name string, check healthz.Checker) error { func (cm *controllerManager) AddHealthzCheck(name string, check healthz.Checker) error {
cm.Lock() cm.Lock()
@ -518,6 +536,8 @@ func (cm *controllerManager) engageStopProcedure(stopComplete <-chan struct{}) e
// Stop all the leader election runnables, which includes reconcilers. // Stop all the leader election runnables, which includes reconcilers.
cm.logger.Info("Stopping and waiting for leader election runnables") cm.logger.Info("Stopping and waiting for leader election runnables")
// Prevent leader election when shutting down a non-elected manager
cm.runnables.LeaderElection.startOnce.Do(func() {})
cm.runnables.LeaderElection.StopAndWait(cm.shutdownCtx) cm.runnables.LeaderElection.StopAndWait(cm.shutdownCtx)
// Stop the caches before the leader election runnables, this is an important // Stop the caches before the leader election runnables, this is an important

View File

@ -34,7 +34,7 @@ import (
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
"k8s.io/client-go/tools/leaderelection/resourcelock" "k8s.io/client-go/tools/leaderelection/resourcelock"
"k8s.io/client-go/tools/record" "k8s.io/client-go/tools/record"
"k8s.io/utils/pointer" "k8s.io/utils/ptr"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
"sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/cache"
@ -67,6 +67,15 @@ type Manager interface {
// election was configured. // election was configured.
Elected() <-chan struct{} Elected() <-chan struct{}
// AddMetricsServerExtraHandler adds an extra handler served on path to the http server that serves metrics.
// Might be useful to register some diagnostic endpoints e.g. pprof.
//
// Note that these endpoints are meant to be sensitive and shouldn't be exposed publicly.
//
// If the simple path -> handler mapping offered here is not enough,
// a new http server/listener should be added as Runnable to the manager via Add method.
AddMetricsServerExtraHandler(path string, handler http.Handler) error
// AddHealthzCheck allows you to add Healthz checker // AddHealthzCheck allows you to add Healthz checker
AddHealthzCheck(name string, check healthz.Checker) error AddHealthzCheck(name string, check healthz.Checker) error
@ -409,10 +418,10 @@ func New(config *rest.Config, options Options) (Manager, error) {
return nil, fmt.Errorf("failed to new pprof listener: %w", err) return nil, fmt.Errorf("failed to new pprof listener: %w", err)
} }
errChan := make(chan error) errChan := make(chan error, 1)
runnables := newRunnables(options.BaseContext, errChan) runnables := newRunnables(options.BaseContext, errChan)
return &controllerManager{ return &controllerManager{
stopProcedureEngaged: pointer.Int64(0), stopProcedureEngaged: ptr.To(int64(0)),
cluster: cluster, cluster: cluster,
runnables: runnables, runnables: runnables,
errChan: errChan, errChan: errChan,

View File

@ -263,6 +263,15 @@ func (r *runnableGroup) Add(rn Runnable, ready runnableCheck) error {
r.start.Unlock() r.start.Unlock()
} }
// Recheck if we're stopped and hold the readlock, given that the stop and start can be called
// at the same time, we can end up in a situation where the runnable is added
// after the group is stopped and the channel is closed.
r.stop.RLock()
defer r.stop.RUnlock()
if r.stopped {
return errRunnableGroupStopped
}
// Enqueue the runnable. // Enqueue the runnable.
r.ch <- readyRunnable r.ch <- readyRunnable
return nil return nil
@ -272,7 +281,11 @@ func (r *runnableGroup) Add(rn Runnable, ready runnableCheck) error {
func (r *runnableGroup) StopAndWait(ctx context.Context) { func (r *runnableGroup) StopAndWait(ctx context.Context) {
r.stopOnce.Do(func() { r.stopOnce.Do(func() {
// Close the reconciler channel once we're done. // Close the reconciler channel once we're done.
defer close(r.ch) defer func() {
r.stop.Lock()
close(r.ch)
r.stop.Unlock()
}()
_ = r.Start(ctx) _ = r.Start(ctx)
r.stop.Lock() r.stop.Lock()

View File

@ -46,6 +46,9 @@ var DefaultBindAddress = ":8080"
// Server is a server that serves metrics. // Server is a server that serves metrics.
type Server interface { type Server interface {
// AddExtraHandler adds extra handler served on path to the http server that serves metrics.
AddExtraHandler(path string, handler http.Handler) error
// NeedLeaderElection implements the LeaderElectionRunnable interface, which indicates // NeedLeaderElection implements the LeaderElectionRunnable interface, which indicates
// the metrics server doesn't need leader election. // the metrics server doesn't need leader election.
NeedLeaderElection() bool NeedLeaderElection() bool
@ -179,6 +182,23 @@ func (*defaultServer) NeedLeaderElection() bool {
return false return false
} }
// AddExtraHandler adds extra handler served on path to the http server that serves metrics.
func (s *defaultServer) AddExtraHandler(path string, handler http.Handler) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.options.ExtraHandlers == nil {
s.options.ExtraHandlers = make(map[string]http.Handler)
}
if path == defaultMetricsEndpoint {
return fmt.Errorf("overriding builtin %s endpoint is not allowed", defaultMetricsEndpoint)
}
if _, found := s.options.ExtraHandlers[path]; found {
return fmt.Errorf("can't register extra handler by duplicate path %q on metrics http server", path)
}
s.options.ExtraHandlers[path] = handler
return nil
}
// Start runs the server. // Start runs the server.
// It will install the metrics related resources depend on the server configuration. // It will install the metrics related resources depend on the server configuration.
func (s *defaultServer) Start(ctx context.Context) error { func (s *defaultServer) Start(ctx context.Context) error {

View File

@ -54,14 +54,14 @@ var (
Subsystem: WorkQueueSubsystem, Subsystem: WorkQueueSubsystem,
Name: QueueLatencyKey, Name: QueueLatencyKey,
Help: "How long in seconds an item stays in workqueue before being requested", Help: "How long in seconds an item stays in workqueue before being requested",
Buckets: prometheus.ExponentialBuckets(10e-9, 10, 10), Buckets: prometheus.ExponentialBuckets(10e-9, 10, 12),
}, []string{"name"}) }, []string{"name"})
workDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{ workDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Subsystem: WorkQueueSubsystem, Subsystem: WorkQueueSubsystem,
Name: WorkDurationKey, Name: WorkDurationKey,
Help: "How long in seconds processing an item from workqueue takes.", Help: "How long in seconds processing an item from workqueue takes.",
Buckets: prometheus.ExponentialBuckets(10e-9, 10, 10), Buckets: prometheus.ExponentialBuckets(10e-9, 10, 12),
}, []string{"name"}) }, []string{"name"})
unfinished = prometheus.NewGaugeVec(prometheus.GaugeOpts{ unfinished = prometheus.NewGaugeVec(prometheus.GaugeOpts{

View File

@ -19,9 +19,11 @@ package reconcile
import ( import (
"context" "context"
"errors" "errors"
"reflect"
"time" "time"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
) )
// Result contains the result of a Reconciler invocation. // Result contains the result of a Reconciler invocation.
@ -97,7 +99,7 @@ type Reconciler interface {
// If the error is nil and the returned Result has a non-zero result.RequeueAfter, the request // If the error is nil and the returned Result has a non-zero result.RequeueAfter, the request
// will be requeued after the specified duration. // will be requeued after the specified duration.
// //
// If the error is nil and result.RequeueAfter is zero and result.Reque is true, the request // If the error is nil and result.RequeueAfter is zero and result.Requeue is true, the request
// will be requeued using exponential backoff. // will be requeued using exponential backoff.
Reconcile(context.Context, Request) (Result, error) Reconcile(context.Context, Request) (Result, error)
} }
@ -110,6 +112,36 @@ var _ Reconciler = Func(nil)
// Reconcile implements Reconciler. // Reconcile implements Reconciler.
func (r Func) Reconcile(ctx context.Context, o Request) (Result, error) { return r(ctx, o) } func (r Func) Reconcile(ctx context.Context, o Request) (Result, error) { return r(ctx, o) }
// ObjectReconciler is a specialized version of Reconciler that acts on instances of client.Object. Each reconciliation
// event gets the associated object from Kubernetes before passing it to Reconcile. An ObjectReconciler can be used in
// Builder.Complete by calling AsReconciler. See Reconciler for more details.
type ObjectReconciler[T client.Object] interface {
Reconcile(context.Context, T) (Result, error)
}
// AsReconciler creates a Reconciler based on the given ObjectReconciler.
func AsReconciler[T client.Object](client client.Client, rec ObjectReconciler[T]) Reconciler {
return &objectReconcilerAdapter[T]{
objReconciler: rec,
client: client,
}
}
type objectReconcilerAdapter[T client.Object] struct {
objReconciler ObjectReconciler[T]
client client.Client
}
// Reconcile implements Reconciler.
func (a *objectReconcilerAdapter[T]) Reconcile(ctx context.Context, req Request) (Result, error) {
o := reflect.New(reflect.TypeOf(*new(T)).Elem()).Interface().(T)
if err := a.client.Get(ctx, req.NamespacedName, o); err != nil {
return Result{}, client.IgnoreNotFound(err)
}
return a.objReconciler.Reconcile(ctx, o)
}
// TerminalError is an error that will not be retried but still be logged // TerminalError is an error that will not be retried but still be logged
// and recorded in metrics. // and recorded in metrics.
func TerminalError(wrapped error) error { func TerminalError(wrapped error) error {

View File

@ -27,12 +27,14 @@ import (
) )
// Defaulter defines functions for setting defaults on resources. // Defaulter defines functions for setting defaults on resources.
// Deprecated: Ue CustomDefaulter instead.
type Defaulter interface { type Defaulter interface {
runtime.Object runtime.Object
Default() Default()
} }
// DefaultingWebhookFor creates a new Webhook for Defaulting the provided type. // DefaultingWebhookFor creates a new Webhook for Defaulting the provided type.
// Deprecated: Use WithCustomDefaulter instead.
func DefaultingWebhookFor(scheme *runtime.Scheme, defaulter Defaulter) *Webhook { func DefaultingWebhookFor(scheme *runtime.Scheme, defaulter Defaulter) *Webhook {
return &Webhook{ return &Webhook{
Handler: &mutatingHandler{defaulter: defaulter, decoder: NewDecoder(scheme)}, Handler: &mutatingHandler{defaulter: defaulter, decoder: NewDecoder(scheme)},

View File

@ -34,6 +34,26 @@ import (
var admissionScheme = runtime.NewScheme() var admissionScheme = runtime.NewScheme()
var admissionCodecs = serializer.NewCodecFactory(admissionScheme) var admissionCodecs = serializer.NewCodecFactory(admissionScheme)
// adapted from https://github.com/kubernetes/kubernetes/blob/c28c2009181fcc44c5f6b47e10e62dacf53e4da0/staging/src/k8s.io/pod-security-admission/cmd/webhook/server/server.go
//
// From https://github.com/kubernetes/apiserver/blob/d6876a0600de06fef75968c4641c64d7da499f25/pkg/server/config.go#L433-L442C5:
//
// 1.5MB is the recommended client request size in byte
// the etcd server should accept. See
// https://github.com/etcd-io/etcd/blob/release-3.4/embed/config.go#L56.
// A request body might be encoded in json, and is converted to
// proto when persisted in etcd, so we allow 2x as the largest request
// body size to be accepted and decoded in a write request.
//
// For the admission request, we can infer that it contains at most two objects
// (the old and new versions of the object being admitted), each of which can
// be at most 3MB in size. For the rest of the request, we can assume that
// it will be less than 1MB in size. Therefore, we can set the max request
// size to 7MB.
// If your use case requires larger max request sizes, please
// open an issue (https://github.com/kubernetes-sigs/controller-runtime/issues/new).
const maxRequestSize = int64(7 * 1024 * 1024)
func init() { func init() {
utilruntime.Must(v1.AddToScheme(admissionScheme)) utilruntime.Must(v1.AddToScheme(admissionScheme))
utilruntime.Must(v1beta1.AddToScheme(admissionScheme)) utilruntime.Must(v1beta1.AddToScheme(admissionScheme))
@ -42,27 +62,30 @@ func init() {
var _ http.Handler = &Webhook{} var _ http.Handler = &Webhook{}
func (wh *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (wh *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var body []byte
var err error
ctx := r.Context() ctx := r.Context()
if wh.WithContextFunc != nil { if wh.WithContextFunc != nil {
ctx = wh.WithContextFunc(ctx, r) ctx = wh.WithContextFunc(ctx, r)
} }
var reviewResponse Response if r.Body == nil || r.Body == http.NoBody {
if r.Body == nil { err := errors.New("request body is empty")
err = errors.New("request body is empty")
wh.getLogger(nil).Error(err, "bad request") wh.getLogger(nil).Error(err, "bad request")
reviewResponse = Errored(http.StatusBadRequest, err) wh.writeResponse(w, Errored(http.StatusBadRequest, err))
wh.writeResponse(w, reviewResponse)
return return
} }
defer r.Body.Close() defer r.Body.Close()
if body, err = io.ReadAll(r.Body); err != nil { limitedReader := &io.LimitedReader{R: r.Body, N: maxRequestSize}
body, err := io.ReadAll(limitedReader)
if err != nil {
wh.getLogger(nil).Error(err, "unable to read the body from the incoming request") wh.getLogger(nil).Error(err, "unable to read the body from the incoming request")
reviewResponse = Errored(http.StatusBadRequest, err) wh.writeResponse(w, Errored(http.StatusBadRequest, err))
wh.writeResponse(w, reviewResponse) return
}
if limitedReader.N <= 0 {
err := fmt.Errorf("request entity is too large; limit is %d bytes", maxRequestSize)
wh.getLogger(nil).Error(err, "unable to read the body from the incoming request; limit reached")
wh.writeResponse(w, Errored(http.StatusRequestEntityTooLarge, err))
return return
} }
@ -70,8 +93,7 @@ func (wh *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if contentType := r.Header.Get("Content-Type"); contentType != "application/json" { if contentType := r.Header.Get("Content-Type"); contentType != "application/json" {
err = fmt.Errorf("contentType=%s, expected application/json", contentType) err = fmt.Errorf("contentType=%s, expected application/json", contentType)
wh.getLogger(nil).Error(err, "unable to process a request with unknown content type") wh.getLogger(nil).Error(err, "unable to process a request with unknown content type")
reviewResponse = Errored(http.StatusBadRequest, err) wh.writeResponse(w, Errored(http.StatusBadRequest, err))
wh.writeResponse(w, reviewResponse)
return return
} }
@ -89,14 +111,12 @@ func (wh *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
_, actualAdmRevGVK, err := admissionCodecs.UniversalDeserializer().Decode(body, nil, &ar) _, actualAdmRevGVK, err := admissionCodecs.UniversalDeserializer().Decode(body, nil, &ar)
if err != nil { if err != nil {
wh.getLogger(nil).Error(err, "unable to decode the request") wh.getLogger(nil).Error(err, "unable to decode the request")
reviewResponse = Errored(http.StatusBadRequest, err) wh.writeResponse(w, Errored(http.StatusBadRequest, err))
wh.writeResponse(w, reviewResponse)
return return
} }
wh.getLogger(&req).V(5).Info("received request") wh.getLogger(&req).V(5).Info("received request")
reviewResponse = wh.Handle(ctx, req) wh.writeResponseTyped(w, wh.Handle(ctx, req), actualAdmRevGVK)
wh.writeResponseTyped(w, reviewResponse, actualAdmRevGVK)
} }
// writeResponse writes response to w generically, i.e. without encoding GVK information. // writeResponse writes response to w generically, i.e. without encoding GVK information.

View File

@ -33,6 +33,7 @@ type Warnings []string
// Validator defines functions for validating an operation. // Validator defines functions for validating an operation.
// The custom resource kind which implements this interface can validate itself. // The custom resource kind which implements this interface can validate itself.
// To validate the custom resource with another specific struct, use CustomValidator instead. // To validate the custom resource with another specific struct, use CustomValidator instead.
// Deprecated: Use CustomValidator instead.
type Validator interface { type Validator interface {
runtime.Object runtime.Object
@ -53,6 +54,7 @@ type Validator interface {
} }
// ValidatingWebhookFor creates a new Webhook for validating the provided type. // ValidatingWebhookFor creates a new Webhook for validating the provided type.
// Deprecated: Use WithCustomValidator instead.
func ValidatingWebhookFor(scheme *runtime.Scheme, validator Validator) *Webhook { func ValidatingWebhookFor(scheme *runtime.Scheme, validator Validator) *Webhook {
return &Webhook{ return &Webhook{
Handler: &validatingHandler{validator: validator, decoder: NewDecoder(scheme)}, Handler: &validatingHandler{validator: validator, decoder: NewDecoder(scheme)},

View File

@ -30,7 +30,6 @@ import (
// CustomValidator defines functions for validating an operation. // CustomValidator defines functions for validating an operation.
// The object to be validated is passed into methods as a parameter. // The object to be validated is passed into methods as a parameter.
type CustomValidator interface { type CustomValidator interface {
// ValidateCreate validates the object on creation. // ValidateCreate validates the object on creation.
// The optional warnings will be added to the response as warning messages. // The optional warnings will be added to the response as warning messages.
// Return an error if the object is invalid. // Return an error if the object is invalid.

View File

@ -24,9 +24,11 @@ import (
// define some aliases for common bits of the webhook functionality // define some aliases for common bits of the webhook functionality
// Defaulter defines functions for setting defaults on resources. // Defaulter defines functions for setting defaults on resources.
// Deprecated: Use CustomDefaulter instead.
type Defaulter = admission.Defaulter type Defaulter = admission.Defaulter
// Validator defines functions for validating an operation. // Validator defines functions for validating an operation.
// Deprecated: Use CustomValidator instead.
type Validator = admission.Validator type Validator = admission.Validator
// CustomDefaulter defines functions for setting defaults on resources. // CustomDefaulter defines functions for setting defaults on resources.