mirror of https://github.com/k3s-io/k3s
Add structured-merge-diff vendor
This is where most of the logic actually lives.pull/564/head
parent
732cb10019
commit
5869ce6dfe
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
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.
|
|
@ -0,0 +1,31 @@
|
|||
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"doc.go",
|
||||
"element.go",
|
||||
"fromvalue.go",
|
||||
"managers.go",
|
||||
"path.go",
|
||||
"set.go",
|
||||
],
|
||||
importmap = "k8s.io/kubernetes/vendor/sigs.k8s.io/structured-merge-diff/fieldpath",
|
||||
importpath = "sigs.k8s.io/structured-merge-diff/fieldpath",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = ["//vendor/sigs.k8s.io/structured-merge-diff/value:go_default_library"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
|
@ -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
|
|
@ -0,0 +1,184 @@
|
|||
/*
|
||||
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/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.
|
||||
Key []value.Field
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// String presents the path element as a human-readable string.
|
||||
func (e PathElement) String() string {
|
||||
switch {
|
||||
case e.FieldName != nil:
|
||||
return "." + *e.FieldName
|
||||
case len(e.Key) > 0:
|
||||
strs := make([]string, len(e.Key))
|
||||
for i, k := range e.Key {
|
||||
strs[i] = fmt.Sprintf("%v=%v", k.Name, k.Value)
|
||||
}
|
||||
// The order must be canonical, since we use the string value
|
||||
// in a set structure.
|
||||
sort.Strings(strs)
|
||||
return "[" + strings.Join(strs, ",") + "]"
|
||||
case e.Value != nil:
|
||||
return fmt.Sprintf("[=%v]", 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.Field {
|
||||
if len(nameValues)%2 != 0 {
|
||||
panic("must have a value for every name")
|
||||
}
|
||||
out := []value.Field{}
|
||||
for i := 0; i < len(nameValues)-1; i += 2 {
|
||||
out = append(out, value.Field{
|
||||
Name: nameValues[i].(string),
|
||||
Value: nameValues[i+1].(value.Value),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// PathElementSet is a set of path elements.
|
||||
// TODO: serialize as a list.
|
||||
type PathElementSet struct {
|
||||
// The strange construction is because there's no way to test
|
||||
// PathElements for equality (it can't be used as a key for a map).
|
||||
members map[string]PathElement
|
||||
}
|
||||
|
||||
// Insert adds pe to the set.
|
||||
func (s *PathElementSet) Insert(pe PathElement) {
|
||||
serialized := pe.String()
|
||||
if s.members == nil {
|
||||
s.members = map[string]PathElement{
|
||||
serialized: pe,
|
||||
}
|
||||
return
|
||||
}
|
||||
if _, ok := s.members[serialized]; !ok {
|
||||
s.members[serialized] = pe
|
||||
}
|
||||
}
|
||||
|
||||
// Union returns a set containing elements that appear in either s or s2.
|
||||
func (s *PathElementSet) Union(s2 *PathElementSet) *PathElementSet {
|
||||
out := &PathElementSet{
|
||||
members: map[string]PathElement{},
|
||||
}
|
||||
for k, v := range s.members {
|
||||
out.members[k] = v
|
||||
}
|
||||
for k, v := range s2.members {
|
||||
out.members[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Intersection returns a set containing elements which appear in both s and s2.
|
||||
func (s *PathElementSet) Intersection(s2 *PathElementSet) *PathElementSet {
|
||||
out := &PathElementSet{
|
||||
members: map[string]PathElement{},
|
||||
}
|
||||
for k, v := range s.members {
|
||||
if _, ok := s2.members[k]; ok {
|
||||
out.members[k] = v
|
||||
}
|
||||
}
|
||||
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{
|
||||
members: map[string]PathElement{},
|
||||
}
|
||||
for k, v := range s.members {
|
||||
if _, ok := s2.members[k]; !ok {
|
||||
out.members[k] = v
|
||||
}
|
||||
}
|
||||
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 {
|
||||
if s.members == nil {
|
||||
return false
|
||||
}
|
||||
_, ok := s.members[pe.String()]
|
||||
return ok
|
||||
}
|
||||
|
||||
// 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 _, ok := s2.members[k]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Iterate calls f for each PathElement in the set.
|
||||
func (s *PathElementSet) Iterate(f func(PathElement)) {
|
||||
for _, pe := range s.members {
|
||||
f(pe)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
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/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,
|
||||
do: func(p Path) { s.Insert(p) },
|
||||
}
|
||||
|
||||
w.walk()
|
||||
return s
|
||||
}
|
||||
|
||||
type objectWalker struct {
|
||||
path Path
|
||||
value value.Value
|
||||
|
||||
do func(Path)
|
||||
}
|
||||
|
||||
func (w *objectWalker) walk() {
|
||||
switch {
|
||||
case w.value.Null:
|
||||
case w.value.FloatValue != nil:
|
||||
case w.value.IntValue != nil:
|
||||
case w.value.StringValue != nil:
|
||||
case w.value.BooleanValue != nil:
|
||||
// All leaf fields handled the same way (after the switch
|
||||
// statement).
|
||||
|
||||
// Descend
|
||||
case w.value.ListValue != nil:
|
||||
// If the list were atomic, we'd break here, but we don't have
|
||||
// a schema, so we can't tell.
|
||||
|
||||
for i, child := range w.value.ListValue.Items {
|
||||
w2 := *w
|
||||
w2.path = append(w.path, GuessBestListPathElement(i, child))
|
||||
w2.value = child
|
||||
w2.walk()
|
||||
}
|
||||
return
|
||||
case w.value.MapValue != nil:
|
||||
// If the map/struct were atomic, we'd break here, but we don't
|
||||
// have a schema, so we can't tell.
|
||||
|
||||
for i := range w.value.MapValue.Items {
|
||||
child := w.value.MapValue.Items[i]
|
||||
w2 := *w
|
||||
w2.path = append(w.path, PathElement{FieldName: &child.Name})
|
||||
w2.value = child.Value
|
||||
w2.walk()
|
||||
}
|
||||
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 GuessBestListPathElement(index int, item value.Value) PathElement {
|
||||
if item.MapValue == nil {
|
||||
// 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}
|
||||
}
|
||||
|
||||
var keys []value.Field
|
||||
for _, name := range AssociativeListCandidateFieldNames {
|
||||
f, ok := item.MapValue.Get(name)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
// only accept primitive/scalar types as keys.
|
||||
if f.Value.Null || f.Value.MapValue != nil || f.Value.ListValue != nil {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, *f)
|
||||
}
|
||||
if len(keys) > 0 {
|
||||
return PathElement{Key: keys}
|
||||
}
|
||||
return PathElement{Index: &index}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
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
|
||||
|
||||
// APIVersion describes the version of an object or of a fieldset.
|
||||
type APIVersion string
|
||||
|
||||
// VersionedSet associates a version to a set.
|
||||
type VersionedSet struct {
|
||||
*Set
|
||||
APIVersion APIVersion
|
||||
}
|
||||
|
||||
// ManagedFields is a map from manager to VersionedSet (what they own in
|
||||
// what version).
|
||||
type ManagedFields map[string]*VersionedSet
|
||||
|
||||
// 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.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.Difference(right.Set).Union(right.Difference(left.Set))
|
||||
if !newSet.Empty() {
|
||||
diff[manager] = &VersionedSet{
|
||||
Set: newSet,
|
||||
APIVersion: right.APIVersion,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for manager, set := range rhs {
|
||||
if _, ok := lhs[manager]; ok {
|
||||
// Already done
|
||||
continue
|
||||
}
|
||||
if !set.Empty() {
|
||||
diff[manager] = set
|
||||
}
|
||||
}
|
||||
|
||||
return diff
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
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/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, "")
|
||||
}
|
||||
|
||||
// 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.Field:
|
||||
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
|
||||
}
|
|
@ -0,0 +1,305 @@
|
|||
/*
|
||||
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 (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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
|
||||
// * and are not children of elements that 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),
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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 map[string]setNode
|
||||
}
|
||||
|
||||
// Descend adds pe to the set if necessary, returning the associated subset.
|
||||
func (s *SetNodeMap) Descend(pe PathElement) *Set {
|
||||
serialized := pe.String()
|
||||
if s.members == nil {
|
||||
s.members = map[string]setNode{}
|
||||
}
|
||||
if n, ok := s.members[serialized]; ok {
|
||||
return n.set
|
||||
}
|
||||
ss := &Set{}
|
||||
s.members[serialized] = setNode{
|
||||
pathElement: pe,
|
||||
set: ss,
|
||||
}
|
||||
return ss
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if s.members == nil {
|
||||
return nil, false
|
||||
}
|
||||
serialized := pe.String()
|
||||
if n, ok := s.members[serialized]; ok {
|
||||
return n.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 k, v := range s.members {
|
||||
v2, ok := s2.members[k]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if !v.set.Equals(v2.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{}
|
||||
for k, sn := range s.members {
|
||||
pe := sn.pathElement
|
||||
if sn2, ok := s2.members[k]; ok {
|
||||
*out.Descend(pe) = *sn.set.Union(sn2.set)
|
||||
} else {
|
||||
*out.Descend(pe) = *sn.set
|
||||
}
|
||||
}
|
||||
for k, sn2 := range s2.members {
|
||||
pe := sn2.pathElement
|
||||
if _, ok := s.members[k]; ok {
|
||||
// already handled
|
||||
continue
|
||||
}
|
||||
*out.Descend(pe) = *sn2.set
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Intersection returns a SetNodeMap with members that appear in both s and s2.
|
||||
func (s *SetNodeMap) Intersection(s2 *SetNodeMap) *SetNodeMap {
|
||||
out := &SetNodeMap{}
|
||||
for k, sn := range s.members {
|
||||
pe := sn.pathElement
|
||||
if sn2, ok := s2.members[k]; ok {
|
||||
i := *sn.set.Intersection(sn2.set)
|
||||
if !i.Empty() {
|
||||
*out.Descend(pe) = i
|
||||
}
|
||||
}
|
||||
}
|
||||
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{}
|
||||
for k, sn := range s.members {
|
||||
pe := sn.pathElement
|
||||
if s2.Members.Has(pe) {
|
||||
continue
|
||||
}
|
||||
if sn2, ok := s2.Children.members[k]; ok {
|
||||
diff := *sn.set.Difference(sn2.set)
|
||||
// We aren't permitted to add nodes with no elements.
|
||||
if !diff.Empty() {
|
||||
*out.Descend(pe) = diff
|
||||
}
|
||||
} else {
|
||||
*out.Descend(pe) = *sn.set
|
||||
}
|
||||
}
|
||||
return 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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"conflict.go",
|
||||
"update.go",
|
||||
],
|
||||
importmap = "k8s.io/kubernetes/vendor/sigs.k8s.io/structured-merge-diff/merge",
|
||||
importpath = "sigs.k8s.io/structured-merge-diff/merge",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//vendor/sigs.k8s.io/structured-merge-diff/fieldpath:go_default_library",
|
||||
"//vendor/sigs.k8s.io/structured-merge-diff/typed:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
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 merge
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/structured-merge-diff/fieldpath"
|
||||
)
|
||||
|
||||
// Conflict is a conflict on a specific field with the current manager of
|
||||
// that field. It does implement the error interface so that it can be
|
||||
// used as an error.
|
||||
type Conflict struct {
|
||||
Manager string
|
||||
Path fieldpath.Path
|
||||
}
|
||||
|
||||
// Conflict is an error.
|
||||
var _ error = Conflict{}
|
||||
|
||||
// Error formats the conflict as an error.
|
||||
func (c Conflict) Error() string {
|
||||
return fmt.Sprintf("conflict with %q: %v", c.Manager, c.Path)
|
||||
}
|
||||
|
||||
// Conflicts accumulates multiple conflicts and aggregates them by managers.
|
||||
type Conflicts []Conflict
|
||||
|
||||
var _ error = Conflicts{}
|
||||
|
||||
// Error prints the list of conflicts, grouped by sorted managers.
|
||||
func (conflicts Conflicts) Error() string {
|
||||
if len(conflicts) == 1 {
|
||||
return conflicts[0].Error()
|
||||
}
|
||||
|
||||
m := map[string][]fieldpath.Path{}
|
||||
for _, conflict := range conflicts {
|
||||
m[conflict.Manager] = append(m[conflict.Manager], conflict.Path)
|
||||
}
|
||||
|
||||
managers := []string{}
|
||||
for manager := range m {
|
||||
managers = append(managers, manager)
|
||||
}
|
||||
|
||||
// Print conflicts by sorted managers.
|
||||
sort.Strings(managers)
|
||||
|
||||
messages := []string{}
|
||||
for _, manager := range managers {
|
||||
messages = append(messages, fmt.Sprintf("conflicts with %q:", manager))
|
||||
for _, path := range m[manager] {
|
||||
messages = append(messages, fmt.Sprintf("- %v", path))
|
||||
}
|
||||
}
|
||||
return strings.Join(messages, "\n")
|
||||
}
|
||||
|
||||
// ConflictsFromManagers creates a list of conflicts given Managers sets.
|
||||
func ConflictsFromManagers(sets fieldpath.ManagedFields) Conflicts {
|
||||
conflicts := []Conflict{}
|
||||
|
||||
for manager, set := range sets {
|
||||
set.Iterate(func(p fieldpath.Path) {
|
||||
conflicts = append(conflicts, Conflict{
|
||||
Manager: manager,
|
||||
Path: p,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return conflicts
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
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 merge
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"sigs.k8s.io/structured-merge-diff/fieldpath"
|
||||
"sigs.k8s.io/structured-merge-diff/typed"
|
||||
)
|
||||
|
||||
// Converter is an interface to the conversion logic. The converter
|
||||
// needs to be able to convert objects from one version to another.
|
||||
type Converter interface {
|
||||
Convert(object typed.TypedValue, version fieldpath.APIVersion) (typed.TypedValue, error)
|
||||
}
|
||||
|
||||
// Updater is the object used to compute updated FieldSets and also
|
||||
// merge the object on Apply.
|
||||
type Updater struct {
|
||||
Converter Converter
|
||||
}
|
||||
|
||||
func (s *Updater) update(oldObject, newObject typed.TypedValue, version fieldpath.APIVersion, managers fieldpath.ManagedFields, workflow string, force bool) (fieldpath.ManagedFields, error) {
|
||||
if managers == nil {
|
||||
managers = fieldpath.ManagedFields{}
|
||||
}
|
||||
conflicts := fieldpath.ManagedFields{}
|
||||
type Versioned struct {
|
||||
oldObject typed.TypedValue
|
||||
newObject typed.TypedValue
|
||||
}
|
||||
versions := map[fieldpath.APIVersion]Versioned{
|
||||
version: Versioned{
|
||||
oldObject: oldObject,
|
||||
newObject: newObject,
|
||||
},
|
||||
}
|
||||
|
||||
for manager, managerSet := range managers {
|
||||
if manager == workflow {
|
||||
continue
|
||||
}
|
||||
versioned, ok := versions[managerSet.APIVersion]
|
||||
if !ok {
|
||||
var err error
|
||||
versioned.oldObject, err = s.Converter.Convert(oldObject, managerSet.APIVersion)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert old object: %v", err)
|
||||
}
|
||||
versioned.newObject, err = s.Converter.Convert(newObject, managerSet.APIVersion)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert new object: %v", err)
|
||||
}
|
||||
versions[managerSet.APIVersion] = versioned
|
||||
}
|
||||
compare, err := versioned.oldObject.Compare(versioned.newObject)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to compare objects: %v", err)
|
||||
}
|
||||
|
||||
conflictSet := managerSet.Intersection(compare.Modified.Union(compare.Added))
|
||||
if !conflictSet.Empty() {
|
||||
conflicts[manager] = &fieldpath.VersionedSet{
|
||||
Set: conflictSet,
|
||||
APIVersion: managerSet.APIVersion,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !force && len(conflicts) != 0 {
|
||||
return nil, ConflictsFromManagers(conflicts)
|
||||
}
|
||||
|
||||
for manager, conflictSet := range conflicts {
|
||||
managers[manager].Set = managers[manager].Set.Difference(conflictSet.Set)
|
||||
}
|
||||
|
||||
return managers, nil
|
||||
}
|
||||
|
||||
// Update is the method you should call once you've merged your final
|
||||
// object on CREATE/UPDATE/PATCH verbs. newObject must be the object
|
||||
// that you intend to persist (after applying the patch if this is for a
|
||||
// PATCH call), and liveObject must be the original object (empty if
|
||||
// this is a CREATE call).
|
||||
func (s *Updater) Update(liveObject, newObject typed.TypedValue, version fieldpath.APIVersion, managers fieldpath.ManagedFields, manager string) (fieldpath.ManagedFields, error) {
|
||||
var err error
|
||||
managers, err = s.update(liveObject, newObject, version, managers, manager, true)
|
||||
if err != nil {
|
||||
return fieldpath.ManagedFields{}, err
|
||||
}
|
||||
compare, err := liveObject.Compare(newObject)
|
||||
if err != nil {
|
||||
return fieldpath.ManagedFields{}, fmt.Errorf("failed to compare live and new objects: %v", err)
|
||||
}
|
||||
if _, ok := managers[manager]; !ok {
|
||||
managers[manager] = &fieldpath.VersionedSet{
|
||||
Set: fieldpath.NewSet(),
|
||||
}
|
||||
}
|
||||
managers[manager].Set = managers[manager].Set.Union(compare.Modified).Union(compare.Added).Difference(compare.Removed)
|
||||
managers[manager].APIVersion = version
|
||||
return managers, nil
|
||||
}
|
||||
|
||||
// Apply should be called when Apply is run, given the current object as
|
||||
// well as the configuration that is applied. This will merge the object
|
||||
// and return it.
|
||||
func (s *Updater) Apply(liveObject, configObject typed.TypedValue, version fieldpath.APIVersion, managers fieldpath.ManagedFields, manager string, force bool) (typed.TypedValue, fieldpath.ManagedFields, error) {
|
||||
newObject, err := liveObject.Merge(configObject)
|
||||
if err != nil {
|
||||
return typed.TypedValue{}, fieldpath.ManagedFields{}, fmt.Errorf("failed to merge config: %v", err)
|
||||
}
|
||||
managers, err = s.update(liveObject, newObject, version, managers, manager, force)
|
||||
if err != nil {
|
||||
return typed.TypedValue{}, fieldpath.ManagedFields{}, err
|
||||
}
|
||||
|
||||
// TODO: Remove unconflicting removed fields
|
||||
|
||||
set, err := configObject.ToFieldSet()
|
||||
if err != nil {
|
||||
return typed.TypedValue{}, fieldpath.ManagedFields{}, fmt.Errorf("failed to get field set: %v", err)
|
||||
}
|
||||
managers[manager] = &fieldpath.VersionedSet{
|
||||
Set: set,
|
||||
APIVersion: version,
|
||||
}
|
||||
return newObject, managers, nil
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"doc.go",
|
||||
"elements.go",
|
||||
"schemaschema.go",
|
||||
],
|
||||
importmap = "k8s.io/kubernetes/vendor/sigs.k8s.io/structured-merge-diff/schema",
|
||||
importpath = "sigs.k8s.io/structured-merge-diff/schema",
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
|
@ -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
|
|
@ -0,0 +1,219 @@
|
|||
/*
|
||||
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
|
||||
|
||||
// Schema is a list of named types.
|
||||
type Schema struct {
|
||||
Types []TypeDef `yaml:"types,omitempty"`
|
||||
}
|
||||
|
||||
// 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.
|
||||
type Atom struct {
|
||||
// Exactly one of the below must be set.
|
||||
*Scalar `yaml:"scalar,omitempty"`
|
||||
*Struct `yaml:"struct,omitempty"`
|
||||
*List `yaml:"list,omitempty"`
|
||||
*Map `yaml:"map,omitempty"`
|
||||
*Untyped `yaml:"untyped,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, structs, untyped).
|
||||
type ElementRelationship string
|
||||
|
||||
const (
|
||||
// Associative only applies to lists (see the documentation there).
|
||||
Associative = ElementRelationship("associative")
|
||||
// Atomic makes container types (lists, maps, structs, untyped) behave
|
||||
// as scalars / leaf fields (which is the default for untyped data).
|
||||
Atomic = ElementRelationship("atomic")
|
||||
// Separable means the items of the container type have no particular
|
||||
// relationship (default behavior for maps and structs).
|
||||
Separable = ElementRelationship("separable")
|
||||
)
|
||||
|
||||
// Struct represents a type which is composed of a number of different fields.
|
||||
// Each field has a name and a type.
|
||||
//
|
||||
// TODO: in the future, we will add one-of groups (sometimes called unions).
|
||||
type Struct 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"`
|
||||
|
||||
// TODO: Implement unions, either this way or by inlining.
|
||||
// Unions are groupings of fields with special rules. They 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.
|
||||
// Unions []Union `yaml:"unions,omitempty"`
|
||||
|
||||
// ElementRelationship states the relationship between the struct'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 structs is `separable`; it's permitted to
|
||||
// leave this unset to get the default behavior.
|
||||
ElementRelationship ElementRelationship `yaml:"elementRelationship,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"`
|
||||
}
|
||||
|
||||
// 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 struct, the list is treated as a map.
|
||||
// - The list element must not be a map or a list itself.
|
||||
// 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
|
||||
// struct, then Keys must have non-zero length, and it lists the fields
|
||||
// of the element's struct 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"`
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// Although serialized the same, maps are different from structs in that each
|
||||
// map item must have the same type.
|
||||
//
|
||||
// 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).
|
||||
type Map struct {
|
||||
// ElementType is the type of the list's elements.
|
||||
ElementType TypeRef `yaml:"elementType,omitempty"`
|
||||
|
||||
// ElementRelationship states the relationship between the map's items.
|
||||
// * `separable` 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.
|
||||
// TODO: find a simple example.
|
||||
// The default behavior for maps is `separable`; it's permitted to
|
||||
// leave this unset to get the default behavior.
|
||||
ElementRelationship ElementRelationship `yaml:"elementRelationship,omitempty"`
|
||||
}
|
||||
|
||||
// Untyped represents types that allow arbitrary content. (Think: plugin
|
||||
// objects.)
|
||||
type Untyped struct {
|
||||
// ElementRelationship states the relationship between the items, if
|
||||
// container-typed data happens to be present here.
|
||||
// * `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.
|
||||
// TODO: support "guess" (guesses at associative list keys)
|
||||
// TODO: support "lookup" (calls a lookup function to figure out the
|
||||
// schema based on the data)
|
||||
// The default behavior for untyped data is `atomic`; it's permitted to
|
||||
// leave this unset to get the default behavior.
|
||||
ElementRelationship ElementRelationship `yaml:"elementRelationship,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) {
|
||||
for _, t := range s.Types {
|
||||
if t.Name == name {
|
||||
return t, true
|
||||
}
|
||||
}
|
||||
return TypeDef{}, false
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package 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
|
||||
struct:
|
||||
fields:
|
||||
- name: types
|
||||
type:
|
||||
list:
|
||||
elementRelationship: associative
|
||||
elementType:
|
||||
namedType: typeDef
|
||||
keys:
|
||||
- name
|
||||
- name: typeDef
|
||||
struct:
|
||||
fields:
|
||||
- name: name
|
||||
type:
|
||||
scalar: string
|
||||
- name: scalar
|
||||
type:
|
||||
scalar: string
|
||||
- name: struct
|
||||
type:
|
||||
namedType: struct
|
||||
- name: list
|
||||
type:
|
||||
namedType: list
|
||||
- name: map
|
||||
type:
|
||||
namedType: map
|
||||
- name: untyped
|
||||
type:
|
||||
namedType: untyped
|
||||
- name: typeRef
|
||||
struct:
|
||||
fields:
|
||||
- name: namedType
|
||||
type:
|
||||
scalar: string
|
||||
- name: scalar
|
||||
type:
|
||||
scalar: string
|
||||
- name: struct
|
||||
type:
|
||||
namedType: struct
|
||||
- name: list
|
||||
type:
|
||||
namedType: list
|
||||
- name: map
|
||||
type:
|
||||
namedType: map
|
||||
- name: untyped
|
||||
type:
|
||||
namedType: untyped
|
||||
- name: scalar
|
||||
scalar: string
|
||||
- name: struct
|
||||
struct:
|
||||
fields:
|
||||
- name: fields
|
||||
type:
|
||||
list:
|
||||
elementType:
|
||||
namedType: structField
|
||||
elementRelationship: associative
|
||||
keys: [ "name" ]
|
||||
- name: elementRelationship
|
||||
type:
|
||||
scalar: string
|
||||
- name: structField
|
||||
struct:
|
||||
fields:
|
||||
- name: name
|
||||
type:
|
||||
scalar: string
|
||||
- name: type
|
||||
type:
|
||||
namedType: typeRef
|
||||
- name: list
|
||||
struct:
|
||||
fields:
|
||||
- name: elementType
|
||||
type:
|
||||
namedType: typeRef
|
||||
- name: elementRelationship
|
||||
type:
|
||||
scalar: string
|
||||
- name: keys
|
||||
type:
|
||||
list:
|
||||
elementType:
|
||||
scalar: string
|
||||
- name: map
|
||||
struct:
|
||||
fields:
|
||||
- name: elementType
|
||||
type:
|
||||
namedType: typeRef
|
||||
- name: elementRelationship
|
||||
type:
|
||||
scalar: string
|
||||
- name: untyped
|
||||
struct:
|
||||
fields:
|
||||
- name: elementRelationship
|
||||
type:
|
||||
scalar: string
|
||||
`
|
|
@ -0,0 +1,36 @@
|
|||
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"doc.go",
|
||||
"helpers.go",
|
||||
"merge.go",
|
||||
"parser.go",
|
||||
"typed.go",
|
||||
"validate.go",
|
||||
],
|
||||
importmap = "k8s.io/kubernetes/vendor/sigs.k8s.io/structured-merge-diff/typed",
|
||||
importpath = "sigs.k8s.io/structured-merge-diff/typed",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//vendor/gopkg.in/yaml.v2:go_default_library",
|
||||
"//vendor/sigs.k8s.io/structured-merge-diff/fieldpath:go_default_library",
|
||||
"//vendor/sigs.k8s.io/structured-merge-diff/schema:go_default_library",
|
||||
"//vendor/sigs.k8s.io/structured-merge-diff/value:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
|
@ -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
|
|
@ -0,0 +1,246 @@
|
|||
/*
|
||||
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/fieldpath"
|
||||
"sigs.k8s.io/structured-merge-diff/schema"
|
||||
"sigs.k8s.io/structured-merge-diff/value"
|
||||
)
|
||||
|
||||
// ValidationError reports an error about a particular field
|
||||
type ValidationError struct {
|
||||
Path fieldpath.Path
|
||||
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")
|
||||
}
|
||||
|
||||
// errorFormatter makes it easy to keep a list of validation errors. They
|
||||
// should all be packed into a single error object before leaving the package
|
||||
// boundary, since it's weird to have functions not return a plain error type.
|
||||
type errorFormatter struct {
|
||||
path fieldpath.Path
|
||||
}
|
||||
|
||||
func (ef *errorFormatter) descend(pe fieldpath.PathElement) {
|
||||
ef.path = append(ef.path, pe)
|
||||
}
|
||||
|
||||
func (ef errorFormatter) errorf(format string, args ...interface{}) ValidationErrors {
|
||||
return ValidationErrors{{
|
||||
Path: append(fieldpath.Path{}, ef.path...),
|
||||
ErrorMessage: fmt.Sprintf(format, args...),
|
||||
}}
|
||||
}
|
||||
|
||||
func (ef errorFormatter) error(err error) ValidationErrors {
|
||||
return ValidationErrors{{
|
||||
Path: append(fieldpath.Path{}, ef.path...),
|
||||
ErrorMessage: err.Error(),
|
||||
}}
|
||||
}
|
||||
|
||||
func (ef errorFormatter) prefixError(prefix string, err error) ValidationErrors {
|
||||
return ValidationErrors{{
|
||||
Path: append(fieldpath.Path{}, ef.path...),
|
||||
ErrorMessage: prefix + err.Error(),
|
||||
}}
|
||||
}
|
||||
|
||||
type atomHandler interface {
|
||||
doScalar(schema.Scalar) ValidationErrors
|
||||
doStruct(schema.Struct) ValidationErrors
|
||||
doList(schema.List) ValidationErrors
|
||||
doMap(schema.Map) ValidationErrors
|
||||
doUntyped(schema.Untyped) ValidationErrors
|
||||
|
||||
errorf(msg string, args ...interface{}) ValidationErrors
|
||||
}
|
||||
|
||||
func resolveSchema(s *schema.Schema, tr schema.TypeRef, ah atomHandler) ValidationErrors {
|
||||
a, ok := s.Resolve(tr)
|
||||
if !ok {
|
||||
return ah.errorf("schema error: no type found matching: %v", *tr.NamedType)
|
||||
}
|
||||
|
||||
switch {
|
||||
case a.Scalar != nil:
|
||||
return ah.doScalar(*a.Scalar)
|
||||
case a.Struct != nil:
|
||||
return ah.doStruct(*a.Struct)
|
||||
case a.List != nil:
|
||||
return ah.doList(*a.List)
|
||||
case a.Map != nil:
|
||||
return ah.doMap(*a.Map)
|
||||
case a.Untyped != nil:
|
||||
return ah.doUntyped(*a.Untyped)
|
||||
}
|
||||
|
||||
name := "inlined"
|
||||
if tr.NamedType != nil {
|
||||
name = "named type: " + *tr.NamedType
|
||||
}
|
||||
|
||||
return ah.errorf("schema error: invalid atom: %v", name)
|
||||
}
|
||||
|
||||
func (ef errorFormatter) validateScalar(t schema.Scalar, v *value.Value, prefix string) (errs ValidationErrors) {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
switch t {
|
||||
case schema.Numeric:
|
||||
if v.FloatValue == nil && v.IntValue == nil {
|
||||
// TODO: should the schema separate int and float?
|
||||
return ef.errorf("%vexpected numeric (int or float), got %v", prefix, v)
|
||||
}
|
||||
case schema.String:
|
||||
if v.StringValue == nil {
|
||||
return ef.errorf("%vexpected string, got %v", prefix, v)
|
||||
}
|
||||
case schema.Boolean:
|
||||
if v.BooleanValue == nil {
|
||||
return ef.errorf("%vexpected boolean, got %v", prefix, v)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Returns the list, or an error. Reminder: nil is a valid list and might be returned.
|
||||
func listValue(val value.Value) (*value.List, error) {
|
||||
switch {
|
||||
case val.Null:
|
||||
// Null is a valid list.
|
||||
return nil, nil
|
||||
case val.ListValue != nil:
|
||||
return val.ListValue, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("expected list, got %v", val)
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the map, or an error. Reminder: nil is a valid map and might be returned.
|
||||
func mapOrStructValue(val value.Value, typeName string) (*value.Map, error) {
|
||||
switch {
|
||||
case val.Null:
|
||||
return nil, nil
|
||||
case val.MapValue != nil:
|
||||
return val.MapValue, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("expected %v, got %v", typeName, val)
|
||||
}
|
||||
}
|
||||
|
||||
func (ef errorFormatter) rejectExtraStructFields(m *value.Map, allowedNames map[string]struct{}, prefix string) (errs ValidationErrors) {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
for _, f := range m.Items {
|
||||
if _, allowed := allowedNames[f.Name]; !allowed {
|
||||
errs = append(errs, ef.errorf("%vfield %v is not mentioned in the schema", prefix, f.Name)...)
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func keyedAssociativeListItemToPathElement(list schema.List, index int, child value.Value) (fieldpath.PathElement, error) {
|
||||
pe := fieldpath.PathElement{}
|
||||
if child.Null {
|
||||
// For now, the keys are required which means that null entries
|
||||
// are illegal.
|
||||
return pe, errors.New("associative list with keys may not have a null element")
|
||||
}
|
||||
if child.MapValue == nil {
|
||||
return pe, errors.New("associative list with keys may not have non-map elements")
|
||||
}
|
||||
for _, fieldName := range list.Keys {
|
||||
var fieldValue value.Value
|
||||
field, ok := child.MapValue.Get(fieldName)
|
||||
if ok {
|
||||
fieldValue = field.Value
|
||||
} else {
|
||||
// Treat keys as required.
|
||||
return pe, fmt.Errorf("associative list with keys has an element that omits key field %q", fieldName)
|
||||
}
|
||||
pe.Key = append(pe.Key, value.Field{
|
||||
Name: fieldName,
|
||||
Value: fieldValue,
|
||||
})
|
||||
}
|
||||
return pe, nil
|
||||
}
|
||||
|
||||
func setItemToPathElement(list schema.List, index int, child value.Value) (fieldpath.PathElement, error) {
|
||||
pe := fieldpath.PathElement{}
|
||||
switch {
|
||||
case child.MapValue != nil:
|
||||
// TODO: atomic maps should be acceptable.
|
||||
return pe, errors.New("associative list without keys has an element that's a map type")
|
||||
case child.ListValue != nil:
|
||||
// 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.Null:
|
||||
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(list schema.List, index int, child value.Value) (fieldpath.PathElement, error) {
|
||||
if list.ElementRelationship == schema.Associative {
|
||||
if len(list.Keys) > 0 {
|
||||
return keyedAssociativeListItemToPathElement(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
|
||||
}
|
|
@ -0,0 +1,410 @@
|
|||
/*
|
||||
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 (
|
||||
"sigs.k8s.io/structured-merge-diff/fieldpath"
|
||||
"sigs.k8s.io/structured-merge-diff/schema"
|
||||
"sigs.k8s.io/structured-merge-diff/value"
|
||||
)
|
||||
|
||||
type mergingWalker struct {
|
||||
errorFormatter
|
||||
lhs *value.Value
|
||||
rhs *value.Value
|
||||
schema *schema.Schema
|
||||
typeRef schema.TypeRef
|
||||
|
||||
// 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 *value.Value
|
||||
|
||||
// internal housekeeping--don't set when constructing.
|
||||
inLeaf bool // Set to true if we're in a "big leaf"--atomic map/list
|
||||
}
|
||||
|
||||
// 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
|
||||
w.out = &v
|
||||
} else if w.lhs != nil {
|
||||
v := *w.lhs
|
||||
w.out = &v
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
// merge sets w.out.
|
||||
func (w *mergingWalker) merge() ValidationErrors {
|
||||
if w.lhs == nil && w.rhs == nil {
|
||||
// check this condidition here instead of everywhere below.
|
||||
return w.errorf("at least one of lhs and rhs must be provided")
|
||||
}
|
||||
errs := resolveSchema(w.schema, w.typeRef, w)
|
||||
if !w.inLeaf && w.postItemHook != nil {
|
||||
w.postItemHook(w)
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// 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, w.validateScalar(t, w.lhs, "lhs: ")...)
|
||||
errs = append(errs, w.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 {
|
||||
w2 := *w
|
||||
w2.typeRef = tr
|
||||
w2.errorFormatter.descend(pe)
|
||||
w2.lhs = nil
|
||||
w2.rhs = nil
|
||||
w2.out = nil
|
||||
return &w2
|
||||
}
|
||||
|
||||
func (w *mergingWalker) visitStructFields(t schema.Struct, lhs, rhs *value.Map) (errs ValidationErrors) {
|
||||
out := &value.Map{}
|
||||
|
||||
valOrNil := func(m *value.Map, name string) *value.Value {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
val, ok := m.Get(name)
|
||||
if ok {
|
||||
return &val.Value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
allowedNames := map[string]struct{}{}
|
||||
for i := range t.Fields {
|
||||
// I don't want to use the loop variable since a reference
|
||||
// might outlive the loop iteration (in an error message).
|
||||
f := t.Fields[i]
|
||||
allowedNames[f.Name] = struct{}{}
|
||||
w2 := w.prepareDescent(fieldpath.PathElement{FieldName: &f.Name}, f.Type)
|
||||
w2.lhs = valOrNil(lhs, f.Name)
|
||||
w2.rhs = valOrNil(rhs, f.Name)
|
||||
if w2.lhs == nil && w2.rhs == nil {
|
||||
// All fields are optional
|
||||
continue
|
||||
}
|
||||
if newErrs := w2.merge(); len(newErrs) > 0 {
|
||||
errs = append(errs, newErrs...)
|
||||
} else if w2.out != nil {
|
||||
out.Set(f.Name, *w2.out)
|
||||
}
|
||||
}
|
||||
|
||||
// All fields may be optional, but unknown fields are not allowed.
|
||||
errs = append(errs, w.rejectExtraStructFields(lhs, allowedNames, "lhs: ")...)
|
||||
errs = append(errs, w.rejectExtraStructFields(rhs, allowedNames, "rhs: ")...)
|
||||
if len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
if len(out.Items) > 0 {
|
||||
w.out = &value.Value{MapValue: out}
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func (w *mergingWalker) derefMapOrStruct(prefix, typeName string, v *value.Value, dest **value.Map) (errs ValidationErrors) {
|
||||
// taking dest as input so that it can be called as a one-liner with
|
||||
// append.
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
m, err := mapOrStructValue(*v, typeName)
|
||||
if err != nil {
|
||||
return w.prefixError(prefix, err)
|
||||
}
|
||||
*dest = m
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *mergingWalker) doStruct(t schema.Struct) (errs ValidationErrors) {
|
||||
var lhs, rhs *value.Map
|
||||
errs = append(errs, w.derefMapOrStruct("lhs: ", "struct", w.lhs, &lhs)...)
|
||||
errs = append(errs, w.derefMapOrStruct("rhs: ", "struct", w.rhs, &rhs)...)
|
||||
if len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
// If both lhs and rhs are empty/null, treat it as a
|
||||
// leaf: this helps preserve the empty/null
|
||||
// distinction.
|
||||
emptyPromoteToLeaf := (lhs == nil || len(lhs.Items) == 0) &&
|
||||
(rhs == nil || len(rhs.Items) == 0)
|
||||
|
||||
if t.ElementRelationship == schema.Atomic || emptyPromoteToLeaf {
|
||||
w.doLeaf()
|
||||
return nil
|
||||
}
|
||||
|
||||
if lhs == nil && rhs == nil {
|
||||
// nil is a valid map!
|
||||
return nil
|
||||
}
|
||||
|
||||
errs = w.visitStructFields(t, lhs, rhs)
|
||||
|
||||
// TODO: Check unions.
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func (w *mergingWalker) visitListItems(t schema.List, lhs, rhs *value.List) (errs ValidationErrors) {
|
||||
out := &value.List{}
|
||||
|
||||
// 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 := []fieldpath.PathElement{}
|
||||
|
||||
// First, collect all RHS children.
|
||||
observedRHS := map[string]value.Value{}
|
||||
if rhs != nil {
|
||||
for i, child := range rhs.Items {
|
||||
pe, err := listItemToPathElement(t, i, child)
|
||||
if err != nil {
|
||||
errs = append(errs, w.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
|
||||
}
|
||||
keyStr := pe.String()
|
||||
if _, found := observedRHS[keyStr]; found {
|
||||
errs = append(errs, w.errorf("rhs: duplicate entries for key %v", keyStr)...)
|
||||
}
|
||||
observedRHS[keyStr] = child
|
||||
rhsOrder = append(rhsOrder, pe)
|
||||
}
|
||||
}
|
||||
|
||||
// Then merge with LHS children.
|
||||
observedLHS := map[string]struct{}{}
|
||||
if lhs != nil {
|
||||
for i, child := range lhs.Items {
|
||||
pe, err := listItemToPathElement(t, i, child)
|
||||
if err != nil {
|
||||
errs = append(errs, w.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
|
||||
}
|
||||
keyStr := pe.String()
|
||||
if _, found := observedLHS[keyStr]; found {
|
||||
errs = append(errs, w.errorf("lhs: duplicate entries for key %v", keyStr)...)
|
||||
continue
|
||||
}
|
||||
observedLHS[keyStr] = struct{}{}
|
||||
w2 := w.prepareDescent(pe, t.ElementType)
|
||||
w2.lhs = &child
|
||||
if rchild, ok := observedRHS[keyStr]; ok {
|
||||
w2.rhs = &rchild
|
||||
}
|
||||
if newErrs := w2.merge(); len(newErrs) > 0 {
|
||||
errs = append(errs, newErrs...)
|
||||
} else if w2.out != nil {
|
||||
out.Items = append(out.Items, *w2.out)
|
||||
}
|
||||
// Keep track of children that have been handled
|
||||
delete(observedRHS, keyStr)
|
||||
}
|
||||
}
|
||||
|
||||
for _, rhsToCheck := range rhsOrder {
|
||||
if unmergedChild, ok := observedRHS[rhsToCheck.String()]; ok {
|
||||
w2 := w.prepareDescent(rhsToCheck, t.ElementType)
|
||||
w2.rhs = &unmergedChild
|
||||
if newErrs := w2.merge(); len(newErrs) > 0 {
|
||||
errs = append(errs, newErrs...)
|
||||
} else if w2.out != nil {
|
||||
out.Items = append(out.Items, *w2.out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(out.Items) > 0 {
|
||||
w.out = &value.Value{ListValue: out}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func (w *mergingWalker) derefList(prefix string, v *value.Value, dest **value.List) (errs ValidationErrors) {
|
||||
// taking dest as input so that it can be called as a one-liner with
|
||||
// append.
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
l, err := listValue(*v)
|
||||
if err != nil {
|
||||
return w.prefixError(prefix, err)
|
||||
}
|
||||
*dest = l
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *mergingWalker) doList(t schema.List) (errs ValidationErrors) {
|
||||
var lhs, rhs *value.List
|
||||
errs = append(errs, w.derefList("lhs: ", w.lhs, &lhs)...)
|
||||
errs = append(errs, w.derefList("rhs: ", w.rhs, &rhs)...)
|
||||
if len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
// If both lhs and rhs are empty/null, treat it as a
|
||||
// leaf: this helps preserve the empty/null
|
||||
// distinction.
|
||||
emptyPromoteToLeaf := (lhs == nil || len(lhs.Items) == 0) &&
|
||||
(rhs == nil || len(rhs.Items) == 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) visitMapItems(t schema.Map, lhs, rhs *value.Map) (errs ValidationErrors) {
|
||||
out := &value.Map{}
|
||||
|
||||
if lhs != nil {
|
||||
for _, litem := range lhs.Items {
|
||||
name := litem.Name
|
||||
w2 := w.prepareDescent(fieldpath.PathElement{FieldName: &name}, t.ElementType)
|
||||
w2.lhs = &litem.Value
|
||||
if rhs != nil {
|
||||
if ritem, ok := rhs.Get(litem.Name); ok {
|
||||
w2.rhs = &ritem.Value
|
||||
}
|
||||
}
|
||||
if newErrs := w2.merge(); len(newErrs) > 0 {
|
||||
errs = append(errs, newErrs...)
|
||||
} else if w2.out != nil {
|
||||
out.Set(name, *w2.out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if rhs != nil {
|
||||
for _, ritem := range rhs.Items {
|
||||
if lhs != nil {
|
||||
if _, ok := lhs.Get(ritem.Name); ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
name := ritem.Name
|
||||
w2 := w.prepareDescent(fieldpath.PathElement{FieldName: &name}, t.ElementType)
|
||||
w2.rhs = &ritem.Value
|
||||
if newErrs := w2.merge(); len(newErrs) > 0 {
|
||||
errs = append(errs, newErrs...)
|
||||
} else if w2.out != nil {
|
||||
out.Set(name, *w2.out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(out.Items) > 0 {
|
||||
w.out = &value.Value{MapValue: out}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func (w *mergingWalker) doMap(t schema.Map) (errs ValidationErrors) {
|
||||
var lhs, rhs *value.Map
|
||||
errs = append(errs, w.derefMapOrStruct("lhs: ", "map", w.lhs, &lhs)...)
|
||||
errs = append(errs, w.derefMapOrStruct("rhs: ", "map", w.rhs, &rhs)...)
|
||||
if len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
// If both lhs and rhs are empty/null, treat it as a
|
||||
// leaf: this helps preserve the empty/null
|
||||
// distinction.
|
||||
emptyPromoteToLeaf := (lhs == nil || len(lhs.Items) == 0) &&
|
||||
(rhs == nil || len(rhs.Items) == 0)
|
||||
|
||||
if t.ElementRelationship == schema.Atomic || emptyPromoteToLeaf {
|
||||
w.doLeaf()
|
||||
return nil
|
||||
}
|
||||
|
||||
if lhs == nil && rhs == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
errs = w.visitMapItems(t, lhs, rhs)
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func (w *mergingWalker) doUntyped(t schema.Untyped) (errs ValidationErrors) {
|
||||
if t.ElementRelationship == "" || t.ElementRelationship == schema.Atomic {
|
||||
// Untyped sections allow anything, and are considered leaf
|
||||
// fields.
|
||||
w.doLeaf()
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
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/schema"
|
||||
"sigs.k8s.io/structured-merge-diff/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(schema YAMLObject) (*Parser, error) {
|
||||
p := Parser{}
|
||||
err := yaml.Unmarshal([]byte(schema), &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)
|
||||
}
|
||||
return create(schema)
|
||||
}
|
||||
|
||||
// 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{
|
||||
parser: p,
|
||||
typename: name,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseableType allows for easy production of typed objects.
|
||||
type ParseableType struct {
|
||||
parser *Parser
|
||||
typename string
|
||||
}
|
||||
|
||||
// IsValid return true if p's schema and typename are valid.
|
||||
func (p *ParseableType) IsValid() bool {
|
||||
_, ok := p.parser.Schema.Resolve(schema.TypeRef{NamedType: &p.typename})
|
||||
return ok
|
||||
}
|
||||
|
||||
// New returns a new empty object with the current schema and the
|
||||
// type "typename".
|
||||
func (p *ParseableType) New() (TypedValue, error) {
|
||||
return p.FromYAML(YAMLObject("{}"))
|
||||
}
|
||||
|
||||
// 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) {
|
||||
v, err := value.FromYAML([]byte(object))
|
||||
if err != nil {
|
||||
return TypedValue{}, err
|
||||
}
|
||||
return AsTyped(v, &p.parser.Schema, p.typename)
|
||||
}
|
||||
|
||||
// FromUnstructured converts a go interface to a TypedValue. It will return an
|
||||
// error if the resulting object fails schema validation.
|
||||
func (p *ParseableType) FromUnstructured(in interface{}) (TypedValue, error) {
|
||||
v, err := value.FromUnstructured(in)
|
||||
if err != nil {
|
||||
return TypedValue{}, err
|
||||
}
|
||||
return AsTyped(v, &p.parser.Schema, p.typename)
|
||||
}
|
|
@ -0,0 +1,213 @@
|
|||
/*
|
||||
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"
|
||||
"reflect"
|
||||
|
||||
"sigs.k8s.io/structured-merge-diff/fieldpath"
|
||||
"sigs.k8s.io/structured-merge-diff/schema"
|
||||
"sigs.k8s.io/structured-merge-diff/value"
|
||||
)
|
||||
|
||||
// TypedValue is a value of some specific type.
|
||||
type TypedValue struct {
|
||||
value value.Value
|
||||
typeRef schema.TypeRef
|
||||
schema *schema.Schema
|
||||
}
|
||||
|
||||
// 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, typeName string) (TypedValue, error) {
|
||||
tv := TypedValue{
|
||||
value: v,
|
||||
typeRef: schema.TypeRef{NamedType: &typeName},
|
||||
schema: s,
|
||||
}
|
||||
if err := tv.Validate(); err != nil {
|
||||
return TypedValue{}, err
|
||||
}
|
||||
return tv, nil
|
||||
}
|
||||
|
||||
// AsValue removes the type from the TypedValue and only keeps the value.
|
||||
func (tv TypedValue) AsValue() *value.Value {
|
||||
return &tv.value
|
||||
}
|
||||
|
||||
// Validate returns an error with a list of every spec violation.
|
||||
func (tv TypedValue) Validate() error {
|
||||
if errs := tv.walker().validate(); len(errs) != 0 {
|
||||
return errs
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToFieldSet creates a set containing every leaf field mentioned in tv, or
|
||||
// validation errors, if any were encountered.
|
||||
func (tv TypedValue) ToFieldSet() (*fieldpath.Set, error) {
|
||||
s := fieldpath.NewSet()
|
||||
w := tv.walker()
|
||||
w.leafFieldCallback = func(p fieldpath.Path) { s.Insert(p) }
|
||||
if errs := w.validate(); len(errs) != 0 {
|
||||
return nil, errs
|
||||
}
|
||||
return s, 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)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// Merged is the result of merging the two objects, as explained in the
|
||||
// comments on TypedValue.Merge().
|
||||
Merged TypedValue
|
||||
|
||||
// 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 {
|
||||
str := fmt.Sprintf("- Merged Object:\n%v\n", c.Merged.AsValue())
|
||||
if !c.Modified.Empty() {
|
||||
str += fmt.Sprintf("- Modified Fields:\n%v\n", c.Modified)
|
||||
}
|
||||
if !c.Added.Empty() {
|
||||
str += fmt.Sprintf("- Added Fields:\n%v\n", c.Added)
|
||||
}
|
||||
if !c.Removed.Empty() {
|
||||
str += fmt.Sprintf("- Removed Fields:\n%v\n", c.Removed)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// 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(),
|
||||
}
|
||||
c.Merged, 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 !reflect.DeepEqual(w.rhs, w.lhs) {
|
||||
// TODO: reflect.DeepEqual is not sufficient for this.
|
||||
// Need to implement equality check on the value type.
|
||||
c.Modified.Insert(w.path)
|
||||
}
|
||||
|
||||
ruleKeepRHS(w)
|
||||
}, 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
|
||||
}
|
||||
|
||||
func merge(lhs, rhs TypedValue, rule, postRule mergeRule) (TypedValue, error) {
|
||||
if lhs.schema != rhs.schema {
|
||||
return TypedValue{}, errorFormatter{}.
|
||||
errorf("expected objects with types from the same schema")
|
||||
}
|
||||
if !reflect.DeepEqual(lhs.typeRef, rhs.typeRef) {
|
||||
return TypedValue{}, errorFormatter{}.
|
||||
errorf("expected objects of the same type, but got %v and %v", lhs.typeRef, rhs.typeRef)
|
||||
}
|
||||
|
||||
mw := mergingWalker{
|
||||
lhs: &lhs.value,
|
||||
rhs: &rhs.value,
|
||||
schema: lhs.schema,
|
||||
typeRef: lhs.typeRef,
|
||||
rule: rule,
|
||||
postItemHook: postRule,
|
||||
}
|
||||
errs := mw.merge()
|
||||
if len(errs) > 0 {
|
||||
return TypedValue{}, errs
|
||||
}
|
||||
|
||||
out := TypedValue{
|
||||
schema: lhs.schema,
|
||||
typeRef: lhs.typeRef,
|
||||
}
|
||||
if mw.out == nil {
|
||||
out.value = value.Value{Null: true}
|
||||
} else {
|
||||
out.value = *mw.out
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// AsTypeUnvalidated is just like WithType, 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, typeName string) TypedValue {
|
||||
tv := TypedValue{
|
||||
value: v,
|
||||
typeRef: schema.TypeRef{NamedType: &typeName},
|
||||
schema: s,
|
||||
}
|
||||
return tv
|
||||
}
|
|
@ -0,0 +1,208 @@
|
|||
/*
|
||||
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 (
|
||||
"sigs.k8s.io/structured-merge-diff/fieldpath"
|
||||
"sigs.k8s.io/structured-merge-diff/schema"
|
||||
"sigs.k8s.io/structured-merge-diff/value"
|
||||
)
|
||||
|
||||
func (tv TypedValue) walker() *validatingObjectWalker {
|
||||
return &validatingObjectWalker{
|
||||
value: tv.value,
|
||||
schema: tv.schema,
|
||||
typeRef: tv.typeRef,
|
||||
}
|
||||
}
|
||||
|
||||
type validatingObjectWalker struct {
|
||||
errorFormatter
|
||||
value value.Value
|
||||
schema *schema.Schema
|
||||
typeRef schema.TypeRef
|
||||
|
||||
// If set, this is called on "leaf fields":
|
||||
// * scalars: int/string/float/bool
|
||||
// * atomic maps and lists
|
||||
// * untyped fields
|
||||
leafFieldCallback func(fieldpath.Path)
|
||||
|
||||
// internal housekeeping--don't set when constructing.
|
||||
inLeaf bool // Set to true if we're in a "big leaf"--atomic map/list
|
||||
}
|
||||
|
||||
func (v validatingObjectWalker) validate() ValidationErrors {
|
||||
return resolveSchema(v.schema, v.typeRef, v)
|
||||
}
|
||||
|
||||
// doLeaf should be called on leaves before descending into children, if there
|
||||
// will be a descent. It modifies v.inLeaf.
|
||||
func (v *validatingObjectWalker) doLeaf() {
|
||||
if v.inLeaf {
|
||||
// We're in a "big leaf", an atomic map or list. Ignore
|
||||
// subsequent leaves.
|
||||
return
|
||||
}
|
||||
v.inLeaf = true
|
||||
|
||||
if v.leafFieldCallback != nil {
|
||||
// At the moment, this is only used to build fieldsets; we can
|
||||
// add more than the path in here if needed.
|
||||
v.leafFieldCallback(v.path)
|
||||
}
|
||||
}
|
||||
|
||||
func (v validatingObjectWalker) doScalar(t schema.Scalar) ValidationErrors {
|
||||
if errs := v.validateScalar(t, &v.value, ""); len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
// All scalars are leaf fields.
|
||||
v.doLeaf()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v validatingObjectWalker) visitStructFields(t schema.Struct, m *value.Map) (errs ValidationErrors) {
|
||||
allowedNames := map[string]struct{}{}
|
||||
for i := range t.Fields {
|
||||
// I don't want to use the loop variable since a reference
|
||||
// might outlive the loop iteration (in an error message).
|
||||
f := t.Fields[i]
|
||||
allowedNames[f.Name] = struct{}{}
|
||||
child, ok := m.Get(f.Name)
|
||||
if !ok {
|
||||
// All fields are optional
|
||||
continue
|
||||
}
|
||||
v2 := v
|
||||
v2.errorFormatter.descend(fieldpath.PathElement{FieldName: &f.Name})
|
||||
v2.value = child.Value
|
||||
v2.typeRef = f.Type
|
||||
errs = append(errs, v2.validate()...)
|
||||
}
|
||||
|
||||
// All fields may be optional, but unknown fields are not allowed.
|
||||
return append(errs, v.rejectExtraStructFields(m, allowedNames, "")...)
|
||||
}
|
||||
|
||||
func (v validatingObjectWalker) doStruct(t schema.Struct) (errs ValidationErrors) {
|
||||
m, err := mapOrStructValue(v.value, "struct")
|
||||
if err != nil {
|
||||
return v.error(err)
|
||||
}
|
||||
|
||||
if t.ElementRelationship == schema.Atomic {
|
||||
v.doLeaf()
|
||||
}
|
||||
|
||||
if m == nil {
|
||||
// nil is a valid map!
|
||||
return nil
|
||||
}
|
||||
|
||||
errs = v.visitStructFields(t, m)
|
||||
|
||||
// TODO: Check unions.
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func (v validatingObjectWalker) visitListItems(t schema.List, list *value.List) (errs ValidationErrors) {
|
||||
observedKeys := map[string]struct{}{}
|
||||
for i, child := range list.Items {
|
||||
pe, err := listItemToPathElement(t, i, child)
|
||||
if err != nil {
|
||||
errs = append(errs, v.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.
|
||||
continue
|
||||
}
|
||||
keyStr := pe.String()
|
||||
if _, found := observedKeys[keyStr]; found {
|
||||
errs = append(errs, v.errorf("duplicate entries for key %v", keyStr)...)
|
||||
}
|
||||
observedKeys[keyStr] = struct{}{}
|
||||
v2 := v
|
||||
v2.errorFormatter.descend(pe)
|
||||
v2.value = child
|
||||
v2.typeRef = t.ElementType
|
||||
errs = append(errs, v2.validate()...)
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func (v validatingObjectWalker) doList(t schema.List) (errs ValidationErrors) {
|
||||
list, err := listValue(v.value)
|
||||
if err != nil {
|
||||
return v.error(err)
|
||||
}
|
||||
|
||||
if t.ElementRelationship == schema.Atomic {
|
||||
v.doLeaf()
|
||||
}
|
||||
|
||||
if list == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
errs = v.visitListItems(t, list)
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func (v validatingObjectWalker) visitMapItems(t schema.Map, m *value.Map) (errs ValidationErrors) {
|
||||
for _, item := range m.Items {
|
||||
v2 := v
|
||||
name := item.Name
|
||||
v2.errorFormatter.descend(fieldpath.PathElement{FieldName: &name})
|
||||
v2.value = item.Value
|
||||
v2.typeRef = t.ElementType
|
||||
errs = append(errs, v2.validate()...)
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func (v validatingObjectWalker) doMap(t schema.Map) (errs ValidationErrors) {
|
||||
m, err := mapOrStructValue(v.value, "map")
|
||||
if err != nil {
|
||||
return v.error(err)
|
||||
}
|
||||
|
||||
if t.ElementRelationship == schema.Atomic {
|
||||
v.doLeaf()
|
||||
}
|
||||
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
errs = v.visitMapItems(t, m)
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func (v validatingObjectWalker) doUntyped(t schema.Untyped) (errs ValidationErrors) {
|
||||
if t.ElementRelationship == "" || t.ElementRelationship == schema.Atomic {
|
||||
// Untyped sections allow anything, and are considered leaf
|
||||
// fields.
|
||||
v.doLeaf()
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"doc.go",
|
||||
"unstructured.go",
|
||||
"value.go",
|
||||
],
|
||||
importmap = "k8s.io/kubernetes/vendor/sigs.k8s.io/structured-merge-diff/value",
|
||||
importpath = "sigs.k8s.io/structured-merge-diff/value",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = ["//vendor/gopkg.in/yaml.v2:go_default_library"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
|
@ -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 value defines types for an in-memory representation of yaml or json
|
||||
// objects, organized for convenient comparison with a schema (as defined by
|
||||
// the sibling schema package). Functions for reading and writing the objects
|
||||
// are also provided.
|
||||
package value
|
|
@ -0,0 +1,234 @@
|
|||
/*
|
||||
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 value
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// FromYAML is a helper function for reading a YAML document; it attempts to
|
||||
// preserve order of keys within maps/structs. This is as a convenience to
|
||||
// humans keeping YAML documents, not because there is a behavior difference.
|
||||
//
|
||||
// Known bug: objects with top-level arrays don't parse correctly.
|
||||
func FromYAML(input []byte) (Value, error) {
|
||||
var decoded interface{}
|
||||
|
||||
if len(input) == 0 || (len(input) == 4 && string(input) == "null") {
|
||||
// Special case since the yaml package doesn't accurately
|
||||
// preserve this.
|
||||
return Value{Null: true}, nil
|
||||
}
|
||||
|
||||
// This attempts to enable order sensitivity; note the yaml package is
|
||||
// broken for documents that have root-level arrays, hence the two-step
|
||||
// approach. TODO: This is a horrific hack. Is it worth it?
|
||||
var ms yaml.MapSlice
|
||||
if err := yaml.Unmarshal(input, &ms); err == nil {
|
||||
decoded = ms
|
||||
} else if err := yaml.Unmarshal(input, &decoded); err != nil {
|
||||
return Value{}, err
|
||||
}
|
||||
|
||||
v, err := FromUnstructured(decoded)
|
||||
if err != nil {
|
||||
return Value{}, fmt.Errorf("failed to interpret (%v):\n%s", err, input)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// FromJSON is a helper function for reading a JSON document
|
||||
func FromJSON(input []byte) (Value, error) {
|
||||
var decoded interface{}
|
||||
|
||||
if err := json.Unmarshal(input, &decoded); err != nil {
|
||||
return Value{}, err
|
||||
}
|
||||
|
||||
v, err := FromUnstructured(decoded)
|
||||
if err != nil {
|
||||
return Value{}, fmt.Errorf("failed to interpret (%v):\n%s", err, input)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// FromUnstructured will convert a go interface to a Value.
|
||||
// It's most commonly expected to be used with map[string]interface{} as the
|
||||
// input. `in` must not have any structures with cycles in them.
|
||||
// yaml.MapSlice may be used for order-preservation.
|
||||
func FromUnstructured(in interface{}) (Value, error) {
|
||||
if in == nil {
|
||||
return Value{Null: true}, nil
|
||||
}
|
||||
switch t := in.(type) {
|
||||
case map[interface{}]interface{}:
|
||||
m := Map{}
|
||||
for rawKey, rawVal := range t {
|
||||
k, ok := rawKey.(string)
|
||||
if !ok {
|
||||
return Value{}, fmt.Errorf("key %#v: not a string", k)
|
||||
}
|
||||
v, err := FromUnstructured(rawVal)
|
||||
if err != nil {
|
||||
return Value{}, fmt.Errorf("key %v: %v", k, err)
|
||||
}
|
||||
m.Set(k, v)
|
||||
}
|
||||
return Value{MapValue: &m}, nil
|
||||
case map[string]interface{}:
|
||||
m := Map{}
|
||||
for k, rawVal := range t {
|
||||
v, err := FromUnstructured(rawVal)
|
||||
if err != nil {
|
||||
return Value{}, fmt.Errorf("key %v: %v", k, err)
|
||||
}
|
||||
m.Set(k, v)
|
||||
}
|
||||
return Value{MapValue: &m}, nil
|
||||
case yaml.MapSlice:
|
||||
m := Map{}
|
||||
for _, item := range t {
|
||||
k, ok := item.Key.(string)
|
||||
if !ok {
|
||||
return Value{}, fmt.Errorf("key %#v is not a string", item.Key)
|
||||
}
|
||||
v, err := FromUnstructured(item.Value)
|
||||
if err != nil {
|
||||
return Value{}, fmt.Errorf("key %v: %v", k, err)
|
||||
}
|
||||
m.Set(k, v)
|
||||
}
|
||||
return Value{MapValue: &m}, nil
|
||||
case []interface{}:
|
||||
l := List{}
|
||||
for i, rawVal := range t {
|
||||
v, err := FromUnstructured(rawVal)
|
||||
if err != nil {
|
||||
return Value{}, fmt.Errorf("index %v: %v", i, err)
|
||||
}
|
||||
l.Items = append(l.Items, v)
|
||||
}
|
||||
return Value{ListValue: &l}, nil
|
||||
case int:
|
||||
n := Int(t)
|
||||
return Value{IntValue: &n}, nil
|
||||
case int8:
|
||||
n := Int(t)
|
||||
return Value{IntValue: &n}, nil
|
||||
case int16:
|
||||
n := Int(t)
|
||||
return Value{IntValue: &n}, nil
|
||||
case int32:
|
||||
n := Int(t)
|
||||
return Value{IntValue: &n}, nil
|
||||
case int64:
|
||||
n := Int(t)
|
||||
return Value{IntValue: &n}, nil
|
||||
case uint:
|
||||
n := Int(t)
|
||||
return Value{IntValue: &n}, nil
|
||||
case uint8:
|
||||
n := Int(t)
|
||||
return Value{IntValue: &n}, nil
|
||||
case uint16:
|
||||
n := Int(t)
|
||||
return Value{IntValue: &n}, nil
|
||||
case uint32:
|
||||
n := Int(t)
|
||||
return Value{IntValue: &n}, nil
|
||||
case float32:
|
||||
f := Float(t)
|
||||
return Value{FloatValue: &f}, nil
|
||||
case float64:
|
||||
f := Float(t)
|
||||
return Value{FloatValue: &f}, nil
|
||||
case string:
|
||||
return StringValue(t), nil
|
||||
case bool:
|
||||
return BooleanValue(t), nil
|
||||
default:
|
||||
return Value{}, fmt.Errorf("type unimplemented: %t", in)
|
||||
}
|
||||
}
|
||||
|
||||
// ToYAML is a helper function for producing a YAML document; it attempts to
|
||||
// preserve order of keys within maps/structs. This is as a convenience to
|
||||
// humans keeping YAML documents, not because there is a behavior difference.
|
||||
func (v *Value) ToYAML() ([]byte, error) {
|
||||
return yaml.Marshal(v.ToUnstructured(true))
|
||||
}
|
||||
|
||||
// ToJSON is a helper function for producing a JSon document.
|
||||
func (v *Value) ToJSON() ([]byte, error) {
|
||||
return json.Marshal(v.ToUnstructured(false))
|
||||
}
|
||||
|
||||
// ToUnstructured will convert the Value into a go-typed object.
|
||||
// If preserveOrder is true, then maps will be converted to the yaml.MapSlice
|
||||
// type. Otherwise, map[string]interface{} must be used-- this destroys
|
||||
// ordering information and is not recommended if the result of this will be
|
||||
// serialized. Other types:
|
||||
// * list -> []interface{}
|
||||
// * others -> corresponding go type, wrapped in an interface{}
|
||||
//
|
||||
// Of note, floats and ints will always come out as float64 and int64,
|
||||
// respectively.
|
||||
func (v *Value) ToUnstructured(preserveOrder bool) interface{} {
|
||||
switch {
|
||||
case v.FloatValue != nil:
|
||||
f := float64(*v.FloatValue)
|
||||
return f
|
||||
case v.IntValue != nil:
|
||||
i := int64(*v.IntValue)
|
||||
return i
|
||||
case v.StringValue != nil:
|
||||
return string(*v.StringValue)
|
||||
case v.BooleanValue != nil:
|
||||
return bool(*v.BooleanValue)
|
||||
case v.ListValue != nil:
|
||||
out := []interface{}{}
|
||||
for _, item := range v.ListValue.Items {
|
||||
out = append(out, item.ToUnstructured(preserveOrder))
|
||||
}
|
||||
return out
|
||||
case v.MapValue != nil:
|
||||
m := v.MapValue
|
||||
if preserveOrder {
|
||||
ms := make(yaml.MapSlice, len(m.Items))
|
||||
for i := range m.Items {
|
||||
ms[i] = yaml.MapItem{
|
||||
Key: m.Items[i].Name,
|
||||
Value: m.Items[i].Value.ToUnstructured(preserveOrder),
|
||||
}
|
||||
}
|
||||
return ms
|
||||
}
|
||||
// This case is unavoidably lossy.
|
||||
out := map[string]interface{}{}
|
||||
for i := range m.Items {
|
||||
out[m.Items[i].Name] = m.Items[i].Value.ToUnstructured(preserveOrder)
|
||||
}
|
||||
return out
|
||||
default:
|
||||
fallthrough
|
||||
case v.Null == true:
|
||||
return nil
|
||||
}
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
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 value
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A Value is an object; it corresponds to an 'atom' in the schema.
|
||||
type Value struct {
|
||||
// Exactly one of the below must be set.
|
||||
FloatValue *Float
|
||||
IntValue *Int
|
||||
StringValue *String
|
||||
BooleanValue *Boolean
|
||||
ListValue *List
|
||||
MapValue *Map
|
||||
Null bool // represents an explicit `"foo" = null`
|
||||
}
|
||||
|
||||
type Int int64
|
||||
type Float float64
|
||||
type String string
|
||||
type Boolean bool
|
||||
|
||||
// Field is an individual key-value pair.
|
||||
type Field struct {
|
||||
Name string
|
||||
Value Value
|
||||
}
|
||||
|
||||
// List is a list of items.
|
||||
type List struct {
|
||||
Items []Value
|
||||
}
|
||||
|
||||
// Map is a map of key-value pairs. It represents both structs and maps. We use
|
||||
// a list and a go-language map to preserve order.
|
||||
//
|
||||
// Set and Get helpers are provided.
|
||||
type Map struct {
|
||||
Items []Field
|
||||
|
||||
// may be nil; lazily constructed.
|
||||
// TODO: Direct modifications to Items above will cause serious problems.
|
||||
index map[string]*Field
|
||||
}
|
||||
|
||||
// Get returns the (Field, true) or (nil, false) if it is not present
|
||||
func (m *Map) Get(key string) (*Field, bool) {
|
||||
if m.index == nil {
|
||||
m.index = map[string]*Field{}
|
||||
for i := range m.Items {
|
||||
f := &m.Items[i]
|
||||
m.index[f.Name] = f
|
||||
}
|
||||
}
|
||||
f, ok := m.index[key]
|
||||
return f, ok
|
||||
}
|
||||
|
||||
// Set inserts or updates the given item.
|
||||
func (m *Map) Set(key string, value Value) {
|
||||
if f, ok := m.Get(key); ok {
|
||||
f.Value = value
|
||||
return
|
||||
}
|
||||
m.Items = append(m.Items, Field{Name: key, Value: value})
|
||||
m.index = nil // Since the append might have reallocated
|
||||
}
|
||||
|
||||
// StringValue returns s as a scalar string Value.
|
||||
func StringValue(s string) Value {
|
||||
s2 := String(s)
|
||||
return Value{StringValue: &s2}
|
||||
}
|
||||
|
||||
// IntValue returns i as a scalar numeric (integer) Value.
|
||||
func IntValue(i int) Value {
|
||||
i2 := Int(i)
|
||||
return Value{IntValue: &i2}
|
||||
}
|
||||
|
||||
// FloatValue returns f as a scalar numeric (float) Value.
|
||||
func FloatValue(f float64) Value {
|
||||
f2 := Float(f)
|
||||
return Value{FloatValue: &f2}
|
||||
}
|
||||
|
||||
// BooleanValue returns b as a scalar boolean Value.
|
||||
func BooleanValue(b bool) Value {
|
||||
b2 := Boolean(b)
|
||||
return Value{BooleanValue: &b2}
|
||||
}
|
||||
|
||||
// String returns a human-readable representation of the value.
|
||||
func (v Value) String() string {
|
||||
switch {
|
||||
case v.FloatValue != nil:
|
||||
return fmt.Sprintf("%v", *v.FloatValue)
|
||||
case v.IntValue != nil:
|
||||
return fmt.Sprintf("%v", *v.IntValue)
|
||||
case v.StringValue != nil:
|
||||
return fmt.Sprintf("%q", *v.StringValue)
|
||||
case v.BooleanValue != nil:
|
||||
return fmt.Sprintf("%v", *v.BooleanValue)
|
||||
case v.ListValue != nil:
|
||||
strs := []string{}
|
||||
for _, item := range v.ListValue.Items {
|
||||
strs = append(strs, item.String())
|
||||
}
|
||||
return "[" + strings.Join(strs, ",") + "]"
|
||||
case v.MapValue != nil:
|
||||
strs := []string{}
|
||||
for _, i := range v.MapValue.Items {
|
||||
strs = append(strs, fmt.Sprintf("%v=%v", i.Name, i.Value))
|
||||
}
|
||||
return "{" + strings.Join(strs, ";") + "}"
|
||||
default:
|
||||
fallthrough
|
||||
case v.Null == true:
|
||||
return "null"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue