rebase: update kubernetes to v1.21.2

Updated kubernetes packages to latest release.
resizefs package has been included into k8s.io/mount-utils
package. updated code to use the same.

Updates: #1968

Signed-off-by: Rakshith R <rar@redhat.com>
This commit is contained in:
Rakshith R
2021-06-25 10:29:51 +05:30
committed by mergify[bot]
parent 8ce5ae16c1
commit 1b23d78113
1115 changed files with 98825 additions and 12365 deletions

View File

@ -19,6 +19,7 @@ package client
import (
"context"
"errors"
"fmt"
"io"
"math/rand"
"net"
@ -49,6 +50,10 @@ type grpcTunnel struct {
conns map[int64]*conn
pendingDialLock sync.RWMutex
connsLock sync.RWMutex
// The tunnel will be closed if the caller fails to read via conn.Read()
// more than readTimeoutSeconds after a packet has been received.
readTimeoutSeconds int
}
type clientConn interface {
@ -75,9 +80,10 @@ func CreateSingleUseGrpcTunnel(address string, opts ...grpc.DialOption) (Tunnel,
}
tunnel := &grpcTunnel{
stream: stream,
pendingDial: make(map[int64]chan<- dialResult),
conns: make(map[int64]*conn),
stream: stream,
pendingDial: make(map[int64]chan<- dialResult),
conns: make(map[int64]*conn),
readTimeoutSeconds: 10,
}
go tunnel.serve(c)
@ -110,10 +116,17 @@ func (t *grpcTunnel) serve(c clientConn) {
if !ok {
klog.V(1).Infoln("DialResp not recognized; dropped")
} else {
ch <- dialResult{
result := dialResult{
err: resp.Error,
connid: resp.ConnectID,
}
select {
case ch <- result:
default:
klog.ErrorS(fmt.Errorf("blocked pending channel"), "Received second dial response for connection request", "connectionID", resp.ConnectID, "dialID", resp.Random)
// On multiple dial responses, avoid leaking serve goroutine.
return
}
}
if resp.Error != "" {
@ -129,7 +142,14 @@ func (t *grpcTunnel) serve(c clientConn) {
t.connsLock.RUnlock()
if ok {
conn.readCh <- resp.Data
timer := time.NewTimer((time.Duration)(t.readTimeoutSeconds) * time.Second)
select {
case conn.readCh <- resp.Data:
timer.Stop()
case <-timer.C:
klog.ErrorS(fmt.Errorf("timeout"), "readTimeout has been reached, the grpc connection to the proxy server will be closed", "connectionID", conn.connID, "readTimeoutSeconds", t.readTimeoutSeconds)
return
}
} else {
klog.V(1).InfoS("connection not recognized", "connectionID", resp.ConnectID)
}
@ -160,8 +180,8 @@ func (t *grpcTunnel) Dial(protocol, address string) (net.Conn, error) {
return nil, errors.New("protocol not supported")
}
random := rand.Int63()
resCh := make(chan dialResult)
random := rand.Int63() /* #nosec G404 */
resCh := make(chan dialResult, 1)
t.pendingDialLock.Lock()
t.pendingDial[random] = resCh
t.pendingDialLock.Unlock()
@ -199,7 +219,7 @@ func (t *grpcTunnel) Dial(protocol, address string) (net.Conn, error) {
}
c.connID = res.connid
c.readCh = make(chan []byte, 10)
c.closeCh = make(chan string)
c.closeCh = make(chan string, 1)
t.connsLock.Lock()
t.conns[res.connid] = c
t.connsLock.Unlock()

View File

@ -0,0 +1,21 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package fieldpath defines a way for referencing path elements (e.g., an
// index in an array, or a key in a map). It provides types for arranging these
// into paths for referencing nested fields, and for grouping those into sets,
// for referencing multiple nested fields.
package fieldpath

View File

@ -0,0 +1,317 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package fieldpath
import (
"fmt"
"sort"
"strings"
"sigs.k8s.io/structured-merge-diff/v4/value"
)
// PathElement describes how to select a child field given a containing object.
type PathElement struct {
// Exactly one of the following fields should be non-nil.
// FieldName selects a single field from a map (reminder: this is also
// how structs are represented). The containing object must be a map.
FieldName *string
// Key selects the list element which has fields matching those given.
// The containing object must be an associative list with map typed
// elements. They are sorted alphabetically.
Key *value.FieldList
// Value selects the list element with the given value. The containing
// object must be an associative list with a primitive typed element
// (i.e., a set).
Value *value.Value
// Index selects a list element by its index number. The containing
// object must be an atomic list.
Index *int
}
// Less provides an order for path elements.
func (e PathElement) Less(rhs PathElement) bool {
return e.Compare(rhs) < 0
}
// Compare provides an order for path elements.
func (e PathElement) Compare(rhs PathElement) int {
if e.FieldName != nil {
if rhs.FieldName == nil {
return -1
}
return strings.Compare(*e.FieldName, *rhs.FieldName)
} else if rhs.FieldName != nil {
return 1
}
if e.Key != nil {
if rhs.Key == nil {
return -1
}
return e.Key.Compare(*rhs.Key)
} else if rhs.Key != nil {
return 1
}
if e.Value != nil {
if rhs.Value == nil {
return -1
}
return value.Compare(*e.Value, *rhs.Value)
} else if rhs.Value != nil {
return 1
}
if e.Index != nil {
if rhs.Index == nil {
return -1
}
if *e.Index < *rhs.Index {
return -1
} else if *e.Index == *rhs.Index {
return 0
}
return 1
} else if rhs.Index != nil {
return 1
}
return 0
}
// Equals returns true if both path elements are equal.
func (e PathElement) Equals(rhs PathElement) bool {
if e.FieldName != nil {
if rhs.FieldName == nil {
return false
}
return *e.FieldName == *rhs.FieldName
} else if rhs.FieldName != nil {
return false
}
if e.Key != nil {
if rhs.Key == nil {
return false
}
return e.Key.Equals(*rhs.Key)
} else if rhs.Key != nil {
return false
}
if e.Value != nil {
if rhs.Value == nil {
return false
}
return value.Equals(*e.Value, *rhs.Value)
} else if rhs.Value != nil {
return false
}
if e.Index != nil {
if rhs.Index == nil {
return false
}
return *e.Index == *rhs.Index
} else if rhs.Index != nil {
return false
}
return true
}
// String presents the path element as a human-readable string.
func (e PathElement) String() string {
switch {
case e.FieldName != nil:
return "." + *e.FieldName
case e.Key != nil:
strs := make([]string, len(*e.Key))
for i, k := range *e.Key {
strs[i] = fmt.Sprintf("%v=%v", k.Name, value.ToString(k.Value))
}
// Keys are supposed to be sorted.
return "[" + strings.Join(strs, ",") + "]"
case e.Value != nil:
return fmt.Sprintf("[=%v]", value.ToString(*e.Value))
case e.Index != nil:
return fmt.Sprintf("[%v]", *e.Index)
default:
return "{{invalid path element}}"
}
}
// KeyByFields is a helper function which constructs a key for an associative
// list type. `nameValues` must have an even number of entries, alternating
// names (type must be string) with values (type must be value.Value). If these
// conditions are not met, KeyByFields will panic--it's intended for static
// construction and shouldn't have user-produced values passed to it.
func KeyByFields(nameValues ...interface{}) *value.FieldList {
if len(nameValues)%2 != 0 {
panic("must have a value for every name")
}
out := value.FieldList{}
for i := 0; i < len(nameValues)-1; i += 2 {
out = append(out, value.Field{Name: nameValues[i].(string), Value: value.NewValueInterface(nameValues[i+1])})
}
out.Sort()
return &out
}
// PathElementSet is a set of path elements.
// TODO: serialize as a list.
type PathElementSet struct {
members sortedPathElements
}
func MakePathElementSet(size int) PathElementSet {
return PathElementSet{
members: make(sortedPathElements, 0, size),
}
}
type sortedPathElements []PathElement
// Implement the sort interface; this would permit bulk creation, which would
// be faster than doing it one at a time via Insert.
func (spe sortedPathElements) Len() int { return len(spe) }
func (spe sortedPathElements) Less(i, j int) bool { return spe[i].Less(spe[j]) }
func (spe sortedPathElements) Swap(i, j int) { spe[i], spe[j] = spe[j], spe[i] }
// Insert adds pe to the set.
func (s *PathElementSet) Insert(pe PathElement) {
loc := sort.Search(len(s.members), func(i int) bool {
return !s.members[i].Less(pe)
})
if loc == len(s.members) {
s.members = append(s.members, pe)
return
}
if s.members[loc].Equals(pe) {
return
}
s.members = append(s.members, PathElement{})
copy(s.members[loc+1:], s.members[loc:])
s.members[loc] = pe
}
// Union returns a set containing elements that appear in either s or s2.
func (s *PathElementSet) Union(s2 *PathElementSet) *PathElementSet {
out := &PathElementSet{}
i, j := 0, 0
for i < len(s.members) && j < len(s2.members) {
if s.members[i].Less(s2.members[j]) {
out.members = append(out.members, s.members[i])
i++
} else {
out.members = append(out.members, s2.members[j])
if !s2.members[j].Less(s.members[i]) {
i++
}
j++
}
}
if i < len(s.members) {
out.members = append(out.members, s.members[i:]...)
}
if j < len(s2.members) {
out.members = append(out.members, s2.members[j:]...)
}
return out
}
// Intersection returns a set containing elements which appear in both s and s2.
func (s *PathElementSet) Intersection(s2 *PathElementSet) *PathElementSet {
out := &PathElementSet{}
i, j := 0, 0
for i < len(s.members) && j < len(s2.members) {
if s.members[i].Less(s2.members[j]) {
i++
} else {
if !s2.members[j].Less(s.members[i]) {
out.members = append(out.members, s.members[i])
i++
}
j++
}
}
return out
}
// Difference returns a set containing elements which appear in s but not in s2.
func (s *PathElementSet) Difference(s2 *PathElementSet) *PathElementSet {
out := &PathElementSet{}
i, j := 0, 0
for i < len(s.members) && j < len(s2.members) {
if s.members[i].Less(s2.members[j]) {
out.members = append(out.members, s.members[i])
i++
} else {
if !s2.members[j].Less(s.members[i]) {
i++
}
j++
}
}
if i < len(s.members) {
out.members = append(out.members, s.members[i:]...)
}
return out
}
// Size retuns the number of elements in the set.
func (s *PathElementSet) Size() int { return len(s.members) }
// Has returns true if pe is a member of the set.
func (s *PathElementSet) Has(pe PathElement) bool {
loc := sort.Search(len(s.members), func(i int) bool {
return !s.members[i].Less(pe)
})
if loc == len(s.members) {
return false
}
if s.members[loc].Equals(pe) {
return true
}
return false
}
// Equals returns true if s and s2 have exactly the same members.
func (s *PathElementSet) Equals(s2 *PathElementSet) bool {
if len(s.members) != len(s2.members) {
return false
}
for k := range s.members {
if !s.members[k].Equals(s2.members[k]) {
return false
}
}
return true
}
// Iterate calls f for each PathElement in the set. The order is deterministic.
func (s *PathElementSet) Iterate(f func(PathElement)) {
for _, pe := range s.members {
f(pe)
}
}

View File

@ -0,0 +1,134 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package fieldpath
import (
"sigs.k8s.io/structured-merge-diff/v4/value"
)
// SetFromValue creates a set containing every leaf field mentioned in v.
func SetFromValue(v value.Value) *Set {
s := NewSet()
w := objectWalker{
path: Path{},
value: v,
allocator: value.NewFreelistAllocator(),
do: func(p Path) { s.Insert(p) },
}
w.walk()
return s
}
type objectWalker struct {
path Path
value value.Value
allocator value.Allocator
do func(Path)
}
func (w *objectWalker) walk() {
switch {
case w.value.IsNull():
case w.value.IsFloat():
case w.value.IsInt():
case w.value.IsString():
case w.value.IsBool():
// All leaf fields handled the same way (after the switch
// statement).
// Descend
case w.value.IsList():
// If the list were atomic, we'd break here, but we don't have
// a schema, so we can't tell.
l := w.value.AsListUsing(w.allocator)
defer w.allocator.Free(l)
iter := l.RangeUsing(w.allocator)
defer w.allocator.Free(iter)
for iter.Next() {
i, value := iter.Item()
w2 := *w
w2.path = append(w.path, w.GuessBestListPathElement(i, value))
w2.value = value
w2.walk()
}
return
case w.value.IsMap():
// If the map/struct were atomic, we'd break here, but we don't
// have a schema, so we can't tell.
m := w.value.AsMapUsing(w.allocator)
defer w.allocator.Free(m)
m.IterateUsing(w.allocator, func(k string, val value.Value) bool {
w2 := *w
w2.path = append(w.path, PathElement{FieldName: &k})
w2.value = val
w2.walk()
return true
})
return
}
// Leaf fields get added to the set.
if len(w.path) > 0 {
w.do(w.path)
}
}
// AssociativeListCandidateFieldNames lists the field names which are
// considered keys if found in a list element.
var AssociativeListCandidateFieldNames = []string{
"key",
"id",
"name",
}
// GuessBestListPathElement guesses whether item is an associative list
// element, which should be referenced by key(s), or if it is not and therefore
// referencing by index is acceptable. Currently this is done by checking
// whether item has any of the fields listed in
// AssociativeListCandidateFieldNames which have scalar values.
func (w *objectWalker) GuessBestListPathElement(index int, item value.Value) PathElement {
if !item.IsMap() {
// Non map items could be parts of sets or regular "atomic"
// lists. We won't try to guess whether something should be a
// set or not.
return PathElement{Index: &index}
}
m := item.AsMapUsing(w.allocator)
defer w.allocator.Free(m)
var keys value.FieldList
for _, name := range AssociativeListCandidateFieldNames {
f, ok := m.Get(name)
if !ok {
continue
}
// only accept primitive/scalar types as keys.
if f.IsNull() || f.IsMap() || f.IsList() {
continue
}
keys = append(keys, value.Field{Name: name, Value: f})
}
if len(keys) > 0 {
keys.Sort()
return PathElement{Key: &keys}
}
return PathElement{Index: &index}
}

View File

@ -0,0 +1,144 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package fieldpath
import (
"fmt"
"strings"
)
// APIVersion describes the version of an object or of a fieldset.
type APIVersion string
type VersionedSet interface {
Set() *Set
APIVersion() APIVersion
Applied() bool
}
// VersionedSet associates a version to a set.
type versionedSet struct {
set *Set
apiVersion APIVersion
applied bool
}
func NewVersionedSet(set *Set, apiVersion APIVersion, applied bool) VersionedSet {
return versionedSet{
set: set,
apiVersion: apiVersion,
applied: applied,
}
}
func (v versionedSet) Set() *Set {
return v.set
}
func (v versionedSet) APIVersion() APIVersion {
return v.apiVersion
}
func (v versionedSet) Applied() bool {
return v.applied
}
// ManagedFields is a map from manager to VersionedSet (what they own in
// what version).
type ManagedFields map[string]VersionedSet
// Equals returns true if the two managedfields are the same, false
// otherwise.
func (lhs ManagedFields) Equals(rhs ManagedFields) bool {
if len(lhs) != len(rhs) {
return false
}
for manager, left := range lhs {
right, ok := rhs[manager]
if !ok {
return false
}
if left.APIVersion() != right.APIVersion() || left.Applied() != right.Applied() {
return false
}
if !left.Set().Equals(right.Set()) {
return false
}
}
return true
}
// Copy the list, this is mostly a shallow copy.
func (lhs ManagedFields) Copy() ManagedFields {
copy := ManagedFields{}
for manager, set := range lhs {
copy[manager] = set
}
return copy
}
// Difference returns a symmetric difference between two Managers. If a
// given user's entry has version X in lhs and version Y in rhs, then
// the return value for that user will be from rhs. If the difference for
// a user is an empty set, that user will not be inserted in the map.
func (lhs ManagedFields) Difference(rhs ManagedFields) ManagedFields {
diff := ManagedFields{}
for manager, left := range lhs {
right, ok := rhs[manager]
if !ok {
if !left.Set().Empty() {
diff[manager] = left
}
continue
}
// If we have sets in both but their version
// differs, we don't even diff and keep the
// entire thing.
if left.APIVersion() != right.APIVersion() {
diff[manager] = right
continue
}
newSet := left.Set().Difference(right.Set()).Union(right.Set().Difference(left.Set()))
if !newSet.Empty() {
diff[manager] = NewVersionedSet(newSet, right.APIVersion(), false)
}
}
for manager, set := range rhs {
if _, ok := lhs[manager]; ok {
// Already done
continue
}
if !set.Set().Empty() {
diff[manager] = set
}
}
return diff
}
func (lhs ManagedFields) String() string {
s := strings.Builder{}
for k, v := range lhs {
fmt.Fprintf(&s, "%s:\n", k)
fmt.Fprintf(&s, "- Applied: %v\n", v.Applied())
fmt.Fprintf(&s, "- APIVersion: %v\n", v.APIVersion())
fmt.Fprintf(&s, "- Set: %v\n", v.Set())
}
return s.String()
}

View File

@ -0,0 +1,118 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package fieldpath
import (
"fmt"
"strings"
"sigs.k8s.io/structured-merge-diff/v4/value"
)
// Path describes how to select a potentially deeply-nested child field given a
// containing object.
type Path []PathElement
func (fp Path) String() string {
strs := make([]string, len(fp))
for i := range fp {
strs[i] = fp[i].String()
}
return strings.Join(strs, "")
}
// Equals returns true if the two paths are equivalent.
func (fp Path) Equals(fp2 Path) bool {
if len(fp) != len(fp2) {
return false
}
for i := range fp {
if !fp[i].Equals(fp2[i]) {
return false
}
}
return true
}
// Less provides a lexical order for Paths.
func (fp Path) Compare(rhs Path) int {
i := 0
for {
if i >= len(fp) && i >= len(rhs) {
// Paths are the same length and all items are equal.
return 0
}
if i >= len(fp) {
// LHS is shorter.
return -1
}
if i >= len(rhs) {
// RHS is shorter.
return 1
}
if c := fp[i].Compare(rhs[i]); c != 0 {
return c
}
// The items are equal; continue.
i++
}
}
func (fp Path) Copy() Path {
new := make(Path, len(fp))
copy(new, fp)
return new
}
// MakePath constructs a Path. The parts may be PathElements, ints, strings.
func MakePath(parts ...interface{}) (Path, error) {
var fp Path
for _, p := range parts {
switch t := p.(type) {
case PathElement:
fp = append(fp, t)
case int:
// TODO: Understand schema and object and convert this to the
// FieldSpecifier below if appropriate.
fp = append(fp, PathElement{Index: &t})
case string:
fp = append(fp, PathElement{FieldName: &t})
case *value.FieldList:
if len(*t) == 0 {
return nil, fmt.Errorf("associative list key type path elements must have at least one key (got zero)")
}
fp = append(fp, PathElement{Key: t})
case value.Value:
// TODO: understand schema and verify that this is a set type
// TODO: make a copy of t
fp = append(fp, PathElement{Value: &t})
default:
return nil, fmt.Errorf("unable to make %#v into a path element", p)
}
}
return fp, nil
}
// MakePathOrDie panics if parts can't be turned into a path. Good for things
// that are known at complie time.
func MakePathOrDie(parts ...interface{}) Path {
fp, err := MakePath(parts...)
if err != nil {
panic(err)
}
return fp
}

View File

@ -0,0 +1,85 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package fieldpath
import (
"sort"
"sigs.k8s.io/structured-merge-diff/v4/value"
)
// PathElementValueMap is a map from PathElement to value.Value.
//
// TODO(apelisse): We have multiple very similar implementation of this
// for PathElementSet and SetNodeMap, so we could probably share the
// code.
type PathElementValueMap struct {
members sortedPathElementValues
}
func MakePathElementValueMap(size int) PathElementValueMap {
return PathElementValueMap{
members: make(sortedPathElementValues, 0, size),
}
}
type pathElementValue struct {
PathElement PathElement
Value value.Value
}
type sortedPathElementValues []pathElementValue
// Implement the sort interface; this would permit bulk creation, which would
// be faster than doing it one at a time via Insert.
func (spev sortedPathElementValues) Len() int { return len(spev) }
func (spev sortedPathElementValues) Less(i, j int) bool {
return spev[i].PathElement.Less(spev[j].PathElement)
}
func (spev sortedPathElementValues) Swap(i, j int) { spev[i], spev[j] = spev[j], spev[i] }
// Insert adds the pathelement and associated value in the map.
func (s *PathElementValueMap) Insert(pe PathElement, v value.Value) {
loc := sort.Search(len(s.members), func(i int) bool {
return !s.members[i].PathElement.Less(pe)
})
if loc == len(s.members) {
s.members = append(s.members, pathElementValue{pe, v})
return
}
if s.members[loc].PathElement.Equals(pe) {
return
}
s.members = append(s.members, pathElementValue{})
copy(s.members[loc+1:], s.members[loc:])
s.members[loc] = pathElementValue{pe, v}
}
// Get retrieves the value associated with the given PathElement from the map.
// (nil, false) is returned if there is no such PathElement.
func (s *PathElementValueMap) Get(pe PathElement) (value.Value, bool) {
loc := sort.Search(len(s.members), func(i int) bool {
return !s.members[i].PathElement.Less(pe)
})
if loc == len(s.members) {
return nil, false
}
if s.members[loc].PathElement.Equals(pe) {
return s.members[loc].Value, true
}
return nil, false
}

View File

@ -0,0 +1,168 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package fieldpath
import (
"errors"
"fmt"
"io"
"strconv"
"strings"
jsoniter "github.com/json-iterator/go"
"sigs.k8s.io/structured-merge-diff/v4/value"
)
var ErrUnknownPathElementType = errors.New("unknown path element type")
const (
// Field indicates that the content of this path element is a field's name
peField = "f"
// Value indicates that the content of this path element is a field's value
peValue = "v"
// Index indicates that the content of this path element is an index in an array
peIndex = "i"
// Key indicates that the content of this path element is a key value map
peKey = "k"
// Separator separates the type of a path element from the contents
peSeparator = ":"
)
var (
peFieldSepBytes = []byte(peField + peSeparator)
peValueSepBytes = []byte(peValue + peSeparator)
peIndexSepBytes = []byte(peIndex + peSeparator)
peKeySepBytes = []byte(peKey + peSeparator)
peSepBytes = []byte(peSeparator)
)
// DeserializePathElement parses a serialized path element
func DeserializePathElement(s string) (PathElement, error) {
b := []byte(s)
if len(b) < 2 {
return PathElement{}, errors.New("key must be 2 characters long:")
}
typeSep, b := b[:2], b[2:]
if typeSep[1] != peSepBytes[0] {
return PathElement{}, fmt.Errorf("missing colon: %v", s)
}
switch typeSep[0] {
case peFieldSepBytes[0]:
// Slice s rather than convert b, to save on
// allocations.
str := s[2:]
return PathElement{
FieldName: &str,
}, nil
case peValueSepBytes[0]:
iter := readPool.BorrowIterator(b)
defer readPool.ReturnIterator(iter)
v, err := value.ReadJSONIter(iter)
if err != nil {
return PathElement{}, err
}
return PathElement{Value: &v}, nil
case peKeySepBytes[0]:
iter := readPool.BorrowIterator(b)
defer readPool.ReturnIterator(iter)
fields := value.FieldList{}
iter.ReadObjectCB(func(iter *jsoniter.Iterator, key string) bool {
v, err := value.ReadJSONIter(iter)
if err != nil {
iter.Error = err
return false
}
fields = append(fields, value.Field{Name: key, Value: v})
return true
})
fields.Sort()
return PathElement{Key: &fields}, iter.Error
case peIndexSepBytes[0]:
i, err := strconv.Atoi(s[2:])
if err != nil {
return PathElement{}, err
}
return PathElement{
Index: &i,
}, nil
default:
return PathElement{}, ErrUnknownPathElementType
}
}
var (
readPool = jsoniter.NewIterator(jsoniter.ConfigCompatibleWithStandardLibrary).Pool()
writePool = jsoniter.NewStream(jsoniter.ConfigCompatibleWithStandardLibrary, nil, 1024).Pool()
)
// SerializePathElement serializes a path element
func SerializePathElement(pe PathElement) (string, error) {
buf := strings.Builder{}
err := serializePathElementToWriter(&buf, pe)
return buf.String(), err
}
func serializePathElementToWriter(w io.Writer, pe PathElement) error {
stream := writePool.BorrowStream(w)
defer writePool.ReturnStream(stream)
switch {
case pe.FieldName != nil:
if _, err := stream.Write(peFieldSepBytes); err != nil {
return err
}
stream.WriteRaw(*pe.FieldName)
case pe.Key != nil:
if _, err := stream.Write(peKeySepBytes); err != nil {
return err
}
stream.WriteObjectStart()
for i, field := range *pe.Key {
if i > 0 {
stream.WriteMore()
}
stream.WriteObjectField(field.Name)
value.WriteJSONStream(field.Value, stream)
}
stream.WriteObjectEnd()
case pe.Value != nil:
if _, err := stream.Write(peValueSepBytes); err != nil {
return err
}
value.WriteJSONStream(*pe.Value, stream)
case pe.Index != nil:
if _, err := stream.Write(peIndexSepBytes); err != nil {
return err
}
stream.WriteInt(*pe.Index)
default:
return errors.New("invalid PathElement")
}
b := stream.Buffer()
err := stream.Flush()
// Help jsoniter manage its buffers--without this, the next
// use of the stream is likely to require an allocation. Look
// at the jsoniter stream code to understand why. They were probably
// optimizing for folks using the buffer directly.
stream.SetBuffer(b[:0])
return err
}

View File

@ -0,0 +1,238 @@
/*
Copyright 2019 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 fieldpath
import (
"bytes"
"io"
"unsafe"
jsoniter "github.com/json-iterator/go"
)
func (s *Set) ToJSON() ([]byte, error) {
buf := bytes.Buffer{}
err := s.ToJSONStream(&buf)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func (s *Set) ToJSONStream(w io.Writer) error {
stream := writePool.BorrowStream(w)
defer writePool.ReturnStream(stream)
var r reusableBuilder
stream.WriteObjectStart()
err := s.emitContentsV1(false, stream, &r)
if err != nil {
return err
}
stream.WriteObjectEnd()
return stream.Flush()
}
func manageMemory(stream *jsoniter.Stream) error {
// Help jsoniter manage its buffers--without this, it does a bunch of
// alloctaions that are not necessary. They were probably optimizing
// for folks using the buffer directly.
b := stream.Buffer()
if len(b) > 4096 || cap(b)-len(b) < 2048 {
if err := stream.Flush(); err != nil {
return err
}
stream.SetBuffer(b[:0])
}
return nil
}
type reusableBuilder struct {
bytes.Buffer
}
func (r *reusableBuilder) unsafeString() string {
b := r.Bytes()
return *(*string)(unsafe.Pointer(&b))
}
func (r *reusableBuilder) reset() *bytes.Buffer {
r.Reset()
return &r.Buffer
}
func (s *Set) emitContentsV1(includeSelf bool, stream *jsoniter.Stream, r *reusableBuilder) error {
mi, ci := 0, 0
first := true
preWrite := func() {
if first {
first = false
return
}
stream.WriteMore()
}
if includeSelf && !(len(s.Members.members) == 0 && len(s.Children.members) == 0) {
preWrite()
stream.WriteObjectField(".")
stream.WriteEmptyObject()
}
for mi < len(s.Members.members) && ci < len(s.Children.members) {
mpe := s.Members.members[mi]
cpe := s.Children.members[ci].pathElement
if c := mpe.Compare(cpe); c < 0 {
preWrite()
if err := serializePathElementToWriter(r.reset(), mpe); err != nil {
return err
}
stream.WriteObjectField(r.unsafeString())
stream.WriteEmptyObject()
mi++
} else if c > 0 {
preWrite()
if err := serializePathElementToWriter(r.reset(), cpe); err != nil {
return err
}
stream.WriteObjectField(r.unsafeString())
stream.WriteObjectStart()
if err := s.Children.members[ci].set.emitContentsV1(false, stream, r); err != nil {
return err
}
stream.WriteObjectEnd()
ci++
} else {
preWrite()
if err := serializePathElementToWriter(r.reset(), cpe); err != nil {
return err
}
stream.WriteObjectField(r.unsafeString())
stream.WriteObjectStart()
if err := s.Children.members[ci].set.emitContentsV1(true, stream, r); err != nil {
return err
}
stream.WriteObjectEnd()
mi++
ci++
}
}
for mi < len(s.Members.members) {
mpe := s.Members.members[mi]
preWrite()
if err := serializePathElementToWriter(r.reset(), mpe); err != nil {
return err
}
stream.WriteObjectField(r.unsafeString())
stream.WriteEmptyObject()
mi++
}
for ci < len(s.Children.members) {
cpe := s.Children.members[ci].pathElement
preWrite()
if err := serializePathElementToWriter(r.reset(), cpe); err != nil {
return err
}
stream.WriteObjectField(r.unsafeString())
stream.WriteObjectStart()
if err := s.Children.members[ci].set.emitContentsV1(false, stream, r); err != nil {
return err
}
stream.WriteObjectEnd()
ci++
}
return manageMemory(stream)
}
// FromJSON clears s and reads a JSON formatted set structure.
func (s *Set) FromJSON(r io.Reader) error {
// The iterator pool is completely useless for memory management, grrr.
iter := jsoniter.Parse(jsoniter.ConfigCompatibleWithStandardLibrary, r, 4096)
found, _ := readIterV1(iter)
if found == nil {
*s = Set{}
} else {
*s = *found
}
return iter.Error
}
// returns true if this subtree is also (or only) a member of parent; s is nil
// if there are no further children.
func readIterV1(iter *jsoniter.Iterator) (children *Set, isMember bool) {
iter.ReadMapCB(func(iter *jsoniter.Iterator, key string) bool {
if key == "." {
isMember = true
iter.Skip()
return true
}
pe, err := DeserializePathElement(key)
if err == ErrUnknownPathElementType {
// Ignore these-- a future version maybe knows what
// they are. We drop these completely rather than try
// to preserve things we don't understand.
iter.Skip()
return true
} else if err != nil {
iter.ReportError("parsing key as path element", err.Error())
iter.Skip()
return true
}
grandchildren, childIsMember := readIterV1(iter)
if childIsMember {
if children == nil {
children = &Set{}
}
m := &children.Members.members
// Since we expect that most of the time these will have been
// serialized in the right order, we just verify that and append.
appendOK := len(*m) == 0 || (*m)[len(*m)-1].Less(pe)
if appendOK {
*m = append(*m, pe)
} else {
children.Members.Insert(pe)
}
}
if grandchildren != nil {
if children == nil {
children = &Set{}
}
// Since we expect that most of the time these will have been
// serialized in the right order, we just verify that and append.
m := &children.Children.members
appendOK := len(*m) == 0 || (*m)[len(*m)-1].pathElement.Less(pe)
if appendOK {
*m = append(*m, setNode{pe, grandchildren})
} else {
*children.Children.Descend(pe) = *grandchildren
}
}
return true
})
if children == nil {
isMember = true
}
return children, isMember
}

View File

@ -0,0 +1,505 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package fieldpath
import (
"sort"
"strings"
"sigs.k8s.io/structured-merge-diff/v4/schema"
)
// Set identifies a set of fields.
type Set struct {
// Members lists fields that are part of the set.
// TODO: will be serialized as a list of path elements.
Members PathElementSet
// Children lists child fields which themselves have children that are
// members of the set. Appearance in this list does not imply membership.
// Note: this is a tree, not an arbitrary graph.
Children SetNodeMap
}
// NewSet makes a set from a list of paths.
func NewSet(paths ...Path) *Set {
s := &Set{}
for _, p := range paths {
s.Insert(p)
}
return s
}
// Insert adds the field identified by `p` to the set. Important: parent fields
// are NOT added to the set; if that is desired, they must be added separately.
func (s *Set) Insert(p Path) {
if len(p) == 0 {
// Zero-length path identifies the entire object; we don't
// track top-level ownership.
return
}
for {
if len(p) == 1 {
s.Members.Insert(p[0])
return
}
s = s.Children.Descend(p[0])
p = p[1:]
}
}
// Union returns a Set containing elements which appear in either s or s2.
func (s *Set) Union(s2 *Set) *Set {
return &Set{
Members: *s.Members.Union(&s2.Members),
Children: *s.Children.Union(&s2.Children),
}
}
// Intersection returns a Set containing leaf elements which appear in both s
// and s2. Intersection can be constructed from Union and Difference operations
// (example in the tests) but it's much faster to do it in one pass.
func (s *Set) Intersection(s2 *Set) *Set {
return &Set{
Members: *s.Members.Intersection(&s2.Members),
Children: *s.Children.Intersection(&s2.Children),
}
}
// Difference returns a Set containing elements which:
// * appear in s
// * do not appear in s2
//
// In other words, for leaf fields, this acts like a regular set difference
// operation. When non leaf fields are compared with leaf fields ("parents"
// which contain "children"), the effect is:
// * parent - child = parent
// * child - parent = {empty set}
func (s *Set) Difference(s2 *Set) *Set {
return &Set{
Members: *s.Members.Difference(&s2.Members),
Children: *s.Children.Difference(s2),
}
}
// RecursiveDifference returns a Set containing elements which:
// * appear in s
// * do not appear in s2
//
// Compared to a regular difference,
// this removes every field **and its children** from s that is contained in s2.
//
// For example, with s containing `a.b.c` and s2 containing `a.b`,
// a RecursiveDifference will result in `a`, as the entire node `a.b` gets removed.
func (s *Set) RecursiveDifference(s2 *Set) *Set {
return &Set{
Members: *s.Members.Difference(&s2.Members),
Children: *s.Children.RecursiveDifference(s2),
}
}
// EnsureNamedFieldsAreMembers returns a Set that contains all the
// fields in s, as well as all the named fields that are typically not
// included. For example, a set made of "a.b.c" will end-up also owning
// "a" if it's a named fields but not "a.b" if it's a map.
func (s *Set) EnsureNamedFieldsAreMembers(sc *schema.Schema, tr schema.TypeRef) *Set {
members := PathElementSet{
members: make(sortedPathElements, 0, s.Members.Size()+len(s.Children.members)),
}
atom, _ := sc.Resolve(tr)
members.members = append(members.members, s.Members.members...)
for _, node := range s.Children.members {
// Only insert named fields.
if node.pathElement.FieldName != nil && atom.Map != nil {
if _, has := atom.Map.FindField(*node.pathElement.FieldName); has {
members.Insert(node.pathElement)
}
}
}
return &Set{
Members: members,
Children: *s.Children.EnsureNamedFieldsAreMembers(sc, tr),
}
}
// Size returns the number of members of the set.
func (s *Set) Size() int {
return s.Members.Size() + s.Children.Size()
}
// Empty returns true if there are no members of the set. It is a separate
// function from Size since it's common to check whether size > 0, and
// potentially much faster to return as soon as a single element is found.
func (s *Set) Empty() bool {
if s.Members.Size() > 0 {
return false
}
return s.Children.Empty()
}
// Has returns true if the field referenced by `p` is a member of the set.
func (s *Set) Has(p Path) bool {
if len(p) == 0 {
// No one owns "the entire object"
return false
}
for {
if len(p) == 1 {
return s.Members.Has(p[0])
}
var ok bool
s, ok = s.Children.Get(p[0])
if !ok {
return false
}
p = p[1:]
}
}
// Equals returns true if s and s2 have exactly the same members.
func (s *Set) Equals(s2 *Set) bool {
return s.Members.Equals(&s2.Members) && s.Children.Equals(&s2.Children)
}
// String returns the set one element per line.
func (s *Set) String() string {
elements := []string{}
s.Iterate(func(p Path) {
elements = append(elements, p.String())
})
return strings.Join(elements, "\n")
}
// Iterate calls f once for each field that is a member of the set (preorder
// DFS). The path passed to f will be reused so make a copy if you wish to keep
// it.
func (s *Set) Iterate(f func(Path)) {
s.iteratePrefix(Path{}, f)
}
func (s *Set) iteratePrefix(prefix Path, f func(Path)) {
s.Members.Iterate(func(pe PathElement) { f(append(prefix, pe)) })
s.Children.iteratePrefix(prefix, f)
}
// WithPrefix returns the subset of paths which begin with the given prefix,
// with the prefix not included.
func (s *Set) WithPrefix(pe PathElement) *Set {
subset, ok := s.Children.Get(pe)
if !ok {
return NewSet()
}
return subset
}
// Leaves returns a set containing only the leaf paths
// of a set.
func (s *Set) Leaves() *Set {
leaves := PathElementSet{}
im := 0
ic := 0
// any members that are not also children are leaves
outer:
for im < len(s.Members.members) {
member := s.Members.members[im]
for ic < len(s.Children.members) {
d := member.Compare(s.Children.members[ic].pathElement)
if d == 0 {
ic++
im++
continue outer
} else if d < 0 {
break
} else /* if d > 0 */ {
ic++
}
}
leaves.members = append(leaves.members, member)
im++
}
return &Set{
Members: leaves,
Children: *s.Children.Leaves(),
}
}
// setNode is a pair of PathElement / Set, for the purpose of expressing
// nested set membership.
type setNode struct {
pathElement PathElement
set *Set
}
// SetNodeMap is a map of PathElement to subset.
type SetNodeMap struct {
members sortedSetNode
}
type sortedSetNode []setNode
// Implement the sort interface; this would permit bulk creation, which would
// be faster than doing it one at a time via Insert.
func (s sortedSetNode) Len() int { return len(s) }
func (s sortedSetNode) Less(i, j int) bool { return s[i].pathElement.Less(s[j].pathElement) }
func (s sortedSetNode) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
// Descend adds pe to the set if necessary, returning the associated subset.
func (s *SetNodeMap) Descend(pe PathElement) *Set {
loc := sort.Search(len(s.members), func(i int) bool {
return !s.members[i].pathElement.Less(pe)
})
if loc == len(s.members) {
s.members = append(s.members, setNode{pathElement: pe, set: &Set{}})
return s.members[loc].set
}
if s.members[loc].pathElement.Equals(pe) {
return s.members[loc].set
}
s.members = append(s.members, setNode{})
copy(s.members[loc+1:], s.members[loc:])
s.members[loc] = setNode{pathElement: pe, set: &Set{}}
return s.members[loc].set
}
// Size returns the sum of the number of members of all subsets.
func (s *SetNodeMap) Size() int {
count := 0
for _, v := range s.members {
count += v.set.Size()
}
return count
}
// Empty returns false if there's at least one member in some child set.
func (s *SetNodeMap) Empty() bool {
for _, n := range s.members {
if !n.set.Empty() {
return false
}
}
return true
}
// Get returns (the associated set, true) or (nil, false) if there is none.
func (s *SetNodeMap) Get(pe PathElement) (*Set, bool) {
loc := sort.Search(len(s.members), func(i int) bool {
return !s.members[i].pathElement.Less(pe)
})
if loc == len(s.members) {
return nil, false
}
if s.members[loc].pathElement.Equals(pe) {
return s.members[loc].set, true
}
return nil, false
}
// Equals returns true if s and s2 have the same structure (same nested
// child sets).
func (s *SetNodeMap) Equals(s2 *SetNodeMap) bool {
if len(s.members) != len(s2.members) {
return false
}
for i := range s.members {
if !s.members[i].pathElement.Equals(s2.members[i].pathElement) {
return false
}
if !s.members[i].set.Equals(s2.members[i].set) {
return false
}
}
return true
}
// Union returns a SetNodeMap with members that appear in either s or s2.
func (s *SetNodeMap) Union(s2 *SetNodeMap) *SetNodeMap {
out := &SetNodeMap{}
i, j := 0, 0
for i < len(s.members) && j < len(s2.members) {
if s.members[i].pathElement.Less(s2.members[j].pathElement) {
out.members = append(out.members, s.members[i])
i++
} else {
if !s2.members[j].pathElement.Less(s.members[i].pathElement) {
out.members = append(out.members, setNode{pathElement: s.members[i].pathElement, set: s.members[i].set.Union(s2.members[j].set)})
i++
} else {
out.members = append(out.members, s2.members[j])
}
j++
}
}
if i < len(s.members) {
out.members = append(out.members, s.members[i:]...)
}
if j < len(s2.members) {
out.members = append(out.members, s2.members[j:]...)
}
return out
}
// Intersection returns a SetNodeMap with members that appear in both s and s2.
func (s *SetNodeMap) Intersection(s2 *SetNodeMap) *SetNodeMap {
out := &SetNodeMap{}
i, j := 0, 0
for i < len(s.members) && j < len(s2.members) {
if s.members[i].pathElement.Less(s2.members[j].pathElement) {
i++
} else {
if !s2.members[j].pathElement.Less(s.members[i].pathElement) {
res := s.members[i].set.Intersection(s2.members[j].set)
if !res.Empty() {
out.members = append(out.members, setNode{pathElement: s.members[i].pathElement, set: res})
}
i++
}
j++
}
}
return out
}
// Difference returns a SetNodeMap with members that appear in s but not in s2.
func (s *SetNodeMap) Difference(s2 *Set) *SetNodeMap {
out := &SetNodeMap{}
i, j := 0, 0
for i < len(s.members) && j < len(s2.Children.members) {
if s.members[i].pathElement.Less(s2.Children.members[j].pathElement) {
out.members = append(out.members, setNode{pathElement: s.members[i].pathElement, set: s.members[i].set})
i++
} else {
if !s2.Children.members[j].pathElement.Less(s.members[i].pathElement) {
diff := s.members[i].set.Difference(s2.Children.members[j].set)
// We aren't permitted to add nodes with no elements.
if !diff.Empty() {
out.members = append(out.members, setNode{pathElement: s.members[i].pathElement, set: diff})
}
i++
}
j++
}
}
if i < len(s.members) {
out.members = append(out.members, s.members[i:]...)
}
return out
}
// RecursiveDifference returns a SetNodeMap with members that appear in s but not in s2.
//
// Compared to a regular difference,
// this removes every field **and its children** from s that is contained in s2.
//
// For example, with s containing `a.b.c` and s2 containing `a.b`,
// a RecursiveDifference will result in `a`, as the entire node `a.b` gets removed.
func (s *SetNodeMap) RecursiveDifference(s2 *Set) *SetNodeMap {
out := &SetNodeMap{}
i, j := 0, 0
for i < len(s.members) && j < len(s2.Children.members) {
if s.members[i].pathElement.Less(s2.Children.members[j].pathElement) {
if !s2.Members.Has(s.members[i].pathElement) {
out.members = append(out.members, setNode{pathElement: s.members[i].pathElement, set: s.members[i].set})
}
i++
} else {
if !s2.Children.members[j].pathElement.Less(s.members[i].pathElement) {
if !s2.Members.Has(s.members[i].pathElement) {
diff := s.members[i].set.RecursiveDifference(s2.Children.members[j].set)
if !diff.Empty() {
out.members = append(out.members, setNode{pathElement: s.members[i].pathElement, set: diff})
}
}
i++
}
j++
}
}
if i < len(s.members) {
for _, c := range s.members[i:] {
if !s2.Members.Has(c.pathElement) {
out.members = append(out.members, c)
}
}
}
return out
}
// EnsureNamedFieldsAreMembers returns a set that contains all the named fields along with the leaves.
func (s *SetNodeMap) EnsureNamedFieldsAreMembers(sc *schema.Schema, tr schema.TypeRef) *SetNodeMap {
out := make(sortedSetNode, 0, s.Size())
atom, _ := sc.Resolve(tr)
for _, member := range s.members {
tr := schema.TypeRef{}
if member.pathElement.FieldName != nil && atom.Map != nil {
tr = atom.Map.ElementType
if sf, ok := atom.Map.FindField(*member.pathElement.FieldName); ok {
tr = sf.Type
}
} else if member.pathElement.Key != nil && atom.List != nil {
tr = atom.List.ElementType
}
out = append(out, setNode{
pathElement: member.pathElement,
set: member.set.EnsureNamedFieldsAreMembers(sc, tr),
})
}
return &SetNodeMap{
members: out,
}
}
// Iterate calls f for each PathElement in the set.
func (s *SetNodeMap) Iterate(f func(PathElement)) {
for _, n := range s.members {
f(n.pathElement)
}
}
func (s *SetNodeMap) iteratePrefix(prefix Path, f func(Path)) {
for _, n := range s.members {
pe := n.pathElement
n.set.iteratePrefix(append(prefix, pe), f)
}
}
// Leaves returns a SetNodeMap containing
// only setNodes with leaf PathElements.
func (s *SetNodeMap) Leaves() *SetNodeMap {
out := &SetNodeMap{}
out.members = make(sortedSetNode, len(s.members))
for i, n := range s.members {
out.members[i] = setNode{
pathElement: n.pathElement,
set: n.set.Leaves(),
}
}
return out
}

View File

@ -0,0 +1,28 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package schema defines a targeted schema language which allows one to
// represent all the schema information necessary to perform "structured"
// merges and diffs.
//
// Due to the targeted nature of the data model, the schema language can fit in
// just a few hundred lines of go code, making it much more understandable and
// concise than e.g. OpenAPI.
//
// This schema was derived by observing the API objects used by Kubernetes, and
// formalizing a model which allows certain operations ("apply") to be more
// well defined. It is currently missing one feature: one-of ("unions").
package schema

View File

@ -0,0 +1,261 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package schema
import "sync"
// Schema is a list of named types.
//
// Schema types are indexed in a map before the first search so this type
// should be considered immutable.
type Schema struct {
Types []TypeDef `yaml:"types,omitempty"`
once sync.Once
m map[string]TypeDef
}
// A TypeSpecifier references a particular type in a schema.
type TypeSpecifier struct {
Type TypeRef `yaml:"type,omitempty"`
Schema Schema `yaml:"schema,omitempty"`
}
// TypeDef represents a named type in a schema.
type TypeDef struct {
// Top level types should be named. Every type must have a unique name.
Name string `yaml:"name,omitempty"`
Atom `yaml:"atom,omitempty,inline"`
}
// TypeRef either refers to a named type or declares an inlined type.
type TypeRef struct {
// Either the name or one member of Atom should be set.
NamedType *string `yaml:"namedType,omitempty"`
Inlined Atom `yaml:",inline,omitempty"`
}
// Atom represents the smallest possible pieces of the type system.
// Each set field in the Atom represents a possible type for the object.
// If none of the fields are set, any object will fail validation against the atom.
type Atom struct {
*Scalar `yaml:"scalar,omitempty"`
*List `yaml:"list,omitempty"`
*Map `yaml:"map,omitempty"`
}
// Scalar (AKA "primitive") represents a type which has a single value which is
// either numeric, string, or boolean.
//
// TODO: split numeric into float/int? Something even more fine-grained?
type Scalar string
const (
Numeric = Scalar("numeric")
String = Scalar("string")
Boolean = Scalar("boolean")
)
// ElementRelationship is an enum of the different possible relationships
// between the elements of container types (maps, lists).
type ElementRelationship string
const (
// Associative only applies to lists (see the documentation there).
Associative = ElementRelationship("associative")
// Atomic makes container types (lists, maps) behave
// as scalars / leaf fields
Atomic = ElementRelationship("atomic")
// Separable means the items of the container type have no particular
// relationship (default behavior for maps).
Separable = ElementRelationship("separable")
)
// Map is a key-value pair. Its default semantics are the same as an
// associative list, but:
// * It is serialized differently:
// map: {"k": {"value": "v"}}
// list: [{"key": "k", "value": "v"}]
// * Keys must be string typed.
// * Keys can't have multiple components.
//
// Optionally, maps may be atomic (for example, imagine representing an RGB
// color value--it doesn't make sense to have different actors own the R and G
// values).
//
// Maps may also represent a type which is composed of a number of different fields.
// Each field has a name and a type.
//
// Fields are indexed in a map before the first search so this type
// should be considered immutable.
type Map struct {
// Each struct field appears exactly once in this list. The order in
// this list defines the canonical field ordering.
Fields []StructField `yaml:"fields,omitempty"`
// A Union is a grouping of fields with special rules. It may refer to
// one or more fields in the above list. A given field from the above
// list may be referenced in exactly 0 or 1 places in the below list.
// One can have multiple unions in the same struct, but the fields can't
// overlap between unions.
Unions []Union `yaml:"unions,omitempty"`
// ElementType is the type of the structs's unknown fields.
ElementType TypeRef `yaml:"elementType,omitempty"`
// ElementRelationship states the relationship between the map's items.
// * `separable` (or unset) implies that each element is 100% independent.
// * `atomic` implies that all elements depend on each other, and this
// is effectively a scalar / leaf field; it doesn't make sense for
// separate actors to set the elements. Example: an RGB color struct;
// it would never make sense to "own" only one component of the
// color.
// The default behavior for maps is `separable`; it's permitted to
// leave this unset to get the default behavior.
ElementRelationship ElementRelationship `yaml:"elementRelationship,omitempty"`
once sync.Once
m map[string]StructField
}
// FindField is a convenience function that returns the referenced StructField,
// if it exists, or (nil, false) if it doesn't.
func (m *Map) FindField(name string) (StructField, bool) {
m.once.Do(func() {
m.m = make(map[string]StructField, len(m.Fields))
for _, field := range m.Fields {
m.m[field.Name] = field
}
})
sf, ok := m.m[name]
return sf, ok
}
// UnionFields are mapping between the fields that are part of the union and
// their discriminated value. The discriminated value has to be set, and
// should not conflict with other discriminated value in the list.
type UnionField struct {
// FieldName is the name of the field that is part of the union. This
// is the serialized form of the field.
FieldName string `yaml:"fieldName"`
// Discriminatorvalue is the value of the discriminator to
// select that field. If the union doesn't have a discriminator,
// this field is ignored.
DiscriminatorValue string `yaml:"discriminatorValue"`
}
// Union, or oneof, means that only one of multiple fields of a structure can be
// set at a time. Setting the discriminator helps clearing oher fields:
// - If discriminator changed to non-nil, and a new field has been added
// that doesn't match, an error is returned,
// - If discriminator hasn't changed and two fields or more are set, an
// error is returned,
// - If discriminator changed to non-nil, all other fields but the
// discriminated one will be cleared,
// - Otherwise, If only one field is left, update discriminator to that value.
type Union struct {
// Discriminator, if present, is the name of the field that
// discriminates fields in the union. The mapping between the value of
// the discriminator and the field is done by using the Fields list
// below.
Discriminator *string `yaml:"discriminator,omitempty"`
// DeduceInvalidDiscriminator indicates if the discriminator
// should be updated automatically based on the fields set. This
// typically defaults to false since we don't want to deduce by
// default (the behavior exists to maintain compatibility on
// existing types and shouldn't be used for new types).
DeduceInvalidDiscriminator bool `yaml:"deduceInvalidDiscriminator,omitempty"`
// This is the list of fields that belong to this union. All the
// fields present in here have to be part of the parent
// structure. Discriminator (if oneOf has one), is NOT included in
// this list. The value for field is how we map the name of the field
// to actual value for discriminator.
Fields []UnionField `yaml:"fields,omitempty"`
}
// StructField pairs a field name with a field type.
type StructField struct {
// Name is the field name.
Name string `yaml:"name,omitempty"`
// Type is the field type.
Type TypeRef `yaml:"type,omitempty"`
// Default value for the field, nil if not present.
Default interface{} `yaml:"default,omitempty"`
}
// List represents a type which contains a zero or more elements, all of the
// same subtype. Lists may be either associative: each element is more or less
// independent and could be managed by separate entities in the system; or
// atomic, where the elements are heavily dependent on each other: it is not
// sensible to change one element without considering the ramifications on all
// the other elements.
type List struct {
// ElementType is the type of the list's elements.
ElementType TypeRef `yaml:"elementType,omitempty"`
// ElementRelationship states the relationship between the list's elements
// and must have one of these values:
// * `atomic`: the list is treated as a single entity, like a scalar.
// * `associative`:
// - If the list element is a scalar, the list is treated as a set.
// - If the list element is a map, the list is treated as a map.
// There is no default for this value for lists; all schemas must
// explicitly state the element relationship for all lists.
ElementRelationship ElementRelationship `yaml:"elementRelationship,omitempty"`
// Iff ElementRelationship is `associative`, and the element type is
// map, then Keys must have non-zero length, and it lists the fields
// of the element's map type which are to be used as the keys of the
// list.
//
// TODO: change this to "non-atomic struct" above and make the code reflect this.
//
// Each key must refer to a single field name (no nesting, not JSONPath).
Keys []string `yaml:"keys,omitempty"`
}
// FindNamedType is a convenience function that returns the referenced TypeDef,
// if it exists, or (nil, false) if it doesn't.
func (s *Schema) FindNamedType(name string) (TypeDef, bool) {
s.once.Do(func() {
s.m = make(map[string]TypeDef, len(s.Types))
for _, t := range s.Types {
s.m[t.Name] = t
}
})
t, ok := s.m[name]
return t, ok
}
// Resolve is a convenience function which returns the atom referenced, whether
// it is inline or named. Returns (Atom{}, false) if the type can't be resolved.
//
// This allows callers to not care about the difference between a (possibly
// inlined) reference and a definition.
func (s *Schema) Resolve(tr TypeRef) (Atom, bool) {
if tr.NamedType != nil {
t, ok := s.FindNamedType(*tr.NamedType)
if !ok {
return Atom{}, false
}
return t.Atom, true
}
return tr.Inlined, true
}

View File

@ -0,0 +1,199 @@
/*
Copyright 2019 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 schema
import "reflect"
// Equals returns true iff the two Schemas are equal.
func (a *Schema) Equals(b *Schema) bool {
if a == nil || b == nil {
return a == nil && b == nil
}
if len(a.Types) != len(b.Types) {
return false
}
for i := range a.Types {
if !a.Types[i].Equals(&b.Types[i]) {
return false
}
}
return true
}
// Equals returns true iff the two TypeRefs are equal.
//
// Note that two typerefs that have an equivalent type but where one is
// inlined and the other is named, are not considered equal.
func (a *TypeRef) Equals(b *TypeRef) bool {
if a == nil || b == nil {
return a == nil && b == nil
}
if (a.NamedType == nil) != (b.NamedType == nil) {
return false
}
if a.NamedType != nil {
if *a.NamedType != *b.NamedType {
return false
}
//return true
}
return a.Inlined.Equals(&b.Inlined)
}
// Equals returns true iff the two TypeDefs are equal.
func (a *TypeDef) Equals(b *TypeDef) bool {
if a == nil || b == nil {
return a == nil && b == nil
}
if a.Name != b.Name {
return false
}
return a.Atom.Equals(&b.Atom)
}
// Equals returns true iff the two Atoms are equal.
func (a *Atom) Equals(b *Atom) bool {
if a == nil || b == nil {
return a == nil && b == nil
}
if (a.Scalar == nil) != (b.Scalar == nil) {
return false
}
if (a.List == nil) != (b.List == nil) {
return false
}
if (a.Map == nil) != (b.Map == nil) {
return false
}
switch {
case a.Scalar != nil:
return *a.Scalar == *b.Scalar
case a.List != nil:
return a.List.Equals(b.List)
case a.Map != nil:
return a.Map.Equals(b.Map)
}
return true
}
// Equals returns true iff the two Maps are equal.
func (a *Map) Equals(b *Map) bool {
if a == nil || b == nil {
return a == nil && b == nil
}
if !a.ElementType.Equals(&b.ElementType) {
return false
}
if a.ElementRelationship != b.ElementRelationship {
return false
}
if len(a.Fields) != len(b.Fields) {
return false
}
for i := range a.Fields {
if !a.Fields[i].Equals(&b.Fields[i]) {
return false
}
}
if len(a.Unions) != len(b.Unions) {
return false
}
for i := range a.Unions {
if !a.Unions[i].Equals(&b.Unions[i]) {
return false
}
}
return true
}
// Equals returns true iff the two Unions are equal.
func (a *Union) Equals(b *Union) bool {
if a == nil || b == nil {
return a == nil && b == nil
}
if (a.Discriminator == nil) != (b.Discriminator == nil) {
return false
}
if a.Discriminator != nil {
if *a.Discriminator != *b.Discriminator {
return false
}
}
if a.DeduceInvalidDiscriminator != b.DeduceInvalidDiscriminator {
return false
}
if len(a.Fields) != len(b.Fields) {
return false
}
for i := range a.Fields {
if !a.Fields[i].Equals(&b.Fields[i]) {
return false
}
}
return true
}
// Equals returns true iff the two UnionFields are equal.
func (a *UnionField) Equals(b *UnionField) bool {
if a == nil || b == nil {
return a == nil && b == nil
}
if a.FieldName != b.FieldName {
return false
}
if a.DiscriminatorValue != b.DiscriminatorValue {
return false
}
return true
}
// Equals returns true iff the two StructFields are equal.
func (a *StructField) Equals(b *StructField) bool {
if a == nil || b == nil {
return a == nil && b == nil
}
if a.Name != b.Name {
return false
}
if !reflect.DeepEqual(a.Default, b.Default) {
return false
}
return a.Type.Equals(&b.Type)
}
// Equals returns true iff the two Lists are equal.
func (a *List) Equals(b *List) bool {
if a == nil || b == nil {
return a == nil && b == nil
}
if !a.ElementType.Equals(&b.ElementType) {
return false
}
if a.ElementRelationship != b.ElementRelationship {
return false
}
if len(a.Keys) != len(b.Keys) {
return false
}
for i := range a.Keys {
if a.Keys[i] != b.Keys[i] {
return false
}
}
return true
}

View File

@ -0,0 +1,161 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package schema
// SchemaSchemaYAML is a schema against which you can validate other schemas.
// It will validate itself. It can be unmarshalled into a Schema type.
var SchemaSchemaYAML = `types:
- name: schema
map:
fields:
- name: types
type:
list:
elementRelationship: associative
elementType:
namedType: typeDef
keys:
- name
- name: typeDef
map:
fields:
- name: name
type:
scalar: string
- name: scalar
type:
scalar: string
- name: map
type:
namedType: map
- name: list
type:
namedType: list
- name: untyped
type:
namedType: untyped
- name: typeRef
map:
fields:
- name: namedType
type:
scalar: string
- name: scalar
type:
scalar: string
- name: map
type:
namedType: map
- name: list
type:
namedType: list
- name: untyped
type:
namedType: untyped
- name: scalar
scalar: string
- name: map
map:
fields:
- name: fields
type:
list:
elementType:
namedType: structField
elementRelationship: associative
keys: [ "name" ]
- name: unions
type:
list:
elementType:
namedType: union
elementRelationship: atomic
- name: elementType
type:
namedType: typeRef
- name: elementRelationship
type:
scalar: string
- name: unionField
map:
fields:
- name: fieldName
type:
scalar: string
- name: discriminatorValue
type:
scalar: string
- name: union
map:
fields:
- name: discriminator
type:
scalar: string
- name: deduceInvalidDiscriminator
type:
scalar: bool
- name: fields
type:
list:
elementRelationship: associative
elementType:
namedType: unionField
keys:
- fieldName
- name: structField
map:
fields:
- name: name
type:
scalar: string
- name: type
type:
namedType: typeRef
- name: default
type:
namedType: __untyped_atomic_
- name: list
map:
fields:
- name: elementType
type:
namedType: typeRef
- name: elementRelationship
type:
scalar: string
- name: keys
type:
list:
elementType:
scalar: string
- name: untyped
map:
fields:
- name: elementRelationship
type:
scalar: string
- name: __untyped_atomic_
scalar: untyped
list:
elementType:
namedType: __untyped_atomic_
elementRelationship: atomic
map:
elementType:
namedType: __untyped_atomic_
elementRelationship: atomic
`

View File

@ -0,0 +1,18 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package typed contains logic for operating on values with given schemas.
package typed

View File

@ -0,0 +1,256 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package typed
import (
"errors"
"fmt"
"strings"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v4/schema"
"sigs.k8s.io/structured-merge-diff/v4/value"
)
// ValidationError reports an error about a particular field
type ValidationError struct {
Path string
ErrorMessage string
}
// Error returns a human readable error message.
func (ve ValidationError) Error() string {
if len(ve.Path) == 0 {
return ve.ErrorMessage
}
return fmt.Sprintf("%s: %v", ve.Path, ve.ErrorMessage)
}
// ValidationErrors accumulates multiple validation error messages.
type ValidationErrors []ValidationError
// Error returns a human readable error message reporting each error in the
// list.
func (errs ValidationErrors) Error() string {
if len(errs) == 1 {
return errs[0].Error()
}
messages := []string{"errors:"}
for _, e := range errs {
messages = append(messages, " "+e.Error())
}
return strings.Join(messages, "\n")
}
// Set the given path to all the validation errors.
func (errs ValidationErrors) WithPath(p string) ValidationErrors {
for i := range errs {
errs[i].Path = p
}
return errs
}
// WithPrefix prefixes all errors path with the given pathelement. This
// is useful when unwinding the stack on errors.
func (errs ValidationErrors) WithPrefix(prefix string) ValidationErrors {
for i := range errs {
errs[i].Path = prefix + errs[i].Path
}
return errs
}
// WithLazyPrefix prefixes all errors path with the given pathelement.
// This is useful when unwinding the stack on errors. Prefix is
// computed lazily only if there is an error.
func (errs ValidationErrors) WithLazyPrefix(fn func() string) ValidationErrors {
if len(errs) == 0 {
return errs
}
prefix := ""
if fn != nil {
prefix = fn()
}
for i := range errs {
errs[i].Path = prefix + errs[i].Path
}
return errs
}
func errorf(format string, args ...interface{}) ValidationErrors {
return ValidationErrors{{
ErrorMessage: fmt.Sprintf(format, args...),
}}
}
type atomHandler interface {
doScalar(*schema.Scalar) ValidationErrors
doList(*schema.List) ValidationErrors
doMap(*schema.Map) ValidationErrors
}
func resolveSchema(s *schema.Schema, tr schema.TypeRef, v value.Value, ah atomHandler) ValidationErrors {
a, ok := s.Resolve(tr)
if !ok {
return errorf("schema error: no type found matching: %v", *tr.NamedType)
}
a = deduceAtom(a, v)
return handleAtom(a, tr, ah)
}
// deduceAtom determines which of the possible types in atom 'atom' applies to value 'val'.
// If val is of a type allowed by atom, return a copy of atom with all other types set to nil.
// if val is nil, or is not of a type allowed by atom, just return the original atom,
// and validation will fail at a later stage. (with a more useful error)
func deduceAtom(atom schema.Atom, val value.Value) schema.Atom {
switch {
case val == nil:
case val.IsFloat(), val.IsInt(), val.IsString(), val.IsBool():
if atom.Scalar != nil {
return schema.Atom{Scalar: atom.Scalar}
}
case val.IsList():
if atom.List != nil {
return schema.Atom{List: atom.List}
}
case val.IsMap():
if atom.Map != nil {
return schema.Atom{Map: atom.Map}
}
}
return atom
}
func handleAtom(a schema.Atom, tr schema.TypeRef, ah atomHandler) ValidationErrors {
switch {
case a.Map != nil:
return ah.doMap(a.Map)
case a.Scalar != nil:
return ah.doScalar(a.Scalar)
case a.List != nil:
return ah.doList(a.List)
}
name := "inlined"
if tr.NamedType != nil {
name = "named type: " + *tr.NamedType
}
return errorf("schema error: invalid atom: %v", name)
}
// Returns the list, or an error. Reminder: nil is a valid list and might be returned.
func listValue(a value.Allocator, val value.Value) (value.List, error) {
if val.IsNull() {
// Null is a valid list.
return nil, nil
}
if !val.IsList() {
return nil, fmt.Errorf("expected list, got %v", val)
}
return val.AsListUsing(a), nil
}
// Returns the map, or an error. Reminder: nil is a valid map and might be returned.
func mapValue(a value.Allocator, val value.Value) (value.Map, error) {
if val == nil {
return nil, fmt.Errorf("expected map, got nil")
}
if val.IsNull() {
// Null is a valid map.
return nil, nil
}
if !val.IsMap() {
return nil, fmt.Errorf("expected map, got %v", val)
}
return val.AsMapUsing(a), nil
}
func getAssociativeKeyDefault(s *schema.Schema, list *schema.List, fieldName string) (interface{}, error) {
atom, ok := s.Resolve(list.ElementType)
if !ok {
return nil, errors.New("invalid elementType for list")
}
if atom.Map == nil {
return nil, errors.New("associative list may not have non-map types")
}
// If the field is not found, we can assume there is no default.
field, _ := atom.Map.FindField(fieldName)
return field.Default, nil
}
func keyedAssociativeListItemToPathElement(a value.Allocator, s *schema.Schema, list *schema.List, index int, child value.Value) (fieldpath.PathElement, error) {
pe := fieldpath.PathElement{}
if child.IsNull() {
// null entries are illegal.
return pe, errors.New("associative list with keys may not have a null element")
}
if !child.IsMap() {
return pe, errors.New("associative list with keys may not have non-map elements")
}
keyMap := value.FieldList{}
m := child.AsMapUsing(a)
defer a.Free(m)
for _, fieldName := range list.Keys {
if val, ok := m.Get(fieldName); ok {
keyMap = append(keyMap, value.Field{Name: fieldName, Value: val})
} else if def, err := getAssociativeKeyDefault(s, list, fieldName); err != nil {
return pe, fmt.Errorf("couldn't find default value for %v: %v", fieldName, err)
} else if def != nil {
keyMap = append(keyMap, value.Field{Name: fieldName, Value: value.NewValueInterface(def)})
} else {
return pe, fmt.Errorf("associative list with keys has an element that omits key field %q (and doesn't have default value)", fieldName)
}
}
keyMap.Sort()
pe.Key = &keyMap
return pe, nil
}
func setItemToPathElement(list *schema.List, index int, child value.Value) (fieldpath.PathElement, error) {
pe := fieldpath.PathElement{}
switch {
case child.IsMap():
// TODO: atomic maps should be acceptable.
return pe, errors.New("associative list without keys has an element that's a map type")
case child.IsList():
// Should we support a set of lists? For the moment
// let's say we don't.
// TODO: atomic lists should be acceptable.
return pe, errors.New("not supported: associative list with lists as elements")
case child.IsNull():
return pe, errors.New("associative list without keys has an element that's an explicit null")
default:
// We are a set type.
pe.Value = &child
return pe, nil
}
}
func listItemToPathElement(a value.Allocator, s *schema.Schema, list *schema.List, index int, child value.Value) (fieldpath.PathElement, error) {
if list.ElementRelationship == schema.Associative {
if len(list.Keys) > 0 {
return keyedAssociativeListItemToPathElement(a, s, list, index, child)
}
// If there's no keys, then we must be a set of primitives.
return setItemToPathElement(list, index, child)
}
// Use the index as a key for atomic lists.
return fieldpath.PathElement{Index: &index}, nil
}

View File

@ -0,0 +1,353 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package typed
import (
"math"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v4/schema"
"sigs.k8s.io/structured-merge-diff/v4/value"
)
type mergingWalker struct {
lhs value.Value
rhs value.Value
schema *schema.Schema
typeRef schema.TypeRef
// Current path that we are merging
path fieldpath.Path
// How to merge. Called after schema validation for all leaf fields.
rule mergeRule
// If set, called after non-leaf items have been merged. (`out` is
// probably already set.)
postItemHook mergeRule
// output of the merge operation (nil if none)
out *interface{}
// internal housekeeping--don't set when constructing.
inLeaf bool // Set to true if we're in a "big leaf"--atomic map/list
// Allocate only as many walkers as needed for the depth by storing them here.
spareWalkers *[]*mergingWalker
allocator value.Allocator
}
// merge rules examine w.lhs and w.rhs (up to one of which may be nil) and
// optionally set w.out. If lhs and rhs are both set, they will be of
// comparable type.
type mergeRule func(w *mergingWalker)
var (
ruleKeepRHS = mergeRule(func(w *mergingWalker) {
if w.rhs != nil {
v := w.rhs.Unstructured()
w.out = &v
} else if w.lhs != nil {
v := w.lhs.Unstructured()
w.out = &v
}
})
)
// merge sets w.out.
func (w *mergingWalker) merge(prefixFn func() string) (errs ValidationErrors) {
if w.lhs == nil && w.rhs == nil {
// check this condidition here instead of everywhere below.
return errorf("at least one of lhs and rhs must be provided")
}
a, ok := w.schema.Resolve(w.typeRef)
if !ok {
return errorf("schema error: no type found matching: %v", *w.typeRef.NamedType)
}
alhs := deduceAtom(a, w.lhs)
arhs := deduceAtom(a, w.rhs)
if alhs.Equals(&arhs) {
errs = append(errs, handleAtom(arhs, w.typeRef, w)...)
} else {
w2 := *w
errs = append(errs, handleAtom(alhs, w.typeRef, &w2)...)
errs = append(errs, handleAtom(arhs, w.typeRef, w)...)
}
if !w.inLeaf && w.postItemHook != nil {
w.postItemHook(w)
}
return errs.WithLazyPrefix(prefixFn)
}
// doLeaf should be called on leaves before descending into children, if there
// will be a descent. It modifies w.inLeaf.
func (w *mergingWalker) doLeaf() {
if w.inLeaf {
// We're in a "big leaf", an atomic map or list. Ignore
// subsequent leaves.
return
}
w.inLeaf = true
// We don't recurse into leaf fields for merging.
w.rule(w)
}
func (w *mergingWalker) doScalar(t *schema.Scalar) (errs ValidationErrors) {
errs = append(errs, validateScalar(t, w.lhs, "lhs: ")...)
errs = append(errs, validateScalar(t, w.rhs, "rhs: ")...)
if len(errs) > 0 {
return errs
}
// All scalars are leaf fields.
w.doLeaf()
return nil
}
func (w *mergingWalker) prepareDescent(pe fieldpath.PathElement, tr schema.TypeRef) *mergingWalker {
if w.spareWalkers == nil {
// first descent.
w.spareWalkers = &[]*mergingWalker{}
}
var w2 *mergingWalker
if n := len(*w.spareWalkers); n > 0 {
w2, *w.spareWalkers = (*w.spareWalkers)[n-1], (*w.spareWalkers)[:n-1]
} else {
w2 = &mergingWalker{}
}
*w2 = *w
w2.typeRef = tr
w2.path = append(w2.path, pe)
w2.lhs = nil
w2.rhs = nil
w2.out = nil
return w2
}
func (w *mergingWalker) finishDescent(w2 *mergingWalker) {
// if the descent caused a realloc, ensure that we reuse the buffer
// for the next sibling.
w.path = w2.path[:len(w2.path)-1]
*w.spareWalkers = append(*w.spareWalkers, w2)
}
func (w *mergingWalker) derefMap(prefix string, v value.Value) (value.Map, ValidationErrors) {
if v == nil {
return nil, nil
}
m, err := mapValue(w.allocator, v)
if err != nil {
return nil, errorf("%v: %v", prefix, err)
}
return m, nil
}
func (w *mergingWalker) visitListItems(t *schema.List, lhs, rhs value.List) (errs ValidationErrors) {
rLen := 0
if rhs != nil {
rLen = rhs.Length()
}
lLen := 0
if lhs != nil {
lLen = lhs.Length()
}
out := make([]interface{}, 0, int(math.Max(float64(rLen), float64(lLen))))
// TODO: ordering is totally wrong.
// TODO: might as well make the map order work the same way.
// This is a cheap hack to at least make the output order stable.
rhsOrder := make([]fieldpath.PathElement, 0, rLen)
// First, collect all RHS children.
observedRHS := fieldpath.MakePathElementValueMap(rLen)
if rhs != nil {
for i := 0; i < rhs.Length(); i++ {
child := rhs.At(i)
pe, err := listItemToPathElement(w.allocator, w.schema, t, i, child)
if err != nil {
errs = append(errs, errorf("rhs: element %v: %v", i, err.Error())...)
// If we can't construct the path element, we can't
// even report errors deeper in the schema, so bail on
// this element.
continue
}
if _, ok := observedRHS.Get(pe); ok {
errs = append(errs, errorf("rhs: duplicate entries for key %v", pe.String())...)
}
observedRHS.Insert(pe, child)
rhsOrder = append(rhsOrder, pe)
}
}
// Then merge with LHS children.
observedLHS := fieldpath.MakePathElementSet(lLen)
if lhs != nil {
for i := 0; i < lhs.Length(); i++ {
child := lhs.At(i)
pe, err := listItemToPathElement(w.allocator, w.schema, t, i, child)
if err != nil {
errs = append(errs, errorf("lhs: element %v: %v", i, err.Error())...)
// If we can't construct the path element, we can't
// even report errors deeper in the schema, so bail on
// this element.
continue
}
if observedLHS.Has(pe) {
errs = append(errs, errorf("lhs: duplicate entries for key %v", pe.String())...)
continue
}
observedLHS.Insert(pe)
w2 := w.prepareDescent(pe, t.ElementType)
w2.lhs = value.Value(child)
if rchild, ok := observedRHS.Get(pe); ok {
w2.rhs = rchild
}
errs = append(errs, w2.merge(pe.String)...)
if w2.out != nil {
out = append(out, *w2.out)
}
w.finishDescent(w2)
}
}
for _, pe := range rhsOrder {
if observedLHS.Has(pe) {
continue
}
value, _ := observedRHS.Get(pe)
w2 := w.prepareDescent(pe, t.ElementType)
w2.rhs = value
errs = append(errs, w2.merge(pe.String)...)
if w2.out != nil {
out = append(out, *w2.out)
}
w.finishDescent(w2)
}
if len(out) > 0 {
i := interface{}(out)
w.out = &i
}
return errs
}
func (w *mergingWalker) derefList(prefix string, v value.Value) (value.List, ValidationErrors) {
if v == nil {
return nil, nil
}
l, err := listValue(w.allocator, v)
if err != nil {
return nil, errorf("%v: %v", prefix, err)
}
return l, nil
}
func (w *mergingWalker) doList(t *schema.List) (errs ValidationErrors) {
lhs, _ := w.derefList("lhs: ", w.lhs)
if lhs != nil {
defer w.allocator.Free(lhs)
}
rhs, _ := w.derefList("rhs: ", w.rhs)
if rhs != nil {
defer w.allocator.Free(rhs)
}
// If both lhs and rhs are empty/null, treat it as a
// leaf: this helps preserve the empty/null
// distinction.
emptyPromoteToLeaf := (lhs == nil || lhs.Length() == 0) && (rhs == nil || rhs.Length() == 0)
if t.ElementRelationship == schema.Atomic || emptyPromoteToLeaf {
w.doLeaf()
return nil
}
if lhs == nil && rhs == nil {
return nil
}
errs = w.visitListItems(t, lhs, rhs)
return errs
}
func (w *mergingWalker) visitMapItem(t *schema.Map, out map[string]interface{}, key string, lhs, rhs value.Value) (errs ValidationErrors) {
fieldType := t.ElementType
if sf, ok := t.FindField(key); ok {
fieldType = sf.Type
}
pe := fieldpath.PathElement{FieldName: &key}
w2 := w.prepareDescent(pe, fieldType)
w2.lhs = lhs
w2.rhs = rhs
errs = append(errs, w2.merge(pe.String)...)
if w2.out != nil {
out[key] = *w2.out
}
w.finishDescent(w2)
return errs
}
func (w *mergingWalker) visitMapItems(t *schema.Map, lhs, rhs value.Map) (errs ValidationErrors) {
out := map[string]interface{}{}
value.MapZipUsing(w.allocator, lhs, rhs, value.Unordered, func(key string, lhsValue, rhsValue value.Value) bool {
errs = append(errs, w.visitMapItem(t, out, key, lhsValue, rhsValue)...)
return true
})
if len(out) > 0 {
i := interface{}(out)
w.out = &i
}
return errs
}
func (w *mergingWalker) doMap(t *schema.Map) (errs ValidationErrors) {
lhs, _ := w.derefMap("lhs: ", w.lhs)
if lhs != nil {
defer w.allocator.Free(lhs)
}
rhs, _ := w.derefMap("rhs: ", w.rhs)
if rhs != nil {
defer w.allocator.Free(rhs)
}
// If both lhs and rhs are empty/null, treat it as a
// leaf: this helps preserve the empty/null
// distinction.
emptyPromoteToLeaf := (lhs == nil || lhs.Empty()) && (rhs == nil || rhs.Empty())
if t.ElementRelationship == schema.Atomic || emptyPromoteToLeaf {
w.doLeaf()
return nil
}
if lhs == nil && rhs == nil {
return nil
}
errs = append(errs, w.visitMapItems(t, lhs, rhs)...)
return errs
}

View File

@ -0,0 +1,151 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package typed
import (
"fmt"
yaml "gopkg.in/yaml.v2"
"sigs.k8s.io/structured-merge-diff/v4/schema"
"sigs.k8s.io/structured-merge-diff/v4/value"
)
// YAMLObject is an object encoded in YAML.
type YAMLObject string
// Parser implements YAMLParser and allows introspecting the schema.
type Parser struct {
Schema schema.Schema
}
// create builds an unvalidated parser.
func create(s YAMLObject) (*Parser, error) {
p := Parser{}
err := yaml.Unmarshal([]byte(s), &p.Schema)
return &p, err
}
func createOrDie(schema YAMLObject) *Parser {
p, err := create(schema)
if err != nil {
panic(fmt.Errorf("failed to create parser: %v", err))
}
return p
}
var ssParser = createOrDie(YAMLObject(schema.SchemaSchemaYAML))
// NewParser will build a YAMLParser from a schema. The schema is validated.
func NewParser(schema YAMLObject) (*Parser, error) {
_, err := ssParser.Type("schema").FromYAML(schema)
if err != nil {
return nil, fmt.Errorf("unable to validate schema: %v", err)
}
p, err := create(schema)
if err != nil {
return nil, err
}
return p, nil
}
// TypeNames returns a list of types this parser understands.
func (p *Parser) TypeNames() (names []string) {
for _, td := range p.Schema.Types {
names = append(names, td.Name)
}
return names
}
// Type returns a helper which can produce objects of the given type. Any
// errors are deferred until a further function is called.
func (p *Parser) Type(name string) ParseableType {
return ParseableType{
Schema: &p.Schema,
TypeRef: schema.TypeRef{NamedType: &name},
}
}
// ParseableType allows for easy production of typed objects.
type ParseableType struct {
TypeRef schema.TypeRef
Schema *schema.Schema
}
// IsValid return true if p's schema and typename are valid.
func (p ParseableType) IsValid() bool {
_, ok := p.Schema.Resolve(p.TypeRef)
return ok
}
// FromYAML parses a yaml string into an object with the current schema
// and the type "typename" or an error if validation fails.
func (p ParseableType) FromYAML(object YAMLObject) (*TypedValue, error) {
var v interface{}
err := yaml.Unmarshal([]byte(object), &v)
if err != nil {
return nil, err
}
return AsTyped(value.NewValueInterface(v), p.Schema, p.TypeRef)
}
// FromUnstructured converts a go "interface{}" type, typically an
// unstructured object in Kubernetes world, to a TypedValue. It returns an
// error if the resulting object fails schema validation.
// The provided interface{} must be one of: map[string]interface{},
// map[interface{}]interface{}, []interface{}, int types, float types,
// string or boolean. Nested interface{} must also be one of these types.
func (p ParseableType) FromUnstructured(in interface{}) (*TypedValue, error) {
return AsTyped(value.NewValueInterface(in), p.Schema, p.TypeRef)
}
// FromStructured converts a go "interface{}" type, typically an structured object in
// Kubernetes, to a TypedValue. It will return an error if the resulting object fails
// schema validation. The provided "interface{}" value must be a pointer so that the
// value can be modified via reflection. The provided "interface{}" may contain structs
// and types that are converted to Values by the jsonMarshaler interface.
func (p ParseableType) FromStructured(in interface{}) (*TypedValue, error) {
v, err := value.NewValueReflect(in)
if err != nil {
return nil, fmt.Errorf("error creating struct value reflector: %v", err)
}
return AsTyped(v, p.Schema, p.TypeRef)
}
// DeducedParseableType is a ParseableType that deduces the type from
// the content of the object.
var DeducedParseableType ParseableType = createOrDie(YAMLObject(`types:
- name: __untyped_atomic_
scalar: untyped
list:
elementType:
namedType: __untyped_atomic_
elementRelationship: atomic
map:
elementType:
namedType: __untyped_atomic_
elementRelationship: atomic
- name: __untyped_deduced_
scalar: untyped
list:
elementType:
namedType: __untyped_atomic_
elementRelationship: atomic
map:
elementType:
namedType: __untyped_deduced_
elementRelationship: separable
`)).Type("__untyped_deduced_")

View File

@ -0,0 +1,295 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package typed
import (
"fmt"
"sync"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v4/schema"
)
var fmPool = sync.Pool{
New: func() interface{} { return &reconcileWithSchemaWalker{} },
}
func (v *reconcileWithSchemaWalker) finished() {
v.fieldSet = nil
v.schema = nil
v.value = nil
v.typeRef = schema.TypeRef{}
v.path = nil
v.toRemove = nil
v.toAdd = nil
fmPool.Put(v)
}
type reconcileWithSchemaWalker struct {
value *TypedValue // root of the live object
schema *schema.Schema // root of the live schema
// state of node being visited by walker
fieldSet *fieldpath.Set
typeRef schema.TypeRef
path fieldpath.Path
isAtomic bool
// the accumulated diff to perform to apply reconciliation
toRemove *fieldpath.Set // paths to remove recursively
toAdd *fieldpath.Set // paths to add after any removals
// Allocate only as many walkers as needed for the depth by storing them here.
spareWalkers *[]*reconcileWithSchemaWalker
}
func (v *reconcileWithSchemaWalker) prepareDescent(pe fieldpath.PathElement, tr schema.TypeRef) *reconcileWithSchemaWalker {
if v.spareWalkers == nil {
// first descent.
v.spareWalkers = &[]*reconcileWithSchemaWalker{}
}
var v2 *reconcileWithSchemaWalker
if n := len(*v.spareWalkers); n > 0 {
v2, *v.spareWalkers = (*v.spareWalkers)[n-1], (*v.spareWalkers)[:n-1]
} else {
v2 = &reconcileWithSchemaWalker{}
}
*v2 = *v
v2.typeRef = tr
v2.path = append(v.path, pe)
v2.value = v.value
return v2
}
func (v *reconcileWithSchemaWalker) finishDescent(v2 *reconcileWithSchemaWalker) {
v2.fieldSet = nil
v2.schema = nil
v2.value = nil
v2.typeRef = schema.TypeRef{}
if cap(v2.path) < 20 { // recycle slices that do not have unexpectedly high capacity
v2.path = v2.path[:0]
} else {
v2.path = nil
}
// merge any accumulated changes into parent walker
if v2.toRemove != nil {
if v.toRemove == nil {
v.toRemove = v2.toRemove
} else {
v.toRemove = v.toRemove.Union(v2.toRemove)
}
}
if v2.toAdd != nil {
if v.toAdd == nil {
v.toAdd = v2.toAdd
} else {
v.toAdd = v.toAdd.Union(v2.toAdd)
}
}
v2.toRemove = nil
v2.toAdd = nil
// if the descent caused a realloc, ensure that we reuse the buffer
// for the next sibling.
*v.spareWalkers = append(*v.spareWalkers, v2)
}
// ReconcileFieldSetWithSchema reconciles the a field set with any changes to the
//// object's schema since the field set was written. Returns the reconciled field set, or nil of
// no changes were made to the field set.
//
// Supports:
// - changing types from atomic to granular
// - changing types from granular to atomic
func ReconcileFieldSetWithSchema(fieldset *fieldpath.Set, tv *TypedValue) (*fieldpath.Set, error) {
v := fmPool.Get().(*reconcileWithSchemaWalker)
v.fieldSet = fieldset
v.value = tv
v.schema = tv.schema
v.typeRef = tv.typeRef
// We don't reconcile deduced types, which are primarily for use by unstructured CRDs. Deduced
// types do not support atomic or granular tags. Nor does the dynamic schema deduction
// interact well with the reconcile logic.
if v.schema == DeducedParseableType.Schema {
return nil, nil
}
defer v.finished()
errs := v.reconcile()
if len(errs) > 0 {
return nil, fmt.Errorf("errors reconciling field set with schema: %s", errs.Error())
}
// If there are any accumulated changes, apply them
if v.toAdd != nil || v.toRemove != nil {
out := v.fieldSet
if v.toRemove != nil {
out = out.RecursiveDifference(v.toRemove)
}
if v.toAdd != nil {
out = out.Union(v.toAdd)
}
return out, nil
}
return nil, nil
}
func (v *reconcileWithSchemaWalker) reconcile() (errs ValidationErrors) {
a, ok := v.schema.Resolve(v.typeRef)
if !ok {
errs = append(errs, errorf("could not resolve %v", v.typeRef)...)
return
}
return handleAtom(a, v.typeRef, v)
}
func (v *reconcileWithSchemaWalker) doScalar(_ *schema.Scalar) (errs ValidationErrors) {
return errs
}
func (v *reconcileWithSchemaWalker) visitListItems(t *schema.List, element *fieldpath.Set) (errs ValidationErrors) {
handleElement := func(pe fieldpath.PathElement, isMember bool) {
var hasChildren bool
v2 := v.prepareDescent(pe, t.ElementType)
v2.fieldSet, hasChildren = element.Children.Get(pe)
v2.isAtomic = isMember && !hasChildren
errs = append(errs, v2.reconcile()...)
v.finishDescent(v2)
}
element.Children.Iterate(func(pe fieldpath.PathElement) {
if element.Members.Has(pe) {
return
}
handleElement(pe, false)
})
element.Members.Iterate(func(pe fieldpath.PathElement) {
handleElement(pe, true)
})
return errs
}
func (v *reconcileWithSchemaWalker) doList(t *schema.List) (errs ValidationErrors) {
// reconcile lists changed from granular to atomic
if !v.isAtomic && t.ElementRelationship == schema.Atomic {
v.toRemove = fieldpath.NewSet(v.path) // remove all root and all children fields
v.toAdd = fieldpath.NewSet(v.path) // add the root of the atomic
return errs
}
// reconcile lists changed from atomic to granular
if v.isAtomic && t.ElementRelationship == schema.Associative {
v.toAdd, errs = buildGranularFieldSet(v.path, v.value)
if errs != nil {
return errs
}
}
if v.fieldSet != nil {
errs = v.visitListItems(t, v.fieldSet)
}
return errs
}
func (v *reconcileWithSchemaWalker) visitMapItems(t *schema.Map, element *fieldpath.Set) (errs ValidationErrors) {
handleElement := func(pe fieldpath.PathElement, isMember bool) {
var hasChildren bool
if tr, ok := typeRefAtPath(t, pe); ok { // ignore fields not in the schema
v2 := v.prepareDescent(pe, tr)
v2.fieldSet, hasChildren = element.Children.Get(pe)
v2.isAtomic = isMember && !hasChildren
errs = append(errs, v2.reconcile()...)
v.finishDescent(v2)
}
}
element.Children.Iterate(func(pe fieldpath.PathElement) {
if element.Members.Has(pe) {
return
}
handleElement(pe, false)
})
element.Members.Iterate(func(pe fieldpath.PathElement) {
handleElement(pe, true)
})
return errs
}
func (v *reconcileWithSchemaWalker) doMap(t *schema.Map) (errs ValidationErrors) {
// reconcile maps and structs changed from granular to atomic
if !v.isAtomic && t.ElementRelationship == schema.Atomic {
if v.fieldSet != nil && v.fieldSet.Size() > 0 {
v.toRemove = fieldpath.NewSet(v.path) // remove all root and all children fields
v.toAdd = fieldpath.NewSet(v.path) // add the root of the atomic
}
return errs
}
// reconcile maps changed from atomic to granular
if v.isAtomic && (t.ElementRelationship == schema.Separable || t.ElementRelationship == "") {
v.toAdd, errs = buildGranularFieldSet(v.path, v.value)
if errs != nil {
return errs
}
}
if v.fieldSet != nil {
errs = v.visitMapItems(t, v.fieldSet)
}
return errs
}
func buildGranularFieldSet(path fieldpath.Path, value *TypedValue) (*fieldpath.Set, ValidationErrors) {
valueFieldSet, err := value.ToFieldSet()
if err != nil {
return nil, errorf("toFieldSet: %v", err)
}
if valueFieldSetAtPath, ok := fieldSetAtPath(valueFieldSet, path); ok {
result := fieldpath.NewSet(path)
resultAtPath := descendToPath(result, path)
*resultAtPath = *valueFieldSetAtPath
return result, nil
}
return nil, nil
}
func fieldSetAtPath(node *fieldpath.Set, path fieldpath.Path) (*fieldpath.Set, bool) {
ok := true
for _, pe := range path {
if node, ok = node.Children.Get(pe); !ok {
break
}
}
return node, ok
}
func descendToPath(node *fieldpath.Set, path fieldpath.Path) *fieldpath.Set {
for _, pe := range path {
node = node.Children.Descend(pe)
}
return node
}
func typeRefAtPath(t *schema.Map, pe fieldpath.PathElement) (schema.TypeRef, bool) {
tr := t.ElementType
if pe.FieldName != nil {
if sf, ok := t.FindField(*pe.FieldName); ok {
tr = sf.Type
}
}
return tr, tr != schema.TypeRef{}
}

View File

@ -0,0 +1,140 @@
/*
Copyright 2019 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 typed
import (
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v4/schema"
"sigs.k8s.io/structured-merge-diff/v4/value"
)
type removingWalker struct {
value value.Value
out interface{}
schema *schema.Schema
toRemove *fieldpath.Set
allocator value.Allocator
shouldExtract bool
}
// removeItemsWithSchema will walk the given value and look for items from the toRemove set.
// Depending on whether shouldExtract is set true or false, it will return a modified version
// of the input value with either:
// 1. only the items in the toRemove set (when shouldExtract is true) or
// 2. the items from the toRemove set removed from the value (when shouldExtract is false).
func removeItemsWithSchema(val value.Value, toRemove *fieldpath.Set, schema *schema.Schema, typeRef schema.TypeRef, shouldExtract bool) value.Value {
w := &removingWalker{
value: val,
schema: schema,
toRemove: toRemove,
allocator: value.NewFreelistAllocator(),
shouldExtract: shouldExtract,
}
resolveSchema(schema, typeRef, val, w)
return value.NewValueInterface(w.out)
}
func (w *removingWalker) doScalar(t *schema.Scalar) ValidationErrors {
w.out = w.value.Unstructured()
return nil
}
func (w *removingWalker) doList(t *schema.List) (errs ValidationErrors) {
l := w.value.AsListUsing(w.allocator)
defer w.allocator.Free(l)
// If list is null, empty, or atomic just return
if l == nil || l.Length() == 0 || t.ElementRelationship == schema.Atomic {
return nil
}
var newItems []interface{}
iter := l.RangeUsing(w.allocator)
defer w.allocator.Free(iter)
for iter.Next() {
i, item := iter.Item()
// Ignore error because we have already validated this list
pe, _ := listItemToPathElement(w.allocator, w.schema, t, i, item)
path, _ := fieldpath.MakePath(pe)
// save items on the path when we shouldExtract
// but ignore them when we are removing (i.e. !w.shouldExtract)
if w.toRemove.Has(path) {
if w.shouldExtract {
newItems = append(newItems, item.Unstructured())
} else {
continue
}
}
if subset := w.toRemove.WithPrefix(pe); !subset.Empty() {
item = removeItemsWithSchema(item, subset, w.schema, t.ElementType, w.shouldExtract)
} else {
// don't save items not on the path when we shouldExtract.
if w.shouldExtract {
continue
}
}
newItems = append(newItems, item.Unstructured())
}
if len(newItems) > 0 {
w.out = newItems
}
return nil
}
func (w *removingWalker) doMap(t *schema.Map) ValidationErrors {
m := w.value.AsMapUsing(w.allocator)
if m != nil {
defer w.allocator.Free(m)
}
// If map is null, empty, or atomic just return
if m == nil || m.Empty() || t.ElementRelationship == schema.Atomic {
return nil
}
fieldTypes := map[string]schema.TypeRef{}
for _, structField := range t.Fields {
fieldTypes[structField.Name] = structField.Type
}
newMap := map[string]interface{}{}
m.Iterate(func(k string, val value.Value) bool {
pe := fieldpath.PathElement{FieldName: &k}
path, _ := fieldpath.MakePath(pe)
fieldType := t.ElementType
if ft, ok := fieldTypes[k]; ok {
fieldType = ft
}
// save values on the path when we shouldExtract
// but ignore them when we are removing (i.e. !w.shouldExtract)
if w.toRemove.Has(path) {
if w.shouldExtract {
newMap[k] = val.Unstructured()
}
return true
}
if subset := w.toRemove.WithPrefix(pe); !subset.Empty() {
val = removeItemsWithSchema(val, subset, w.schema, fieldType, w.shouldExtract)
} else {
// don't save values not on the path when we shouldExtract.
if w.shouldExtract {
return true
}
}
newMap[k] = val.Unstructured()
return true
})
if len(newMap) > 0 {
w.out = newMap
}
return nil
}

View File

@ -0,0 +1,168 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package typed
import (
"sync"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v4/schema"
"sigs.k8s.io/structured-merge-diff/v4/value"
)
var tPool = sync.Pool{
New: func() interface{} { return &toFieldSetWalker{} },
}
func (tv TypedValue) toFieldSetWalker() *toFieldSetWalker {
v := tPool.Get().(*toFieldSetWalker)
v.value = tv.value
v.schema = tv.schema
v.typeRef = tv.typeRef
v.set = &fieldpath.Set{}
v.allocator = value.NewFreelistAllocator()
return v
}
func (v *toFieldSetWalker) finished() {
v.schema = nil
v.typeRef = schema.TypeRef{}
v.path = nil
v.set = nil
tPool.Put(v)
}
type toFieldSetWalker struct {
value value.Value
schema *schema.Schema
typeRef schema.TypeRef
set *fieldpath.Set
path fieldpath.Path
// Allocate only as many walkers as needed for the depth by storing them here.
spareWalkers *[]*toFieldSetWalker
allocator value.Allocator
}
func (v *toFieldSetWalker) prepareDescent(pe fieldpath.PathElement, tr schema.TypeRef) *toFieldSetWalker {
if v.spareWalkers == nil {
// first descent.
v.spareWalkers = &[]*toFieldSetWalker{}
}
var v2 *toFieldSetWalker
if n := len(*v.spareWalkers); n > 0 {
v2, *v.spareWalkers = (*v.spareWalkers)[n-1], (*v.spareWalkers)[:n-1]
} else {
v2 = &toFieldSetWalker{}
}
*v2 = *v
v2.typeRef = tr
v2.path = append(v2.path, pe)
return v2
}
func (v *toFieldSetWalker) finishDescent(v2 *toFieldSetWalker) {
// if the descent caused a realloc, ensure that we reuse the buffer
// for the next sibling.
v.path = v2.path[:len(v2.path)-1]
*v.spareWalkers = append(*v.spareWalkers, v2)
}
func (v *toFieldSetWalker) toFieldSet() ValidationErrors {
return resolveSchema(v.schema, v.typeRef, v.value, v)
}
func (v *toFieldSetWalker) doScalar(t *schema.Scalar) ValidationErrors {
v.set.Insert(v.path)
return nil
}
func (v *toFieldSetWalker) visitListItems(t *schema.List, list value.List) (errs ValidationErrors) {
for i := 0; i < list.Length(); i++ {
child := list.At(i)
pe, _ := listItemToPathElement(v.allocator, v.schema, t, i, child)
v2 := v.prepareDescent(pe, t.ElementType)
v2.value = child
errs = append(errs, v2.toFieldSet()...)
v2.set.Insert(v2.path)
v.finishDescent(v2)
}
return errs
}
func (v *toFieldSetWalker) doList(t *schema.List) (errs ValidationErrors) {
list, _ := listValue(v.allocator, v.value)
if list != nil {
defer v.allocator.Free(list)
}
if t.ElementRelationship == schema.Atomic {
v.set.Insert(v.path)
return nil
}
if list == nil {
return nil
}
errs = v.visitListItems(t, list)
return errs
}
func (v *toFieldSetWalker) visitMapItems(t *schema.Map, m value.Map) (errs ValidationErrors) {
m.Iterate(func(key string, val value.Value) bool {
pe := fieldpath.PathElement{FieldName: &key}
tr := t.ElementType
if sf, ok := t.FindField(key); ok {
tr = sf.Type
}
v2 := v.prepareDescent(pe, tr)
v2.value = val
errs = append(errs, v2.toFieldSet()...)
if val.IsNull() || (val.IsMap() && val.AsMap().Length() == 0) {
v2.set.Insert(v2.path)
} else if _, ok := t.FindField(key); !ok {
v2.set.Insert(v2.path)
}
v.finishDescent(v2)
return true
})
return errs
}
func (v *toFieldSetWalker) doMap(t *schema.Map) (errs ValidationErrors) {
m, _ := mapValue(v.allocator, v.value)
if m != nil {
defer v.allocator.Free(m)
}
if t.ElementRelationship == schema.Atomic {
v.set.Insert(v.path)
return nil
}
if m == nil {
return nil
}
errs = v.visitMapItems(t, m)
return errs
}

View File

@ -0,0 +1,321 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package typed
import (
"fmt"
"strings"
"sync"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v4/schema"
"sigs.k8s.io/structured-merge-diff/v4/value"
)
// AsTyped accepts a value and a type and returns a TypedValue. 'v' must have
// type 'typeName' in the schema. An error is returned if the v doesn't conform
// to the schema.
func AsTyped(v value.Value, s *schema.Schema, typeRef schema.TypeRef) (*TypedValue, error) {
tv := &TypedValue{
value: v,
typeRef: typeRef,
schema: s,
}
if err := tv.Validate(); err != nil {
return nil, err
}
return tv, nil
}
// AsTypeUnvalidated is just like AsTyped, but doesn't validate that the type
// conforms to the schema, for cases where that has already been checked or
// where you're going to call a method that validates as a side-effect (like
// ToFieldSet).
func AsTypedUnvalidated(v value.Value, s *schema.Schema, typeRef schema.TypeRef) *TypedValue {
tv := &TypedValue{
value: v,
typeRef: typeRef,
schema: s,
}
return tv
}
// TypedValue is a value of some specific type.
type TypedValue struct {
value value.Value
typeRef schema.TypeRef
schema *schema.Schema
}
// TypeRef is the type of the value.
func (tv TypedValue) TypeRef() schema.TypeRef {
return tv.typeRef
}
// AsValue removes the type from the TypedValue and only keeps the value.
func (tv TypedValue) AsValue() value.Value {
return tv.value
}
// Schema gets the schema from the TypedValue.
func (tv TypedValue) Schema() *schema.Schema {
return tv.schema
}
// Validate returns an error with a list of every spec violation.
func (tv TypedValue) Validate() error {
w := tv.walker()
defer w.finished()
if errs := w.validate(nil); len(errs) != 0 {
return errs
}
return nil
}
// ToFieldSet creates a set containing every leaf field and item mentioned, or
// validation errors, if any were encountered.
func (tv TypedValue) ToFieldSet() (*fieldpath.Set, error) {
w := tv.toFieldSetWalker()
defer w.finished()
if errs := w.toFieldSet(); len(errs) != 0 {
return nil, errs
}
return w.set, nil
}
// Merge returns the result of merging tv and pso ("partially specified
// object") together. Of note:
// * No fields can be removed by this operation.
// * If both tv and pso specify a given leaf field, the result will keep pso's
// value.
// * Container typed elements will have their items ordered:
// * like tv, if pso doesn't change anything in the container
// * like pso, if pso does change something in the container.
// tv and pso must both be of the same type (their Schema and TypeRef must
// match), or an error will be returned. Validation errors will be returned if
// the objects don't conform to the schema.
func (tv TypedValue) Merge(pso *TypedValue) (*TypedValue, error) {
return merge(&tv, pso, ruleKeepRHS, nil)
}
// Compare compares the two objects. See the comments on the `Comparison`
// struct for details on the return value.
//
// tv and rhs must both be of the same type (their Schema and TypeRef must
// match), or an error will be returned. Validation errors will be returned if
// the objects don't conform to the schema.
func (tv TypedValue) Compare(rhs *TypedValue) (c *Comparison, err error) {
c = &Comparison{
Removed: fieldpath.NewSet(),
Modified: fieldpath.NewSet(),
Added: fieldpath.NewSet(),
}
_, err = merge(&tv, rhs, func(w *mergingWalker) {
if w.lhs == nil {
c.Added.Insert(w.path)
} else if w.rhs == nil {
c.Removed.Insert(w.path)
} else if !value.Equals(w.rhs, w.lhs) {
// TODO: Equality is not sufficient for this.
// Need to implement equality check on the value type.
c.Modified.Insert(w.path)
}
}, func(w *mergingWalker) {
if w.lhs == nil {
c.Added.Insert(w.path)
} else if w.rhs == nil {
c.Removed.Insert(w.path)
}
})
if err != nil {
return nil, err
}
return c, nil
}
// RemoveItems removes each provided list or map item from the value.
func (tv TypedValue) RemoveItems(items *fieldpath.Set) *TypedValue {
tv.value = removeItemsWithSchema(tv.value, items, tv.schema, tv.typeRef, false)
return &tv
}
// ExtractItems returns a value with only the provided list or map items extracted from the value.
func (tv TypedValue) ExtractItems(items *fieldpath.Set) *TypedValue {
tv.value = removeItemsWithSchema(tv.value, items, tv.schema, tv.typeRef, true)
return &tv
}
// NormalizeUnions takes the new object and normalizes the union:
// - If discriminator changed to non-nil, and a new field has been added
// that doesn't match, an error is returned,
// - If discriminator hasn't changed and two fields or more are set, an
// error is returned,
// - If discriminator changed to non-nil, all other fields but the
// discriminated one will be cleared,
// - Otherwise, If only one field is left, update discriminator to that value.
//
// Please note: union behavior isn't finalized yet and this is still experimental.
func (tv TypedValue) NormalizeUnions(new *TypedValue) (*TypedValue, error) {
var errs ValidationErrors
var normalizeFn = func(w *mergingWalker) {
if w.rhs != nil {
v := w.rhs.Unstructured()
w.out = &v
}
if err := normalizeUnions(w); err != nil {
errs = append(errs, errorf(err.Error())...)
}
}
out, mergeErrs := merge(&tv, new, func(w *mergingWalker) {}, normalizeFn)
if mergeErrs != nil {
errs = append(errs, mergeErrs.(ValidationErrors)...)
}
if len(errs) > 0 {
return nil, errs
}
return out, nil
}
// NormalizeUnionsApply specifically normalize unions on apply. It
// validates that the applied union is correct (there should be no
// ambiguity there), and clear the fields according to the sent intent.
//
// Please note: union behavior isn't finalized yet and this is still experimental.
func (tv TypedValue) NormalizeUnionsApply(new *TypedValue) (*TypedValue, error) {
var errs ValidationErrors
var normalizeFn = func(w *mergingWalker) {
if w.rhs != nil {
v := w.rhs.Unstructured()
w.out = &v
}
if err := normalizeUnionsApply(w); err != nil {
errs = append(errs, errorf(err.Error())...)
}
}
out, mergeErrs := merge(&tv, new, func(w *mergingWalker) {}, normalizeFn)
if mergeErrs != nil {
errs = append(errs, mergeErrs.(ValidationErrors)...)
}
if len(errs) > 0 {
return nil, errs
}
return out, nil
}
func (tv TypedValue) Empty() *TypedValue {
tv.value = value.NewValueInterface(nil)
return &tv
}
var mwPool = sync.Pool{
New: func() interface{} { return &mergingWalker{} },
}
func merge(lhs, rhs *TypedValue, rule, postRule mergeRule) (*TypedValue, error) {
if lhs.schema != rhs.schema {
return nil, errorf("expected objects with types from the same schema")
}
if !lhs.typeRef.Equals(&rhs.typeRef) {
return nil, errorf("expected objects of the same type, but got %v and %v", lhs.typeRef, rhs.typeRef)
}
mw := mwPool.Get().(*mergingWalker)
defer func() {
mw.lhs = nil
mw.rhs = nil
mw.schema = nil
mw.typeRef = schema.TypeRef{}
mw.rule = nil
mw.postItemHook = nil
mw.out = nil
mw.inLeaf = false
mwPool.Put(mw)
}()
mw.lhs = lhs.value
mw.rhs = rhs.value
mw.schema = lhs.schema
mw.typeRef = lhs.typeRef
mw.rule = rule
mw.postItemHook = postRule
if mw.allocator == nil {
mw.allocator = value.NewFreelistAllocator()
}
errs := mw.merge(nil)
if len(errs) > 0 {
return nil, errs
}
out := &TypedValue{
schema: lhs.schema,
typeRef: lhs.typeRef,
}
if mw.out != nil {
out.value = value.NewValueInterface(*mw.out)
}
return out, nil
}
// Comparison is the return value of a TypedValue.Compare() operation.
//
// No field will appear in more than one of the three fieldsets. If all of the
// fieldsets are empty, then the objects must have been equal.
type Comparison struct {
// Removed contains any fields removed by rhs (the right-hand-side
// object in the comparison).
Removed *fieldpath.Set
// Modified contains fields present in both objects but different.
Modified *fieldpath.Set
// Added contains any fields added by rhs.
Added *fieldpath.Set
}
// IsSame returns true if the comparison returned no changes (the two
// compared objects are similar).
func (c *Comparison) IsSame() bool {
return c.Removed.Empty() && c.Modified.Empty() && c.Added.Empty()
}
// String returns a human readable version of the comparison.
func (c *Comparison) String() string {
bld := strings.Builder{}
if !c.Modified.Empty() {
bld.WriteString(fmt.Sprintf("- Modified Fields:\n%v\n", c.Modified))
}
if !c.Added.Empty() {
bld.WriteString(fmt.Sprintf("- Added Fields:\n%v\n", c.Added))
}
if !c.Removed.Empty() {
bld.WriteString(fmt.Sprintf("- Removed Fields:\n%v\n", c.Removed))
}
return bld.String()
}
// ExcludeFields fields from the compare recursively removes the fields
// from the entire comparison
func (c *Comparison) ExcludeFields(fields *fieldpath.Set) *Comparison {
if fields == nil || fields.Empty() {
return c
}
c.Removed = c.Removed.RecursiveDifference(fields)
c.Modified = c.Modified.RecursiveDifference(fields)
c.Added = c.Added.RecursiveDifference(fields)
return c
}

View File

@ -0,0 +1,276 @@
/*
Copyright 2019 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 typed
import (
"fmt"
"strings"
"sigs.k8s.io/structured-merge-diff/v4/schema"
"sigs.k8s.io/structured-merge-diff/v4/value"
)
func normalizeUnions(w *mergingWalker) error {
atom, found := w.schema.Resolve(w.typeRef)
if !found {
panic(fmt.Sprintf("Unable to resolve schema in normalize union: %v/%v", w.schema, w.typeRef))
}
// Unions can only be in structures, and the struct must not have been removed
if atom.Map == nil || w.out == nil {
return nil
}
var old value.Map
if w.lhs != nil && !w.lhs.IsNull() {
old = w.lhs.AsMap()
}
for _, union := range atom.Map.Unions {
if err := newUnion(&union).Normalize(old, w.rhs.AsMap(), value.NewValueInterface(*w.out).AsMap()); err != nil {
return err
}
}
return nil
}
func normalizeUnionsApply(w *mergingWalker) error {
atom, found := w.schema.Resolve(w.typeRef)
if !found {
panic(fmt.Sprintf("Unable to resolve schema in normalize union: %v/%v", w.schema, w.typeRef))
}
// Unions can only be in structures, and the struct must not have been removed
if atom.Map == nil || w.out == nil {
return nil
}
var old value.Map
if w.lhs != nil && !w.lhs.IsNull() {
old = w.lhs.AsMap()
}
for _, union := range atom.Map.Unions {
out := value.NewValueInterface(*w.out)
if err := newUnion(&union).NormalizeApply(old, w.rhs.AsMap(), out.AsMap()); err != nil {
return err
}
*w.out = out.Unstructured()
}
return nil
}
type discriminated string
type field string
type discriminatedNames struct {
f2d map[field]discriminated
d2f map[discriminated]field
}
func newDiscriminatedName(f2d map[field]discriminated) discriminatedNames {
d2f := map[discriminated]field{}
for key, value := range f2d {
d2f[value] = key
}
return discriminatedNames{
f2d: f2d,
d2f: d2f,
}
}
func (dn discriminatedNames) toField(d discriminated) field {
if f, ok := dn.d2f[d]; ok {
return f
}
return field(d)
}
func (dn discriminatedNames) toDiscriminated(f field) discriminated {
if d, ok := dn.f2d[f]; ok {
return d
}
return discriminated(f)
}
type discriminator struct {
name string
}
func (d *discriminator) Set(m value.Map, v discriminated) {
if d == nil {
return
}
m.Set(d.name, value.NewValueInterface(string(v)))
}
func (d *discriminator) Get(m value.Map) discriminated {
if d == nil || m == nil {
return ""
}
val, ok := m.Get(d.name)
if !ok {
return ""
}
if !val.IsString() {
return ""
}
return discriminated(val.AsString())
}
type fieldsSet map[field]struct{}
// newFieldsSet returns a map of the fields that are part of the union and are set
// in the given map.
func newFieldsSet(m value.Map, fields []field) fieldsSet {
if m == nil {
return nil
}
set := fieldsSet{}
for _, f := range fields {
if subField, ok := m.Get(string(f)); ok && !subField.IsNull() {
set.Add(f)
}
}
return set
}
func (fs fieldsSet) Add(f field) {
if fs == nil {
fs = map[field]struct{}{}
}
fs[f] = struct{}{}
}
func (fs fieldsSet) One() *field {
for f := range fs {
return &f
}
return nil
}
func (fs fieldsSet) Has(f field) bool {
_, ok := fs[f]
return ok
}
func (fs fieldsSet) List() []field {
fields := []field{}
for f := range fs {
fields = append(fields, f)
}
return fields
}
func (fs fieldsSet) Difference(o fieldsSet) fieldsSet {
n := fieldsSet{}
for f := range fs {
if !o.Has(f) {
n.Add(f)
}
}
return n
}
func (fs fieldsSet) String() string {
s := []string{}
for k := range fs {
s = append(s, string(k))
}
return strings.Join(s, ", ")
}
type union struct {
deduceInvalidDiscriminator bool
d *discriminator
dn discriminatedNames
f []field
}
func newUnion(su *schema.Union) *union {
u := &union{}
if su.Discriminator != nil {
u.d = &discriminator{name: *su.Discriminator}
}
f2d := map[field]discriminated{}
for _, f := range su.Fields {
u.f = append(u.f, field(f.FieldName))
f2d[field(f.FieldName)] = discriminated(f.DiscriminatorValue)
}
u.dn = newDiscriminatedName(f2d)
u.deduceInvalidDiscriminator = su.DeduceInvalidDiscriminator
return u
}
// clear removes all the fields in map that are part of the union, but
// the one we decided to keep.
func (u *union) clear(m value.Map, f field) {
for _, fieldName := range u.f {
if field(fieldName) != f {
m.Delete(string(fieldName))
}
}
}
func (u *union) Normalize(old, new, out value.Map) error {
os := newFieldsSet(old, u.f)
ns := newFieldsSet(new, u.f)
diff := ns.Difference(os)
if u.d.Get(old) != u.d.Get(new) && u.d.Get(new) != "" {
if len(diff) == 1 && u.d.Get(new) != u.dn.toDiscriminated(*diff.One()) {
return fmt.Errorf("discriminator (%v) and field changed (%v) don't match", u.d.Get(new), diff.One())
}
if len(diff) > 1 {
return fmt.Errorf("multiple new fields added: %v", diff)
}
u.clear(out, u.dn.toField(u.d.Get(new)))
return nil
}
if len(ns) > 1 {
return fmt.Errorf("multiple fields set without discriminator change: %v", ns)
}
// Set discriminiator if it needs to be deduced.
if u.deduceInvalidDiscriminator && len(ns) == 1 {
u.d.Set(out, u.dn.toDiscriminated(*ns.One()))
}
return nil
}
func (u *union) NormalizeApply(applied, merged, out value.Map) error {
as := newFieldsSet(applied, u.f)
if len(as) > 1 {
return fmt.Errorf("more than one field of union applied: %v", as)
}
if len(as) == 0 {
// None is set, just leave.
return nil
}
// We have exactly one, discriminiator must match if set
if u.d.Get(applied) != "" && u.d.Get(applied) != u.dn.toDiscriminated(*as.One()) {
return fmt.Errorf("applied discriminator (%v) doesn't match applied field (%v)", u.d.Get(applied), *as.One())
}
// Update discriminiator if needed
if u.deduceInvalidDiscriminator {
u.d.Set(out, u.dn.toDiscriminated(*as.One()))
}
// Clear others fields.
u.clear(out, *as.One())
return nil
}

View File

@ -0,0 +1,195 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package typed
import (
"sync"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v4/schema"
"sigs.k8s.io/structured-merge-diff/v4/value"
)
var vPool = sync.Pool{
New: func() interface{} { return &validatingObjectWalker{} },
}
func (tv TypedValue) walker() *validatingObjectWalker {
v := vPool.Get().(*validatingObjectWalker)
v.value = tv.value
v.schema = tv.schema
v.typeRef = tv.typeRef
if v.allocator == nil {
v.allocator = value.NewFreelistAllocator()
}
return v
}
func (v *validatingObjectWalker) finished() {
v.schema = nil
v.typeRef = schema.TypeRef{}
vPool.Put(v)
}
type validatingObjectWalker struct {
value value.Value
schema *schema.Schema
typeRef schema.TypeRef
// Allocate only as many walkers as needed for the depth by storing them here.
spareWalkers *[]*validatingObjectWalker
allocator value.Allocator
}
func (v *validatingObjectWalker) prepareDescent(tr schema.TypeRef) *validatingObjectWalker {
if v.spareWalkers == nil {
// first descent.
v.spareWalkers = &[]*validatingObjectWalker{}
}
var v2 *validatingObjectWalker
if n := len(*v.spareWalkers); n > 0 {
v2, *v.spareWalkers = (*v.spareWalkers)[n-1], (*v.spareWalkers)[:n-1]
} else {
v2 = &validatingObjectWalker{}
}
*v2 = *v
v2.typeRef = tr
return v2
}
func (v *validatingObjectWalker) finishDescent(v2 *validatingObjectWalker) {
// if the descent caused a realloc, ensure that we reuse the buffer
// for the next sibling.
*v.spareWalkers = append(*v.spareWalkers, v2)
}
func (v *validatingObjectWalker) validate(prefixFn func() string) ValidationErrors {
return resolveSchema(v.schema, v.typeRef, v.value, v).WithLazyPrefix(prefixFn)
}
func validateScalar(t *schema.Scalar, v value.Value, prefix string) (errs ValidationErrors) {
if v == nil {
return nil
}
if v.IsNull() {
return nil
}
switch *t {
case schema.Numeric:
if !v.IsFloat() && !v.IsInt() {
// TODO: should the schema separate int and float?
return errorf("%vexpected numeric (int or float), got %T", prefix, v.Unstructured())
}
case schema.String:
if !v.IsString() {
return errorf("%vexpected string, got %#v", prefix, v)
}
case schema.Boolean:
if !v.IsBool() {
return errorf("%vexpected boolean, got %v", prefix, v)
}
}
return nil
}
func (v *validatingObjectWalker) doScalar(t *schema.Scalar) ValidationErrors {
if errs := validateScalar(t, v.value, ""); len(errs) > 0 {
return errs
}
return nil
}
func (v *validatingObjectWalker) visitListItems(t *schema.List, list value.List) (errs ValidationErrors) {
observedKeys := fieldpath.MakePathElementSet(list.Length())
for i := 0; i < list.Length(); i++ {
child := list.AtUsing(v.allocator, i)
defer v.allocator.Free(child)
var pe fieldpath.PathElement
if t.ElementRelationship != schema.Associative {
pe.Index = &i
} else {
var err error
pe, err = listItemToPathElement(v.allocator, v.schema, t, i, child)
if err != nil {
errs = append(errs, errorf("element %v: %v", i, err.Error())...)
// If we can't construct the path element, we can't
// even report errors deeper in the schema, so bail on
// this element.
return
}
if observedKeys.Has(pe) {
errs = append(errs, errorf("duplicate entries for key %v", pe.String())...)
}
observedKeys.Insert(pe)
}
v2 := v.prepareDescent(t.ElementType)
v2.value = child
errs = append(errs, v2.validate(pe.String)...)
v.finishDescent(v2)
}
return errs
}
func (v *validatingObjectWalker) doList(t *schema.List) (errs ValidationErrors) {
list, err := listValue(v.allocator, v.value)
if err != nil {
return errorf(err.Error())
}
if list == nil {
return nil
}
defer v.allocator.Free(list)
errs = v.visitListItems(t, list)
return errs
}
func (v *validatingObjectWalker) visitMapItems(t *schema.Map, m value.Map) (errs ValidationErrors) {
m.IterateUsing(v.allocator, func(key string, val value.Value) bool {
pe := fieldpath.PathElement{FieldName: &key}
tr := t.ElementType
if sf, ok := t.FindField(key); ok {
tr = sf.Type
} else if (t.ElementType == schema.TypeRef{}) {
errs = append(errs, errorf("field not declared in schema").WithPrefix(pe.String())...)
return false
}
v2 := v.prepareDescent(tr)
v2.value = val
// Giving pe.String as a parameter actually increases the allocations.
errs = append(errs, v2.validate(func() string { return pe.String() })...)
v.finishDescent(v2)
return true
})
return errs
}
func (v *validatingObjectWalker) doMap(t *schema.Map) (errs ValidationErrors) {
m, err := mapValue(v.allocator, v.value)
if err != nil {
return errorf(err.Error())
}
if m == nil {
return nil
}
defer v.allocator.Free(m)
errs = v.visitMapItems(t, m)
return errs
}