kyaml: initial support for yaml and resource manipulation

This commit is contained in:
Phillip Wittrock
2019-11-04 11:27:47 -08:00
parent 588297f1f9
commit efd7c8e3f7
92 changed files with 13733 additions and 0 deletions

View 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},
}

View 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},
}

View 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},
}

View 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()
}

View 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
}

View 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},
}

View 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{}