mirror of
https://github.com/kubernetes-sigs/kustomize.git
synced 2026-06-12 01:14:22 +00:00
kyaml: initial support for yaml and resource manipulation
This commit is contained in:
44
kyaml/yaml/alias.go
Normal file
44
kyaml/yaml/alias.go
Normal file
@@ -0,0 +1,44 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package yaml
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Expose the yaml.v3 functions so this package can be used as a replacement
|
||||
|
||||
type Decoder = yaml.Decoder
|
||||
type Encoder = yaml.Encoder
|
||||
type IsZeroer = yaml.IsZeroer
|
||||
type Kind = yaml.Kind
|
||||
type Marshaler = yaml.Marshaler
|
||||
type Node = yaml.Node
|
||||
type Style = yaml.Style
|
||||
type TypeError = yaml.TypeError
|
||||
type Unmarshaler = yaml.Unmarshaler
|
||||
|
||||
var Marshal = yaml.Marshal
|
||||
var UnMarshal = yaml.Unmarshal
|
||||
var NewDecoder = yaml.NewDecoder
|
||||
var NewEncoder = func(w io.Writer) *yaml.Encoder {
|
||||
e := yaml.NewEncoder(w)
|
||||
e.SetIndent(2)
|
||||
return e
|
||||
}
|
||||
|
||||
var AliasNode yaml.Kind = yaml.AliasNode
|
||||
var DocumentNode yaml.Kind = yaml.DocumentNode
|
||||
var MappingNode yaml.Kind = yaml.MappingNode
|
||||
var ScalarNode yaml.Kind = yaml.ScalarNode
|
||||
var SequenceNode yaml.Kind = yaml.SequenceNode
|
||||
|
||||
var DoubleQuotedStyle yaml.Style = yaml.DoubleQuotedStyle
|
||||
var FlowStyle yaml.Style = yaml.FlowStyle
|
||||
var FoldedStyle yaml.Style = yaml.FoldedStyle
|
||||
var LiteralStyle yaml.Style = yaml.LiteralStyle
|
||||
var SingleQuotedStyle yaml.Style = yaml.SingleQuotedStyle
|
||||
var TaggedStyle yaml.Style = yaml.TaggedStyle
|
||||
12
kyaml/yaml/doc.go
Normal file
12
kyaml/yaml/doc.go
Normal file
@@ -0,0 +1,12 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package yaml contains low-level libraries for manipulating individual Kubernetes Resource
|
||||
// Configuration yaml.
|
||||
//
|
||||
// It exports the public pieces of "gopkg.in/yaml.v3", so can be used as a drop in replacement.
|
||||
//
|
||||
// This package should be used over sigs.k8s.io/yaml:
|
||||
// - If retaining or modifying yaml comments, structure, formatting
|
||||
// - If Resources should be round tripped without dropping fields
|
||||
package yaml
|
||||
841
kyaml/yaml/example_test.go
Normal file
841
kyaml/yaml/example_test.go
Normal file
@@ -0,0 +1,841 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package yaml_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
. "sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
func Example() {
|
||||
obj, err := Parse(`apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: app
|
||||
labels:
|
||||
app: java
|
||||
annotations:
|
||||
a.b.c: d.e.f
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
containers, err := Parse(`
|
||||
- name: nginx # first container
|
||||
image: nginx
|
||||
- name: nginx2 # second container
|
||||
image: nginx2
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
node, err := obj.Pipe(
|
||||
LookupCreate(SequenceNode, "spec", "template", "spec", "containers"),
|
||||
Append(containers.YNode().Content...))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println(node.String())
|
||||
fmt.Println(obj.String())
|
||||
// Output:
|
||||
// <nil>
|
||||
// apiVersion: apps/v1
|
||||
// kind: Deployment
|
||||
// metadata:
|
||||
// name: app
|
||||
// labels:
|
||||
// app: java
|
||||
// annotations:
|
||||
// a.b.c: d.e.f
|
||||
// spec:
|
||||
// template:
|
||||
// spec:
|
||||
// containers:
|
||||
// - name: nginx # first container
|
||||
// image: nginx
|
||||
// - name: nginx2 # second container
|
||||
// image: nginx2
|
||||
// <nil>
|
||||
}
|
||||
|
||||
func ExampleAppend_appendScalars() {
|
||||
obj, err := Parse(`
|
||||
- a
|
||||
- b
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
_, err = obj.Pipe(Append(&Node{Value: "c", Kind: ScalarNode}))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
node, err := obj.Pipe(Append(
|
||||
&Node{Value: "c", Kind: ScalarNode},
|
||||
&Node{Value: "d", Kind: ScalarNode},
|
||||
))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println(node.String())
|
||||
fmt.Println(obj.String())
|
||||
// Output:
|
||||
// <nil>
|
||||
// - a
|
||||
// - b
|
||||
// - c
|
||||
// - c
|
||||
// - d
|
||||
// <nil>
|
||||
}
|
||||
|
||||
func ExampleAppend_appendMap() {
|
||||
obj, err := Parse(`
|
||||
- name: foo
|
||||
- name: bar
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
elem, err := Parse("name: baz")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
node, err := obj.Pipe(Append(elem.YNode()))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Expect the node to contain the appended element because only
|
||||
// 1 element was appended
|
||||
fmt.Println(node.String())
|
||||
fmt.Println(obj.String())
|
||||
// Output:
|
||||
// name: baz
|
||||
// <nil>
|
||||
// - name: foo
|
||||
// - name: bar
|
||||
// - name: baz
|
||||
// <nil>
|
||||
}
|
||||
|
||||
func ExampleClear() {
|
||||
obj, err := Parse(`
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: app
|
||||
annotations:
|
||||
a.b.c: d.e.f
|
||||
g: h
|
||||
spec:
|
||||
template: {}
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
node, err := obj.Pipe(Clear("metadata"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println(node.String())
|
||||
fmt.Println(obj.String())
|
||||
// Output:
|
||||
// name: app
|
||||
// annotations:
|
||||
// a.b.c: d.e.f
|
||||
// g: h
|
||||
// <nil>
|
||||
// kind: Deployment
|
||||
// spec:
|
||||
// template: {}
|
||||
// <nil>
|
||||
}
|
||||
|
||||
func ExampleGet() {
|
||||
obj, err := Parse(`apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: app
|
||||
annotations:
|
||||
a.b.c: d.e.f
|
||||
g: h
|
||||
spec:
|
||||
template: {}
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
node, err := obj.Pipe(Get("metadata"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println(node.String())
|
||||
fmt.Println(obj.String())
|
||||
// Output:
|
||||
// name: app
|
||||
// annotations:
|
||||
// a.b.c: d.e.f
|
||||
// g: h
|
||||
// <nil>
|
||||
// apiVersion: apps/v1
|
||||
// kind: Deployment
|
||||
// metadata:
|
||||
// name: app
|
||||
// annotations:
|
||||
// a.b.c: d.e.f
|
||||
// g: h
|
||||
// spec:
|
||||
// template: {}
|
||||
// <nil>
|
||||
}
|
||||
|
||||
func ExampleGet_notFound() {
|
||||
obj, err := Parse(`apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
spec:
|
||||
template: {}
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
node, err := obj.Pipe(FieldMatcher{Name: "metadata"})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println(node.String())
|
||||
fmt.Println(obj.String())
|
||||
// Output:
|
||||
// <nil>
|
||||
// apiVersion: apps/v1
|
||||
// kind: Deployment
|
||||
// spec:
|
||||
// template: {}
|
||||
// <nil>
|
||||
}
|
||||
|
||||
func ExampleElementMatcher_Filter() {
|
||||
obj, err := Parse(`
|
||||
- a
|
||||
- b
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
elem, err := obj.Pipe(ElementMatcher{
|
||||
FieldValue: "c", Create: NewScalarRNode("c"),
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println(elem.String())
|
||||
fmt.Println(obj.String())
|
||||
// Output:
|
||||
// c
|
||||
// <nil>
|
||||
// - a
|
||||
// - b
|
||||
// - c
|
||||
// <nil>
|
||||
}
|
||||
|
||||
func ExampleElementMatcher_Filter_primitiveFound() {
|
||||
obj, err := Parse(`
|
||||
- a
|
||||
- b
|
||||
- c
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
elem, err := obj.Pipe(ElementMatcher{
|
||||
FieldValue: "c", Create: NewScalarRNode("c"),
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println(elem.String())
|
||||
fmt.Println(obj.String())
|
||||
// Output:
|
||||
// c
|
||||
// <nil>
|
||||
// - a
|
||||
// - b
|
||||
// - c
|
||||
// <nil>
|
||||
}
|
||||
|
||||
func ExampleElementMatcher_Filter_objectNotFound() {
|
||||
obj, err := Parse(`
|
||||
- name: foo
|
||||
- name: bar
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
append, err := Parse(`
|
||||
name: baz
|
||||
image: nginx
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
elem, err := obj.Pipe(ElementMatcher{
|
||||
FieldName: "name", FieldValue: "baz", Create: append})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println(elem.String())
|
||||
fmt.Println(obj.String())
|
||||
// Output:
|
||||
// name: baz
|
||||
// image: nginx
|
||||
// <nil>
|
||||
// - name: foo
|
||||
// - name: bar
|
||||
// - name: baz
|
||||
// image: nginx
|
||||
// <nil>
|
||||
}
|
||||
|
||||
func ExampleElementMatcher_Filter_objectFound() {
|
||||
obj, err := Parse(`
|
||||
- name: foo
|
||||
- name: bar
|
||||
- name: baz
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
append, err := Parse(`
|
||||
name: baz
|
||||
image: nginx
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
elem, err := obj.Pipe(ElementMatcher{
|
||||
FieldName: "name", FieldValue: "baz", Create: append})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println(elem.String())
|
||||
fmt.Println(obj.String())
|
||||
// Output:
|
||||
// name: baz
|
||||
// <nil>
|
||||
// - name: foo
|
||||
// - name: bar
|
||||
// - name: baz
|
||||
// <nil>
|
||||
}
|
||||
|
||||
func ExampleFieldMatcher_Filter() {
|
||||
obj, err := Parse(`
|
||||
kind: Deployment
|
||||
spec:
|
||||
template: {}
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
value, err := Parse(`
|
||||
name: app
|
||||
annotations:
|
||||
a.b.c: d.e.f
|
||||
g: h
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
elem, err := obj.Pipe(FieldMatcher{
|
||||
Name: "metadata", Value: value, Create: value})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println(elem.String())
|
||||
fmt.Println(obj.String())
|
||||
// Output:
|
||||
// name: app
|
||||
// annotations:
|
||||
// a.b.c: d.e.f
|
||||
// g: h
|
||||
// <nil>
|
||||
// kind: Deployment
|
||||
// spec:
|
||||
// template: {}
|
||||
// metadata:
|
||||
// name: app
|
||||
// annotations:
|
||||
// a.b.c: d.e.f
|
||||
// g: h
|
||||
// <nil>
|
||||
}
|
||||
|
||||
func ExampleLookup_element() {
|
||||
obj, err := Parse(`apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app: java
|
||||
annotations:
|
||||
a.b.c: d.e.f
|
||||
name: app
|
||||
spec:
|
||||
templates:
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:latest
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
value, err := obj.Pipe(Lookup(
|
||||
"spec", "templates", "spec", "containers", "[name=nginx]"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println(value.String())
|
||||
// Output:
|
||||
// name: nginx
|
||||
// image: nginx:latest
|
||||
// <nil>
|
||||
}
|
||||
|
||||
func ExampleLookup_sequence() {
|
||||
obj, err := Parse(`apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app: java
|
||||
annotations:
|
||||
a.b.c: d.e.f
|
||||
name: app
|
||||
spec:
|
||||
templates:
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:latest
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
value, err := obj.Pipe(Lookup(
|
||||
"spec", "templates", "spec", "containers"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println(value.String())
|
||||
// Output:
|
||||
// - name: nginx
|
||||
// image: nginx:latest
|
||||
// <nil>
|
||||
}
|
||||
|
||||
func ExampleLookup_scalar() {
|
||||
obj, err := Parse(`apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app: java
|
||||
annotations:
|
||||
a.b.c: d.e.f
|
||||
name: app
|
||||
spec:
|
||||
templates:
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:latest
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
value, err := obj.Pipe(Lookup(
|
||||
"spec", "templates", "spec", "containers", "[name=nginx]", "image"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println(value.String())
|
||||
// Output:
|
||||
// nginx:latest
|
||||
// <nil>
|
||||
}
|
||||
|
||||
func ExampleLookupCreate_element() {
|
||||
obj, err := Parse(`
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app: java
|
||||
annotations:
|
||||
a.b.c: d.e.f
|
||||
name: app
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
rs, err := obj.Pipe(LookupCreate(
|
||||
MappingNode, "spec", "templates", "spec", "containers", "[name=nginx]"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println(rs.String())
|
||||
fmt.Println("---")
|
||||
fmt.Println(obj.String())
|
||||
// Output:
|
||||
// name: nginx
|
||||
// <nil>
|
||||
// ---
|
||||
// kind: Deployment
|
||||
// metadata:
|
||||
// labels:
|
||||
// app: java
|
||||
// annotations:
|
||||
// a.b.c: d.e.f
|
||||
// name: app
|
||||
// spec:
|
||||
// templates:
|
||||
// spec:
|
||||
// containers:
|
||||
// - name: nginx
|
||||
// <nil>
|
||||
}
|
||||
|
||||
func ExampleLookupCreate_sequence() {
|
||||
obj, err := Parse(`
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app: java
|
||||
annotations:
|
||||
a.b.c: d.e.f
|
||||
name: app
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
rs, err := obj.Pipe(LookupCreate(
|
||||
SequenceNode, "spec", "templates", "spec", "containers"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println(rs.String())
|
||||
fmt.Println("---")
|
||||
fmt.Println(obj.String())
|
||||
// Output:
|
||||
// []
|
||||
// <nil>
|
||||
// ---
|
||||
// kind: Deployment
|
||||
// metadata:
|
||||
// labels:
|
||||
// app: java
|
||||
// annotations:
|
||||
// a.b.c: d.e.f
|
||||
// name: app
|
||||
// spec:
|
||||
// templates:
|
||||
// spec:
|
||||
// containers: []
|
||||
// <nil>
|
||||
}
|
||||
|
||||
func ExamplePathGetter_Filter() {
|
||||
obj, err := Parse(`
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app: java
|
||||
annotations:
|
||||
a.b.c: d.e.f
|
||||
name: app
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
rs, err := obj.Pipe(PathGetter{
|
||||
Path: []string{"spec", "templates", "spec", "containers", "[name=nginx]", "image"},
|
||||
Create: ScalarNode,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
rs.Document().Style = SingleQuotedStyle
|
||||
|
||||
fmt.Println(rs.String())
|
||||
fmt.Println("---")
|
||||
fmt.Println(obj.String())
|
||||
// Output:
|
||||
// ''
|
||||
// <nil>
|
||||
// ---
|
||||
// kind: Deployment
|
||||
// metadata:
|
||||
// labels:
|
||||
// app: java
|
||||
// annotations:
|
||||
// a.b.c: d.e.f
|
||||
// name: app
|
||||
// spec:
|
||||
// templates:
|
||||
// spec:
|
||||
// containers:
|
||||
// - name: nginx
|
||||
// image: ''
|
||||
// <nil>
|
||||
}
|
||||
|
||||
func ExampleLookupCreate_object() {
|
||||
obj, err := Parse(`
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app: java
|
||||
annotations:
|
||||
a.b.c: d.e.f
|
||||
name: app
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
rs, err := obj.Pipe(LookupCreate(
|
||||
MappingNode, "spec", "templates", "spec"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println(rs.String())
|
||||
fmt.Println("---")
|
||||
fmt.Println(obj.String())
|
||||
// Output:
|
||||
// {}
|
||||
// <nil>
|
||||
// ---
|
||||
// kind: Deployment
|
||||
// metadata:
|
||||
// labels:
|
||||
// app: java
|
||||
// annotations:
|
||||
// a.b.c: d.e.f
|
||||
// name: app
|
||||
// spec:
|
||||
// templates:
|
||||
// spec: {}
|
||||
// <nil>
|
||||
}
|
||||
|
||||
func ExampleLookup_notFound() {
|
||||
obj, err := Parse(`
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app: java
|
||||
annotations:
|
||||
a.b.c: d.e.f
|
||||
name: app
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
rs, err := obj.Pipe(Lookup("spec", "templates", "spec"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println(rs)
|
||||
fmt.Println("---")
|
||||
fmt.Println(obj.String())
|
||||
// Output:
|
||||
// <nil>
|
||||
// ---
|
||||
// kind: Deployment
|
||||
// metadata:
|
||||
// labels:
|
||||
// app: java
|
||||
// annotations:
|
||||
// a.b.c: d.e.f
|
||||
// name: app
|
||||
// <nil>
|
||||
}
|
||||
|
||||
func ExampleSetField_stringValue() {
|
||||
obj, err := Parse(`apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: app
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
_, err = obj.Pipe(SetField("foo", NewScalarRNode("bar")))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println(obj.String())
|
||||
// Output:
|
||||
// apiVersion: apps/v1
|
||||
// kind: Deployment
|
||||
// metadata:
|
||||
// name: app
|
||||
// foo: bar
|
||||
// <nil>
|
||||
}
|
||||
|
||||
func ExampleSetField_stringValueOverwrite() {
|
||||
obj, err := Parse(`apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: app
|
||||
foo: baz
|
||||
`)
|
||||
if err != nil {
|
||||
// handle error
|
||||
}
|
||||
// set metadata.annotations.foo = bar
|
||||
_, err = obj.Pipe(SetField("foo", NewScalarRNode("bar")))
|
||||
if err != nil {
|
||||
// handle error
|
||||
}
|
||||
|
||||
fmt.Println(obj.String())
|
||||
// Output:
|
||||
// apiVersion: apps/v1
|
||||
// kind: Deployment
|
||||
// metadata:
|
||||
// name: app
|
||||
// foo: bar
|
||||
// <nil>
|
||||
}
|
||||
|
||||
func ExampleSetField() {
|
||||
obj, err := Parse(`apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: app
|
||||
labels:
|
||||
app: java
|
||||
annotations:
|
||||
a.b.c: d.e.f
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
containers, err := Parse(`
|
||||
- name: nginx # first container
|
||||
image: nginx
|
||||
- name: nginx2 # second container
|
||||
image: nginx2
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = obj.Pipe(
|
||||
LookupCreate(MappingNode, "spec", "template", "spec"),
|
||||
SetField("containers", containers))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println(obj.String())
|
||||
// Output:
|
||||
// apiVersion: apps/v1
|
||||
// kind: Deployment
|
||||
// metadata:
|
||||
// name: app
|
||||
// labels:
|
||||
// app: java
|
||||
// annotations:
|
||||
// a.b.c: d.e.f
|
||||
// spec:
|
||||
// template:
|
||||
// spec:
|
||||
// containers:
|
||||
// - name: nginx # first container
|
||||
// image: nginx
|
||||
// - name: nginx2 # second container
|
||||
// image: nginx2
|
||||
// <nil>
|
||||
}
|
||||
|
||||
func ExampleTee() {
|
||||
obj, err := Parse(`apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx-deployment
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.7.9
|
||||
ports:
|
||||
- containerPort: 80
|
||||
`)
|
||||
if err != nil {
|
||||
// handle error
|
||||
}
|
||||
// set metadata.annotations.foo = bar
|
||||
_, err = obj.Pipe(
|
||||
Lookup("spec", "template", "spec", "containers", "[name=nginx]"),
|
||||
Tee(SetField("filter", NewListRNode("foo"))),
|
||||
SetField("args", NewListRNode("baz", "bar")))
|
||||
if err != nil {
|
||||
// handle error
|
||||
}
|
||||
|
||||
fmt.Println(obj.String())
|
||||
// Output:
|
||||
// apiVersion: apps/v1
|
||||
// kind: Deployment
|
||||
// metadata:
|
||||
// name: nginx-deployment
|
||||
// labels:
|
||||
// app: nginx
|
||||
// spec:
|
||||
// replicas: 3
|
||||
// selector:
|
||||
// matchLabels:
|
||||
// app: nginx
|
||||
// template:
|
||||
// metadata:
|
||||
// labels:
|
||||
// app: nginx
|
||||
// spec:
|
||||
// containers:
|
||||
// - name: nginx
|
||||
// image: nginx:1.7.9
|
||||
// ports:
|
||||
// - containerPort: 80
|
||||
// filter:
|
||||
// - foo
|
||||
// args:
|
||||
// - baz
|
||||
// - bar
|
||||
// <nil>
|
||||
}
|
||||
140
kyaml/yaml/filters.go
Normal file
140
kyaml/yaml/filters.go
Normal file
@@ -0,0 +1,140 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package yaml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var Filters = map[string]func() Filter{
|
||||
"AnnotationClearer": func() Filter { return &AnnotationClearer{} },
|
||||
"AnnotationGetter": func() Filter { return &AnnotationGetter{} },
|
||||
"AnnotationSetter": func() Filter { return &AnnotationSetter{} },
|
||||
"ElementAppender": func() Filter { return &ElementAppender{} },
|
||||
"ElementMatcher": func() Filter { return &ElementMatcher{} },
|
||||
"FieldClearer": func() Filter { return &FieldClearer{} },
|
||||
"FilterMatcher": func() Filter { return &FilterMatcher{} },
|
||||
"FieldMatcher": func() Filter { return &FieldMatcher{} },
|
||||
"FieldSetter": func() Filter { return &FieldSetter{} },
|
||||
"PathGetter": func() Filter { return &PathGetter{} },
|
||||
"PathMatcher": func() Filter { return &PathMatcher{} },
|
||||
"Parser": func() Filter { return &Parser{} },
|
||||
"PrefixSetter": func() Filter { return &PrefixSetter{} },
|
||||
"ValueReplacer": func() Filter { return &ValueReplacer{} },
|
||||
"SuffixSetter": func() Filter { return &SuffixSetter{} },
|
||||
"TeePiper": func() Filter { return &TeePiper{} },
|
||||
}
|
||||
|
||||
// YFilter wraps the GrepFilter interface so it can be unmarshalled into a struct.
|
||||
type YFilter struct {
|
||||
Filter
|
||||
}
|
||||
|
||||
func (y YFilter) MarshalYAML() (interface{}, error) {
|
||||
return y.Filter, nil
|
||||
}
|
||||
|
||||
func (y *YFilter) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
meta := &ResourceMeta{}
|
||||
if err := unmarshal(meta); err != nil {
|
||||
return err
|
||||
}
|
||||
if filter, found := Filters[meta.Kind]; !found {
|
||||
var knownFilters []string
|
||||
for k := range Filters {
|
||||
knownFilters = append(knownFilters, k)
|
||||
}
|
||||
sort.Strings(knownFilters)
|
||||
return fmt.Errorf("unsupported GrepFilter Kind %s: may be one of: [%s]",
|
||||
meta.Kind, strings.Join(knownFilters, ","))
|
||||
} else {
|
||||
y.Filter = filter()
|
||||
}
|
||||
if err := unmarshal(y.Filter); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type YFilters []YFilter
|
||||
|
||||
func (y YFilters) Filters() []Filter {
|
||||
var f []Filter
|
||||
for i := range y {
|
||||
f = append(f, y[i].Filter)
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
type FilterMatcher struct {
|
||||
Kind string `yaml:"kind"`
|
||||
|
||||
// Filters are the set of Filters run by TeePiper.
|
||||
Filters YFilters `yaml:"pipeline,omitempty"`
|
||||
}
|
||||
|
||||
func (t FilterMatcher) Filter(rn *RNode) (*RNode, error) {
|
||||
v, err := rn.Pipe(t.Filters.Filters()...)
|
||||
if v == nil || err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// return the original input if the pipeline resolves to true
|
||||
return rn, err
|
||||
}
|
||||
|
||||
type ValueReplacer struct {
|
||||
Kind string `yaml:"kind"`
|
||||
|
||||
StringMatch string `yaml:"stringMatch"`
|
||||
RegexMatch string `yaml:"regexMatch"`
|
||||
Replace string `yaml:"replace"`
|
||||
Count int `yaml:"count"`
|
||||
}
|
||||
|
||||
func (s ValueReplacer) Filter(object *RNode) (*RNode, error) {
|
||||
if s.Count == 0 {
|
||||
s.Count = -1
|
||||
}
|
||||
if s.StringMatch != "" {
|
||||
object.value.Value = strings.Replace(object.value.Value, s.StringMatch, s.Replace, s.Count)
|
||||
} else if s.RegexMatch != "" {
|
||||
r, err := regexp.Compile(s.RegexMatch)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ValueReplacer RegexMatch does not compile: %v", err)
|
||||
}
|
||||
object.value.Value = r.ReplaceAllString(object.value.Value, s.Replace)
|
||||
} else {
|
||||
return nil, fmt.Errorf("ValueReplacer missing StringMatch and RegexMatch")
|
||||
}
|
||||
return object, nil
|
||||
}
|
||||
|
||||
type PrefixSetter struct {
|
||||
Kind string `yaml:"kind"`
|
||||
|
||||
Value string `yaml:"value"`
|
||||
}
|
||||
|
||||
func (s PrefixSetter) Filter(object *RNode) (*RNode, error) {
|
||||
if !strings.HasPrefix(object.value.Value, s.Value) {
|
||||
object.value.Value = s.Value + object.value.Value
|
||||
}
|
||||
return object, nil
|
||||
}
|
||||
|
||||
type SuffixSetter struct {
|
||||
Kind string `yaml:"kind"`
|
||||
|
||||
Value string `yaml:"value"`
|
||||
}
|
||||
|
||||
func (s SuffixSetter) Filter(object *RNode) (*RNode, error) {
|
||||
if !strings.HasSuffix(object.value.Value, s.Value) {
|
||||
object.value.Value = object.value.Value + s.Value
|
||||
}
|
||||
return object, nil
|
||||
}
|
||||
585
kyaml/yaml/fns.go
Normal file
585
kyaml/yaml/fns.go
Normal file
@@ -0,0 +1,585 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package yaml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/go-errors/errors"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Append creates an ElementAppender
|
||||
func Append(elements ...*yaml.Node) ElementAppender {
|
||||
return ElementAppender{Elements: elements}
|
||||
}
|
||||
|
||||
// ElementAppender adds all element to a SequenceNode's Content.
|
||||
// Returns Elements[0] if len(Elements) == 1, otherwise returns nil.
|
||||
type ElementAppender struct {
|
||||
Kind string `yaml:"kind,omitempty"`
|
||||
|
||||
// Elem is the value to append.
|
||||
Elements []*yaml.Node `yaml:"elements,omitempty"`
|
||||
}
|
||||
|
||||
func (a ElementAppender) Filter(rn *RNode) (*RNode, error) {
|
||||
if err := ErrorIfInvalid(rn, yaml.SequenceNode); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range a.Elements {
|
||||
rn.YNode().Content = append(rn.Content(), a.Elements[i])
|
||||
}
|
||||
if len(a.Elements) == 1 {
|
||||
return NewRNode(a.Elements[0]), nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// ElementSetter sets the value for an Element in an associative list. ElementSetter
|
||||
// will remove any elements which are empty.
|
||||
type ElementSetter struct {
|
||||
Kind string `yaml:"kind,omitempty"`
|
||||
|
||||
// Element is the new value to set -- remove the existing element if nil
|
||||
Element *Node
|
||||
|
||||
// Key is a field on the elements. It is used to find the matching element to
|
||||
// update / delete.
|
||||
Key string `yaml:"key,omitempty"`
|
||||
|
||||
// Value is a field value on the elements. It is used to find matching elements to
|
||||
// update / delete.
|
||||
Value string `yaml:"value,omitempty"`
|
||||
}
|
||||
|
||||
func (e ElementSetter) Filter(rn *RNode) (*RNode, error) {
|
||||
if err := ErrorIfInvalid(rn, SequenceNode); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// build the new Content slice
|
||||
var newContent []*yaml.Node
|
||||
matchingElementFound := false
|
||||
for i := range rn.YNode().Content {
|
||||
elem := rn.Content()[i]
|
||||
|
||||
// empty elements are not valid -- they at least need an associative key
|
||||
if IsEmpty(NewRNode(elem)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// check if this is the element we are matching
|
||||
val, err := NewRNode(elem).Pipe(FieldMatcher{Name: e.Key, StringValue: e.Value})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if val == nil {
|
||||
// not the element we are looking for, keep it in the Content
|
||||
newContent = append(newContent, elem)
|
||||
continue
|
||||
}
|
||||
matchingElementFound = true
|
||||
|
||||
// deletion operation -- remove the element from the new Content
|
||||
if e.Element == nil {
|
||||
continue
|
||||
}
|
||||
// replace operation -- replace the element in the Content
|
||||
newContent = append(newContent, e.Element)
|
||||
}
|
||||
rn.YNode().Content = newContent
|
||||
|
||||
// deletion operation -- return nil
|
||||
if IsMissingOrNull(NewRNode(e.Element)) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// append operation -- add the element to the Content
|
||||
if !matchingElementFound {
|
||||
rn.YNode().Content = append(rn.YNode().Content, e.Element)
|
||||
}
|
||||
|
||||
return NewRNode(e.Element), nil
|
||||
}
|
||||
|
||||
// Clear returns a FieldClearer
|
||||
func Clear(name string) FieldClearer {
|
||||
return FieldClearer{Name: name}
|
||||
}
|
||||
|
||||
// FieldClearer removes the field or map key.
|
||||
// Returns a RNode with the removed field or map entry.
|
||||
type FieldClearer struct {
|
||||
Kind string `yaml:"kind,omitempty"`
|
||||
|
||||
// Name is the name of the field or key in the map.
|
||||
Name string `yaml:"name,omitempty"`
|
||||
|
||||
IfEmpty bool `yaml:"ifEmpty,omitempty"`
|
||||
}
|
||||
|
||||
func (c FieldClearer) Filter(rn *RNode) (*RNode, error) {
|
||||
if err := ErrorIfInvalid(rn, yaml.MappingNode); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i := 0; i < len(rn.Content()); i += 2 {
|
||||
|
||||
// if name matches, remove these 2 elements from the list because
|
||||
// they are treated as a fieldName/fieldValue pair.
|
||||
if rn.Content()[i].Value == c.Name {
|
||||
if c.IfEmpty {
|
||||
if len(rn.Content()[i+1].Content) > 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// save the item we are about to remove
|
||||
removed := NewRNode(rn.Content()[i+1])
|
||||
if len(rn.YNode().Content) > i+2 {
|
||||
// remove from the middle of the list
|
||||
rn.YNode().Content = append(
|
||||
rn.Content()[:i],
|
||||
rn.Content()[i+2:len(rn.YNode().Content)]...)
|
||||
} else {
|
||||
// remove from the end of the list
|
||||
rn.YNode().Content = rn.Content()[:i]
|
||||
}
|
||||
|
||||
// return the removed field name and value
|
||||
return removed, nil
|
||||
}
|
||||
}
|
||||
// nothing removed
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func MatchElement(field, value string) ElementMatcher {
|
||||
return ElementMatcher{FieldName: field, FieldValue: value}
|
||||
}
|
||||
|
||||
// ElementMatcher returns the first element from a Sequence matching the
|
||||
// specified field's value.
|
||||
type ElementMatcher struct {
|
||||
Kind string `yaml:"kind,omitempty"`
|
||||
|
||||
// FieldName will attempt to match this field in each list element.
|
||||
// Optional. Leave empty for lists of primitives (ScalarNode).
|
||||
FieldName string `yaml:"name,omitempty"`
|
||||
|
||||
// FieldValue will attempt to match each element field to this value.
|
||||
// For lists of primitives, this will be used to match the primitive value.
|
||||
FieldValue string `yaml:"value,omitempty"`
|
||||
|
||||
// Create will create the Element if it is not found
|
||||
Create *RNode `yaml:"create,omitempty"`
|
||||
}
|
||||
|
||||
func (e ElementMatcher) Filter(rn *RNode) (*RNode, error) {
|
||||
if err := ErrorIfInvalid(rn, yaml.SequenceNode); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// SequenceNode Content is a slice of ScalarNodes. Each ScalarNode has a
|
||||
// YNode containing the primitive data.
|
||||
if len(e.FieldName) == 0 {
|
||||
for i := range rn.Content() {
|
||||
if rn.Content()[i].Value == e.FieldValue {
|
||||
return &RNode{value: rn.Content()[i]}, nil
|
||||
}
|
||||
}
|
||||
if e.Create != nil {
|
||||
return rn.Pipe(Append(e.Create.YNode()))
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// SequenceNode Content is a slice of MappingNodes. Each MappingNode has Content
|
||||
// with a slice of key-value pairs containing the fields.
|
||||
for i := range rn.Content() {
|
||||
// cast the entry to a RNode so we can operate on it
|
||||
elem := NewRNode(rn.Content()[i])
|
||||
|
||||
field, err := elem.Pipe(MatchField(e.FieldName, e.FieldValue))
|
||||
if IsFoundOrError(field, err) {
|
||||
return elem, err
|
||||
}
|
||||
}
|
||||
|
||||
// create the element
|
||||
if e.Create != nil {
|
||||
return rn.Pipe(Append(e.Create.YNode()))
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func Get(name string) FieldMatcher {
|
||||
return FieldMatcher{Name: name}
|
||||
}
|
||||
|
||||
func MatchField(name, value string) FieldMatcher {
|
||||
return FieldMatcher{Name: name, Value: NewScalarRNode(value)}
|
||||
}
|
||||
|
||||
func Match(value string) FieldMatcher {
|
||||
return FieldMatcher{Value: NewScalarRNode(value)}
|
||||
}
|
||||
|
||||
// FieldMatcher returns the value of a named field or map entry.
|
||||
type FieldMatcher struct {
|
||||
Kind string `yaml:"kind,omitempty"`
|
||||
|
||||
// Name of the field to return
|
||||
Name string `yaml:"name,omitempty"`
|
||||
|
||||
// YNode of the field to return.
|
||||
// Optional. Will only need to match field name if unset.
|
||||
Value *RNode `yaml:"value,omitempty"`
|
||||
|
||||
StringValue string `yaml:"stringValue,omitempty"`
|
||||
|
||||
StringRegexValue string `yaml:"stringRegexValue,omitempty"`
|
||||
|
||||
// Create will cause the field to be created with this value
|
||||
// if it is set.
|
||||
Create *RNode `yaml:"create,omitempty"`
|
||||
}
|
||||
|
||||
func (f FieldMatcher) Filter(rn *RNode) (*RNode, error) {
|
||||
if f.StringValue != "" && f.Value == nil {
|
||||
f.Value = NewScalarRNode(f.StringValue)
|
||||
}
|
||||
|
||||
// never match nil or null fields
|
||||
if IsMissingOrNull(rn) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if f.Name == "" {
|
||||
if err := ErrorIfInvalid(rn, yaml.ScalarNode); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if f.StringRegexValue != "" {
|
||||
// TODO(pwittrock): pre-compile this when unmarshalling and cache to a field
|
||||
rg, err := regexp.Compile(f.StringRegexValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if match := rg.MatchString(rn.value.Value); match {
|
||||
return rn, nil
|
||||
}
|
||||
return nil, nil
|
||||
} else if rn.value.Value == f.Value.YNode().Value {
|
||||
return rn, nil
|
||||
} else {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
if err := ErrorIfInvalid(rn, yaml.MappingNode); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i := 0; i < len(rn.Content()); IncrementFieldIndex(&i) {
|
||||
isMatchingField := rn.Content()[i].Value == f.Name
|
||||
if isMatchingField {
|
||||
requireMatchFieldValue := f.Value != nil
|
||||
if !requireMatchFieldValue || rn.Content()[i+1].Value == f.Value.YNode().Value {
|
||||
return NewRNode(rn.Content()[i+1]), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if f.Create != nil {
|
||||
return rn.Pipe(SetField(f.Name, f.Create))
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func Lookup(path ...string) PathGetter {
|
||||
return PathGetter{Path: path}
|
||||
}
|
||||
|
||||
func LookupCreate(kind yaml.Kind, path ...string) PathGetter {
|
||||
return PathGetter{Path: path, Create: kind}
|
||||
}
|
||||
|
||||
// PathGetter returns the RNode under Path.
|
||||
type PathGetter struct {
|
||||
Kind string `yaml:"kind,omitempty"`
|
||||
|
||||
// Path is a slice of parts leading to the RNode to lookup.
|
||||
// Each path part may be one of:
|
||||
// * FieldMatcher -- e.g. "spec"
|
||||
// * Map Key -- e.g. "app.k8s.io/version"
|
||||
// * List Entry -- e.g. "[name=nginx]" or "[=-jar]"
|
||||
//
|
||||
// Map Keys and Fields are equivalent.
|
||||
// See FieldMatcher for more on Fields and Map Keys.
|
||||
//
|
||||
// List Entries are specified as map entry to match [fieldName=fieldValue].
|
||||
// See Elem for more on List Entries.
|
||||
//
|
||||
// Examples:
|
||||
// * spec.template.spec.container with matching name: [name=nginx]
|
||||
// * spec.template.spec.container.argument matching a value: [=-jar]
|
||||
Path []string `yaml:"path,omitempty"`
|
||||
|
||||
// Create will cause missing path parts to be created as they are walked.
|
||||
//
|
||||
// * The leaf Node (final path) will be created with a Kind matching Create
|
||||
// * Intermediary Nodes will be created as either a MappingNodes or
|
||||
// SequenceNodes as appropriate for each's Path location.
|
||||
Create yaml.Kind `yaml:"create,omitempty"`
|
||||
|
||||
// Style is the style to apply to created value Nodes.
|
||||
// Created key Nodes keep an unspecified Style.
|
||||
Style yaml.Style `yaml:"style,omitempty"`
|
||||
}
|
||||
|
||||
func (l PathGetter) Filter(rn *RNode) (*RNode, error) {
|
||||
var err error
|
||||
fieldPath := append([]string{}, rn.FieldPath()...)
|
||||
match := rn
|
||||
|
||||
// iterate over path until encountering an error or missing value
|
||||
l.Path = cleanPath(l.Path)
|
||||
for i := range l.Path {
|
||||
var part, nextPart string
|
||||
part = l.Path[i]
|
||||
if len(l.Path) > i+1 {
|
||||
nextPart = l.Path[i+1]
|
||||
}
|
||||
if IsListIndex(part) {
|
||||
match, err = l.doElem(match, part)
|
||||
} else {
|
||||
fieldPath = append(fieldPath, part)
|
||||
match, err = l.doField(match, part, l.getKind(nextPart))
|
||||
}
|
||||
if IsMissingOrError(match, err) {
|
||||
return nil, err
|
||||
}
|
||||
match.AppendToFieldPath(fieldPath...)
|
||||
}
|
||||
return match, nil
|
||||
}
|
||||
|
||||
func (l PathGetter) doElem(rn *RNode, part string) (*RNode, error) {
|
||||
var match *RNode
|
||||
name, value, err := SplitIndexNameValue(part)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !IsCreate(l.Create) {
|
||||
return rn.Pipe(MatchElement(name, value))
|
||||
}
|
||||
|
||||
var elem *RNode
|
||||
primitiveElement := len(name) == 0
|
||||
if primitiveElement {
|
||||
// append a ScalarNode
|
||||
elem = NewScalarRNode(value)
|
||||
elem.YNode().Style = l.Style
|
||||
match = elem
|
||||
} else {
|
||||
// append a MappingNode
|
||||
match = NewRNode(&yaml.Node{Kind: yaml.ScalarNode, Value: value, Style: l.Style})
|
||||
elem = NewRNode(&yaml.Node{
|
||||
Kind: yaml.MappingNode,
|
||||
Content: []*yaml.Node{{Kind: yaml.ScalarNode, Value: name}, match.YNode()},
|
||||
Style: l.Style,
|
||||
})
|
||||
}
|
||||
// Append the Node
|
||||
return rn.Pipe(ElementMatcher{FieldName: name, FieldValue: value, Create: elem})
|
||||
}
|
||||
|
||||
func (l PathGetter) doField(
|
||||
rn *RNode, name string, kind yaml.Kind) (*RNode, error) {
|
||||
if !IsCreate(l.Create) {
|
||||
return rn.Pipe(Get(name))
|
||||
}
|
||||
return rn.Pipe(FieldMatcher{Name: name, Create: &RNode{value: &yaml.Node{Kind: kind, Style: l.Style}}})
|
||||
}
|
||||
|
||||
func (l PathGetter) getKind(nextPart string) yaml.Kind {
|
||||
if IsListIndex(nextPart) {
|
||||
// if nextPart is of the form [a=b], then it is an index into a Sequence
|
||||
// so the current part must be a SequenceNode
|
||||
return yaml.SequenceNode
|
||||
}
|
||||
if nextPart == "" {
|
||||
// final name in the path, use the l.Create defined Kind
|
||||
return l.Create
|
||||
}
|
||||
|
||||
// non-sequence intermediate Node
|
||||
return yaml.MappingNode
|
||||
}
|
||||
|
||||
func SetField(name string, value *RNode) FieldSetter {
|
||||
return FieldSetter{Name: name, Value: value}
|
||||
}
|
||||
|
||||
func Set(value *RNode) FieldSetter {
|
||||
return FieldSetter{Value: value}
|
||||
}
|
||||
|
||||
// FieldSetter sets a field or map entry to a value.
|
||||
type FieldSetter struct {
|
||||
Kind string `yaml:"kind,omitempty"`
|
||||
|
||||
// Name is the name of the field or key to lookup in a MappingNode.
|
||||
// If Name is unspecified, and the input is a ScalarNode, FieldSetter will set the
|
||||
// value on the ScalarNode.
|
||||
Name string `yaml:"name,omitempty"`
|
||||
|
||||
// YNode is the value to set.
|
||||
// Optional if Kind is set.
|
||||
Value *RNode `yaml:"value,omitempty"`
|
||||
|
||||
StringValue string `yaml:"stringValue,omitempty"`
|
||||
}
|
||||
|
||||
// FieldSetter returns an GrepFilter that sets the named field to the given value.
|
||||
func (s FieldSetter) Filter(rn *RNode) (*RNode, error) {
|
||||
if s.StringValue != "" && s.Value == nil {
|
||||
s.Value = NewScalarRNode(s.StringValue)
|
||||
}
|
||||
|
||||
if s.Name == "" {
|
||||
if err := ErrorIfInvalid(rn, yaml.ScalarNode); err != nil {
|
||||
return rn, err
|
||||
}
|
||||
rn.SetYNode(s.Value.YNode())
|
||||
return rn, nil
|
||||
}
|
||||
|
||||
// Clear the field if it is empty, or explicitly null
|
||||
if s.Value == nil || IsNull(s.Value) {
|
||||
return rn.Pipe(Clear(s.Name))
|
||||
}
|
||||
|
||||
field, err := rn.Pipe(FieldMatcher{Name: s.Name})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if field != nil {
|
||||
// need to def ref the Node since field is ephemeral
|
||||
field.SetYNode(s.Value.YNode())
|
||||
return field, nil
|
||||
}
|
||||
|
||||
// create the field
|
||||
rn.YNode().Content = append(rn.YNode().Content,
|
||||
&yaml.Node{Kind: yaml.ScalarNode, Value: s.Name},
|
||||
s.Value.YNode())
|
||||
return s.Value, nil
|
||||
}
|
||||
|
||||
// Tee calls the provided Filters, and returns its argument rather than the result
|
||||
// of the filters.
|
||||
// May be used to fork sub-filters from a call.
|
||||
// e.g. locate field, set value; locate another field, set another value
|
||||
func Tee(filters ...Filter) Filter {
|
||||
return TeePiper{Filters: filters}
|
||||
}
|
||||
|
||||
// TeePiper Calls a slice of Filters and returns its input.
|
||||
// May be used to fork sub-filters from a call.
|
||||
// e.g. locate field, set value; locate another field, set another value
|
||||
type TeePiper struct {
|
||||
Kind string `yaml:"kind,omitempty"`
|
||||
|
||||
// Filters are the set of Filters run by TeePiper.
|
||||
Filters []Filter `yaml:"filters,omitempty"`
|
||||
}
|
||||
|
||||
func (t TeePiper) Filter(rn *RNode) (*RNode, error) {
|
||||
_, err := rn.Pipe(t.Filters...)
|
||||
return rn, err
|
||||
}
|
||||
|
||||
// IsCreate returns true if kind is specified
|
||||
func IsCreate(kind yaml.Kind) bool {
|
||||
return kind != 0
|
||||
}
|
||||
|
||||
// IsMissingOrError returns true if rn is NOT found or err is non-nil
|
||||
func IsMissingOrError(rn *RNode, err error) bool {
|
||||
return rn == nil || err != nil
|
||||
}
|
||||
|
||||
// IsFoundOrError returns true if rn is found or err is non-nil
|
||||
func IsFoundOrError(rn *RNode, err error) bool {
|
||||
return rn != nil || err != nil
|
||||
}
|
||||
|
||||
func ErrorIfAnyInvalidAndNonNull(kind yaml.Kind, rn ...*RNode) error {
|
||||
for i := range rn {
|
||||
if IsEmpty(rn[i]) {
|
||||
continue
|
||||
}
|
||||
if err := ErrorIfInvalid(rn[i], kind); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ErrorIfInvalid(rn *RNode, kind yaml.Kind) error {
|
||||
if rn == nil || rn.YNode() == nil || IsNull(rn) {
|
||||
// node has no type, pass validation
|
||||
return nil
|
||||
}
|
||||
|
||||
if rn.YNode().Kind != kind {
|
||||
s, _ := rn.String()
|
||||
return errors.Errorf(
|
||||
"wrong Node Kind for %s expected: %v was %v: value: {%s}",
|
||||
strings.Join(rn.FieldPath(), "."),
|
||||
kind, rn.YNode().Kind, strings.TrimSpace(s))
|
||||
}
|
||||
|
||||
if kind == yaml.MappingNode {
|
||||
if len(rn.YNode().Content)%2 != 0 {
|
||||
return fmt.Errorf("yaml MappingNodes must have even length contents: %v", spew.Sdump(rn))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsListIndex returns true if p is an index into a Val.
|
||||
// e.g. [fieldName=fieldValue]
|
||||
// e.g. [=primitiveValue]
|
||||
func IsListIndex(p string) bool {
|
||||
return strings.HasPrefix(p, "[") && strings.HasSuffix(p, "]")
|
||||
}
|
||||
|
||||
// SplitIndexNameValue splits a lookup part Val index into the field name
|
||||
// and field value to match.
|
||||
// e.g. splits [name=nginx] into (name, nginx)
|
||||
// e.g. splits [=-jar] into ("", jar)
|
||||
func SplitIndexNameValue(p string) (string, string, error) {
|
||||
elem := strings.TrimSuffix(p, "]")
|
||||
elem = strings.TrimPrefix(elem, "[")
|
||||
parts := strings.SplitN(elem, "=", 2)
|
||||
if len(parts) == 1 {
|
||||
return "", "", fmt.Errorf("list path element must contain fieldName=fieldValue for element to match")
|
||||
}
|
||||
return parts[0], parts[1], nil
|
||||
}
|
||||
|
||||
// IncrementFieldIndex increments i to point to the next field name element in
|
||||
// a slice of Contents.
|
||||
func IncrementFieldIndex(i *int) {
|
||||
*i = *i + 2
|
||||
}
|
||||
704
kyaml/yaml/fns_test.go
Normal file
704
kyaml/yaml/fns_test.go
Normal file
@@ -0,0 +1,704 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package yaml_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/yaml.v3"
|
||||
. "sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
func TestResourceNode_SetValue(t *testing.T) {
|
||||
instance := *NewScalarRNode("foo")
|
||||
copy := instance
|
||||
instance.SetYNode(&yaml.Node{Kind: yaml.ScalarNode, Value: "bar"})
|
||||
assert.Equal(t, `bar
|
||||
`, assertNoErrorString(t)(copy.String()))
|
||||
assert.Equal(t, `bar
|
||||
`, assertNoErrorString(t)(instance.String()))
|
||||
|
||||
instance = *NewScalarRNode("foo")
|
||||
copy = instance
|
||||
instance.SetYNode(nil)
|
||||
instance.SetYNode(&yaml.Node{Kind: yaml.ScalarNode, Value: "bar"})
|
||||
assert.Equal(t, `foo
|
||||
`, assertNoErrorString(t)(copy.String()))
|
||||
assert.Equal(t, `bar
|
||||
`, assertNoErrorString(t)(instance.String()))
|
||||
}
|
||||
|
||||
func TestAppend(t *testing.T) {
|
||||
s := `n: o
|
||||
a: b
|
||||
c: d
|
||||
`
|
||||
node, err := Parse(s)
|
||||
assert.NoError(t, err)
|
||||
rn, err := node.Pipe(Append(NewScalarRNode("").YNode()))
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "wrong Node Kind")
|
||||
}
|
||||
assert.Nil(t, rn)
|
||||
|
||||
s = `- a
|
||||
- b
|
||||
`
|
||||
node, err = Parse(s)
|
||||
assert.NoError(t, err)
|
||||
rn, err = node.Pipe(Append())
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, rn)
|
||||
}
|
||||
|
||||
func TestClearField_Fn(t *testing.T) {
|
||||
s := `n: o
|
||||
a: b
|
||||
c: d
|
||||
`
|
||||
node, err := Parse(s)
|
||||
assert.NoError(t, err)
|
||||
rn, err := node.Pipe(FieldClearer{Name: "a"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "n: o\nc: d\n", assertNoErrorString(t)(node.String()))
|
||||
assert.Equal(t, "b\n", assertNoErrorString(t)(rn.String()))
|
||||
|
||||
s = `n: o
|
||||
a: b
|
||||
c: d
|
||||
`
|
||||
node, err = Parse(s)
|
||||
assert.NoError(t, err)
|
||||
rn, err = node.Pipe(FieldClearer{Name: "n"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "a: b\nc: d\n", assertNoErrorString(t)(node.String()))
|
||||
assert.Equal(t, "o\n", assertNoErrorString(t)(rn.String()))
|
||||
|
||||
node, err = Parse(s)
|
||||
assert.NoError(t, err)
|
||||
rn, err = node.Pipe(FieldClearer{Name: "c"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "n: o\na: b\n", assertNoErrorString(t)(node.String()))
|
||||
assert.Equal(t, "d\n", assertNoErrorString(t)(rn.String()))
|
||||
|
||||
s = `n: o
|
||||
a: b
|
||||
`
|
||||
node, err = Parse(s)
|
||||
assert.NoError(t, err)
|
||||
rn, err = node.Pipe(FieldClearer{Name: "o"})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, rn)
|
||||
assert.Equal(t, "n: o\na: b\n", assertNoErrorString(t)(node.String()))
|
||||
|
||||
s = `- a
|
||||
- b
|
||||
`
|
||||
node, err = Parse(s)
|
||||
assert.NoError(t, err)
|
||||
rn, err = node.Pipe(FieldClearer{Name: "a"})
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "wrong Node Kind")
|
||||
}
|
||||
assert.Nil(t, rn)
|
||||
assert.Equal(t, s, assertNoErrorString(t)(node.String()))
|
||||
|
||||
// should not clear n because it is not empty
|
||||
s = `n:
|
||||
k: v
|
||||
a: b
|
||||
c: d
|
||||
`
|
||||
node, err = Parse(s)
|
||||
assert.NoError(t, err)
|
||||
rn, err = node.Pipe(FieldClearer{Name: "n", IfEmpty: true})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "n:\n k: v\na: b\nc: d\n", assertNoErrorString(t)(node.String()))
|
||||
assert.Equal(t, "", assertNoErrorString(t)(rn.String()))
|
||||
|
||||
// should clear n because it is empty
|
||||
s = `n: {}
|
||||
a: b
|
||||
c: d
|
||||
`
|
||||
node, err = Parse(s)
|
||||
assert.NoError(t, err)
|
||||
rn, err = node.Pipe(FieldClearer{Name: "n", IfEmpty: true})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "a: b\nc: d\n", assertNoErrorString(t)(node.String()))
|
||||
assert.Equal(t, "{}\n", assertNoErrorString(t)(rn.String()))
|
||||
}
|
||||
|
||||
var s = `n: o
|
||||
a:
|
||||
l: m
|
||||
b:
|
||||
- f: g
|
||||
- c: e
|
||||
- h: i
|
||||
r: s
|
||||
`
|
||||
|
||||
func TestLookup_Fn_create(t *testing.T) {
|
||||
|
||||
// primitive
|
||||
node, err := Parse(s)
|
||||
assert.NoError(t, err)
|
||||
rn, err := node.Pipe(PathGetter{
|
||||
Path: []string{"a", "b", "[c=d]", "t", "f", "[=h]"},
|
||||
Create: yaml.ScalarNode,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `n: o
|
||||
a:
|
||||
l: m
|
||||
b:
|
||||
- f: g
|
||||
- c: e
|
||||
- h: i
|
||||
- c: d
|
||||
t:
|
||||
f:
|
||||
- h
|
||||
r: s
|
||||
`, assertNoErrorString(t)(node.String()))
|
||||
assert.Equal(t, `h
|
||||
`, assertNoErrorString(t)(rn.String()))
|
||||
|
||||
}
|
||||
|
||||
func TestLookup_Fn_create2(t *testing.T) {
|
||||
node, err := Parse(s)
|
||||
assert.NoError(t, err)
|
||||
rn, err := node.Pipe(PathGetter{
|
||||
Path: []string{"a", "b", "[c=d]", "t", "f"},
|
||||
Create: yaml.SequenceNode,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `n: o
|
||||
a:
|
||||
l: m
|
||||
b:
|
||||
- f: g
|
||||
- c: e
|
||||
- h: i
|
||||
- c: d
|
||||
t:
|
||||
f: []
|
||||
r: s
|
||||
`, assertNoErrorString(t)(node.String()))
|
||||
assert.Equal(t, `[]
|
||||
`, assertNoErrorString(t)(rn.String()))
|
||||
}
|
||||
|
||||
func TestLookup_Fn_create3(t *testing.T) {
|
||||
node, err := Parse(s)
|
||||
assert.NoError(t, err)
|
||||
rn, err := node.Pipe(LookupCreate(yaml.MappingNode, "a", "b", "[c=d]", "t"))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `n: o
|
||||
a:
|
||||
l: m
|
||||
b:
|
||||
- f: g
|
||||
- c: e
|
||||
- h: i
|
||||
- c: d
|
||||
t: {}
|
||||
r: s
|
||||
`, assertNoErrorString(t)(node.String()))
|
||||
assert.Equal(t, `{}
|
||||
`, assertNoErrorString(t)(rn.String()))
|
||||
}
|
||||
|
||||
func TestLookupCreate_4(t *testing.T) {
|
||||
node, err := Parse(`
|
||||
a: {}
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
rn, err := node.Pipe(
|
||||
LookupCreate(yaml.MappingNode, "a", "b", "[c=d]", "t", "f", "[=h]"))
|
||||
|
||||
node.YNode().Style = yaml.FlowStyle
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "{a: {b: [{c: d, t: {f: [h]}}]}}\n", assertNoErrorString(t)(node.String()))
|
||||
assert.Equal(t, "h\n", assertNoErrorString(t)(rn.String()))
|
||||
}
|
||||
|
||||
func TestLookup(t *testing.T) {
|
||||
s := `n: o
|
||||
a:
|
||||
l: m
|
||||
b:
|
||||
- f: g
|
||||
- c: e
|
||||
- c: d
|
||||
t:
|
||||
u: v
|
||||
f:
|
||||
- g
|
||||
- h
|
||||
- i
|
||||
j: k
|
||||
- h: i
|
||||
p: q
|
||||
r: s
|
||||
`
|
||||
node, err := Parse(s)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// primitive
|
||||
rn, err := node.Pipe(Lookup("a", "b", "[c=d]", "t", "f", "[=h]"))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, s, assertNoErrorString(t)(node.String()))
|
||||
assert.Equal(t, `h
|
||||
`, assertNoErrorString(t)(rn.String()))
|
||||
|
||||
// seq
|
||||
rn, err = node.Pipe(Lookup("a", "b", "[c=d]", "t", "f"))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, s, assertNoErrorString(t)(node.String()))
|
||||
assert.Equal(t, `- g
|
||||
- h
|
||||
- i
|
||||
`, assertNoErrorString(t)(rn.String()))
|
||||
|
||||
rn, err = node.Pipe(Lookup("a", "b", "[c=d]", "t"))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, s, assertNoErrorString(t)(node.String()))
|
||||
assert.Equal(t, `u: v
|
||||
f:
|
||||
- g
|
||||
- h
|
||||
- i
|
||||
`, assertNoErrorString(t)(rn.String()))
|
||||
|
||||
rn, err = node.Pipe(Lookup("a", "b", "[c=d]"))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, s, assertNoErrorString(t)(node.String()))
|
||||
assert.Equal(t, `c: d
|
||||
t:
|
||||
u: v
|
||||
f:
|
||||
- g
|
||||
- h
|
||||
- i
|
||||
j: k
|
||||
`, assertNoErrorString(t)(rn.String()))
|
||||
|
||||
rn, err = node.Pipe(Lookup("a", "b"))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, s, assertNoErrorString(t)(node.String()))
|
||||
assert.Equal(t, `- f: g
|
||||
- c: e
|
||||
- c: d
|
||||
t:
|
||||
u: v
|
||||
f:
|
||||
- g
|
||||
- h
|
||||
- i
|
||||
j: k
|
||||
- h: i
|
||||
p: q
|
||||
`, assertNoErrorString(t)(rn.String()))
|
||||
|
||||
rn, err = node.Pipe(Lookup("l"))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, s, assertNoErrorString(t)(node.String()))
|
||||
assert.Nil(t, rn)
|
||||
|
||||
rn, err = node.Pipe(Lookup("zzz"))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, s, assertNoErrorString(t)(node.String()))
|
||||
assert.Nil(t, rn)
|
||||
|
||||
rn, err = node.Pipe(Lookup("[a=b]"))
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, s, assertNoErrorString(t)(node.String()))
|
||||
assert.Nil(t, rn)
|
||||
|
||||
rn, err = node.Pipe(Lookup("a", "b", "f"))
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, s, assertNoErrorString(t)(node.String()))
|
||||
assert.Nil(t, rn)
|
||||
|
||||
rn, err = node.Pipe(Lookup("a", "b", "c=zzz"))
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, s, assertNoErrorString(t)(node.String()))
|
||||
assert.Nil(t, rn)
|
||||
|
||||
rn, err = node.Pipe(Lookup(" ", "a", "", "b", " ", "[c=d]", "\n", "t", "\t", "f", " ", "[=h]", " "))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, s, assertNoErrorString(t)(node.String()))
|
||||
assert.Equal(t, `h
|
||||
`, assertNoErrorString(t)(rn.String()))
|
||||
|
||||
rn, err = node.Pipe(Lookup(" ", "a", "", "b", " ", "[]"))
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, rn)
|
||||
|
||||
rn, err = node.Pipe(Lookup("a", "b", "[c=d]", "t", "f", "[=c]"))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, s, assertNoErrorString(t)(node.String()))
|
||||
assert.Nil(t, rn)
|
||||
|
||||
rn, err = node.Pipe(Lookup("a", "b", "[z=z]"))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, s, assertNoErrorString(t)(node.String()))
|
||||
assert.Nil(t, rn)
|
||||
}
|
||||
|
||||
func TestSetField_Fn(t *testing.T) {
|
||||
// Change field
|
||||
node, err := Parse(`
|
||||
foo: baz
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
instance := FieldSetter{
|
||||
Name: "foo",
|
||||
Value: NewScalarRNode("bar"),
|
||||
}
|
||||
k, err := instance.Filter(node)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `foo: bar
|
||||
`, assertNoErrorString(t)(node.String()))
|
||||
assert.Equal(t, `bar
|
||||
`, assertNoErrorString(t)(k.String()))
|
||||
|
||||
// Add field
|
||||
node, err = Parse(`
|
||||
foo: baz
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
instance = FieldSetter{
|
||||
Name: "bar",
|
||||
Value: NewScalarRNode("buz"),
|
||||
}
|
||||
k, err = instance.Filter(node)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `foo: baz
|
||||
bar: buz
|
||||
`, assertNoErrorString(t)(node.String()))
|
||||
assert.Equal(t, `buz
|
||||
`, assertNoErrorString(t)(k.String()))
|
||||
|
||||
// Clear field
|
||||
node, err = Parse(`
|
||||
foo: baz
|
||||
bar: buz
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
instance = FieldSetter{
|
||||
Name: "foo",
|
||||
}
|
||||
k, err = instance.Filter(node)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `bar: buz
|
||||
`, assertNoErrorString(t)(node.String()))
|
||||
assert.Equal(t, `baz
|
||||
`, assertNoErrorString(t)(k.String()))
|
||||
|
||||
// Encounter error
|
||||
node, err = Parse(`
|
||||
-a
|
||||
-b
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
instance = FieldSetter{
|
||||
Name: "foo",
|
||||
Value: NewScalarRNode("v"),
|
||||
}
|
||||
k, err = instance.Filter(node)
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "wrong Node Kind")
|
||||
}
|
||||
assert.Nil(t, k)
|
||||
}
|
||||
|
||||
func TestSet_Fn(t *testing.T) {
|
||||
node, err := Parse(`
|
||||
foo: baz
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
k, err := node.Pipe(Get("foo"), Set(NewScalarRNode("bar")))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `foo: bar
|
||||
`, assertNoErrorString(t)(node.String()))
|
||||
assert.Equal(t, `bar
|
||||
`, assertNoErrorString(t)(k.String()))
|
||||
|
||||
node, err = Parse(`
|
||||
foo: baz
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
_, err = node.Pipe(Set(NewScalarRNode("bar")))
|
||||
if !assert.Error(t, err) {
|
||||
return
|
||||
}
|
||||
assert.Contains(t, err.Error(), "wrong Node Kind")
|
||||
assert.Equal(t, `foo: baz
|
||||
`, assertNoErrorString(t)(node.String()))
|
||||
|
||||
}
|
||||
|
||||
func TestErrorIfInvalid(t *testing.T) {
|
||||
err := ErrorIfInvalid(
|
||||
NewRNode(&yaml.Node{Kind: yaml.SequenceNode}), yaml.SequenceNode)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// nil values should pass validation -- they were not specified
|
||||
err = ErrorIfInvalid(&RNode{}, yaml.SequenceNode)
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
err = ErrorIfInvalid(NewRNode(&Node{Content: []*yaml.Node{{Value: "hello"}}}), yaml.SequenceNode)
|
||||
if !assert.Error(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.Contains(t, err.Error(), "wrong Node Kind")
|
||||
|
||||
err = ErrorIfInvalid(NewRNode(&yaml.Node{}), yaml.SequenceNode)
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "wrong Node Kind")
|
||||
}
|
||||
err = ErrorIfInvalid(NewRNode(&yaml.Node{}), yaml.MappingNode)
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "wrong Node Kind")
|
||||
}
|
||||
|
||||
err = ErrorIfInvalid(NewRNode(&yaml.Node{
|
||||
Kind: yaml.MappingNode,
|
||||
Content: []*yaml.Node{{}, {}},
|
||||
}), yaml.MappingNode)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = ErrorIfInvalid(NewRNode(&yaml.Node{}), yaml.SequenceNode)
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "wrong Node Kind")
|
||||
}
|
||||
|
||||
err = ErrorIfInvalid(NewRNode(&yaml.Node{
|
||||
Kind: yaml.MappingNode,
|
||||
Content: []*yaml.Node{{}},
|
||||
}), yaml.MappingNode)
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "even length")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitIndexNameValue(t *testing.T) {
|
||||
k, v, err := SplitIndexNameValue("")
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "fieldName=fieldValue")
|
||||
}
|
||||
assert.Equal(t, "", k)
|
||||
assert.Equal(t, "", v)
|
||||
|
||||
k, v, err = SplitIndexNameValue("a=b")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "a", k)
|
||||
assert.Equal(t, "b", v)
|
||||
|
||||
k, v, err = SplitIndexNameValue("=b")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", k)
|
||||
assert.Equal(t, "b", v)
|
||||
|
||||
k, v, err = SplitIndexNameValue("a=b=c")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "a", k)
|
||||
assert.Equal(t, "b=c", v)
|
||||
}
|
||||
|
||||
type filter struct {
|
||||
fn func(object *RNode) (*RNode, error)
|
||||
}
|
||||
|
||||
func (c filter) Filter(object *RNode) (*RNode, error) {
|
||||
return c.fn(object)
|
||||
}
|
||||
|
||||
func TestResourceNode_Pipe(t *testing.T) {
|
||||
r0, r1, r2, r3 := &RNode{}, &RNode{}, &RNode{}, &RNode{}
|
||||
var called []string
|
||||
|
||||
// check the nil value case
|
||||
r0 = nil
|
||||
_, err := r0.Pipe(FieldMatcher{Name: "foo"})
|
||||
assert.NoError(t, err)
|
||||
|
||||
r0 = &RNode{}
|
||||
// all filters successful
|
||||
v, err := r0.Pipe(
|
||||
filter{fn: func(object *RNode) (*RNode, error) {
|
||||
assert.True(t, r0 == object)
|
||||
called = append(called, "a")
|
||||
return r1, nil
|
||||
}},
|
||||
filter{fn: func(object *RNode) (*RNode, error) {
|
||||
assert.True(t, object == r1, "function arg doesn't match last function output")
|
||||
called = append(called, "b")
|
||||
return r2, nil
|
||||
}},
|
||||
filter{fn: func(object *RNode) (*RNode, error) {
|
||||
assert.True(t, object == r2, "function arg doesn't match last function output")
|
||||
return r3, nil
|
||||
}},
|
||||
)
|
||||
assert.True(t, v == r3, "expected r3")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, called, []string{"a", "b"})
|
||||
|
||||
// filter returns nil
|
||||
called = []string{}
|
||||
v, err = r0.Pipe(
|
||||
filter{fn: func(object *RNode) (*RNode, error) {
|
||||
assert.True(t, r0 == object)
|
||||
called = append(called, "a")
|
||||
return r1, nil
|
||||
}},
|
||||
filter{fn: func(object *RNode) (*RNode, error) {
|
||||
assert.True(t, object == r1, "function arg doesn't match last function output")
|
||||
called = append(called, "b")
|
||||
return nil, nil
|
||||
}},
|
||||
filter{fn: func(object *RNode) (*RNode, error) {
|
||||
assert.Fail(t, "function should be run after error")
|
||||
return nil, nil
|
||||
}},
|
||||
)
|
||||
assert.Nil(t, v)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, called, []string{"a", "b"})
|
||||
|
||||
// filter returns an error
|
||||
called = []string{}
|
||||
v, err = r0.Pipe(
|
||||
filter{fn: func(object *RNode) (*RNode, error) {
|
||||
assert.True(t, r0 == object)
|
||||
called = append(called, "a")
|
||||
return r1, nil
|
||||
}},
|
||||
filter{fn: func(object *RNode) (*RNode, error) {
|
||||
assert.True(t, object == r1, "function arg doesn't match last function output")
|
||||
called = append(called, "b")
|
||||
return r1, fmt.Errorf("expected-error")
|
||||
}},
|
||||
filter{fn: func(object *RNode) (*RNode, error) {
|
||||
assert.Fail(t, "function should be run after error")
|
||||
return nil, nil
|
||||
}},
|
||||
)
|
||||
assert.True(t, v == r1, "expected r1 as value")
|
||||
assert.EqualError(t, err, "expected-error")
|
||||
assert.Equal(t, called, []string{"a", "b"})
|
||||
}
|
||||
|
||||
func TestClearAnnotation(t *testing.T) {
|
||||
// create metadata.annotations field
|
||||
r0 := assertNoError(t)(Parse(`apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
z: y
|
||||
a.b.c: d.e.f
|
||||
s: t
|
||||
`))
|
||||
|
||||
rn := assertNoError(t)(r0.Pipe(ClearAnnotation("a.b.c")))
|
||||
assert.Equal(t, "d.e.f\n", assertNoErrorString(t)(rn.String()))
|
||||
assert.Equal(t, `apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
z: y
|
||||
s: t
|
||||
`, assertNoErrorString(t)(r0.String()))
|
||||
}
|
||||
|
||||
func TestGetAnnotation(t *testing.T) {
|
||||
r0 := assertNoError(t)(Parse(`apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app: java
|
||||
annotations:
|
||||
a.b.c: d.e.f
|
||||
g: h
|
||||
i: j
|
||||
k: l
|
||||
name: app`))
|
||||
|
||||
rn := assertNoError(t)(
|
||||
r0.Pipe(GetAnnotation("a.b.c")))
|
||||
assert.Equal(t, "d.e.f\n", assertNoErrorString(t)(rn.String()))
|
||||
}
|
||||
|
||||
func TestSetAnnotation_Fn(t *testing.T) {
|
||||
// create metadata.annotations field
|
||||
r0 := assertNoError(t)(Parse(`apiVersion: apps/v1
|
||||
kind: Deployment`))
|
||||
|
||||
rn := assertNoError(t)(r0.Pipe(SetAnnotation("a.b.c", "d.e.f")))
|
||||
assert.Equal(t, "d.e.f\n", assertNoErrorString(t)(rn.String()))
|
||||
assert.Equal(t, `apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
a.b.c: d.e.f
|
||||
`, assertNoErrorString(t)(r0.String()))
|
||||
}
|
||||
|
||||
func TestRNode_GetMeta(t *testing.T) {
|
||||
s := `apiVersion: v1/apps
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: foo
|
||||
namespace: bar
|
||||
labels:
|
||||
kl: vl
|
||||
annotations:
|
||||
ka: va
|
||||
`
|
||||
node, err := Parse(s)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
meta, err := node.GetMeta()
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, ResourceMeta{
|
||||
Kind: "Deployment",
|
||||
ApiVersion: "v1/apps",
|
||||
ObjectMeta: ObjectMeta{
|
||||
Name: "foo",
|
||||
Namespace: "bar",
|
||||
Annotations: map[string]string{
|
||||
"ka": "va",
|
||||
},
|
||||
Labels: map[string]string{
|
||||
"kl": "vl",
|
||||
},
|
||||
},
|
||||
}, meta)
|
||||
}
|
||||
|
||||
func assertNoError(t *testing.T) func(o *RNode, err error) *RNode {
|
||||
return func(o *RNode, err error) *RNode {
|
||||
assert.NoError(t, err)
|
||||
return o
|
||||
}
|
||||
}
|
||||
|
||||
func assertNoErrorString(t *testing.T) func(string, error) string {
|
||||
return func(s string, err error) string {
|
||||
assert.NoError(t, err)
|
||||
return s
|
||||
}
|
||||
}
|
||||
66
kyaml/yaml/kfns.go
Normal file
66
kyaml/yaml/kfns.go
Normal file
@@ -0,0 +1,66 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package yaml
|
||||
|
||||
import "gopkg.in/yaml.v3"
|
||||
|
||||
// AnnotationClearer removes an annotation at metadata.annotations.
|
||||
// Returns nil if the annotation or field does not exist.
|
||||
type AnnotationClearer struct {
|
||||
Kind string `yaml:"kind,omitempty"`
|
||||
Key string `yaml:"key,omitempty"`
|
||||
}
|
||||
|
||||
func (c AnnotationClearer) Filter(rn *RNode) (*RNode, error) {
|
||||
return rn.Pipe(
|
||||
PathGetter{Path: []string{"metadata", "annotations"}},
|
||||
FieldClearer{Name: c.Key})
|
||||
}
|
||||
|
||||
func ClearAnnotation(key string) AnnotationClearer {
|
||||
return AnnotationClearer{Key: key}
|
||||
}
|
||||
|
||||
// AnnotationSetter sets an annotation at metadata.annotations.
|
||||
// Creates metadata.annotations if does not exist.
|
||||
type AnnotationSetter struct {
|
||||
Kind string `yaml:"kind,omitempty"`
|
||||
Key string `yaml:"key,omitempty"`
|
||||
Value string `yaml:"value,omitempty"`
|
||||
}
|
||||
|
||||
func (s AnnotationSetter) Filter(rn *RNode) (*RNode, error) {
|
||||
return rn.Pipe(
|
||||
PathGetter{Path: []string{"metadata", "annotations"}, Create: yaml.MappingNode},
|
||||
FieldSetter{Name: s.Key, Value: NewScalarRNode(s.Value)})
|
||||
}
|
||||
|
||||
func SetAnnotation(key, value string) AnnotationSetter {
|
||||
return AnnotationSetter{Key: key, Value: value}
|
||||
}
|
||||
|
||||
// AnnotationGetter gets an annotation at metadata.annotations.
|
||||
// Returns nil if metadata.annotations does not exist.
|
||||
type AnnotationGetter struct {
|
||||
Kind string `yaml:"kind,omitempty"`
|
||||
Key string `yaml:"key,omitempty"`
|
||||
Value string `yaml:"value,omitempty"`
|
||||
}
|
||||
|
||||
// AnnotationGetter returns the annotation value.
|
||||
// Returns "", nil if the annotation does not exist.
|
||||
func (g AnnotationGetter) Filter(rn *RNode) (*RNode, error) {
|
||||
v, err := rn.Pipe(PathGetter{Path: []string{"metadata", "annotations", g.Key}})
|
||||
if v == nil || err != nil {
|
||||
return v, err
|
||||
}
|
||||
if g.Value == "" || v.value.Value == g.Value {
|
||||
return v, err
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func GetAnnotation(key string) AnnotationGetter {
|
||||
return AnnotationGetter{Key: key}
|
||||
}
|
||||
208
kyaml/yaml/match.go
Normal file
208
kyaml/yaml/match.go
Normal file
@@ -0,0 +1,208 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package yaml
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PathMatcher returns all RNodes matching the path wrapped in a SequenceNode.
|
||||
// Lists may have multiple elements matching the path, and each matching element
|
||||
// is added to the return result.
|
||||
// If Path points to a SequenceNode, the SequenceNode is wrapped in another SequenceNode
|
||||
// If Path does not contain any lists, the result is still wrapped in a SequenceNode of len == 1
|
||||
type PathMatcher struct {
|
||||
Kind string `yaml:"kind,omitempty"`
|
||||
|
||||
// Path is a slice of parts leading to the RNode to lookup.
|
||||
// Each path part may be one of:
|
||||
// * FieldMatcher -- e.g. "spec"
|
||||
// * Map Key -- e.g. "app.k8s.io/version"
|
||||
// * List Entry -- e.g. "[name=nginx]" or "[=-jar]"
|
||||
//
|
||||
// Map Keys and Fields are equivalent.
|
||||
// See FieldMatcher for more on Fields and Map Keys.
|
||||
//
|
||||
// List Entries are specified as map entry to match [fieldName=fieldValue].
|
||||
// See Elem for more on List Entries.
|
||||
//
|
||||
// Examples:
|
||||
// * spec.template.spec.container with matching name: [name=nginx] -- match 'name': 'nginx'
|
||||
// * spec.template.spec.container.argument matching a value: [=-jar] -- match '-jar'
|
||||
Path []string `yaml:"path,omitempty"`
|
||||
|
||||
// Matches is set by PathMatch to publish the matched element values for each node.
|
||||
// After running PathMatcher.Filter, each node from the SequenceNode result may be
|
||||
// looked up in Matches to find the field values that were matched.
|
||||
Matches map[*Node][]string
|
||||
|
||||
// StripComments may be set to remove the comments on the matching Nodes.
|
||||
// This is useful for if the nodes are to be printed in FlowStyle.
|
||||
StripComments bool
|
||||
|
||||
val *RNode
|
||||
field string
|
||||
matchRegex string
|
||||
}
|
||||
|
||||
func (p *PathMatcher) stripComments(n *Node) {
|
||||
if n == nil {
|
||||
return
|
||||
}
|
||||
if p.StripComments {
|
||||
n.LineComment = ""
|
||||
n.HeadComment = ""
|
||||
n.FootComment = ""
|
||||
for i := range n.Content {
|
||||
p.stripComments(n.Content[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PathMatcher) Filter(rn *RNode) (*RNode, error) {
|
||||
val, err := p.filter(rn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.stripComments(val.YNode())
|
||||
return val, err
|
||||
}
|
||||
|
||||
func (p *PathMatcher) filter(rn *RNode) (*RNode, error) {
|
||||
p.Matches = map[*Node][]string{}
|
||||
|
||||
if len(p.Path) == 0 {
|
||||
// return the element wrapped in a SequenceNode
|
||||
p.appendRNode("", rn)
|
||||
return p.val, nil
|
||||
}
|
||||
|
||||
if IsListIndex(p.Path[0]) {
|
||||
// match seq elements
|
||||
return p.doSeq(rn)
|
||||
} else {
|
||||
// match a field
|
||||
return p.doField(rn)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PathMatcher) doField(rn *RNode) (*RNode, error) {
|
||||
// lookup the field
|
||||
field, err := rn.Pipe(Get(p.Path[0]))
|
||||
if err != nil || field == nil {
|
||||
// if the field doesn't exist, return nil
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// recurse on the field, removing the first element of the path
|
||||
pm := &PathMatcher{Path: p.Path[1:]}
|
||||
p.val, err = pm.filter(field)
|
||||
p.Matches = pm.Matches
|
||||
return p.val, err
|
||||
}
|
||||
|
||||
// doSeq iterates over a sequence and appends elements matching the path regex to p.Val
|
||||
func (p *PathMatcher) doSeq(rn *RNode) (*RNode, error) {
|
||||
|
||||
// parse the field + match pair
|
||||
var err error
|
||||
p.field, p.matchRegex, err = SplitIndexNameValue(p.Path[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if p.field == "" {
|
||||
err = rn.VisitElements(p.visitPrimitiveElem)
|
||||
} else {
|
||||
err = rn.VisitElements(p.visitElem)
|
||||
}
|
||||
if err != nil || p.val == nil || len(p.val.YNode().Content) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p.val, nil
|
||||
}
|
||||
|
||||
func (p *PathMatcher) visitPrimitiveElem(elem *RNode) error {
|
||||
r, err := regexp.Compile(p.matchRegex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
str, err := elem.String()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
str = strings.TrimSpace(str)
|
||||
if !r.MatchString(str) {
|
||||
return nil
|
||||
}
|
||||
|
||||
p.appendRNode("", elem)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PathMatcher) visitElem(elem *RNode) error {
|
||||
r, err := regexp.Compile(p.matchRegex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check if this elements field matches the regex
|
||||
val := elem.Field(p.field)
|
||||
if val == nil || val.Value == nil {
|
||||
return nil
|
||||
}
|
||||
str, err := val.Value.String()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
str = strings.TrimSpace(str)
|
||||
if !r.MatchString(str) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// recurse on the matching element
|
||||
pm := &PathMatcher{Path: p.Path[1:]}
|
||||
add, err := pm.filter(elem)
|
||||
for k, v := range pm.Matches {
|
||||
p.Matches[k] = v
|
||||
}
|
||||
if err != nil || add == nil {
|
||||
return err
|
||||
}
|
||||
p.append(str, add.Content()...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PathMatcher) appendRNode(path string, node *RNode) {
|
||||
p.append(path, node.YNode())
|
||||
}
|
||||
|
||||
func (p *PathMatcher) append(path string, nodes ...*Node) {
|
||||
if p.val == nil {
|
||||
p.val = NewRNode(&Node{Kind: SequenceNode})
|
||||
}
|
||||
for i := range nodes {
|
||||
node := nodes[i]
|
||||
p.val.YNode().Content = append(p.val.YNode().Content, node)
|
||||
// record the path if specified
|
||||
if path != "" {
|
||||
p.Matches[node] = append(p.Matches[node], path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cleanPath(path []string) []string {
|
||||
var p []string
|
||||
for _, elem := range path {
|
||||
elem = strings.TrimSpace(elem)
|
||||
if len(elem) == 0 {
|
||||
continue
|
||||
}
|
||||
p = append(p, elem)
|
||||
}
|
||||
return p
|
||||
}
|
||||
88
kyaml/yaml/match_test.go
Normal file
88
kyaml/yaml/match_test.go
Normal file
@@ -0,0 +1,88 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package yaml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPathMatcher_Filter(t *testing.T) {
|
||||
node := MustParse(`apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx-deployment
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.7.9
|
||||
ports:
|
||||
- containerPort: 80
|
||||
- name: sidecar
|
||||
image: sidecar:1.0.0
|
||||
ports:
|
||||
- containerPort: 8081
|
||||
- containerPort: 9090
|
||||
`)
|
||||
|
||||
updates := []struct {
|
||||
path []string
|
||||
value string
|
||||
}{
|
||||
{[]string{
|
||||
"spec", "template", "spec", "containers", "[name=.*]"},
|
||||
"- name: nginx\n image: nginx:1.7.9\n ports:\n - containerPort: 80\n" +
|
||||
"- name: sidecar\n image: sidecar:1.0.0\n ports:\n - containerPort: 8081\n - containerPort: 9090\n"},
|
||||
{[]string{
|
||||
"spec", "template", "spec", "containers", "[name=.*]", "image"},
|
||||
"- nginx:1.7.9\n- sidecar:1.0.0\n"},
|
||||
{[]string{
|
||||
"spec", "template", "spec", "containers", "[name=n.*]", "image"},
|
||||
"- nginx:1.7.9\n"},
|
||||
{[]string{
|
||||
"spec", "template", "spec", "containers", "[name=s.*]", "image"},
|
||||
"- sidecar:1.0.0\n"},
|
||||
{[]string{
|
||||
"spec", "template", "spec", "containers", "[name=.*x]", "image"},
|
||||
"- nginx:1.7.9\n"},
|
||||
{[]string{
|
||||
"spec", "template", "spec", "containers", "[name=.*]", "ports"},
|
||||
"- - containerPort: 80\n- - containerPort: 8081\n - containerPort: 9090\n"},
|
||||
{[]string{
|
||||
"spec", "template", "spec", "containers", "[name=.*]", "ports", "[containerPort=8.*]"},
|
||||
"- containerPort: 80\n- containerPort: 8081\n"},
|
||||
{[]string{
|
||||
"spec", "template", "spec", "containers", "[name=.*]", "ports", "[containerPort=.*1]"},
|
||||
"- containerPort: 8081\n"},
|
||||
{[]string{
|
||||
"spec", "template", "spec", "containers", "[name=.*]", "ports", "[containerPort=9.*]"},
|
||||
"- containerPort: 9090\n"},
|
||||
{[]string{
|
||||
"spec", "template", "spec", "containers", "[name=s.*]", "ports", "[containerPort=8.*]"},
|
||||
"- containerPort: 8081\n"},
|
||||
{[]string{
|
||||
"spec", "template", "spec", "containers", "[name=s.*]", "ports", "[containerPort=.*2]"},
|
||||
""},
|
||||
}
|
||||
for i, u := range updates {
|
||||
result, err := node.Pipe(&PathMatcher{Path: u.path})
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, u.value, result.MustString(), fmt.Sprintf("%d", i))
|
||||
}
|
||||
}
|
||||
265
kyaml/yaml/merge2/element_test.go
Normal file
265
kyaml/yaml/merge2/element_test.go
Normal file
@@ -0,0 +1,265 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package merge2_test
|
||||
|
||||
var elementTestCases = []testCase{
|
||||
{`merge Element -- keep field in dest`,
|
||||
`
|
||||
kind: Deployment
|
||||
items:
|
||||
- name: foo
|
||||
image: foo:v1
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
items:
|
||||
- name: foo
|
||||
image: foo:v0
|
||||
command: ['run.sh']
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
items:
|
||||
- name: foo
|
||||
image: foo:v1
|
||||
command:
|
||||
- run.sh
|
||||
`,
|
||||
},
|
||||
|
||||
{`merge Element -- add field to dest`,
|
||||
`
|
||||
kind: Deployment
|
||||
items:
|
||||
- name: foo
|
||||
image: foo:v1
|
||||
command: ['run.sh']
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
items:
|
||||
- name: foo
|
||||
image: foo:v0
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
items:
|
||||
- name: foo
|
||||
image: foo:v1
|
||||
command:
|
||||
- run.sh
|
||||
`,
|
||||
},
|
||||
|
||||
{`merge Element -- add list, empty in dest`,
|
||||
`
|
||||
kind: Deployment
|
||||
items:
|
||||
- name: foo
|
||||
image: foo:v1
|
||||
command: ['run.sh']
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
items: {}
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
items:
|
||||
- name: foo
|
||||
image: foo:v1
|
||||
command:
|
||||
- run.sh
|
||||
`,
|
||||
},
|
||||
|
||||
{`merge Element -- add list, missing from dest`,
|
||||
`
|
||||
kind: Deployment
|
||||
items:
|
||||
- name: foo
|
||||
image: foo:v1
|
||||
command: ['run.sh']
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
items:
|
||||
- name: foo
|
||||
image: foo:v1
|
||||
command:
|
||||
- run.sh
|
||||
`,
|
||||
},
|
||||
|
||||
{`merge Element -- add Element first`,
|
||||
`
|
||||
kind: Deployment
|
||||
items:
|
||||
- name: bar
|
||||
image: bar:v1
|
||||
command: ['run2.sh']
|
||||
- name: foo
|
||||
image: foo:v1
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
items:
|
||||
- name: foo
|
||||
image: foo:v0
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
items:
|
||||
- name: foo
|
||||
image: foo:v1
|
||||
- name: bar
|
||||
image: bar:v1
|
||||
command:
|
||||
- run2.sh
|
||||
`,
|
||||
},
|
||||
|
||||
{`merge Element -- add Element second`,
|
||||
`
|
||||
kind: Deployment
|
||||
items:
|
||||
- name: foo
|
||||
image: foo:v1
|
||||
- name: bar
|
||||
image: bar:v1
|
||||
command: ['run2.sh']
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
items:
|
||||
- name: foo
|
||||
image: foo:v0
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
items:
|
||||
- name: foo
|
||||
image: foo:v1
|
||||
- name: bar
|
||||
image: bar:v1
|
||||
command:
|
||||
- run2.sh
|
||||
`,
|
||||
},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`keep list -- list missing from src`,
|
||||
`
|
||||
kind: Deployment
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
items:
|
||||
- name: foo
|
||||
image: foo:v1
|
||||
- name: bar
|
||||
image: bar:v1
|
||||
command: ['run2.sh']
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
items:
|
||||
- name: foo
|
||||
image: foo:v1
|
||||
- name: bar
|
||||
image: bar:v1
|
||||
command:
|
||||
- run2.sh
|
||||
`,
|
||||
},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`keep Element -- element missing in src`,
|
||||
`
|
||||
kind: Deployment
|
||||
items:
|
||||
- name: foo
|
||||
image: foo:v1
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
items:
|
||||
- name: foo
|
||||
image: foo:v0
|
||||
- name: bar
|
||||
image: bar:v1
|
||||
command: ['run2.sh']
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
items:
|
||||
- name: foo
|
||||
image: foo:v1
|
||||
- name: bar
|
||||
image: bar:v1
|
||||
command:
|
||||
- run2.sh
|
||||
`,
|
||||
},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`keep element -- empty list in src`,
|
||||
`
|
||||
kind: Deployment
|
||||
items: {}
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
items:
|
||||
- name: foo
|
||||
image: foo:v1
|
||||
- name: bar
|
||||
image: bar:v1
|
||||
command:
|
||||
- run2.sh
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
items:
|
||||
- name: foo
|
||||
image: foo:v1
|
||||
- name: bar
|
||||
image: bar:v1
|
||||
command:
|
||||
- run2.sh
|
||||
`,
|
||||
},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`remove Element -- null in src`,
|
||||
`
|
||||
kind: Deployment
|
||||
items: null
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
items:
|
||||
- name: foo
|
||||
image: foo:v1
|
||||
- name: bar
|
||||
image: bar:v1
|
||||
command:
|
||||
- run2.sh
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
`,
|
||||
},
|
||||
}
|
||||
140
kyaml/yaml/merge2/list_test.go
Normal file
140
kyaml/yaml/merge2/list_test.go
Normal file
@@ -0,0 +1,140 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package merge2_test
|
||||
|
||||
var listTestCases = []testCase{
|
||||
{`replace List -- different value in dest`,
|
||||
`
|
||||
kind: Deployment
|
||||
items:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
items:
|
||||
- 0
|
||||
- 1
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
items:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
`,
|
||||
},
|
||||
|
||||
{`replace List -- missing from dest`,
|
||||
`
|
||||
kind: Deployment
|
||||
items:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
items:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
`,
|
||||
},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`keep List -- same value in src and dest`,
|
||||
`
|
||||
kind: Deployment
|
||||
items:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
items:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
items:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
`,
|
||||
},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`keep List -- unspecified in src`,
|
||||
`
|
||||
kind: Deployment
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
items:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
items:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
`,
|
||||
},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`remove List -- null in src`,
|
||||
`
|
||||
kind: Deployment
|
||||
items: null
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
items:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
`,
|
||||
},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`remove list -- empty in src`,
|
||||
`
|
||||
kind: Deployment
|
||||
items: {}
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
items:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
items: {}
|
||||
`,
|
||||
},
|
||||
}
|
||||
186
kyaml/yaml/merge2/map_test.go
Normal file
186
kyaml/yaml/merge2/map_test.go
Normal file
@@ -0,0 +1,186 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package merge2_test
|
||||
|
||||
var mapTestCases = []testCase{
|
||||
{`merge Map -- update field in dest`,
|
||||
`
|
||||
kind: Deployment
|
||||
spec:
|
||||
foo: bar1
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
spec:
|
||||
foo: bar0
|
||||
baz: buz
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
spec:
|
||||
foo: bar1
|
||||
baz: buz
|
||||
`,
|
||||
},
|
||||
|
||||
{`merge Map -- add field to dest`,
|
||||
`
|
||||
kind: Deployment
|
||||
spec:
|
||||
foo: bar1
|
||||
baz: buz
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
spec:
|
||||
foo: bar0
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
spec:
|
||||
foo: bar1
|
||||
baz: buz
|
||||
`,
|
||||
},
|
||||
|
||||
{`merge Map -- add list, empty in dest`,
|
||||
`
|
||||
kind: Deployment
|
||||
spec:
|
||||
foo: bar1
|
||||
baz: buz
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
spec: {}
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
spec:
|
||||
foo: bar1
|
||||
baz: buz
|
||||
`,
|
||||
},
|
||||
|
||||
{`merge Map -- add list, missing from dest`,
|
||||
`
|
||||
kind: Deployment
|
||||
spec:
|
||||
foo: bar1
|
||||
baz: buz
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
spec:
|
||||
foo: bar1
|
||||
baz: buz
|
||||
`,
|
||||
},
|
||||
|
||||
{`merge Map -- add Map first`,
|
||||
`
|
||||
kind: Deployment
|
||||
spec:
|
||||
foo: bar1
|
||||
baz: buz
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
spec:
|
||||
foo: bar1
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
spec:
|
||||
foo: bar1
|
||||
baz: buz
|
||||
`,
|
||||
},
|
||||
|
||||
{`merge Map -- add Map second`,
|
||||
`
|
||||
kind: Deployment
|
||||
spec:
|
||||
baz: buz
|
||||
foo: bar1
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
spec:
|
||||
foo: bar1
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
spec:
|
||||
foo: bar1
|
||||
baz: buz
|
||||
`,
|
||||
},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`keep map -- map missing from src`,
|
||||
`
|
||||
kind: Deployment
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
spec:
|
||||
foo: bar1
|
||||
baz: buz
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
spec:
|
||||
foo: bar1
|
||||
baz: buz
|
||||
`,
|
||||
},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`keep map -- empty list in src`,
|
||||
`
|
||||
kind: Deployment
|
||||
items: {}
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
spec:
|
||||
foo: bar1
|
||||
baz: buz
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
spec:
|
||||
foo: bar1
|
||||
baz: buz
|
||||
items: {}
|
||||
`,
|
||||
},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`remove Map -- null in src`,
|
||||
`
|
||||
kind: Deployment
|
||||
spec: null
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
spec:
|
||||
foo: bar1
|
||||
baz: buz
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
`,
|
||||
},
|
||||
}
|
||||
201
kyaml/yaml/merge2/merge2.go
Normal file
201
kyaml/yaml/merge2/merge2.go
Normal file
@@ -0,0 +1,201 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package merge contains libraries for merging fields from one RNode to another
|
||||
// RNode
|
||||
package merge2
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml/walk"
|
||||
)
|
||||
|
||||
const Help = `
|
||||
Description:
|
||||
|
||||
merge merges fields from a source to a destination, overriding the destination fields
|
||||
where they differ.
|
||||
|
||||
### Merge Rules
|
||||
|
||||
Fields are recursively merged using the following rules:
|
||||
|
||||
- scalars
|
||||
- if present only in the dest, it keeps its value
|
||||
- if present in the src and is non-null, take the src value -- if ` + "`null`" + `, clear it
|
||||
` + " - example src: `5`, dest: `3` => result: `5`" + `
|
||||
|
||||
- non-associative lists -- lists without a merge key
|
||||
- if present only in the dest, it keeps its value
|
||||
- if present in the src and is non-null, take the src value -- if ` + "`null`" + `, clear it
|
||||
` + " - example src: `[1, 2, 3]`, dest: `[a, b, c]` => result: `[1, 2, 3]`" + `
|
||||
|
||||
- map keys and fields -- paired by the map-key / field-name
|
||||
- if present only in the dest, it keeps its value
|
||||
- if present only in the src, it is added to the dest
|
||||
- if the field is present in both the src and dest, and the src value is 'null', the field is removed from the dest
|
||||
- if the field is present in both the src and dest, the value is recursively merged
|
||||
` + " - example src: `{'key1': 'value1', 'key2': 'value2'}`, dest: `{'key2': 'value0', 'key3': 'value3'}` => result: `{'key1': 'value1', 'key2': 'value2', 'key3': 'value3'}`" + `
|
||||
|
||||
- associative list elements -- paired by the associative key
|
||||
- if present only in the dest, it keeps its value in the list
|
||||
- if present only in the src, it is added to the dest list
|
||||
- if the field is present in both the src and dest, the value is recursively merged
|
||||
|
||||
### Associative Keys
|
||||
|
||||
Associative keys are used to identify "same" elements within 2 different lists, and merge them.
|
||||
The following fields are recognized as associative keys:
|
||||
|
||||
` + "[`mountPath`, `devicePath`, `ip`, `type`, `topologyKey`, `name`, `containerPort`]" + `
|
||||
|
||||
Any lists where all of the elements contain associative keys will be merged as associative lists.
|
||||
|
||||
### Example
|
||||
|
||||
> Source
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
spec:
|
||||
replicas: 3 # scalar
|
||||
template:
|
||||
spec:
|
||||
containers: # associative list -- (name)
|
||||
- name: nginx
|
||||
image: nginx:1.7
|
||||
command: ['new_run.sh', 'arg1'] # non-associative list
|
||||
- name: sidecar2
|
||||
image: sidecar2:v1
|
||||
|
||||
> Destination
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
spec:
|
||||
replicas: 1
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.6
|
||||
command: ['old_run.sh', 'arg0']
|
||||
- name: sidecar1
|
||||
image: sidecar1:v1
|
||||
|
||||
> Result
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
spec:
|
||||
replicas: 3 # scalar
|
||||
template:
|
||||
spec:
|
||||
containers: # associative list -- (name)
|
||||
- name: nginx
|
||||
image: nginx:1.7
|
||||
command: ['new_run.sh', 'arg1'] # non-associative list
|
||||
- name: sidecar1
|
||||
image: sidecar1:v1
|
||||
- name: sidecar2
|
||||
image: sidecar2:v1
|
||||
`
|
||||
|
||||
// Merge merges fields from src into dest.
|
||||
func Merge(src, dest *yaml.RNode) (*yaml.RNode, error) {
|
||||
return walk.Walker{Sources: []*yaml.RNode{dest, src}, Visitor: Merger{}}.Walk()
|
||||
}
|
||||
|
||||
// Merge parses the arguments, and merges fields from srcStr into destStr.
|
||||
func MergeStrings(srcStr, destStr string) (string, error) {
|
||||
src, err := yaml.Parse(srcStr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
dest, err := yaml.Parse(destStr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
result, err := Merge(src, dest)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
|
||||
type Merger struct {
|
||||
// for forwards compatibility when new functions are added to the interface
|
||||
}
|
||||
|
||||
var _ walk.Visitor = Merger{}
|
||||
|
||||
func (m Merger) VisitMap(nodes walk.Sources) (*yaml.RNode, error) {
|
||||
if err := m.SetComments(nodes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if yaml.IsEmpty(nodes.Dest()) {
|
||||
// Add
|
||||
return nodes.Origin(), nil
|
||||
}
|
||||
if yaml.IsNull(nodes.Origin()) {
|
||||
// clear the value
|
||||
return walk.ClearNode, nil
|
||||
}
|
||||
// Recursively Merge dest
|
||||
return nodes.Dest(), nil
|
||||
}
|
||||
|
||||
func (m Merger) VisitScalar(nodes walk.Sources) (*yaml.RNode, error) {
|
||||
if err := m.SetComments(nodes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Override value
|
||||
if nodes.Origin() != nil {
|
||||
return nodes.Origin(), nil
|
||||
}
|
||||
// Keep
|
||||
return nodes.Dest(), nil
|
||||
}
|
||||
|
||||
func (m Merger) VisitList(nodes walk.Sources, kind walk.ListKind) (*yaml.RNode, error) {
|
||||
if err := m.SetComments(nodes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if kind == walk.NonAssociateList {
|
||||
// Override value
|
||||
if nodes.Origin() != nil {
|
||||
return nodes.Origin(), nil
|
||||
}
|
||||
// Keep
|
||||
return nodes.Dest(), nil
|
||||
}
|
||||
|
||||
// Add
|
||||
if yaml.IsEmpty(nodes.Dest()) {
|
||||
return nodes.Origin(), nil
|
||||
}
|
||||
// Clear
|
||||
if yaml.IsNull(nodes.Origin()) {
|
||||
return walk.ClearNode, nil
|
||||
}
|
||||
// Recursively Merge dest
|
||||
return nodes.Dest(), nil
|
||||
}
|
||||
|
||||
// SetComments copies the dest comments to the source comments if they are present
|
||||
// on the source.
|
||||
func (m Merger) SetComments(sources walk.Sources) error {
|
||||
source := sources.Origin()
|
||||
dest := sources.Dest()
|
||||
if source != nil && source.YNode().FootComment != "" {
|
||||
dest.YNode().FootComment = source.YNode().FootComment
|
||||
}
|
||||
if source != nil && source.YNode().HeadComment != "" {
|
||||
dest.YNode().HeadComment = source.YNode().HeadComment
|
||||
}
|
||||
if source != nil && source.YNode().LineComment != "" {
|
||||
dest.YNode().LineComment = source.YNode().LineComment
|
||||
}
|
||||
return nil
|
||||
}
|
||||
510
kyaml/yaml/merge2/merge2_old_test.go
Normal file
510
kyaml/yaml/merge2/merge2_old_test.go
Normal file
@@ -0,0 +1,510 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package merge2_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"sigs.k8s.io/kustomize/kyaml/kio/filters"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
. "sigs.k8s.io/kustomize/kyaml/yaml/merge2"
|
||||
)
|
||||
|
||||
const dest = `
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: app
|
||||
labels:
|
||||
app: java
|
||||
annotations:
|
||||
a.b.c: d.e.f
|
||||
g: h1
|
||||
i: j
|
||||
m: n2
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.7.9
|
||||
args: ['c', 'a', 'b']
|
||||
env:
|
||||
- name: DEMO_GREETING
|
||||
value: "Hello from the environment"
|
||||
- name: DEMO_FAREWELL
|
||||
value: "Such a sweet sorrow"
|
||||
`
|
||||
|
||||
func TestMerge_map(t *testing.T) {
|
||||
dest := yaml.MustParse(dest)
|
||||
src := yaml.MustParse(`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: app
|
||||
labels:
|
||||
app: java
|
||||
annotations:
|
||||
a.b.c: d.e.f
|
||||
g: h2
|
||||
k: l
|
||||
m: n1
|
||||
`)
|
||||
|
||||
result, err := Merge(src, dest)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
actual, err := result.String()
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
expected := `
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: app
|
||||
labels:
|
||||
app: java
|
||||
annotations:
|
||||
a.b.c: d.e.f
|
||||
g: h2
|
||||
i: j
|
||||
k: l
|
||||
m: n1
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.7.9
|
||||
args:
|
||||
- c
|
||||
- a
|
||||
- b
|
||||
env:
|
||||
- name: DEMO_GREETING
|
||||
value: "Hello from the environment"
|
||||
- name: DEMO_FAREWELL
|
||||
value: "Such a sweet sorrow"
|
||||
`
|
||||
b, err := filters.FormatInput(bytes.NewBufferString(expected))
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
expected = b.String()
|
||||
|
||||
b, err = filters.FormatInput(bytes.NewBufferString(actual))
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
actual = b.String()
|
||||
|
||||
assert.Equal(t, expected, actual)
|
||||
}
|
||||
|
||||
func TestMerge_clear(t *testing.T) {
|
||||
dest := yaml.MustParse(dest)
|
||||
src := yaml.MustParse(`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations: null
|
||||
`)
|
||||
|
||||
result, err := Merge(src, dest)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
actual, err := result.String()
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
expected := `
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: app
|
||||
labels:
|
||||
app: java
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.7.9
|
||||
args:
|
||||
- c
|
||||
- a
|
||||
- b
|
||||
env:
|
||||
- name: DEMO_GREETING
|
||||
value: "Hello from the environment"
|
||||
- name: DEMO_FAREWELL
|
||||
value: "Such a sweet sorrow"
|
||||
`
|
||||
b, err := filters.FormatInput(bytes.NewBufferString(expected))
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
expected = b.String()
|
||||
|
||||
b, err = filters.FormatInput(bytes.NewBufferString(actual))
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
actual = b.String()
|
||||
|
||||
assert.Equal(t, expected, actual)
|
||||
}
|
||||
|
||||
func TestMerge_mapInverse(t *testing.T) {
|
||||
dest := yaml.MustParse(dest)
|
||||
src := yaml.MustParse(`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: app
|
||||
labels:
|
||||
app: java
|
||||
annotations:
|
||||
a.b.c: d.e.f
|
||||
g: h2
|
||||
k: l
|
||||
m: n1
|
||||
`)
|
||||
|
||||
result, err := Merge(dest, src)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
actual, err := result.String()
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
expected := `
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: app
|
||||
labels:
|
||||
app: java
|
||||
annotations:
|
||||
a.b.c: d.e.f
|
||||
g: h1
|
||||
i: j
|
||||
k: l
|
||||
m: n2
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.7.9
|
||||
args:
|
||||
- c
|
||||
- a
|
||||
- b
|
||||
env:
|
||||
- name: DEMO_GREETING
|
||||
value: "Hello from the environment"
|
||||
- name: DEMO_FAREWELL
|
||||
value: "Such a sweet sorrow"
|
||||
`
|
||||
b, err := filters.FormatInput(bytes.NewBufferString(expected))
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
expected = b.String()
|
||||
|
||||
b, err = filters.FormatInput(bytes.NewBufferString(actual))
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
actual = b.String()
|
||||
|
||||
assert.Equal(t, expected, actual)
|
||||
}
|
||||
|
||||
func TestMerge_listElem(t *testing.T) {
|
||||
dest := yaml.MustParse(dest)
|
||||
src := yaml.MustParse(`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
env:
|
||||
- name: DEMO_GREETING
|
||||
value: "New Demo Greeting"
|
||||
- name: NEW_DEMO_VALUE
|
||||
value: "Another Env Not In The Dest"
|
||||
`)
|
||||
|
||||
result, err := Merge(src, dest)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
actual, err := result.String()
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
expected := `apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: app
|
||||
labels:
|
||||
app: java
|
||||
annotations:
|
||||
a.b.c: d.e.f
|
||||
g: h1
|
||||
i: j
|
||||
m: n2
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.7.9
|
||||
args:
|
||||
- c
|
||||
- a
|
||||
- b
|
||||
env:
|
||||
- name: DEMO_GREETING
|
||||
value: "New Demo Greeting"
|
||||
- name: DEMO_FAREWELL
|
||||
value: "Such a sweet sorrow"
|
||||
- name: NEW_DEMO_VALUE
|
||||
value: "Another Env Not In The Dest"
|
||||
`
|
||||
|
||||
b, err := filters.FormatInput(bytes.NewBufferString(expected))
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
expected = b.String()
|
||||
|
||||
b, err = filters.FormatInput(bytes.NewBufferString(actual))
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
actual = b.String()
|
||||
|
||||
assert.Equal(t, expected, actual)
|
||||
}
|
||||
|
||||
func TestMerge_list(t *testing.T) {
|
||||
dest := yaml.MustParse(dest)
|
||||
src := yaml.MustParse(`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
args: ['e', 'd', 'f']
|
||||
`)
|
||||
|
||||
result, err := Merge(src, dest)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
actual, err := result.String()
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
expected := `apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: app
|
||||
labels:
|
||||
app: java
|
||||
annotations:
|
||||
a.b.c: d.e.f
|
||||
g: h1
|
||||
i: j
|
||||
m: n2
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.7.9
|
||||
args:
|
||||
- e
|
||||
- d
|
||||
- f
|
||||
env:
|
||||
- name: DEMO_GREETING
|
||||
value: "Hello from the environment"
|
||||
- name: DEMO_FAREWELL
|
||||
value: "Such a sweet sorrow"
|
||||
`
|
||||
|
||||
b, err := filters.FormatInput(bytes.NewBufferString(expected))
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
expected = b.String()
|
||||
|
||||
b, err = filters.FormatInput(bytes.NewBufferString(actual))
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
actual = b.String()
|
||||
|
||||
assert.Equal(t, expected, actual)
|
||||
}
|
||||
|
||||
func TestMerge_commentsKept(t *testing.T) {
|
||||
actual, err := MergeStrings(`
|
||||
a:
|
||||
b:
|
||||
c: e
|
||||
`,
|
||||
`
|
||||
a:
|
||||
b:
|
||||
# header comment
|
||||
c: d
|
||||
`)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, `a:
|
||||
b:
|
||||
# header comment
|
||||
c: e
|
||||
`, actual)
|
||||
|
||||
actual, err = MergeStrings(`
|
||||
a:
|
||||
b:
|
||||
c: e
|
||||
`,
|
||||
`
|
||||
a:
|
||||
b:
|
||||
c: d
|
||||
# footer comment
|
||||
`)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, `a:
|
||||
b:
|
||||
c: e
|
||||
# footer comment
|
||||
`, actual)
|
||||
|
||||
actual, err = MergeStrings(`
|
||||
a:
|
||||
b:
|
||||
c: e
|
||||
`,
|
||||
`
|
||||
a:
|
||||
b:
|
||||
c: d # line comment
|
||||
`)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, `a:
|
||||
b:
|
||||
c: e
|
||||
`, actual)
|
||||
}
|
||||
|
||||
func TestMerge_commentsOverride(t *testing.T) {
|
||||
actual, err := MergeStrings(`
|
||||
a:
|
||||
b:
|
||||
# header comment
|
||||
c: e
|
||||
`,
|
||||
`
|
||||
a:
|
||||
b:
|
||||
# replace comment
|
||||
c: d
|
||||
`)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, `a:
|
||||
b:
|
||||
# replace comment
|
||||
c: e
|
||||
`, actual)
|
||||
|
||||
actual, err = MergeStrings(`
|
||||
a:
|
||||
b:
|
||||
c: e
|
||||
# footer comment
|
||||
`,
|
||||
`
|
||||
a:
|
||||
b:
|
||||
c: d
|
||||
# replace comment
|
||||
`)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, `a:
|
||||
b:
|
||||
c: e
|
||||
# replace comment
|
||||
`, actual)
|
||||
|
||||
actual, err = MergeStrings(`
|
||||
a:
|
||||
b:
|
||||
c: e # line comment
|
||||
`,
|
||||
`
|
||||
a:
|
||||
b:
|
||||
c: d # replace comment
|
||||
`)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, `a:
|
||||
b:
|
||||
c: e # line comment
|
||||
`, actual)
|
||||
|
||||
actual, err = MergeStrings(`
|
||||
a:
|
||||
b:
|
||||
c: d # line comment
|
||||
`,
|
||||
`
|
||||
a:
|
||||
b:
|
||||
c: d # replace comment
|
||||
`)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, `a:
|
||||
b:
|
||||
c: d # line comment
|
||||
`, actual)
|
||||
}
|
||||
47
kyaml/yaml/merge2/merge2_test.go
Normal file
47
kyaml/yaml/merge2/merge2_test.go
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package merge2_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"sigs.k8s.io/kustomize/kyaml/kio/filters"
|
||||
. "sigs.k8s.io/kustomize/kyaml/yaml/merge2"
|
||||
)
|
||||
|
||||
var testCases = [][]testCase{scalarTestCases, listTestCases, elementTestCases, mapTestCases}
|
||||
|
||||
func TestMerge(t *testing.T) {
|
||||
for i := range testCases {
|
||||
for _, tc := range testCases[i] {
|
||||
actual, err := MergeStrings(tc.source, tc.dest)
|
||||
if !assert.NoError(t, err, tc.description) {
|
||||
t.FailNow()
|
||||
}
|
||||
e, err := filters.FormatInput(bytes.NewBufferString(tc.expected))
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
estr := strings.TrimSpace(e.String())
|
||||
a, err := filters.FormatInput(bytes.NewBufferString(actual))
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
astr := strings.TrimSpace(a.String())
|
||||
if !assert.Equal(t, estr, astr, tc.description) {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
description string
|
||||
source string
|
||||
dest string
|
||||
expected string
|
||||
}
|
||||
138
kyaml/yaml/merge2/scalar_test.go
Normal file
138
kyaml/yaml/merge2/scalar_test.go
Normal file
@@ -0,0 +1,138 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package merge2_test
|
||||
|
||||
var scalarTestCases = []testCase{
|
||||
{`replace scalar -- different value in dest`,
|
||||
`
|
||||
kind: Deployment
|
||||
field: value1
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
field: value0
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
field: value1
|
||||
`,
|
||||
},
|
||||
|
||||
{`replace scalar -- missing from dest`,
|
||||
`
|
||||
kind: Deployment
|
||||
field: value1
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
field: value1
|
||||
`,
|
||||
},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`keep scalar -- same value in src and dest`,
|
||||
`
|
||||
kind: Deployment
|
||||
field: value1
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
field: value1
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
field: value1
|
||||
`,
|
||||
},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`keep scalar -- unspecified in src`,
|
||||
`
|
||||
kind: Deployment
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
field: value1
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
field: value1
|
||||
`,
|
||||
},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`remove scalar -- null in src`,
|
||||
`
|
||||
kind: Deployment
|
||||
field: null
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
field: value1
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
`,
|
||||
},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`remove scalar -- empty in src`,
|
||||
`
|
||||
kind: Deployment
|
||||
field: {}
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
field: value1
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
field: {}
|
||||
`,
|
||||
},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`remove scalar -- null in src, missing in dest`,
|
||||
`
|
||||
kind: Deployment
|
||||
field: null
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
`,
|
||||
},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`merge an empty value`,
|
||||
`
|
||||
kind: Deployment
|
||||
field: {}
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
field: {}
|
||||
`,
|
||||
},
|
||||
}
|
||||
422
kyaml/yaml/merge3/element_test.go
Normal file
422
kyaml/yaml/merge3/element_test.go
Normal file
@@ -0,0 +1,422 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package merge3_test
|
||||
|
||||
var elementTestCases = []testCase{
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`Add an element to an existing list`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- name: foo
|
||||
image: foo:1
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- name: foo
|
||||
image: foo:1
|
||||
- name: baz
|
||||
image: baz:2
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- name: foo
|
||||
image: foo:1
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- name: foo
|
||||
image: foo:1
|
||||
- image: baz:2
|
||||
name: baz
|
||||
|
||||
`, nil},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`Add an element to a non-existing list`,
|
||||
`
|
||||
kind: Deployment`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- name: foo
|
||||
image: foo:bar
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- image: foo:bar
|
||||
name: foo
|
||||
`, nil},
|
||||
|
||||
{`Add an element to a non-existing list, existing in dest`,
|
||||
`
|
||||
kind: Deployment`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- name: foo
|
||||
image: foo:bar
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- name: baz
|
||||
image: baz:bar
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- name: baz
|
||||
image: baz:bar
|
||||
- image: foo:bar
|
||||
name: foo
|
||||
`, nil},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
// TODO(pwittrock): Figure out if there is something better we can do here
|
||||
// This element is missing from the destination -- only the new fields are added
|
||||
{`Add a field to the element, element missing from dest`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- name: foo
|
||||
image: foo:bar`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- name: foo
|
||||
image: foo:bar
|
||||
command:
|
||||
- run.sh
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- command:
|
||||
- run.sh
|
||||
name: foo
|
||||
`, nil},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`Update a field on the elem, element missing from the dest`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- name: foo
|
||||
image: foo:bar
|
||||
command:
|
||||
- run.sh
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- name: foo
|
||||
image: foo:bar
|
||||
command:
|
||||
- run2.sh
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- command:
|
||||
- run2.sh
|
||||
name: foo
|
||||
`, nil},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`Update a field on the elem, element present in the dest`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- name: foo
|
||||
image: foo:bar
|
||||
command: ['run.sh']
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- name: foo
|
||||
image: foo:bar
|
||||
command: ['run2.sh']
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- name: foo
|
||||
image: foo:bar
|
||||
command: ['run.sh']
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- name: foo
|
||||
image: foo:bar
|
||||
command:
|
||||
- run2.sh
|
||||
`, nil},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`Add a field on the elem, element present in the dest`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- name: foo
|
||||
image: foo:bar
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- name: foo
|
||||
image: foo:bar
|
||||
command: ['run2.sh']
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- name: foo
|
||||
image: foo:bar
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- name: foo
|
||||
image: foo:bar
|
||||
command:
|
||||
- run2.sh
|
||||
`, nil},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`Add a field on the elem, element and field present in the dest`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- name: foo
|
||||
image: foo:bar
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- name: foo
|
||||
image: foo:bar
|
||||
command: ['run2.sh']
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- name: foo
|
||||
image: foo:bar
|
||||
command: ['run.sh']
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- name: foo
|
||||
image: foo:bar
|
||||
command:
|
||||
- run2.sh
|
||||
`, nil},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`Ignore an element`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers: {}
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers: {}
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- name: foo
|
||||
image: foo:bar
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- name: foo
|
||||
image: foo:bar
|
||||
`, nil},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`Leave deleted`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- name: foo
|
||||
image: foo:bar
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
`, nil},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`Remove an element -- matching`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- name: foo
|
||||
image: foo:bar
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- name: foo
|
||||
image: foo:bar
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
`, nil},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`Remove an element -- field missing from update`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- name: foo
|
||||
image: foo:bar
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- name: foo
|
||||
image: foo:bar
|
||||
command: ['run.sh']
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
`, nil},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`Remove an element -- element missing`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- name: foo
|
||||
image: foo:bar
|
||||
- name: baz
|
||||
image: baz:bar
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- name: foo
|
||||
image: foo:bar
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- name: foo
|
||||
image: foo:bar
|
||||
command: ['run.sh']
|
||||
- name: baz
|
||||
image: baz:bar
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- name: foo
|
||||
image: foo:bar
|
||||
command:
|
||||
- run.sh
|
||||
`, nil},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`Remove an element -- empty containers`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- name: foo
|
||||
image: foo:bar
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers: {}
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- name: foo
|
||||
image: foo:bar
|
||||
command: ['run.sh']
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
`, nil},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`Remove an element -- missing list field`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- name: foo
|
||||
image: foo:bar
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
containers:
|
||||
- name: foo
|
||||
image: foo:bar
|
||||
command: ['run.sh']
|
||||
`,
|
||||
`
|
||||
kind: Deployment
|
||||
`, nil},
|
||||
}
|
||||
232
kyaml/yaml/merge3/list_test.go
Normal file
232
kyaml/yaml/merge3/list_test.go
Normal file
@@ -0,0 +1,232 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package merge3_test
|
||||
|
||||
var listTestCases = []testCase{
|
||||
// List Field Test Cases
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`Replace list`,
|
||||
`
|
||||
list:
|
||||
- 1
|
||||
- 2
|
||||
- 3`,
|
||||
`
|
||||
list:
|
||||
- 2
|
||||
- 3
|
||||
- 4`,
|
||||
`
|
||||
list:
|
||||
- 1
|
||||
- 2
|
||||
- 3`,
|
||||
`
|
||||
list:
|
||||
- 2
|
||||
- 3
|
||||
- 4`, nil},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`Add an updated list`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
list: # old value
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
list: # new value
|
||||
- 2
|
||||
- 3
|
||||
- 4
|
||||
`,
|
||||
`
|
||||
apiVersion: apps/v1`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
list:
|
||||
- 2
|
||||
- 3
|
||||
- 4
|
||||
`, nil},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`Add keep an omitted field`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
list: # not present in sources
|
||||
- 2
|
||||
- 3
|
||||
- 4
|
||||
`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
list: # not present in sources
|
||||
- 2
|
||||
- 3
|
||||
- 4
|
||||
kind: StatefulSet
|
||||
`, nil},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
// TODO(#36): consider making this an error
|
||||
{`Change an updated field`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
list: # old value
|
||||
- 1
|
||||
- 2
|
||||
- 3`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
list: # new value
|
||||
- 2
|
||||
- 3
|
||||
- 4`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
list: # conflicting value
|
||||
- a
|
||||
- b
|
||||
- c`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
list: # conflicting value
|
||||
- 2
|
||||
- 3
|
||||
- 4
|
||||
`, nil},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`Ignore a field -- set`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
list: # ignore value
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
list: # ignore value
|
||||
- 1
|
||||
- 2
|
||||
- 3`, `
|
||||
apiVersion: apps/v1
|
||||
list:
|
||||
- 2
|
||||
- 3
|
||||
- 4
|
||||
`, `
|
||||
apiVersion: apps/v1
|
||||
list:
|
||||
- 2
|
||||
- 3
|
||||
- 4
|
||||
`, nil},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`Ignore a field -- empty`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
list: # ignore value
|
||||
- 1
|
||||
- 2
|
||||
- 3`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
list: # ignore value
|
||||
- 1
|
||||
- 2
|
||||
- 3`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
`, nil},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`Explicitly clear a field`,
|
||||
`
|
||||
apiVersion: apps/v1`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
list: null # clear`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
list: # value to clear
|
||||
- 1
|
||||
- 2
|
||||
- 3`,
|
||||
`
|
||||
apiVersion: apps/v1`, nil},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`Implicitly clear a field`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
list: # clear value
|
||||
- 1
|
||||
- 2
|
||||
- 3`,
|
||||
`
|
||||
apiVersion: apps/v1`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
list: # old value
|
||||
- 1
|
||||
- 2
|
||||
- 3`,
|
||||
`
|
||||
apiVersion: apps/v1`, nil},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
// TODO(#36): consider making this an error
|
||||
{`Implicitly clear a changed field`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
list: # old value
|
||||
- 1
|
||||
- 2
|
||||
- 3`,
|
||||
`
|
||||
apiVersion: apps/v1`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
list: # old value
|
||||
- a
|
||||
- b
|
||||
- c`,
|
||||
`
|
||||
apiVersion: apps/v1`, nil},
|
||||
}
|
||||
298
kyaml/yaml/merge3/map_test.go
Normal file
298
kyaml/yaml/merge3/map_test.go
Normal file
@@ -0,0 +1,298 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package merge3_test
|
||||
|
||||
var mapTestCases = []testCase{
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`Add the annotations map field`,
|
||||
`
|
||||
kind: Deployment`,
|
||||
`
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
d: e # add these annotations
|
||||
`,
|
||||
`
|
||||
kind: Deployment`,
|
||||
`
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
d: e # add these annotations`, nil},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`Add an annotation to the field`,
|
||||
`
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
a: b`,
|
||||
`
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
a: b
|
||||
d: e # add these annotations`,
|
||||
`
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
g: h # keep these annotations`,
|
||||
`
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
g: h # keep these annotations
|
||||
d: e # add these annotations`, nil},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`Add an annotation to the field, field missing from dest`,
|
||||
`
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
a: b # ignored because unchanged`,
|
||||
`
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
a: b # ignore because unchanged
|
||||
d: e`,
|
||||
`
|
||||
kind: Deployment`,
|
||||
`
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
d: e`, nil},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`Update an annotation on the field, field messing rom the dest`,
|
||||
`
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
a: b
|
||||
d: c`,
|
||||
`
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
a: b
|
||||
d: e # set these annotations`,
|
||||
`
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
g: h # keep these annotations`,
|
||||
`
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
g: h # keep these annotations
|
||||
d: e # set these annotations`, nil},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`Add an annotation to the field, field missing from dest`,
|
||||
`
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
a: b # ignored because unchanged`,
|
||||
`
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
a: b # ignore because unchanged
|
||||
d: e`,
|
||||
`
|
||||
kind: Deployment`,
|
||||
`
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
d: e`, nil},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`Remove an annotation`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
a: b`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations: {}`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
c: d
|
||||
a: b`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
c: d`, nil},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
// TODO(#36) support ~annotations~: {} deletion
|
||||
{`Specify a field as empty that isn't present in the source`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: foo`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: foo
|
||||
annotations: null`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: foo
|
||||
annotations:
|
||||
a: b`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: foo`, nil},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`Remove an annotation`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
a: b`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
c: d
|
||||
a: b`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
c: d`, nil},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`Remove annotations field`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
a: b`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: foo`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: foo
|
||||
`, nil},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`Remove annotations field, but keep in dest`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
a: b`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: foo
|
||||
annotations:
|
||||
foo: bar # keep this annotation even though the parent field was removed`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: foo
|
||||
annotations:
|
||||
foo: bar # keep this annotation even though the parent field was removed`, nil},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`Remove annotations, but they are already empty`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: foo
|
||||
annotations:
|
||||
a: b
|
||||
`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: foo
|
||||
`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: foo
|
||||
annotations: {}
|
||||
`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: foo
|
||||
annotations: {}
|
||||
`, nil},
|
||||
}
|
||||
88
kyaml/yaml/merge3/merge3.go
Normal file
88
kyaml/yaml/merge3/merge3.go
Normal file
@@ -0,0 +1,88 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package merge contains libraries for merging fields from one RNode to another
|
||||
// RNode
|
||||
package merge3
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml/walk"
|
||||
)
|
||||
|
||||
const Help = `
|
||||
Description:
|
||||
|
||||
merge3 identifies changes between an original source + updated source and merges the result
|
||||
into a destination, overriding the destination fields where they have changed between
|
||||
original and updated.
|
||||
|
||||
### Resource MergeRules
|
||||
|
||||
- Resources present in the original and deleted from the update are deleted.
|
||||
- Resources missing from the original and added in the update are added.
|
||||
- Resources present only in the dest are kept without changes.
|
||||
- Resources present in both the update and the dest are merged *original + update + dest => dest*.
|
||||
|
||||
### Field Merge Rules
|
||||
|
||||
Fields are recursively merged using the following rules:
|
||||
|
||||
- scalars
|
||||
- if present in either dest or updated and 'null', clear the value
|
||||
- if unchanged between original and updated, keep dest value
|
||||
- if changed between original and updated (added, deleted, changed), take the updated value
|
||||
|
||||
- non-associative lists -- lists without a merge key
|
||||
- if present in either dest or updated and 'null', clear the value
|
||||
- if unchanged between original and updated, keep dest value
|
||||
- if changed between original and updated (added, deleted, changed), take the updated value
|
||||
|
||||
- map keys and fields -- paired by the map-key / field-name
|
||||
- if present in either dest or updated and 'null', clear the value
|
||||
- if present only in the dest, it keeps its value
|
||||
- if not-present in the dest, add the delta between original-updated as a field
|
||||
- otherwise recursively merge the value between original, updated, dest
|
||||
|
||||
- associative list elements -- paired by the associative key
|
||||
- if present only in the dest, it keeps its value
|
||||
- if not-present in the dest, add the delta between original-updated as a field
|
||||
- otherwise recursively merge the value between original, updated, dest
|
||||
|
||||
### Associative Keys
|
||||
|
||||
Associative keys are used to identify "same" elements within 2 different lists, and merge them.
|
||||
The following fields are recognized as associative keys:
|
||||
|
||||
` + "[`mountPath`, `devicePath`, `ip`, `type`, `topologyKey`, `name`, `containerPort`]" + `
|
||||
|
||||
Any lists where all of the elements contain associative keys will be merged as associative lists.
|
||||
`
|
||||
|
||||
func Merge(dest, original, update *yaml.RNode) (*yaml.RNode, error) {
|
||||
// if update == nil && original != nil => declarative deletion
|
||||
|
||||
return walk.Walker{Visitor: Visitor{},
|
||||
Sources: []*yaml.RNode{dest, original, update}}.Walk()
|
||||
}
|
||||
|
||||
func MergeStrings(dest, original, update string) (string, error) {
|
||||
srcOriginal, err := yaml.Parse(original)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
srcUpdated, err := yaml.Parse(update)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
d, err := yaml.Parse(dest)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
result, err := Merge(d, srcOriginal, srcUpdated)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
47
kyaml/yaml/merge3/merge3_test.go
Normal file
47
kyaml/yaml/merge3/merge3_test.go
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package merge3_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
. "sigs.k8s.io/kustomize/kyaml/yaml/merge3"
|
||||
)
|
||||
|
||||
var testCases = [][]testCase{scalarTestCases, listTestCases, mapTestCases, elementTestCases}
|
||||
|
||||
func TestMerge(t *testing.T) {
|
||||
for i := range testCases {
|
||||
for _, tc := range testCases[i] {
|
||||
actual, err := MergeStrings(tc.local, tc.origin, tc.update)
|
||||
if tc.err == nil {
|
||||
if !assert.NoError(t, err, tc.description) {
|
||||
t.FailNow()
|
||||
}
|
||||
if !assert.Equal(t,
|
||||
strings.TrimSpace(tc.expected), strings.TrimSpace(actual), tc.description) {
|
||||
t.FailNow()
|
||||
}
|
||||
} else {
|
||||
if !assert.Errorf(t, err, tc.description) {
|
||||
t.FailNow()
|
||||
}
|
||||
if !assert.Contains(t, tc.err.Error(), err.Error()) {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
description string
|
||||
origin string
|
||||
update string
|
||||
local string
|
||||
expected string
|
||||
err error
|
||||
}
|
||||
135
kyaml/yaml/merge3/scalar_test.go
Normal file
135
kyaml/yaml/merge3/scalar_test.go
Normal file
@@ -0,0 +1,135 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package merge3_test
|
||||
|
||||
var scalarTestCases = []testCase{
|
||||
// Scalar Field Test Cases
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`Set and updated a field`,
|
||||
`kind: Deployment`,
|
||||
`kind: StatefulSet`,
|
||||
`kind: Deployment`,
|
||||
`kind: StatefulSet`, nil},
|
||||
|
||||
{`Add an updated field`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment # old value`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet # new value`,
|
||||
`
|
||||
apiVersion: apps/v1`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet # new value`, nil},
|
||||
|
||||
{`Add keep an omitted field`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
spec: foo # field not present in source
|
||||
`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
spec: foo # field not present in source
|
||||
kind: StatefulSet
|
||||
`, nil},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
// TODO(#36): consider making this an error
|
||||
{`Change an updated field`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment # old value`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet # new value`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: Service # conflicting value`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet # new value`, nil},
|
||||
|
||||
{`Ignore a field`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment # ignore this field`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment # ignore this field`,
|
||||
`
|
||||
apiVersion: apps/v1`,
|
||||
`
|
||||
apiVersion: apps/v1`, nil},
|
||||
|
||||
{`Explicitly clear a field`,
|
||||
`
|
||||
apiVersion: apps/v1`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: null # clear this value`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment # value to be cleared`,
|
||||
`
|
||||
apiVersion: apps/v1`, nil},
|
||||
|
||||
{`Implicitly clear a field`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment # clear this field`,
|
||||
`
|
||||
apiVersion: apps/v1`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment # clear this field`,
|
||||
`
|
||||
apiVersion: apps/v1`, nil},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
// TODO(#36): consider making this an error
|
||||
{`Implicitly clear a changed field`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment`,
|
||||
`
|
||||
apiVersion: apps/v1`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet`,
|
||||
`
|
||||
apiVersion: apps/v1`, nil},
|
||||
|
||||
//
|
||||
// Test Case
|
||||
//
|
||||
{`Merge an empty scalar value`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: {}
|
||||
`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
`,
|
||||
`
|
||||
apiVersion: apps/v1
|
||||
kind: {}
|
||||
`, nil},
|
||||
}
|
||||
161
kyaml/yaml/merge3/visitor.go
Normal file
161
kyaml/yaml/merge3/visitor.go
Normal file
@@ -0,0 +1,161 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package merge3
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml/walk"
|
||||
)
|
||||
|
||||
type ConflictStrategy uint
|
||||
|
||||
const (
|
||||
// TODO: Support more strategies
|
||||
TakeUpdate ConflictStrategy = 1 + iota
|
||||
)
|
||||
|
||||
type Visitor struct{}
|
||||
|
||||
func (m Visitor) VisitMap(nodes walk.Sources) (*yaml.RNode, error) {
|
||||
if yaml.IsNull(nodes.Updated()) || yaml.IsNull(nodes.Dest()) {
|
||||
// explicitly cleared from either dest or update
|
||||
return walk.ClearNode, nil
|
||||
}
|
||||
if nodes.Dest() == nil && nodes.Updated() == nil {
|
||||
// implicitly cleared missing from both dest and update
|
||||
return walk.ClearNode, nil
|
||||
}
|
||||
|
||||
if nodes.Dest() == nil {
|
||||
// not cleared, but missing from the dest
|
||||
// initialize a new value that can be recursively merged
|
||||
return yaml.NewRNode(&yaml.Node{Kind: yaml.MappingNode}), nil
|
||||
}
|
||||
// recursively merge the dest with the original and updated
|
||||
return nodes.Dest(), nil
|
||||
}
|
||||
|
||||
func (m Visitor) visitAList(nodes walk.Sources) (*yaml.RNode, error) {
|
||||
if yaml.IsEmpty(nodes.Updated()) && !yaml.IsEmpty(nodes.Origin()) {
|
||||
// implicitly cleared from update -- element was deleted
|
||||
return walk.ClearNode, nil
|
||||
}
|
||||
if yaml.IsEmpty(nodes.Dest()) {
|
||||
// not cleared, but missing from the dest
|
||||
// initialize a new value that can be recursively merged
|
||||
return yaml.NewRNode(&yaml.Node{Kind: yaml.SequenceNode}), nil
|
||||
}
|
||||
|
||||
// recursively merge the dest with the original and updated
|
||||
return nodes.Dest(), nil
|
||||
}
|
||||
|
||||
func (m Visitor) VisitScalar(nodes walk.Sources) (*yaml.RNode, error) {
|
||||
if yaml.IsNull(nodes.Updated()) || yaml.IsNull(nodes.Dest()) {
|
||||
// explicitly cleared from either dest or update
|
||||
return nil, nil
|
||||
}
|
||||
if yaml.IsEmpty(nodes.Updated()) != yaml.IsEmpty(nodes.Origin()) {
|
||||
// value added or removed in update
|
||||
return nodes.Updated(), nil
|
||||
}
|
||||
if yaml.IsEmpty(nodes.Updated()) && yaml.IsEmpty(nodes.Origin()) {
|
||||
// value added or removed in update
|
||||
return nodes.Dest(), nil
|
||||
}
|
||||
|
||||
if nodes.Updated().YNode().Value != nodes.Origin().YNode().Value {
|
||||
// value changed in update
|
||||
return nodes.Updated(), nil
|
||||
}
|
||||
|
||||
// unchanged between origin and update, keep the dest
|
||||
return nodes.Dest(), nil
|
||||
}
|
||||
|
||||
func (m Visitor) visitNAList(nodes walk.Sources) (*yaml.RNode, error) {
|
||||
if yaml.IsNull(nodes.Updated()) || yaml.IsNull(nodes.Dest()) {
|
||||
// explicitly cleared from either dest or update
|
||||
return walk.ClearNode, nil
|
||||
}
|
||||
|
||||
if yaml.IsEmpty(nodes.Updated()) != yaml.IsEmpty(nodes.Origin()) {
|
||||
// value added or removed in update
|
||||
return nodes.Updated(), nil
|
||||
}
|
||||
if yaml.IsEmpty(nodes.Updated()) && yaml.IsEmpty(nodes.Origin()) {
|
||||
// value not present in source or dest
|
||||
return nodes.Dest(), nil
|
||||
}
|
||||
|
||||
// compare origin and update values to see if they have changed
|
||||
values, err := m.getStrValues(nodes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if values.Update != values.Origin {
|
||||
// value changed in update
|
||||
return nodes.Updated(), nil
|
||||
}
|
||||
|
||||
// unchanged between origin and update, keep the dest
|
||||
return nodes.Dest(), nil
|
||||
}
|
||||
|
||||
func (m Visitor) VisitList(nodes walk.Sources, kind walk.ListKind) (*yaml.RNode, error) {
|
||||
if kind == walk.AssociativeList {
|
||||
return m.visitAList(nodes)
|
||||
}
|
||||
// non-associative list
|
||||
return m.visitNAList(nodes)
|
||||
}
|
||||
|
||||
func (m Visitor) getStrValues(nodes walk.Sources) (strValues, error) {
|
||||
var uStr, oStr, dStr string
|
||||
var err error
|
||||
if nodes.Updated() != nil && nodes.Updated().YNode() != nil {
|
||||
s := nodes.Updated().YNode().Style
|
||||
defer func() {
|
||||
nodes.Updated().YNode().Style = s
|
||||
}()
|
||||
nodes.Updated().YNode().Style = yaml.FlowStyle | yaml.SingleQuotedStyle
|
||||
uStr, err = nodes.Updated().String()
|
||||
if err != nil {
|
||||
return strValues{}, err
|
||||
}
|
||||
}
|
||||
if nodes.Origin() != nil && nodes.Origin().YNode() != nil {
|
||||
s := nodes.Origin().YNode().Style
|
||||
defer func() {
|
||||
nodes.Origin().YNode().Style = s
|
||||
}()
|
||||
nodes.Origin().YNode().Style = yaml.FlowStyle | yaml.SingleQuotedStyle
|
||||
oStr, err = nodes.Origin().String()
|
||||
if err != nil {
|
||||
return strValues{}, err
|
||||
}
|
||||
|
||||
}
|
||||
if nodes.Dest() != nil && nodes.Dest().YNode() != nil {
|
||||
s := nodes.Dest().YNode().Style
|
||||
defer func() {
|
||||
nodes.Dest().YNode().Style = s
|
||||
}()
|
||||
nodes.Dest().YNode().Style = yaml.FlowStyle | yaml.SingleQuotedStyle
|
||||
dStr, err = nodes.Dest().String()
|
||||
if err != nil {
|
||||
return strValues{}, err
|
||||
}
|
||||
}
|
||||
|
||||
return strValues{Origin: oStr, Update: uStr, Dest: dStr}, nil
|
||||
}
|
||||
|
||||
type strValues struct {
|
||||
Origin string
|
||||
Update string
|
||||
Dest string
|
||||
}
|
||||
|
||||
var _ walk.Visitor = Visitor{}
|
||||
108
kyaml/yaml/order.go
Normal file
108
kyaml/yaml/order.go
Normal file
@@ -0,0 +1,108 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package yaml
|
||||
|
||||
// fieldSortOrder contains the relative ordering of fields when formatting an
|
||||
// object.
|
||||
var fieldSortOrder = []string{
|
||||
// top-level metadata
|
||||
"name", "generateName", "namespace", "clusterName",
|
||||
"apiVersion", "kind", "metadata", "type",
|
||||
"labels", "annotations",
|
||||
"spec", "status",
|
||||
|
||||
// secret and configmap
|
||||
"stringData", "data", "binaryData",
|
||||
|
||||
//cronjobspec, daemonsetspec, deploymentspec, statefulsetspec,
|
||||
// jobspec fields
|
||||
"parallelism", "completions", "activeDeadlineSeconds", "backoffLimit",
|
||||
"replicas", "selector", "manualSelector", "template",
|
||||
"ttlSecondsAfterFinished", "volumeClaimTemplates", "service", "serviceName",
|
||||
"podManagementPolicy", "updateStrategy", "strategy", "minReadySeconds",
|
||||
"revision", "revisionHistoryLimit", "paused", "progressDeadlineSeconds",
|
||||
|
||||
// podspec
|
||||
// podspec scalars
|
||||
"restartPolicy", "terminationGracePeriodSeconds",
|
||||
"activeDeadlineSeconds", "dnsPolicy", "serviceAccountName",
|
||||
"serviceAccount", "automountServiceAccountToken", "nodeName",
|
||||
"hostNetwork", "hostPID", "hostIPC", "shareProcessNamespace", "hostname",
|
||||
"subdomain", "schedulerName", "priorityClassName", "priority",
|
||||
"runtimeClassName", "enableServiceLinks",
|
||||
|
||||
// podspec lists and maps
|
||||
"nodeSelector", "hostAliases",
|
||||
|
||||
// podspec objects
|
||||
"initContainers", "containers", "volumes", "securityContext",
|
||||
"imagePullSecrets", "affinity", "tolerations", "dnsConfig",
|
||||
"readinessGates",
|
||||
|
||||
// containers
|
||||
"image", "command", "args", "workingDir", "ports", "envFrom", "env",
|
||||
"resources", "volumeMounts", "volumeDevices", "livenessProbe",
|
||||
"readinessProbe", "lifecycle", "terminationMessagePath",
|
||||
"terminationMessagePolicy", "imagePullPolicy", "securityContext",
|
||||
"stdin", "stdinOnce", "tty",
|
||||
|
||||
// service
|
||||
"clusterIP", "externalIPs", "loadBalancerIP", "loadBalancerSourceRanges",
|
||||
"externalName", "externalTrafficPolicy", "sessionAffinity",
|
||||
|
||||
// ports
|
||||
"protocol", "port", "targetPort", "hostPort", "containerPort", "hostIP",
|
||||
|
||||
// volumemount
|
||||
"readOnly", "mountPath", "subPath", "subPathExpr", "mountPropagation",
|
||||
|
||||
// envvar + envvarsource
|
||||
"value", "valueFrom", "fieldRef", "resourceFieldRef", "configMapKeyRef",
|
||||
"secretKeyRef", "prefix", "configMapRef", "secretRef",
|
||||
}
|
||||
|
||||
type set map[string]interface{}
|
||||
|
||||
func newSet(values ...string) set {
|
||||
m := map[string]interface{}{}
|
||||
for _, value := range values {
|
||||
m[value] = nil
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (s set) Has(key string) bool {
|
||||
_, found := s[key]
|
||||
return found
|
||||
}
|
||||
|
||||
// whitelistedListSortKinds contains the set of kinds that are whitelisted
|
||||
// for sorting list field elements
|
||||
var WhitelistedListSortKinds = newSet(
|
||||
"CronJob", "DaemonSet", "Deployment", "Job", "ReplicaSet", "StatefulSet",
|
||||
"ValidatingWebhookConfiguration")
|
||||
|
||||
// whitelistedListSortApis contains the set of apis that are whitelisted for
|
||||
// sorting list field elements
|
||||
var WhitelistedListSortApis = newSet(
|
||||
"apps/v1", "apps/v1beta1", "apps/v1beta2", "batch/v1", "batch/v1beta1",
|
||||
"extensions/v1beta1", "v1", "admissionregistration.k8s.io/v1beta1")
|
||||
|
||||
// whitelistedListSortFields contains json paths to list fields that should
|
||||
// be sorted, and the field they should be sorted by
|
||||
var WhitelistedListSortFields = map[string]string{
|
||||
".spec.template.spec.containers": "name",
|
||||
".webhooks.rules.operations": "",
|
||||
}
|
||||
|
||||
// FieldOrder indexes fields and maps them to relative precedence
|
||||
var FieldOrder map[string]int
|
||||
|
||||
func init() {
|
||||
// create an index of field orderings
|
||||
FieldOrder = map[string]int{}
|
||||
for i, f := range fieldSortOrder {
|
||||
FieldOrder[f] = i + 1
|
||||
}
|
||||
}
|
||||
534
kyaml/yaml/types.go
Normal file
534
kyaml/yaml/types.go
Normal file
@@ -0,0 +1,534 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package yaml
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
"sigs.k8s.io/kustomize/kyaml/sets"
|
||||
)
|
||||
|
||||
const (
|
||||
// NullNodeTag is the tag set for a yaml.Document that contains no data -- e.g. it isn't a
|
||||
// Map, Slice, Document, etc
|
||||
NullNodeTag = "!!null"
|
||||
)
|
||||
|
||||
func NullNode() *RNode {
|
||||
return NewRNode(&Node{Tag: NullNodeTag})
|
||||
}
|
||||
|
||||
func IsMissingOrNull(node *RNode) bool {
|
||||
if node == nil || node.YNode() == nil || node.YNode().Tag == NullNodeTag {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsEmpty(node *RNode) bool {
|
||||
if node == nil || node.YNode() == nil || node.YNode().Tag == NullNodeTag {
|
||||
return true
|
||||
}
|
||||
|
||||
if node.YNode().Kind == yaml.MappingNode && len(node.YNode().Content) == 0 {
|
||||
return true
|
||||
}
|
||||
if node.YNode().Kind == yaml.SequenceNode && len(node.YNode().Content) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func IsNull(node *RNode) bool {
|
||||
return node != nil && node.YNode() != nil && node.YNode().Tag == NullNodeTag
|
||||
}
|
||||
|
||||
func IsFieldEmpty(node *MapNode) bool {
|
||||
if node == nil || node.Value == nil || node.Value.YNode() == nil ||
|
||||
node.Value.YNode().Tag == NullNodeTag {
|
||||
return true
|
||||
}
|
||||
|
||||
if node.Value.YNode().Kind == yaml.MappingNode && len(node.Value.YNode().Content) == 0 {
|
||||
return true
|
||||
}
|
||||
if node.Value.YNode().Kind == yaml.SequenceNode && len(node.Value.YNode().Content) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func IsFieldNull(node *MapNode) bool {
|
||||
return node != nil && node.Value != nil && node.Value.YNode() != nil &&
|
||||
node.Value.YNode().Tag == NullNodeTag
|
||||
}
|
||||
|
||||
// Parser parses values into configuration.
|
||||
type Parser struct {
|
||||
Kind string `yaml:"kind,omitempty"`
|
||||
Value string `yaml:"value,omitempty"`
|
||||
}
|
||||
|
||||
func (p Parser) Filter(_ *RNode) (*RNode, error) {
|
||||
d := yaml.NewDecoder(bytes.NewBuffer([]byte(p.Value)))
|
||||
o := &RNode{value: &yaml.Node{}}
|
||||
return o, d.Decode(o.value)
|
||||
}
|
||||
|
||||
// Parse parses a yaml string into an *RNode
|
||||
func Parse(value string) (*RNode, error) {
|
||||
return Parser{Value: value}.Filter(nil)
|
||||
}
|
||||
|
||||
// TODO(pwittrock): test this
|
||||
func GetStyle(styles ...string) Style {
|
||||
var style Style
|
||||
for _, s := range styles {
|
||||
switch s {
|
||||
case "TaggedStyle":
|
||||
style |= TaggedStyle
|
||||
case "DoubleQuotedStyle":
|
||||
style |= DoubleQuotedStyle
|
||||
case "SingleQuotedStyle":
|
||||
style |= SingleQuotedStyle
|
||||
case "LiteralStyle":
|
||||
style |= LiteralStyle
|
||||
case "FoldedStyle":
|
||||
style |= FoldedStyle
|
||||
case "FlowStyle":
|
||||
style |= FlowStyle
|
||||
}
|
||||
}
|
||||
return style
|
||||
}
|
||||
|
||||
// MustParse parses a yaml string into an *RNode and panics if there is an error
|
||||
func MustParse(value string) *RNode {
|
||||
v, err := Parser{Value: value}.Filter(nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// NewScalarRNode returns a new Scalar *RNode containing the provided value.
|
||||
func NewScalarRNode(value string) *RNode {
|
||||
return &RNode{
|
||||
value: &yaml.Node{
|
||||
Kind: yaml.ScalarNode,
|
||||
Value: value,
|
||||
}}
|
||||
}
|
||||
|
||||
// NewListRNode returns a new List *RNode containing the provided value.
|
||||
func NewListRNode(values ...string) *RNode {
|
||||
seq := &RNode{value: &yaml.Node{Kind: yaml.SequenceNode}}
|
||||
for _, v := range values {
|
||||
seq.value.Content = append(seq.value.Content, &yaml.Node{
|
||||
Kind: yaml.ScalarNode,
|
||||
Value: v,
|
||||
})
|
||||
}
|
||||
return seq
|
||||
}
|
||||
|
||||
// NewRNode returns a new *RNode containing the provided value.
|
||||
func NewRNode(value *yaml.Node) *RNode {
|
||||
if value != nil {
|
||||
value.Style = 0
|
||||
}
|
||||
return &RNode{value: value}
|
||||
}
|
||||
|
||||
// GrepFilter may modify or walk the RNode.
|
||||
// When possible, Filters should be serializable to yaml so that they can be described
|
||||
// declaratively as data.
|
||||
//
|
||||
// Analogous to http://www.linfo.org/filters.html
|
||||
type Filter interface {
|
||||
Filter(object *RNode) (*RNode, error)
|
||||
}
|
||||
|
||||
type FilterFunc func(object *RNode) (*RNode, error)
|
||||
|
||||
func (f FilterFunc) Filter(object *RNode) (*RNode, error) {
|
||||
return f(object)
|
||||
}
|
||||
|
||||
// RNode provides functions for manipulating Kubernetes Resources
|
||||
// Objects unmarshalled into *yaml.Nodes
|
||||
type RNode struct {
|
||||
// fieldPath contains the path from the root of the KubernetesObject to
|
||||
// this field.
|
||||
// Only field names are captured in the path.
|
||||
// e.g. a image field in a Deployment would be
|
||||
// 'spec.template.spec.containers.image'
|
||||
fieldPath []string
|
||||
|
||||
// FieldValue contains the value.
|
||||
// FieldValue is always set:
|
||||
// field: field value
|
||||
// list entry: list entry value
|
||||
// object root: object root
|
||||
value *yaml.Node
|
||||
|
||||
Match []string
|
||||
}
|
||||
|
||||
// MapNode wraps a field key and value.
|
||||
type MapNode struct {
|
||||
Key *RNode
|
||||
Value *RNode
|
||||
}
|
||||
|
||||
type MapNodeSlice []*MapNode
|
||||
|
||||
func (m MapNodeSlice) Keys() []*RNode {
|
||||
var keys []*RNode
|
||||
for i := range m {
|
||||
if m[i] != nil {
|
||||
keys = append(keys, m[i].Key)
|
||||
}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func (m MapNodeSlice) Values() []*RNode {
|
||||
var values []*RNode
|
||||
for i := range m {
|
||||
if m[i] != nil {
|
||||
values = append(values, m[i].Value)
|
||||
} else {
|
||||
values = append(values, nil)
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
// ResourceMeta contains the metadata for a Resource.
|
||||
type ResourceMeta struct {
|
||||
ApiVersion string `yaml:"apiVersion,omitempty"`
|
||||
Kind string `yaml:"kind,omitempty"`
|
||||
ObjectMeta `yaml:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
func NewResourceMeta(name string, typeMeta ResourceMeta) ResourceMeta {
|
||||
return ResourceMeta{
|
||||
Kind: typeMeta.Kind,
|
||||
ApiVersion: typeMeta.ApiVersion,
|
||||
ObjectMeta: ObjectMeta{Name: name},
|
||||
}
|
||||
}
|
||||
|
||||
type ObjectMeta struct {
|
||||
Name string `yaml:"name,omitempty"`
|
||||
Namespace string `yaml:"namespace,omitempty"`
|
||||
Labels map[string]string `yaml:"labels,omitempty"`
|
||||
Annotations map[string]string `yaml:"annotations,omitempty"`
|
||||
}
|
||||
|
||||
var MissingMetaError = errors.New("missing Resource metadata")
|
||||
|
||||
func (rn *RNode) GetMeta() (ResourceMeta, error) {
|
||||
m := ResourceMeta{}
|
||||
b := &bytes.Buffer{}
|
||||
e := NewEncoder(b)
|
||||
if err := e.Encode(rn.YNode()); err != nil {
|
||||
return m, err
|
||||
}
|
||||
if err := e.Close(); err != nil {
|
||||
return m, err
|
||||
}
|
||||
d := yaml.NewDecoder(b)
|
||||
d.KnownFields(false) // only want to parse the metadata
|
||||
if err := d.Decode(&m); err != nil {
|
||||
return m, err
|
||||
}
|
||||
if reflect.DeepEqual(m, ResourceMeta{}) {
|
||||
return m, MissingMetaError
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Pipe sequentially invokes each GrepFilter, and passes the result to the next
|
||||
// GrepFilter.
|
||||
//
|
||||
// Analogous to http://www.linfo.org/pipes.html
|
||||
//
|
||||
// * rn is provided as input to the first GrepFilter.
|
||||
// * if any GrepFilter returns an error, immediately return the error
|
||||
// * if any GrepFilter returns a nil RNode, immediately return nil, nil
|
||||
// * if all Filters succeed with non-empty results, return the final result
|
||||
func (rn *RNode) Pipe(functions ...Filter) (*RNode, error) {
|
||||
// check if rn is nil to make chaining Pipe calls easier
|
||||
if rn == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var err error
|
||||
var v *RNode
|
||||
if rn.value != nil && rn.value.Kind == yaml.DocumentNode {
|
||||
// the first node may be a DocumentNode containing a single MappingNode
|
||||
v = &RNode{value: rn.value.Content[0]}
|
||||
} else {
|
||||
v = rn
|
||||
}
|
||||
|
||||
// return each fn in sequence until encountering an error or missing value
|
||||
for _, c := range functions {
|
||||
v, err = c.Filter(v)
|
||||
if err != nil || v == nil {
|
||||
return v, err
|
||||
}
|
||||
}
|
||||
return v, err
|
||||
}
|
||||
|
||||
// Document returns the Node RNode for the value. Does not unwrap the node if it is a
|
||||
// DocumentNodes
|
||||
func (rn *RNode) Document() *yaml.Node {
|
||||
return rn.value
|
||||
}
|
||||
|
||||
// YNode returns the yaml.Node value. If the yaml.Node value is a DocumentNode,
|
||||
// YNode will return the DocumentNode Content entry instead of the DocumentNode.
|
||||
func (rn *RNode) YNode() *yaml.Node {
|
||||
if rn == nil || rn.value == nil {
|
||||
return nil
|
||||
}
|
||||
if rn.value.Kind == yaml.DocumentNode {
|
||||
return rn.value.Content[0]
|
||||
}
|
||||
return rn.value
|
||||
}
|
||||
|
||||
// SetYNode sets the yaml.Node value.
|
||||
func (rn *RNode) SetYNode(node *yaml.Node) {
|
||||
if rn.value == nil || node == nil {
|
||||
rn.value = node
|
||||
return
|
||||
}
|
||||
*rn.value = *node
|
||||
}
|
||||
|
||||
// SetYNode sets the value on a Document.
|
||||
func (rn *RNode) AppendToFieldPath(parts ...string) {
|
||||
rn.fieldPath = append(rn.fieldPath, parts...)
|
||||
}
|
||||
|
||||
// FieldPath returns the field path from the object root to rn. Does not include list indexes.
|
||||
func (rn *RNode) FieldPath() []string {
|
||||
return rn.fieldPath
|
||||
}
|
||||
|
||||
const (
|
||||
Trim = "Trim"
|
||||
Flow = "Flow"
|
||||
)
|
||||
|
||||
func String(node *yaml.Node, opts ...string) (string, error) {
|
||||
if node == nil {
|
||||
return "", nil
|
||||
}
|
||||
optsSet := sets.String{}
|
||||
optsSet.Insert(opts...)
|
||||
if optsSet.Has(Flow) {
|
||||
oldStyle := node.Style
|
||||
defer func() {
|
||||
node.Style = oldStyle
|
||||
}()
|
||||
node.Style = yaml.FlowStyle
|
||||
}
|
||||
|
||||
b := &bytes.Buffer{}
|
||||
e := NewEncoder(b)
|
||||
err := e.Encode(node)
|
||||
e.Close()
|
||||
val := b.String()
|
||||
if optsSet.Has(Trim) {
|
||||
val = strings.TrimSpace(val)
|
||||
}
|
||||
return val, err
|
||||
}
|
||||
|
||||
// NewScalarRNode returns the yaml NewScalarRNode representation of the RNode value.
|
||||
func (rn *RNode) String() (string, error) {
|
||||
if rn == nil {
|
||||
return "", nil
|
||||
}
|
||||
return String(rn.value)
|
||||
}
|
||||
|
||||
func (rn *RNode) MustString() string {
|
||||
s, err := rn.String()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Content returns the value node's Content field.
|
||||
func (rn *RNode) Content() []*yaml.Node {
|
||||
if rn == nil {
|
||||
return nil
|
||||
}
|
||||
return rn.YNode().Content
|
||||
}
|
||||
|
||||
// Fields returns the list of fields for a ResourceNode containing a MappingNode
|
||||
// value.
|
||||
func (rn *RNode) Fields() ([]string, error) {
|
||||
if err := ErrorIfInvalid(rn, yaml.MappingNode); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var fields []string
|
||||
for i := 0; i < len(rn.Content()); i += 2 {
|
||||
fields = append(fields, rn.Content()[i].Value)
|
||||
}
|
||||
return fields, nil
|
||||
}
|
||||
|
||||
// Field returns the fieldName, fieldValue pair for MappingNodes. Returns nil for non-MappingNodes.
|
||||
func (rn *RNode) Field(field string) *MapNode {
|
||||
if rn.YNode().Kind != yaml.MappingNode {
|
||||
return nil
|
||||
}
|
||||
for i := 0; i < len(rn.Content()); IncrementFieldIndex(&i) {
|
||||
isMatchingField := rn.Content()[i].Value == field
|
||||
if isMatchingField {
|
||||
return &MapNode{Key: NewRNode(rn.Content()[i]), Value: NewRNode(rn.Content()[i+1])}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// VisitFields calls fn for each field in rn.
|
||||
func (rn *RNode) VisitFields(fn func(node *MapNode) error) error {
|
||||
// get the list of srcFieldNames
|
||||
srcFieldNames, err := rn.Fields()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// visit each field
|
||||
for _, fieldName := range srcFieldNames {
|
||||
if err := fn(rn.Field(fieldName)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Elements returns a list of elements for a ResourceNode containing a
|
||||
// SequenceNode value.
|
||||
func (rn *RNode) Elements() ([]*RNode, error) {
|
||||
if err := ErrorIfInvalid(rn, yaml.SequenceNode); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var elements []*RNode
|
||||
for i := 0; i < len(rn.Content()); i += 1 {
|
||||
elements = append(elements, NewRNode(rn.Content()[i]))
|
||||
}
|
||||
return elements, nil
|
||||
}
|
||||
|
||||
func (rn *RNode) ElementValues(key string) ([]string, error) {
|
||||
if err := ErrorIfInvalid(rn, yaml.SequenceNode); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var elements []string
|
||||
for i := 0; i < len(rn.Content()); i += 1 {
|
||||
field := NewRNode(rn.Content()[i]).Field(key)
|
||||
if !IsFieldEmpty(field) {
|
||||
elements = append(elements, field.Value.YNode().Value)
|
||||
}
|
||||
}
|
||||
return elements, nil
|
||||
}
|
||||
|
||||
// Element returns the element in the list which contains the field matching the value.
|
||||
// Returns nil for non-SequenceNodes
|
||||
func (rn *RNode) Element(key, value string) *RNode {
|
||||
if rn.YNode().Kind != yaml.SequenceNode {
|
||||
return nil
|
||||
}
|
||||
elem, err := rn.Pipe(MatchElement(key, value))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return elem
|
||||
}
|
||||
|
||||
// VisitElements calls fn for each element in the list.
|
||||
func (rn *RNode) VisitElements(fn func(node *RNode) error) error {
|
||||
elements, err := rn.Elements()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range elements {
|
||||
if err := fn(elements[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AssociativeSequencePaths is a map of paths to sequences that have associative keys.
|
||||
// The order sets the precedence of the merge keys -- if multiple keys are present
|
||||
// in the list, then the FIRST key which ALL elements have is used as the
|
||||
// associative key.
|
||||
var AssociativeSequenceKeys = []string{
|
||||
"mountPath", "devicePath", "ip", "type", "topologyKey", "name", "containerPort",
|
||||
}
|
||||
|
||||
func IsAssociative(nodes []*RNode) bool {
|
||||
for i := range nodes {
|
||||
node := nodes[i]
|
||||
if IsEmpty(node) {
|
||||
continue
|
||||
}
|
||||
if node.IsAssociative() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsAssociative returns true if the RNode is for an associative list.
|
||||
func (rn *RNode) IsAssociative() bool {
|
||||
return rn.GetAssociativeKey() != ""
|
||||
}
|
||||
|
||||
// GetAssociativeKey returns the associative key used to merge the list, or "" if the
|
||||
// list is not associative.
|
||||
func (rn *RNode) GetAssociativeKey() string {
|
||||
// look for any associative keys in the first element
|
||||
for _, key := range AssociativeSequenceKeys {
|
||||
if checkKey(key, rn.Content()) {
|
||||
return key
|
||||
}
|
||||
}
|
||||
|
||||
// element doesn't have an associative keys
|
||||
return ""
|
||||
}
|
||||
|
||||
// checkKey returns true if all elems have the key
|
||||
func checkKey(key string, elems []*Node) bool {
|
||||
count := 0
|
||||
for i := range elems {
|
||||
elem := NewRNode(elems[i])
|
||||
if elem.Field(key) != nil {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count == len(elems)
|
||||
}
|
||||
128
kyaml/yaml/walk/associative_sequence.go
Normal file
128
kyaml/yaml/walk/associative_sequence.go
Normal file
@@ -0,0 +1,128 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package walk
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"sigs.k8s.io/kustomize/kyaml/sets"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
func (l *Walker) walkAssociativeSequence() (*yaml.RNode, error) {
|
||||
|
||||
// may require initializing the dest node
|
||||
dest, err := l.Sources.setDestNode(l.VisitList(l.Sources, AssociativeList))
|
||||
if dest == nil || err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// find the list of elements we need to recursively walk
|
||||
key, err := l.elementKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values, err := l.elementValues(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// recursively set the elements in the list
|
||||
for _, value := range values {
|
||||
val, err := Walker{Visitor: l,
|
||||
Sources: l.elementValue(key, value)}.Walk()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if yaml.IsEmpty(val) {
|
||||
_, err = dest.Pipe(yaml.ElementSetter{Key: key, Value: value})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if val.Field(key) == nil {
|
||||
// make sure the key is set on the field
|
||||
_, err = val.Pipe(yaml.SetField(key, yaml.NewScalarRNode(value)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// this handles empty and non-empty values
|
||||
_, err = dest.Pipe(yaml.ElementSetter{Element: val.YNode(), Key: key, Value: value})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// field is empty
|
||||
if yaml.IsEmpty(dest) {
|
||||
return nil, nil
|
||||
}
|
||||
return dest, nil
|
||||
}
|
||||
|
||||
// elementKey returns the merge key to use for the associative list
|
||||
func (l Walker) elementKey() (string, error) {
|
||||
var key string
|
||||
for i := range l.Sources {
|
||||
if l.Sources[i] != nil && len(l.Sources[i].Content()) > 0 {
|
||||
newKey := l.Sources[i].GetAssociativeKey()
|
||||
if key != "" && key != newKey {
|
||||
return "", errors.Errorf(
|
||||
"conflicting merge keys [%s,%s] for field %s",
|
||||
key, newKey, strings.Join(l.Path, "."))
|
||||
}
|
||||
key = newKey
|
||||
}
|
||||
}
|
||||
if key == "" {
|
||||
return "", errors.Errorf("no merge key found for field %s",
|
||||
strings.Join(l.Path, "."))
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// elementValues returns a slice containing all values for the field across all elements
|
||||
// from all sources.
|
||||
// Return value slice is ordered using the original ordering from the elements, where
|
||||
// elements missing from earlier sources appear later.
|
||||
func (l Walker) elementValues(key string) ([]string, error) {
|
||||
// use slice to to keep elements in the original order
|
||||
// dest node must be first
|
||||
var returnValues []string
|
||||
seen := sets.String{}
|
||||
for i := range l.Sources {
|
||||
if l.Sources[i] == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// add the value of the field for each element
|
||||
// don't check error, we know this is a list node
|
||||
values, _ := l.Sources[i].ElementValues(key)
|
||||
for _, s := range values {
|
||||
if seen.Has(s) {
|
||||
continue
|
||||
}
|
||||
returnValues = append(returnValues, s)
|
||||
seen.Insert(s)
|
||||
}
|
||||
}
|
||||
return returnValues, nil
|
||||
}
|
||||
|
||||
// fieldValue returns a slice containing each source's value for fieldName
|
||||
func (l Walker) elementValue(key, value string) []*yaml.RNode {
|
||||
var fields []*yaml.RNode
|
||||
for i := range l.Sources {
|
||||
if l.Sources[i] == nil {
|
||||
fields = append(fields, nil)
|
||||
continue
|
||||
}
|
||||
fields = append(fields, l.Sources[i].Element(key, value))
|
||||
}
|
||||
return fields
|
||||
}
|
||||
81
kyaml/yaml/walk/map.go
Normal file
81
kyaml/yaml/walk/map.go
Normal file
@@ -0,0 +1,81 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package walk
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"sigs.k8s.io/kustomize/kyaml/sets"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// walkMap returns the value of VisitMap
|
||||
//
|
||||
// - call VisitMap
|
||||
// - set the return value on l.Dest
|
||||
// - walk each source field
|
||||
// - set each source field value on l.Dest
|
||||
func (l Walker) walkMap() (*yaml.RNode, error) {
|
||||
// get the new map value
|
||||
dest, err := l.Sources.setDestNode(l.VisitMap(l.Sources))
|
||||
if dest == nil || err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// recursively set the field values on the map
|
||||
for _, key := range l.fieldNames() {
|
||||
val, err := Walker{Visitor: l,
|
||||
Sources: l.fieldValue(key), Path: append(l.Path, key)}.Walk()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// this handles empty and non-empty values
|
||||
_, err = dest.Pipe(yaml.FieldSetter{Name: key, Value: val})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return dest, nil
|
||||
}
|
||||
|
||||
// valueIfPresent returns node.Value if node is non-nil, otherwise returns nil
|
||||
func (l Walker) valueIfPresent(node *yaml.MapNode) *yaml.RNode {
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
return node.Value
|
||||
}
|
||||
|
||||
// fieldNames returns a sorted slice containing the names of all fields that appear in any of
|
||||
// the sources
|
||||
func (l Walker) fieldNames() []string {
|
||||
fields := sets.String{}
|
||||
for _, s := range l.Sources {
|
||||
if s == nil {
|
||||
continue
|
||||
}
|
||||
// don't check error, we know this is a mapping node
|
||||
sFields, _ := s.Fields()
|
||||
fields.Insert(sFields...)
|
||||
}
|
||||
result := fields.List()
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
|
||||
// fieldValue returns a slice containing each source's value for fieldName
|
||||
func (l Walker) fieldValue(fieldName string) []*yaml.RNode {
|
||||
var fields []*yaml.RNode
|
||||
for i := range l.Sources {
|
||||
if l.Sources[i] == nil {
|
||||
fields = append(fields, nil)
|
||||
continue
|
||||
}
|
||||
field := l.Sources[i].Field(fieldName)
|
||||
fields = append(fields, l.valueIfPresent(field))
|
||||
}
|
||||
return fields
|
||||
}
|
||||
13
kyaml/yaml/walk/nonassociative_sequence.go
Normal file
13
kyaml/yaml/walk/nonassociative_sequence.go
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package walk
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// walkNonAssociativeSequence returns the value of VisitList
|
||||
func (l Walker) walkNonAssociativeSequence() (*yaml.RNode, error) {
|
||||
return l.VisitList(l.Sources, NonAssociateList)
|
||||
}
|
||||
11
kyaml/yaml/walk/scalar.go
Normal file
11
kyaml/yaml/walk/scalar.go
Normal file
@@ -0,0 +1,11 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package walk
|
||||
|
||||
import "sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
|
||||
// walkScalar returns the value of VisitScalar
|
||||
func (l Walker) walkScalar() (*yaml.RNode, error) {
|
||||
return l.VisitScalar(l.Sources)
|
||||
}
|
||||
38
kyaml/yaml/walk/visitor.go
Normal file
38
kyaml/yaml/walk/visitor.go
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright 2019 Google LLC
|
||||
//
|
||||
// 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 walk
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
type ListKind int32
|
||||
|
||||
const (
|
||||
AssociativeList ListKind = 1 + iota
|
||||
NonAssociateList
|
||||
)
|
||||
|
||||
// Visitor is invoked by walk with source and destination node pairs
|
||||
type Visitor interface {
|
||||
VisitMap(nodes Sources) (*yaml.RNode, error)
|
||||
|
||||
VisitScalar(nodes Sources) (*yaml.RNode, error)
|
||||
|
||||
VisitList(nodes Sources, kind ListKind) (*yaml.RNode, error)
|
||||
}
|
||||
|
||||
// NoOp is returned if GrepFilter should do nothing after calling Set
|
||||
var ClearNode *yaml.RNode = nil
|
||||
144
kyaml/yaml/walk/walk.go
Normal file
144
kyaml/yaml/walk/walk.go
Normal file
@@ -0,0 +1,144 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package walk
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// Filter walks the Source RNode and modifies the RNode provided to GrepFilter.
|
||||
type Walker struct {
|
||||
// Visitor is invoked by GrepFilter
|
||||
Visitor
|
||||
|
||||
// Source is the RNode to walk. All Source fields and associative list elements
|
||||
// will be visited.
|
||||
Sources Sources
|
||||
|
||||
// Path is the field path to the current Source Node.
|
||||
Path []string
|
||||
}
|
||||
|
||||
func (l Walker) Kind() yaml.Kind {
|
||||
for _, s := range l.Sources {
|
||||
if !yaml.IsEmpty(s) {
|
||||
return s.YNode().Kind
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// GrepFilter implements yaml.GrepFilter
|
||||
func (l Walker) Walk() (*yaml.RNode, error) {
|
||||
// invoke the handler for the corresponding node type
|
||||
switch l.Kind() {
|
||||
case yaml.MappingNode:
|
||||
if err := yaml.ErrorIfAnyInvalidAndNonNull(yaml.MappingNode, l.Sources...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return l.walkMap()
|
||||
case yaml.SequenceNode:
|
||||
if err := yaml.ErrorIfAnyInvalidAndNonNull(yaml.SequenceNode, l.Sources...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if yaml.IsAssociative(l.Sources) {
|
||||
return l.walkAssociativeSequence()
|
||||
} else {
|
||||
return l.walkNonAssociativeSequence()
|
||||
}
|
||||
case yaml.ScalarNode:
|
||||
if err := yaml.ErrorIfAnyInvalidAndNonNull(yaml.ScalarNode, l.Sources...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return l.walkScalar()
|
||||
case 0:
|
||||
// walk empty nodes as maps
|
||||
return l.walkMap()
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
DestIndex = iota
|
||||
OriginIndex
|
||||
UpdatedIndex
|
||||
)
|
||||
|
||||
type Sources []*yaml.RNode
|
||||
|
||||
// Dest returns the destination node
|
||||
func (s Sources) Dest() *yaml.RNode {
|
||||
if len(s) <= DestIndex {
|
||||
return nil
|
||||
}
|
||||
return s[DestIndex]
|
||||
}
|
||||
|
||||
// Origin returns the origin node
|
||||
func (s Sources) Origin() *yaml.RNode {
|
||||
if len(s) <= OriginIndex {
|
||||
return nil
|
||||
}
|
||||
return s[OriginIndex]
|
||||
}
|
||||
|
||||
// Updated returns the updated node
|
||||
func (s Sources) Updated() *yaml.RNode {
|
||||
if len(s) <= UpdatedIndex {
|
||||
return nil
|
||||
}
|
||||
return s[UpdatedIndex]
|
||||
}
|
||||
|
||||
func (s Sources) String() string {
|
||||
var values []string
|
||||
for i := range s {
|
||||
str, err := s[i].String()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
}
|
||||
values = append(values, str)
|
||||
}
|
||||
return strings.Join(values, "\n")
|
||||
}
|
||||
|
||||
// setDestNode sets the destination source node
|
||||
func (s Sources) setDestNode(node *yaml.RNode, err error) (*yaml.RNode, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s[0] = node
|
||||
return node, nil
|
||||
}
|
||||
|
||||
type FieldSources []*yaml.MapNode
|
||||
|
||||
// Dest returns the destination node
|
||||
func (s FieldSources) Dest() *yaml.MapNode {
|
||||
if len(s) <= DestIndex {
|
||||
return nil
|
||||
}
|
||||
return s[DestIndex]
|
||||
}
|
||||
|
||||
// Origin returns the origin node
|
||||
func (s FieldSources) Origin() *yaml.MapNode {
|
||||
if len(s) <= OriginIndex {
|
||||
return nil
|
||||
}
|
||||
return s[OriginIndex]
|
||||
}
|
||||
|
||||
// Updated returns the updated node
|
||||
func (s FieldSources) Updated() *yaml.MapNode {
|
||||
if len(s) <= UpdatedIndex {
|
||||
return nil
|
||||
}
|
||||
return s[UpdatedIndex]
|
||||
}
|
||||
Reference in New Issue
Block a user