Bump sigs.k8s.io/controller-runtime to v0.17.5
Signed-off-by: wei-chenglai <qazwsx0939059006@gmail.com>
This commit is contained in:
parent
81b8c4c811
commit
409c1c703b
6
go.mod
6
go.mod
|
@ -6,7 +6,7 @@ require (
|
|||
github.com/adhocore/gronx v1.6.3
|
||||
github.com/distribution/reference v0.5.0
|
||||
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/gogo/protobuf v1.3.2
|
||||
github.com/google/go-cmp v0.6.0
|
||||
|
@ -53,7 +53,7 @@ require (
|
|||
k8s.io/utils v0.0.0-20231127182322-b307cd553661
|
||||
layeh.com/gopher-json v0.0.0-20201124131017-552bb3c4c3bf
|
||||
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/kind v0.22.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-logr/logr v1.4.1 // 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/jsonreference v0.20.4 // indirect
|
||||
github.com/go-openapi/swag v0.22.7 // indirect
|
||||
|
|
15
go.sum
15
go.sum
|
@ -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/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
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 v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
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/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.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.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/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4=
|
||||
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.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.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/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
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/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.2.4/go.mod h1:FyHWQIzQORZ0QVE1BtVHv3cKtNLuXsbNLtpuhNapBOA=
|
||||
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
|
||||
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.17.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.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
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/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
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/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.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
|
||||
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||
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=
|
||||
|
@ -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/go.mod h1:ZSEP01t8oT6104gB4ljsOwwp5uJcI8SWy8IFp2HUvrc=
|
||||
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.16.3/go.mod h1:j7bialYoSn142nv9sCOJmQgDXQXxnroFU4VnX/brVJ0=
|
||||
sigs.k8s.io/controller-runtime v0.17.5 h1:1FI9Lm7NiOOmBsgTV36/s2XrEFXnO2C4sbg/Zme72Rw=
|
||||
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/custom-metrics-apiserver v1.29.0 h1:uUoUjbPrE6nVBE82bo8siIkUDMsfbaSTBB6jAx/LJ9M=
|
||||
sigs.k8s.io/custom-metrics-apiserver v1.29.0/go.mod h1:4XXz92s/SEmP3L2nlUu6lMWorxEQXAD39AdL22IQkDA=
|
||||
|
|
|
@ -51,7 +51,7 @@ func TestSetLeaseOwnerFunc(t *testing.T) {
|
|||
},
|
||||
wantErr: false,
|
||||
want: &coordinationv1.Lease{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{
|
||||
{APIVersion: "cluster.karmada.io/v1alpha1", Kind: "Cluster", Name: "test"}}}},
|
||||
{Name: "test"}}}},
|
||||
},
|
||||
{
|
||||
name: "cluster not found",
|
||||
|
|
|
@ -61,7 +61,6 @@ func TestCreateOrUpdateEndpointSlice(t *testing.T) {
|
|||
},
|
||||
},
|
||||
want: &discoveryv1.EndpointSlice{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: "discovery.k8s.io/v1", Kind: "EndpointSlice"},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "eps", Namespace: "ns",
|
||||
Labels: map[string]string{"foo": "foo1"},
|
||||
|
@ -97,7 +96,6 @@ func TestCreateOrUpdateEndpointSlice(t *testing.T) {
|
|||
},
|
||||
},
|
||||
want: &discoveryv1.EndpointSlice{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: "discovery.k8s.io/v1", Kind: "EndpointSlice"},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "eps", Namespace: "ns",
|
||||
Labels: map[string]string{"foo": "foo1"},
|
||||
|
@ -145,7 +143,6 @@ func TestCreateOrUpdateEndpointSlice(t *testing.T) {
|
|||
},
|
||||
},
|
||||
want: &discoveryv1.EndpointSlice{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: "discovery.k8s.io/v1", Kind: "EndpointSlice"},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "eps", Namespace: "ns",
|
||||
Labels: map[string]string{"foo": "foo1"},
|
||||
|
|
|
@ -85,7 +85,7 @@ func NewClusterScaleClientSet(clusterName string, client client.Client) (*Cluste
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mapper, err := apiutil.NewDiscoveryRESTMapper(clusterConfig, httpClient)
|
||||
mapper, err := apiutil.NewDynamicRESTMapper(clusterConfig, httpClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -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 'K' 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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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] + "'"
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -2,9 +2,12 @@ package jsonpatch
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
|
||||
"github.com/evanphx/json-patch/v5/internal/json"
|
||||
)
|
||||
|
||||
func merge(cur, patch *lazyNode, mergeMerge bool) *lazyNode {
|
||||
|
@ -88,14 +91,14 @@ func pruneDocNulls(doc *partialDoc) *partialDoc {
|
|||
func pruneAryNulls(ary *partialArray) *partialArray {
|
||||
newAry := []*lazyNode{}
|
||||
|
||||
for _, v := range *ary {
|
||||
for _, v := range ary.nodes {
|
||||
if v != nil {
|
||||
pruneNulls(v)
|
||||
}
|
||||
newAry = append(newAry, v)
|
||||
}
|
||||
|
||||
*ary = newAry
|
||||
ary.nodes = newAry
|
||||
|
||||
return ary
|
||||
}
|
||||
|
@ -117,20 +120,28 @@ func MergePatch(docData, patchData []byte) ([]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{}
|
||||
|
||||
docErr := json.Unmarshal(docData, doc)
|
||||
docErr := doc.UnmarshalJSON(docData)
|
||||
|
||||
patch := &partialDoc{}
|
||||
|
||||
patchErr := json.Unmarshal(patchData, patch)
|
||||
patchErr := patch.UnmarshalJSON(patchData)
|
||||
|
||||
if isSyntaxError(docErr) {
|
||||
return nil, errBadJSONDoc
|
||||
}
|
||||
|
||||
if isSyntaxError(patchErr) {
|
||||
return nil, errBadJSONPatch
|
||||
return patchData, 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 {
|
||||
return nil, errBadJSONPatch
|
||||
return patchData, nil
|
||||
}
|
||||
|
||||
if docErr != nil || patchErr != nil {
|
||||
|
@ -151,15 +162,19 @@ func doMergePatch(docData, patchData []byte, mergeMerge bool) ([]byte, error) {
|
|||
}
|
||||
} else {
|
||||
patchAry := &partialArray{}
|
||||
patchErr = json.Unmarshal(patchData, patchAry)
|
||||
patchErr = unmarshal(patchData, &patchAry.nodes)
|
||||
|
||||
if patchErr != nil {
|
||||
// Not an array either, a literal is the result directly.
|
||||
if json.Valid(patchData) {
|
||||
return patchData, nil
|
||||
}
|
||||
return nil, errBadJSONPatch
|
||||
}
|
||||
|
||||
pruneAryNulls(patchAry)
|
||||
|
||||
out, patchErr := json.Marshal(patchAry)
|
||||
out, patchErr := json.Marshal(patchAry.nodes)
|
||||
|
||||
if patchErr != nil {
|
||||
return nil, errBadJSONPatch
|
||||
|
@ -175,6 +190,12 @@ func doMergePatch(docData, patchData []byte, mergeMerge bool) ([]byte, error) {
|
|||
}
|
||||
|
||||
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 {
|
||||
return true
|
||||
}
|
||||
|
@ -227,12 +248,12 @@ func createObjectMergePatch(originalJSON, modifiedJSON []byte) ([]byte, error) {
|
|||
originalDoc := map[string]interface{}{}
|
||||
modifiedDoc := map[string]interface{}{}
|
||||
|
||||
err := json.Unmarshal(originalJSON, &originalDoc)
|
||||
err := unmarshal(originalJSON, &originalDoc)
|
||||
if err != nil {
|
||||
return nil, errBadJSONDoc
|
||||
}
|
||||
|
||||
err = json.Unmarshal(modifiedJSON, &modifiedDoc)
|
||||
err = unmarshal(modifiedJSON, &modifiedDoc)
|
||||
if err != nil {
|
||||
return nil, errBadJSONDoc
|
||||
}
|
||||
|
@ -245,6 +266,10 @@ func createObjectMergePatch(originalJSON, modifiedJSON []byte) ([]byte, error) {
|
|||
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
|
||||
// of converting the original document to the modified document for each
|
||||
// pair of JSON documents provided in the arrays.
|
||||
|
@ -253,12 +278,12 @@ func createArrayMergePatch(originalJSON, modifiedJSON []byte) ([]byte, error) {
|
|||
originalDocs := []json.RawMessage{}
|
||||
modifiedDocs := []json.RawMessage{}
|
||||
|
||||
err := json.Unmarshal(originalJSON, &originalDocs)
|
||||
err := unmarshal(originalJSON, &originalDocs)
|
||||
if err != nil {
|
||||
return nil, errBadJSONDoc
|
||||
}
|
||||
|
||||
err = json.Unmarshal(modifiedJSON, &modifiedDocs)
|
||||
err = unmarshal(modifiedJSON, &modifiedDocs)
|
||||
if err != nil {
|
||||
return nil, errBadJSONDoc
|
||||
}
|
||||
|
@ -314,6 +339,11 @@ func matchesValue(av, bv interface{}) bool {
|
|||
if bt == at {
|
||||
return true
|
||||
}
|
||||
case json.Number:
|
||||
bt := bv.(json.Number)
|
||||
if bt == at {
|
||||
return true
|
||||
}
|
||||
case float64:
|
||||
bt := bv.(float64)
|
||||
if bt == at {
|
||||
|
@ -377,7 +407,7 @@ func getDiff(a, b map[string]interface{}) (map[string]interface{}, error) {
|
|||
if len(dst) > 0 {
|
||||
into[key] = dst
|
||||
}
|
||||
case string, float64, bool:
|
||||
case string, float64, bool, json.Number:
|
||||
if !matchesValue(av, bv) {
|
||||
into[key] = bv
|
||||
}
|
||||
|
|
|
@ -2,11 +2,12 @@ package jsonpatch
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/evanphx/json-patch/v5/internal/json"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
|
@ -45,7 +46,7 @@ var (
|
|||
type lazyNode struct {
|
||||
raw *json.RawMessage
|
||||
doc *partialDoc
|
||||
ary partialArray
|
||||
ary *partialArray
|
||||
which int
|
||||
}
|
||||
|
||||
|
@ -56,11 +57,15 @@ type Operation map[string]*json.RawMessage
|
|||
type Patch []Operation
|
||||
|
||||
type partialDoc struct {
|
||||
self *lazyNode
|
||||
keys []string
|
||||
obj map[string]*lazyNode
|
||||
}
|
||||
|
||||
type partialArray []*lazyNode
|
||||
type partialArray struct {
|
||||
self *lazyNode
|
||||
nodes []*lazyNode
|
||||
}
|
||||
|
||||
type container interface {
|
||||
get(key string, options *ApplyOptions) (*lazyNode, error)
|
||||
|
@ -107,14 +112,14 @@ func newRawMessage(buf []byte) *json.RawMessage {
|
|||
return &ra
|
||||
}
|
||||
|
||||
func (n *lazyNode) MarshalJSON() ([]byte, error) {
|
||||
func (n *lazyNode) RedirectMarshalJSON() (any, error) {
|
||||
switch n.which {
|
||||
case eRaw:
|
||||
return json.Marshal(n.raw)
|
||||
return n.raw, nil
|
||||
case eDoc:
|
||||
return json.Marshal(n.doc)
|
||||
return n.doc, nil
|
||||
case eAry:
|
||||
return json.Marshal(n.ary)
|
||||
return n.ary.nodes, nil
|
||||
default:
|
||||
return nil, ErrUnknownType
|
||||
}
|
||||
|
@ -128,39 +133,38 @@ func (n *lazyNode) UnmarshalJSON(data []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (n *partialDoc) MarshalJSON() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
if _, err := buf.WriteString("{"); err != nil {
|
||||
return nil, err
|
||||
func (n *partialDoc) TrustMarshalJSON(buf *bytes.Buffer) error {
|
||||
if err := buf.WriteByte('{'); err != nil {
|
||||
return err
|
||||
}
|
||||
for i, k := range n.keys {
|
||||
if i > 0 {
|
||||
if _, err := buf.WriteString(", "); err != nil {
|
||||
return nil, err
|
||||
if err := buf.WriteByte(','); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
key, err := json.Marshal(k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
if _, err := buf.Write(key); err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
if _, err := buf.WriteString(": "); err != nil {
|
||||
return nil, err
|
||||
if err := buf.WriteByte(':'); err != nil {
|
||||
return err
|
||||
}
|
||||
value, err := json.Marshal(n.obj[k])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
if _, err := buf.Write(value); err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, err := buf.WriteString("}"); err != nil {
|
||||
return nil, err
|
||||
if err := buf.WriteByte('}'); err != nil {
|
||||
return err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
return nil
|
||||
}
|
||||
|
||||
type syntaxError struct {
|
||||
|
@ -172,70 +176,29 @@ func (err *syntaxError) Error() string {
|
|||
}
|
||||
|
||||
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
|
||||
}
|
||||
buffer := bytes.NewBuffer(data)
|
||||
d := json.NewDecoder(buffer)
|
||||
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)
|
||||
}
|
||||
|
||||
n.keys = keys
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func skipValue(d *json.Decoder) error {
|
||||
t, err := d.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if t != startObject && t != startArray {
|
||||
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 (n *partialArray) UnmarshalJSON(data []byte) error {
|
||||
return json.UnmarshalValid(data, &n.nodes)
|
||||
}
|
||||
|
||||
func (n *partialArray) RedirectMarshalJSON() (interface{}, error) {
|
||||
return n.nodes, nil
|
||||
}
|
||||
|
||||
func deepCopy(src *lazyNode) (*lazyNode, int, error) {
|
||||
if src == nil {
|
||||
return nil, 0, nil
|
||||
}
|
||||
a, err := src.MarshalJSON()
|
||||
a, err := json.Marshal(src)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
@ -243,6 +206,16 @@ func deepCopy(src *lazyNode) (*lazyNode, int, error) {
|
|||
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) {
|
||||
if n.which == eDoc {
|
||||
return n.doc, nil
|
||||
|
@ -252,7 +225,15 @@ func (n *lazyNode) intoDoc() (*partialDoc, error) {
|
|||
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 {
|
||||
return nil, err
|
||||
|
@ -264,21 +245,21 @@ func (n *lazyNode) intoDoc() (*partialDoc, error) {
|
|||
|
||||
func (n *lazyNode) intoAry() (*partialArray, error) {
|
||||
if n.which == eAry {
|
||||
return &n.ary, nil
|
||||
return n.ary, nil
|
||||
}
|
||||
|
||||
if n.raw == nil {
|
||||
return nil, ErrInvalid
|
||||
}
|
||||
|
||||
err := json.Unmarshal(*n.raw, &n.ary)
|
||||
err := unmarshal(*n.raw, &n.ary)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
n.which = eAry
|
||||
return &n.ary, nil
|
||||
return n.ary, nil
|
||||
}
|
||||
|
||||
func (n *lazyNode) compact() []byte {
|
||||
|
@ -302,12 +283,16 @@ func (n *lazyNode) tryDoc() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
err := json.Unmarshal(*n.raw, &n.doc)
|
||||
err := unmarshal(*n.raw, &n.doc)
|
||||
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if n.doc == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
n.which = eDoc
|
||||
return true
|
||||
}
|
||||
|
@ -317,7 +302,7 @@ func (n *lazyNode) tryAry() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
err := json.Unmarshal(*n.raw, &n.ary)
|
||||
err := unmarshal(*n.raw, &n.ary)
|
||||
|
||||
if err != nil {
|
||||
return false
|
||||
|
@ -327,6 +312,18 @@ func (n *lazyNode) tryAry() bool {
|
|||
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 {
|
||||
if n.which == eRaw {
|
||||
if !n.tryDoc() && !n.tryAry() {
|
||||
|
@ -334,7 +331,27 @@ func (n *lazyNode) equal(o *lazyNode) bool {
|
|||
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
|
||||
}
|
||||
|
||||
if len(n.ary) != len(o.ary) {
|
||||
if len(n.ary.nodes) != len(o.ary.nodes) {
|
||||
return false
|
||||
}
|
||||
|
||||
for idx, val := range n.ary {
|
||||
if !val.equal(o.ary[idx]) {
|
||||
for idx, val := range n.ary.nodes {
|
||||
if !val.equal(o.ary.nodes[idx]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@ -398,7 +415,7 @@ func (o Operation) Kind() string {
|
|||
if obj, ok := o["op"]; ok && obj != nil {
|
||||
var op string
|
||||
|
||||
err := json.Unmarshal(*obj, &op)
|
||||
err := unmarshal(*obj, &op)
|
||||
|
||||
if err != nil {
|
||||
return "unknown"
|
||||
|
@ -415,7 +432,7 @@ func (o Operation) Path() (string, error) {
|
|||
if obj, ok := o["path"]; ok && obj != nil {
|
||||
var op string
|
||||
|
||||
err := json.Unmarshal(*obj, &op)
|
||||
err := unmarshal(*obj, &op)
|
||||
|
||||
if err != nil {
|
||||
return "unknown", err
|
||||
|
@ -432,7 +449,7 @@ func (o Operation) From() (string, error) {
|
|||
if obj, ok := o["from"]; ok && obj != nil {
|
||||
var op string
|
||||
|
||||
err := json.Unmarshal(*obj, &op)
|
||||
err := unmarshal(*obj, &op)
|
||||
|
||||
if err != nil {
|
||||
return "unknown", err
|
||||
|
@ -446,6 +463,10 @@ func (o Operation) From() (string, error) {
|
|||
|
||||
func (o Operation) value() *lazyNode {
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -454,10 +475,14 @@ func (o Operation) value() *lazyNode {
|
|||
|
||||
// ValueInterface decodes the operation value into an interface.
|
||||
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{}
|
||||
|
||||
err := json.Unmarshal(*obj, &v)
|
||||
err := unmarshal(*obj, &v)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -493,6 +518,9 @@ func findObject(pd *container, path string, options *ApplyOptions) (container, s
|
|||
split := strings.Split(path, "/")
|
||||
|
||||
if len(split) < 2 {
|
||||
if path == "" {
|
||||
return doc, ""
|
||||
}
|
||||
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) {
|
||||
if key == "" {
|
||||
return d.self, nil
|
||||
}
|
||||
v, ok := d.obj[key]
|
||||
if !ok {
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
idx += len(*d)
|
||||
idx += len(d.nodes)
|
||||
}
|
||||
|
||||
(*d)[idx] = val
|
||||
d.nodes[idx] = val
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *partialArray) add(key string, val *lazyNode, options *ApplyOptions) error {
|
||||
if key == "-" {
|
||||
*d = append(*d, val)
|
||||
d.nodes = append(d.nodes, val)
|
||||
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)
|
||||
}
|
||||
|
||||
sz := len(*d) + 1
|
||||
sz := len(d.nodes) + 1
|
||||
|
||||
ary := make([]*lazyNode, sz)
|
||||
|
||||
cur := *d
|
||||
cur := d
|
||||
|
||||
if idx >= len(ary) {
|
||||
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)
|
||||
}
|
||||
|
||||
copy(ary[0:idx], cur[0:idx])
|
||||
copy(ary[0:idx], cur.nodes[0:idx])
|
||||
ary[idx] = val
|
||||
copy(ary[idx+1:], cur[idx:])
|
||||
copy(ary[idx+1:], cur.nodes[idx:])
|
||||
|
||||
*d = ary
|
||||
d.nodes = ary
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *partialArray) get(key string, options *ApplyOptions) (*lazyNode, error) {
|
||||
if key == "" {
|
||||
return d.self, nil
|
||||
}
|
||||
|
||||
idx, err := strconv.Atoi(key)
|
||||
|
||||
if err != nil {
|
||||
|
@ -647,17 +682,17 @@ func (d *partialArray) get(key string, options *ApplyOptions) (*lazyNode, error)
|
|||
if !options.SupportNegativeIndices {
|
||||
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)
|
||||
}
|
||||
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 (*d)[idx], nil
|
||||
return d.nodes[idx], nil
|
||||
}
|
||||
|
||||
func (d *partialArray) remove(key string, options *ApplyOptions) error {
|
||||
|
@ -666,9 +701,9 @@ func (d *partialArray) remove(key string, options *ApplyOptions) error {
|
|||
return err
|
||||
}
|
||||
|
||||
cur := *d
|
||||
cur := d
|
||||
|
||||
if idx >= len(cur) {
|
||||
if idx >= len(cur.nodes) {
|
||||
if options.AllowMissingPathOnRemove {
|
||||
return nil
|
||||
}
|
||||
|
@ -679,21 +714,21 @@ func (d *partialArray) remove(key string, options *ApplyOptions) error {
|
|||
if !options.SupportNegativeIndices {
|
||||
return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
|
||||
}
|
||||
if idx < -len(cur) {
|
||||
if idx < -len(cur.nodes) {
|
||||
if options.AllowMissingPathOnRemove {
|
||||
return nil
|
||||
}
|
||||
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[idx:], cur[idx+1:])
|
||||
copy(ary[0:idx], cur.nodes[0:idx])
|
||||
copy(ary[idx:], cur.nodes[idx+1:])
|
||||
|
||||
*d = ary
|
||||
d.nodes = ary
|
||||
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")
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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 {
|
||||
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.
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -794,7 +855,10 @@ func ensurePathExists(pd *container, path string, options *ApplyOptions) error {
|
|||
newNode := newLazyNode(newRawMessage(rawJSONObject))
|
||||
|
||||
doc.add(part, newNode, options)
|
||||
doc, _ = newNode.intoDoc()
|
||||
doc, err = newNode.intoDoc()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if isArray(*target.raw) {
|
||||
|
@ -816,6 +880,43 @@ func ensurePathExists(pd *container, path string, options *ApplyOptions) error {
|
|||
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 {
|
||||
path, err := op.Path()
|
||||
if err != nil {
|
||||
|
@ -858,7 +959,7 @@ func (p Patch) replace(doc *container, op Operation, options *ApplyOptions) erro
|
|||
|
||||
switch val.which {
|
||||
case eAry:
|
||||
*doc = &val.ary
|
||||
*doc = val.ary
|
||||
case eDoc:
|
||||
*doc = val.doc
|
||||
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")
|
||||
}
|
||||
|
||||
if from == "" {
|
||||
return errors.Wrapf(ErrInvalid, "unable to move entire document to another path")
|
||||
}
|
||||
|
||||
con, key := findObject(doc, from, options)
|
||||
|
||||
if con == nil {
|
||||
|
@ -942,7 +1047,7 @@ func (p Patch) test(doc *container, op Operation, options *ApplyOptions) error {
|
|||
self.doc = sv
|
||||
self.which = eDoc
|
||||
case *partialArray:
|
||||
self.ary = *sv
|
||||
self.ary = sv
|
||||
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)
|
||||
}
|
||||
|
||||
ov := op.value()
|
||||
|
||||
if val == nil {
|
||||
if op.value().raw == nil {
|
||||
if ov.isNull() {
|
||||
return nil
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -989,7 +1096,7 @@ func (p Patch) copy(doc *container, op Operation, accumulatedCopySize *int64, op
|
|||
con, key := findObject(doc, from, options)
|
||||
|
||||
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)
|
||||
|
@ -1036,14 +1143,22 @@ func Equal(a, b []byte) bool {
|
|||
|
||||
// DecodePatch decodes the passed JSON document as an RFC 6902 patch.
|
||||
func DecodePatch(buf []byte) (Patch, error) {
|
||||
if !json.Valid(buf) {
|
||||
return nil, ErrInvalid
|
||||
}
|
||||
|
||||
var p Patch
|
||||
|
||||
err := json.Unmarshal(buf, &p)
|
||||
err := unmarshal(buf, &p)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := validatePatch(p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
|
@ -1072,14 +1187,25 @@ func (p Patch) ApplyIndentWithOptions(doc []byte, indent string, options *ApplyO
|
|||
return doc, nil
|
||||
}
|
||||
|
||||
var pd container
|
||||
if doc[0] == '[' {
|
||||
pd = &partialArray{}
|
||||
} else {
|
||||
pd = &partialDoc{}
|
||||
if !json.Valid(doc) {
|
||||
return nil, ErrInvalid
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil, err
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -2,12 +2,17 @@ Zapr :zap:
|
|||
==========
|
||||
|
||||
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
|
||||
-----
|
||||
|
||||
Via logr:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"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
|
||||
--------------------
|
||||
|
||||
|
@ -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 --
|
||||
it's illegal to pass a strongly-typed Zap field in a key position to any
|
||||
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.
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -31,14 +31,14 @@ limitations under the License.
|
|||
// Package zapr defines an implementation of the github.com/go-logr/logr
|
||||
// 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
|
||||
// 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
|
||||
// logr.
|
||||
|
@ -168,15 +168,6 @@ func (zl *zapLogger) handleFields(lvl int, args []interface{}, additional ...zap
|
|||
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{}) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -79,9 +79,10 @@ github.com/emirpasic/gods/utils
|
|||
# github.com/evanphx/json-patch v5.7.0+incompatible
|
||||
## explicit
|
||||
github.com/evanphx/json-patch
|
||||
# github.com/evanphx/json-patch/v5 v5.6.0
|
||||
## explicit; go 1.12
|
||||
# github.com/evanphx/json-patch/v5 v5.8.0
|
||||
## explicit; go 1.18
|
||||
github.com/evanphx/json-patch/v5
|
||||
github.com/evanphx/json-patch/v5/internal/json
|
||||
# github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d
|
||||
## explicit
|
||||
github.com/exponent-io/jsonpath
|
||||
|
@ -107,11 +108,12 @@ github.com/go-errors/errors
|
|||
## explicit; go 1.18
|
||||
github.com/go-logr/logr
|
||||
github.com/go-logr/logr/funcr
|
||||
github.com/go-logr/logr/slogr
|
||||
# github.com/go-logr/stdr v1.2.2
|
||||
## explicit; go 1.16
|
||||
github.com/go-logr/stdr
|
||||
# github.com/go-logr/zapr v1.2.4
|
||||
## explicit; go 1.16
|
||||
# github.com/go-logr/zapr v1.3.0
|
||||
## explicit; go 1.18
|
||||
github.com/go-logr/zapr
|
||||
# github.com/go-openapi/jsonpointer v0.20.2
|
||||
## 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/secret
|
||||
sigs.k8s.io/cluster-api/util/version
|
||||
# sigs.k8s.io/controller-runtime v0.16.3
|
||||
## explicit; go 1.20
|
||||
# sigs.k8s.io/controller-runtime v0.17.5
|
||||
## explicit; go 1.21
|
||||
sigs.k8s.io/controller-runtime
|
||||
sigs.k8s.io/controller-runtime/pkg/builder
|
||||
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/recorder
|
||||
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/log
|
||||
sigs.k8s.io/controller-runtime/pkg/manager
|
||||
|
|
|
@ -59,9 +59,9 @@ linters-settings:
|
|||
- pkg: sigs.k8s.io/controller-runtime
|
||||
alias: ctrl
|
||||
staticcheck:
|
||||
go: "1.20"
|
||||
go: "1.21"
|
||||
stylecheck:
|
||||
go: "1.20"
|
||||
go: "1.21"
|
||||
revive:
|
||||
rules:
|
||||
# The following rules are recommended https://github.com/mgechev/revive#recommended-configuration
|
||||
|
|
|
@ -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
|
||||
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
|
||||
|
||||
|
@ -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
|
||||
|
||||
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]
|
||||
|
||||
> **Note**
|
||||
|
@ -24,12 +24,12 @@ You will need to use the [kubebuilder-release-tools][kubebuilder-release-tools]
|
|||
|
||||
### 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 !
|
||||
|
||||
### 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)
|
||||
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`
|
||||
|
||||
[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
|
||||
|
|
|
@ -20,6 +20,7 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"golang.org/x/exp/maps"
|
||||
|
@ -33,7 +34,7 @@ import (
|
|||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
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/client"
|
||||
|
@ -83,6 +84,9 @@ type Informers interface {
|
|||
// of the underlying object.
|
||||
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.
|
||||
// It blocks.
|
||||
Start(ctx context.Context) error
|
||||
|
@ -121,6 +125,8 @@ type Informer interface {
|
|||
|
||||
// HasSynced return true if the informers underlying store has synced.
|
||||
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
|
||||
|
@ -199,6 +205,12 @@ type Options struct {
|
|||
// unless there is already one set in ByObject or DefaultNamespaces.
|
||||
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
|
||||
// for everything that doesn't specify this.
|
||||
//
|
||||
|
@ -369,7 +381,8 @@ func newCache(restConfig *rest.Config, opts Options) newCacheFunc {
|
|||
Field: config.FieldSelector,
|
||||
},
|
||||
Transform: config.Transform,
|
||||
UnsafeDisableDeepCopy: pointer.BoolDeref(config.UnsafeDisableDeepCopy, false),
|
||||
WatchErrorHandler: opts.DefaultWatchErrorHandler,
|
||||
UnsafeDisableDeepCopy: ptr.Deref(config.UnsafeDisableDeepCopy, false),
|
||||
NewInformer: opts.newInformer,
|
||||
}),
|
||||
readerFailOnMissingInformer: opts.ReaderFailOnMissingInformer,
|
||||
|
@ -400,20 +413,12 @@ func defaultOpts(config *rest.Config, opts Options) (Options, error) {
|
|||
// Construct a new Mapper if unset
|
||||
if opts.Mapper == nil {
|
||||
var err error
|
||||
opts.Mapper, err = apiutil.NewDiscoveryRESTMapper(config, opts.HTTPClient)
|
||||
opts.Mapper, err = apiutil.NewDynamicRESTMapper(config, opts.HTTPClient)
|
||||
if err != nil {
|
||||
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 {
|
||||
isNamespaced, err := apiutil.IsObjectNamespaced(obj, opts.Scheme, opts.Mapper)
|
||||
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)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// 1. Default from the undefaulted type-level config
|
||||
config = defaultConfig(config, byObjectToConfig(byObject))
|
||||
|
@ -449,19 +459,35 @@ func defaultOpts(config *rest.Config, opts Options) (Options, error) {
|
|||
byObject.Namespaces[namespace] = config
|
||||
}
|
||||
|
||||
defaultedConfig := defaultConfig(byObjectToConfig(byObject), optionDefaultsToConfig(&opts))
|
||||
byObject.Label = defaultedConfig.LabelSelector
|
||||
byObject.Field = defaultedConfig.FieldSelector
|
||||
byObject.Transform = defaultedConfig.Transform
|
||||
byObject.UnsafeDisableDeepCopy = defaultedConfig.UnsafeDisableDeepCopy
|
||||
|
||||
if isNamespaced && byObject.Namespaces == nil {
|
||||
byObject.Namespaces = opts.DefaultNamespaces
|
||||
// Only default ByObject iself if it isn't namespaced or has no namespaces configured, as only
|
||||
// then any of this will be honored.
|
||||
if !isNamespaced || len(byObject.Namespaces) == 0 {
|
||||
defaultedConfig := defaultConfig(byObjectToConfig(byObject), optionDefaultsToConfig(&opts))
|
||||
byObject.Label = defaultedConfig.LabelSelector
|
||||
byObject.Field = defaultedConfig.FieldSelector
|
||||
byObject.Transform = defaultedConfig.Transform
|
||||
byObject.UnsafeDisableDeepCopy = defaultedConfig.UnsafeDisableDeepCopy
|
||||
}
|
||||
|
||||
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
|
||||
if opts.SyncPeriod == nil {
|
||||
opts.SyncPeriod = &defaultSyncPeriod
|
||||
|
@ -486,20 +512,21 @@ func defaultConfig(toDefault, defaultFrom Config) Config {
|
|||
return toDefault
|
||||
}
|
||||
|
||||
func namespaceAllSelector(namespaces []string) fields.Selector {
|
||||
func namespaceAllSelector(namespaces []string) []fields.Selector {
|
||||
selectors := make([]fields.Selector, 0, len(namespaces)-1)
|
||||
sort.Strings(namespaces)
|
||||
for _, namespace := range namespaces {
|
||||
if namespace != metav1.NamespaceAll {
|
||||
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) {
|
||||
return []T{a, b}
|
||||
return append(a, b)
|
||||
}
|
||||
return []T{a}
|
||||
return a
|
||||
}
|
||||
|
|
|
@ -52,6 +52,14 @@ func (dbt *delegatingByGVKCache) List(ctx context.Context, list client.ObjectLis
|
|||
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) {
|
||||
cache, err := dbt.cacheForObject(obj)
|
||||
if err != nil {
|
||||
|
|
|
@ -190,6 +190,17 @@ func (ic *informerCache) getInformerForKind(ctx context.Context, gvk schema.Grou
|
|||
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
|
||||
// to indicate that this can be started without requiring the leader lock.
|
||||
func (ic *informerCache) NeedLeaderElection() bool {
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
apimeta "k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
@ -117,16 +118,14 @@ func (c *CacheReader) List(_ context.Context, out client.ObjectList, opts ...cli
|
|||
|
||||
switch {
|
||||
case listOpts.FieldSelector != nil:
|
||||
// TODO(directxman12): support more complicated field selectors by
|
||||
// combining multiple indices, GetIndexers, etc
|
||||
field, val, requiresExact := selector.RequiresExactMatch(listOpts.FieldSelector)
|
||||
requiresExact := selector.RequiresExactMatch(listOpts.FieldSelector)
|
||||
if !requiresExact {
|
||||
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
|
||||
// namespaced index key. Otherwise, ask for the non-namespaced variant by using the fake "all namespaces"
|
||||
// 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 != "":
|
||||
objs, err = c.indexer.ByIndex(cache.NamespaceIndex, listOpts.Namespace)
|
||||
default:
|
||||
|
@ -178,6 +177,54 @@ func (c *CacheReader) List(_ context.Context, out client.ObjectList, opts ...cli
|
|||
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.
|
||||
// It's akin to MetaNamespaceKeyFunc. It's separate from
|
||||
// String to allow keeping the key format easily in sync with
|
||||
|
|
|
@ -36,6 +36,7 @@ import (
|
|||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/internal/syncs"
|
||||
)
|
||||
|
||||
// InformersOpts configures an InformerMap.
|
||||
|
@ -49,6 +50,7 @@ type InformersOpts struct {
|
|||
Selector Selector
|
||||
Transform cache.TransformFunc
|
||||
UnsafeDisableDeepCopy bool
|
||||
WatchErrorHandler cache.WatchErrorHandler
|
||||
}
|
||||
|
||||
// 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,
|
||||
unsafeDisableDeepCopy: options.UnsafeDisableDeepCopy,
|
||||
newInformer: newInformer,
|
||||
watchErrorHandler: options.WatchErrorHandler,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -86,6 +89,20 @@ type Cache struct {
|
|||
|
||||
// CacheReader wraps Informer and implements the CacheReader interface for a single type
|
||||
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 {
|
||||
|
@ -159,6 +176,11 @@ type Informers struct {
|
|||
|
||||
// NewInformer allows overriding of the shared index informer constructor for testing.
|
||||
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.
|
||||
|
@ -173,13 +195,13 @@ func (ip *Informers) Start(ctx context.Context) error {
|
|||
|
||||
// Start each informer
|
||||
for _, i := range ip.tracker.Structured {
|
||||
ip.startInformerLocked(i.Informer)
|
||||
ip.startInformerLocked(i)
|
||||
}
|
||||
for _, i := range ip.tracker.Unstructured {
|
||||
ip.startInformerLocked(i.Informer)
|
||||
ip.startInformerLocked(i)
|
||||
}
|
||||
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.
|
||||
|
@ -194,7 +216,7 @@ func (ip *Informers) Start(ctx context.Context) error {
|
|||
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
|
||||
// the waitGroup to finish, since waitGroups don't support waiting and adding
|
||||
// at the same time.
|
||||
|
@ -205,7 +227,7 @@ func (ip *Informers) startInformerLocked(informer cache.SharedIndexInformer) {
|
|||
ip.waitGroup.Add(1)
|
||||
go func() {
|
||||
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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
switch obj.(type) {
|
||||
case runtime.Unstructured:
|
||||
|
@ -323,6 +360,13 @@ func (ip *Informers) addInformerToMap(gvk schema.GroupVersionKind, obj runtime.O
|
|||
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
|
||||
if err := sharedIndexInformer.SetTransform(ip.transform); err != nil {
|
||||
return nil, false, err
|
||||
|
@ -342,13 +386,14 @@ func (ip *Informers) addInformerToMap(gvk schema.GroupVersionKind, obj runtime.O
|
|||
scopeName: mapping.Scope.Name(),
|
||||
disableDeepCopy: ip.unsafeDisableDeepCopy,
|
||||
},
|
||||
stop: make(chan struct{}),
|
||||
}
|
||||
ip.informersByType(obj)[gvk] = i
|
||||
|
||||
// Start the informer in case the InformersMap has started, otherwise it will be
|
||||
// started when the InformersMap starts.
|
||||
if ip.started {
|
||||
ip.startInformerLocked(i.Informer)
|
||||
ip.startInformerLocked(i)
|
||||
}
|
||||
return i, ip.started, nil
|
||||
}
|
||||
|
|
|
@ -109,6 +109,27 @@ func (c *multiNamespaceCache) GetInformer(ctx context.Context, obj client.Object
|
|||
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) {
|
||||
// If the object is cluster scoped, get the informer from clusterCache,
|
||||
// if not use the namespaced caches.
|
||||
|
@ -391,3 +412,13 @@ func (i *multiNamespaceInformer) HasSynced() bool {
|
|||
}
|
||||
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
|
||||
}
|
||||
|
|
|
@ -31,11 +31,9 @@ import (
|
|||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"k8s.io/client-go/discovery"
|
||||
"k8s.io/client-go/dynamic"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/restmapper"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -60,25 +58,6 @@ func AddToProtobufScheme(addToScheme func(*runtime.Scheme) error) error {
|
|||
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.
|
||||
// For unstructured objects the gvk is found from the object itself.
|
||||
func IsObjectNamespaced(obj runtime.Object, scheme *runtime.Scheme, restmapper meta.RESTMapper) (bool, error) {
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
"net/http"
|
||||
"sync"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"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.
|
||||
type mapper struct {
|
||||
mapper meta.RESTMapper
|
||||
client *discovery.DiscoveryClient
|
||||
client discovery.DiscoveryInterface
|
||||
knownGroups map[string]*restmapper.APIGroupResources
|
||||
apiGroups map[string]*metav1.APIGroup
|
||||
|
||||
|
@ -166,8 +167,10 @@ func (m *mapper) addKnownGroupAndReload(groupName string, versions ...string) er
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, version := range apiGroup.Versions {
|
||||
versions = append(versions, version.Version)
|
||||
if apiGroup != nil {
|
||||
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},
|
||||
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 {
|
||||
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.
|
||||
// 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
|
||||
for _, v := range groupResources.Group.Versions {
|
||||
if v.Version == version {
|
||||
|
@ -254,21 +262,17 @@ func (m *mapper) findAPIGroupByName(groupName string) (*metav1.APIGroup, error)
|
|||
m.mu.Unlock()
|
||||
|
||||
// Looking in the cache again.
|
||||
{
|
||||
m.mu.RLock()
|
||||
group, ok := m.apiGroups[groupName]
|
||||
m.mu.RUnlock()
|
||||
if ok {
|
||||
return group, nil
|
||||
}
|
||||
}
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
// If there is still nothing, return an error.
|
||||
return nil, fmt.Errorf("failed to find API group %q", groupName)
|
||||
// Don't return an error here if the API group is not present.
|
||||
// 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.
|
||||
func (m *mapper) fetchGroupVersionResources(groupName string, versions ...string) (map[schema.GroupVersion]*metav1.APIResourceList, error) {
|
||||
// fetchGroupVersionResourcesLocked fetches the resources for the specified group and its versions.
|
||||
// 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)
|
||||
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}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if apiResourceList != nil {
|
||||
// even in case of error, some fallback might have been returned.
|
||||
groupVersionResources[groupVersion] = apiResourceList
|
||||
|
@ -292,3 +307,29 @@ func (m *mapper) fetchGroupVersionResources(groupName string, versions ...string
|
|||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -90,11 +90,18 @@ type CacheOptions struct {
|
|||
type NewClientFunc func(config *rest.Config, options Options) (Client, error)
|
||||
|
||||
// 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
|
||||
// corresponding group, version, and kind for the given type. In the
|
||||
// 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{}
|
||||
|
||||
// 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.
|
||||
type client struct {
|
||||
typedClient typedClient
|
||||
|
|
|
@ -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
|
||||
// we save as the very same type, otherwise subsequent List requests will fail.
|
||||
func convertFromUnstructuredIfNecessary(s *runtime.Scheme, o runtime.Object) (runtime.Object, error) {
|
||||
gvk := o.GetObjectKind().GroupVersionKind()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -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")})
|
||||
}
|
||||
|
||||
gvk := obj.GetObjectKind().GroupVersionKind()
|
||||
if gvk.Empty() {
|
||||
gvk, err = apiutil.GVKForObject(obj, t.scheme)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gvk, err := apiutil.GVKForObject(obj, t.scheme)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
gvk, err := apiutil.GVKForObject(obj, c.scheme)
|
||||
if err != nil {
|
||||
return err
|
||||
if _, isUnstructured := obj.(runtime.Unstructured); isUnstructured {
|
||||
gvk, err := apiutil.GVKForObject(obj, c.scheme)
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
decoder := scheme.Codecs.UniversalDecoder()
|
||||
zero(obj)
|
||||
_, _, err = decoder.Decode(j, nil, obj)
|
||||
return err
|
||||
return json.Unmarshal(j, obj)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
ta, err := meta.TypeAccessor(o)
|
||||
if err != nil {
|
||||
return err
|
||||
if _, isUnstructured := obj.(runtime.Unstructured); isUnstructured {
|
||||
ta, err := meta.TypeAccessor(o)
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
decoder := scheme.Codecs.UniversalDecoder()
|
||||
zero(obj)
|
||||
_, _, err = decoder.Decode(j, nil, obj)
|
||||
if err != nil {
|
||||
if err := json.Unmarshal(j, obj); err != nil {
|
||||
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) {
|
||||
// We only allow filtering on the basis of a single field to ensure consistency with the
|
||||
// behavior of the cache reader (which we're faking here).
|
||||
fieldKey, fieldVal, requiresExact := selector.RequiresExactMatch(fs)
|
||||
requiresExact := selector.RequiresExactMatch(fs)
|
||||
if !requiresExact {
|
||||
return nil, fmt.Errorf("field selector %s is not in one of the two supported forms \"key==val\" or \"key=val\"",
|
||||
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
|
||||
// if there are no indexes registered for the GroupVersionKind of the objects in the list.
|
||||
indexes := c.indexes[gvk]
|
||||
if len(indexes) == 0 || indexes[fieldKey] == nil {
|
||||
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, fieldKey, fieldKey, gvk)
|
||||
for _, req := range fs.Requirements() {
|
||||
if len(indexes) == 0 || indexes[req.Field] == nil {
|
||||
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))
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -862,21 +868,22 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client
|
|||
if !handled {
|
||||
panic("tracker could not handle patch method")
|
||||
}
|
||||
ta, err := meta.TypeAccessor(o)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
if _, isUnstructured := obj.(runtime.Unstructured); isUnstructured {
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
decoder := scheme.Codecs.UniversalDecoder()
|
||||
zero(obj)
|
||||
_, _, err = decoder.Decode(j, nil, obj)
|
||||
return err
|
||||
return json.Unmarshal(j, obj)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return nil, err
|
||||
}
|
||||
case types.StrategicMergePatchType, types.ApplyPatchType:
|
||||
case types.StrategicMergePatchType:
|
||||
mergedByte, err := strategicpatch.StrategicMergePatch(old, action.GetPatch(), obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -948,8 +955,10 @@ func dryPatch(action testing.PatchActionImpl, tracker testing.ObjectTracker) (ru
|
|||
if err = json.Unmarshal(mergedByte, obj); err != nil {
|
||||
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:
|
||||
return nil, fmt.Errorf("PatchType is not supported")
|
||||
return nil, fmt.Errorf("%s PatchType is not supported", action.GetPatchType())
|
||||
}
|
||||
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: "PriorityLevelConfiguration"},
|
||||
{Group: "flowcontrol.apiserver.k8s.io", Version: "v1", Kind: "FlowSchema"},
|
||||
{Group: "flowcontrol.apiserver.k8s.io", Version: "v1", Kind: "PriorityLevelConfiguration"},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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...)...)
|
||||
}
|
|
@ -419,7 +419,7 @@ type ListOptions struct {
|
|||
LabelSelector labels.Selector
|
||||
// FieldSelector filters results by a particular field. In order
|
||||
// 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
|
||||
|
||||
// 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) {
|
||||
// TODO(directxman12): can we avoid reserializing this over and over?
|
||||
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.
|
||||
noValidSel := labels.SelectorFromValidatedSet(map[string]string(m))
|
||||
|
|
94
vendor/sigs.k8s.io/controller-runtime/pkg/controller/controllerutil/controllerutil.go
generated
vendored
94
vendor/sigs.k8s.io/controller-runtime/pkg/controller/controllerutil/controllerutil.go
generated
vendored
|
@ -27,7 +27,7 @@ import (
|
|||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"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/apiutil"
|
||||
|
@ -77,8 +77,8 @@ func SetControllerReference(owner, controlled metav1.Object, scheme *runtime.Sch
|
|||
Kind: gvk.Kind,
|
||||
Name: owner.GetName(),
|
||||
UID: owner.GetUID(),
|
||||
BlockOwnerDeletion: pointer.Bool(true),
|
||||
Controller: pointer.Bool(true),
|
||||
BlockOwnerDeletion: ptr.To(true),
|
||||
Controller: ptr.To(true),
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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) {
|
||||
owners := object.GetOwnerReferences()
|
||||
if idx := indexOwnerRef(owners, ref); idx == -1 {
|
||||
|
@ -166,7 +244,6 @@ func referSameObject(a, b metav1.OwnerReference) bool {
|
|||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
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.
|
||||
//
|
||||
// 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) {
|
||||
key := client.ObjectKeyFromObject(obj)
|
||||
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.
|
||||
//
|
||||
// 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) {
|
||||
key := client.ObjectKeyFromObject(obj)
|
||||
if err := c.Get(ctx, key, obj); err != nil {
|
||||
|
|
|
@ -42,7 +42,7 @@ import (
|
|||
// 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.
|
||||
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)
|
||||
|
||||
// Update is called in response to an update event - e.g. Pod Updated.
|
||||
|
|
|
@ -22,14 +22,16 @@ import (
|
|||
)
|
||||
|
||||
// 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()
|
||||
if len(reqs) != 1 {
|
||||
return "", "", false
|
||||
if len(reqs) == 0 {
|
||||
return false
|
||||
}
|
||||
req := reqs[0]
|
||||
if req.Operator != selection.Equals && req.Operator != selection.DoubleEquals {
|
||||
return "", "", false
|
||||
|
||||
for _, req := range reqs {
|
||||
if req.Operator != selection.Equals && req.Operator != selection.DoubleEquals {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return req.Field, req.Value, true
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -179,6 +179,24 @@ func (cm *controllerManager) add(r Runnable) error {
|
|||
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.
|
||||
func (cm *controllerManager) AddHealthzCheck(name string, check healthz.Checker) error {
|
||||
cm.Lock()
|
||||
|
@ -518,6 +536,8 @@ func (cm *controllerManager) engageStopProcedure(stopComplete <-chan struct{}) e
|
|||
|
||||
// Stop all the leader election runnables, which includes reconcilers.
|
||||
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)
|
||||
|
||||
// Stop the caches before the leader election runnables, this is an important
|
||||
|
|
|
@ -34,7 +34,7 @@ import (
|
|||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/leaderelection/resourcelock"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"k8s.io/utils/pointer"
|
||||
"k8s.io/utils/ptr"
|
||||
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
|
||||
|
||||
"sigs.k8s.io/controller-runtime/pkg/cache"
|
||||
|
@ -67,6 +67,15 @@ type Manager interface {
|
|||
// election was configured.
|
||||
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(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)
|
||||
}
|
||||
|
||||
errChan := make(chan error)
|
||||
errChan := make(chan error, 1)
|
||||
runnables := newRunnables(options.BaseContext, errChan)
|
||||
return &controllerManager{
|
||||
stopProcedureEngaged: pointer.Int64(0),
|
||||
stopProcedureEngaged: ptr.To(int64(0)),
|
||||
cluster: cluster,
|
||||
runnables: runnables,
|
||||
errChan: errChan,
|
||||
|
|
|
@ -263,6 +263,15 @@ func (r *runnableGroup) Add(rn Runnable, ready runnableCheck) error {
|
|||
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.
|
||||
r.ch <- readyRunnable
|
||||
return nil
|
||||
|
@ -272,7 +281,11 @@ func (r *runnableGroup) Add(rn Runnable, ready runnableCheck) error {
|
|||
func (r *runnableGroup) StopAndWait(ctx context.Context) {
|
||||
r.stopOnce.Do(func() {
|
||||
// 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.stop.Lock()
|
||||
|
|
|
@ -46,6 +46,9 @@ var DefaultBindAddress = ":8080"
|
|||
|
||||
// Server is a server that serves metrics.
|
||||
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
|
||||
// the metrics server doesn't need leader election.
|
||||
NeedLeaderElection() bool
|
||||
|
@ -179,6 +182,23 @@ func (*defaultServer) NeedLeaderElection() bool {
|
|||
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.
|
||||
// It will install the metrics related resources depend on the server configuration.
|
||||
func (s *defaultServer) Start(ctx context.Context) error {
|
||||
|
|
|
@ -54,14 +54,14 @@ var (
|
|||
Subsystem: WorkQueueSubsystem,
|
||||
Name: QueueLatencyKey,
|
||||
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"})
|
||||
|
||||
workDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Subsystem: WorkQueueSubsystem,
|
||||
Name: WorkDurationKey,
|
||||
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"})
|
||||
|
||||
unfinished = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
|
|
|
@ -19,9 +19,11 @@ package reconcile
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
// 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
|
||||
// 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.
|
||||
Reconcile(context.Context, Request) (Result, error)
|
||||
}
|
||||
|
@ -110,6 +112,36 @@ var _ Reconciler = Func(nil)
|
|||
// Reconcile implements Reconciler.
|
||||
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
|
||||
// and recorded in metrics.
|
||||
func TerminalError(wrapped error) error {
|
||||
|
|
|
@ -27,12 +27,14 @@ import (
|
|||
)
|
||||
|
||||
// Defaulter defines functions for setting defaults on resources.
|
||||
// Deprecated: Ue CustomDefaulter instead.
|
||||
type Defaulter interface {
|
||||
runtime.Object
|
||||
Default()
|
||||
}
|
||||
|
||||
// DefaultingWebhookFor creates a new Webhook for Defaulting the provided type.
|
||||
// Deprecated: Use WithCustomDefaulter instead.
|
||||
func DefaultingWebhookFor(scheme *runtime.Scheme, defaulter Defaulter) *Webhook {
|
||||
return &Webhook{
|
||||
Handler: &mutatingHandler{defaulter: defaulter, decoder: NewDecoder(scheme)},
|
||||
|
|
|
@ -34,6 +34,26 @@ import (
|
|||
var admissionScheme = runtime.NewScheme()
|
||||
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() {
|
||||
utilruntime.Must(v1.AddToScheme(admissionScheme))
|
||||
utilruntime.Must(v1beta1.AddToScheme(admissionScheme))
|
||||
|
@ -42,27 +62,30 @@ func init() {
|
|||
var _ http.Handler = &Webhook{}
|
||||
|
||||
func (wh *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
var body []byte
|
||||
var err error
|
||||
ctx := r.Context()
|
||||
if wh.WithContextFunc != nil {
|
||||
ctx = wh.WithContextFunc(ctx, r)
|
||||
}
|
||||
|
||||
var reviewResponse Response
|
||||
if r.Body == nil {
|
||||
err = errors.New("request body is empty")
|
||||
if r.Body == nil || r.Body == http.NoBody {
|
||||
err := errors.New("request body is empty")
|
||||
wh.getLogger(nil).Error(err, "bad request")
|
||||
reviewResponse = Errored(http.StatusBadRequest, err)
|
||||
wh.writeResponse(w, reviewResponse)
|
||||
wh.writeResponse(w, Errored(http.StatusBadRequest, err))
|
||||
return
|
||||
}
|
||||
|
||||
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")
|
||||
reviewResponse = Errored(http.StatusBadRequest, err)
|
||||
wh.writeResponse(w, reviewResponse)
|
||||
wh.writeResponse(w, Errored(http.StatusBadRequest, err))
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -70,8 +93,7 @@ func (wh *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
if contentType := r.Header.Get("Content-Type"); contentType != "application/json" {
|
||||
err = fmt.Errorf("contentType=%s, expected application/json", contentType)
|
||||
wh.getLogger(nil).Error(err, "unable to process a request with unknown content type")
|
||||
reviewResponse = Errored(http.StatusBadRequest, err)
|
||||
wh.writeResponse(w, reviewResponse)
|
||||
wh.writeResponse(w, Errored(http.StatusBadRequest, err))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -89,14 +111,12 @@ func (wh *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
_, actualAdmRevGVK, err := admissionCodecs.UniversalDeserializer().Decode(body, nil, &ar)
|
||||
if err != nil {
|
||||
wh.getLogger(nil).Error(err, "unable to decode the request")
|
||||
reviewResponse = Errored(http.StatusBadRequest, err)
|
||||
wh.writeResponse(w, reviewResponse)
|
||||
wh.writeResponse(w, Errored(http.StatusBadRequest, err))
|
||||
return
|
||||
}
|
||||
wh.getLogger(&req).V(5).Info("received request")
|
||||
|
||||
reviewResponse = wh.Handle(ctx, req)
|
||||
wh.writeResponseTyped(w, reviewResponse, actualAdmRevGVK)
|
||||
wh.writeResponseTyped(w, wh.Handle(ctx, req), actualAdmRevGVK)
|
||||
}
|
||||
|
||||
// writeResponse writes response to w generically, i.e. without encoding GVK information.
|
||||
|
|
|
@ -33,6 +33,7 @@ type Warnings []string
|
|||
// Validator defines functions for validating an operation.
|
||||
// The custom resource kind which implements this interface can validate itself.
|
||||
// To validate the custom resource with another specific struct, use CustomValidator instead.
|
||||
// Deprecated: Use CustomValidator instead.
|
||||
type Validator interface {
|
||||
runtime.Object
|
||||
|
||||
|
@ -53,6 +54,7 @@ type Validator interface {
|
|||
}
|
||||
|
||||
// ValidatingWebhookFor creates a new Webhook for validating the provided type.
|
||||
// Deprecated: Use WithCustomValidator instead.
|
||||
func ValidatingWebhookFor(scheme *runtime.Scheme, validator Validator) *Webhook {
|
||||
return &Webhook{
|
||||
Handler: &validatingHandler{validator: validator, decoder: NewDecoder(scheme)},
|
||||
|
|
1
vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/validator_custom.go
generated
vendored
1
vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/validator_custom.go
generated
vendored
|
@ -30,7 +30,6 @@ import (
|
|||
// CustomValidator defines functions for validating an operation.
|
||||
// The object to be validated is passed into methods as a parameter.
|
||||
type CustomValidator interface {
|
||||
|
||||
// ValidateCreate validates the object on creation.
|
||||
// The optional warnings will be added to the response as warning messages.
|
||||
// Return an error if the object is invalid.
|
||||
|
|
|
@ -24,9 +24,11 @@ import (
|
|||
// define some aliases for common bits of the webhook functionality
|
||||
|
||||
// Defaulter defines functions for setting defaults on resources.
|
||||
// Deprecated: Use CustomDefaulter instead.
|
||||
type Defaulter = admission.Defaulter
|
||||
|
||||
// Validator defines functions for validating an operation.
|
||||
// Deprecated: Use CustomValidator instead.
|
||||
type Validator = admission.Validator
|
||||
|
||||
// CustomDefaulter defines functions for setting defaults on resources.
|
||||
|
|
Loading…
Reference in New Issue