Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions pkg/lib/update/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ func PkgHasUpdatedUpstream(local, origin string) (bool, error) {
return false, errors.E(op, types.UniquePath(local), err)
}

if originKf.Upstream == nil {
return false, nil
}
Comment on lines +40 to +42

// If the upstream information in local has changed from origin, it
Comment thread
tgzsolt marked this conversation as resolved.
// means the user had updated the package independently and we don't
// want to override it.
Expand Down
3 changes: 1 addition & 2 deletions pkg/lib/update/merge3/tuple.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import (
"sigs.k8s.io/kustomize/kyaml/kio/filters"
"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
"sigs.k8s.io/kustomize/kyaml/yaml"
"sigs.k8s.io/kustomize/kyaml/yaml/merge3"
"sigs.k8s.io/kustomize/kyaml/yaml/walk"
)

Expand All @@ -35,7 +34,7 @@ type tuple struct {
func (t *tuple) merge() (*yaml.RNode, error) {
return walk.Walker{
// same as in merge3.Merge()
Visitor: merge3.Visitor{},
Visitor: &Visitor{},
VisitKeysAsScalars: true,
Comment thread
tgzsolt marked this conversation as resolved.
Sources: []*yaml.RNode{t.dest, t.original, t.updated},
Comment on lines 35 to 41

Expand Down
179 changes: 179 additions & 0 deletions pkg/lib/update/merge3/visitor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
// Copyright 2019 The Kubernetes Authors.
// Copyright 2026 The kpt Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// copied from sigs.k8s.io/kustomize/kyaml@v0.21.1/yaml/merge3/visitor.go with modifications

package merge3

import (
"sigs.k8s.io/kustomize/kyaml/openapi"
"sigs.k8s.io/kustomize/kyaml/yaml"
"sigs.k8s.io/kustomize/kyaml/yaml/merge3"
"sigs.k8s.io/kustomize/kyaml/yaml/walk"
)

type Visitor struct {
*merge3.Visitor
}
Comment thread
tgzsolt marked this conversation as resolved.
Outdated

func (m *Visitor) VisitMap(nodes walk.Sources, _ *openapi.ResourceSchema) (*yaml.RNode, error) {
if m.isCleared(nodes.Origin(), nodes.Updated()) || m.isCleared(nodes.Origin(), nodes.Dest()) { // MODIFIED
// 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, _ *openapi.ResourceSchema) (*yaml.RNode, error) {
if yaml.IsMissingOrNull(nodes.Updated()) && !yaml.IsMissingOrNull(nodes.Origin()) {
// implicitly cleared from update -- element was deleted
return walk.ClearNode, nil
}
if yaml.IsMissingOrNull(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, _ *openapi.ResourceSchema) (*yaml.RNode, error) {
if m.isCleared(nodes.Origin(), nodes.Updated()) || m.isCleared(nodes.Origin(), nodes.Dest()) { // MODIFIED
// explicitly cleared from either dest or update
return nil, nil
Comment thread
tgzsolt marked this conversation as resolved.
}
if yaml.IsMissingOrNull(nodes.Updated()) != yaml.IsMissingOrNull(nodes.Origin()) {
// value added or removed in update
return nodes.Updated(), nil
}
if yaml.IsMissingOrNull(nodes.Updated()) && yaml.IsMissingOrNull(nodes.Origin()) {
// value absent in both origin and update
return nodes.Dest(), nil
}
Comment thread
tgzsolt marked this conversation as resolved.
Comment thread
tgzsolt marked this conversation as resolved.

values, err := m.getStrValues(nodes)
if err != nil {
return nil, err
}
Comment thread
tgzsolt marked this conversation as resolved.
Outdated

if (values.Dest == "" || values.Dest == values.Origin) && values.Origin != values.Update {
// if local is nil or is unchanged but there is new update
return nodes.Updated(), 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 m.isCleared(nodes.Origin(), nodes.Updated()) || m.isCleared(nodes.Origin(), nodes.Dest()) { // MODIFIED
// explicitly cleared from either dest or update
return walk.ClearNode, nil
}

if yaml.IsMissingOrNull(nodes.Updated()) != yaml.IsMissingOrNull(nodes.Origin()) {
// value added or removed in update
return nodes.Updated(), nil
}
if yaml.IsMissingOrNull(nodes.Updated()) && yaml.IsMissingOrNull(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
}

// NEW
// isCleared returns if the node has been present in `left` but explicitly removed in `right`
func (*Visitor) isCleared(left, right *yaml.RNode) bool {
return !left.IsTaggedNull() && right.IsTaggedNull()
}
Comment thread
tgzsolt marked this conversation as resolved.
Comment on lines +110 to +114

func (m *Visitor) VisitList(nodes walk.Sources, s *openapi.ResourceSchema, kind walk.ListKind) (*yaml.RNode, error) {
if kind == walk.AssociativeList {
return m.visitAList(nodes, s)
}
// non-associative list
return m.visitNAList(nodes)
}

// SIMPLIFIED
func (m *Visitor) getStrValues(nodes walk.Sources) (strValues, error) {
var uStr, oStr, dStr string
var err error

for _, p := range []struct {
rnode *yaml.RNode
str *string
}{
{nodes.Updated(), &uStr},
{nodes.Origin(), &oStr},
{nodes.Dest(), &dStr},
} {
rnode := p.rnode
if rnode == nil || rnode.YNode() == nil {
continue
}
s := rnode.YNode().Style
defer func() {
rnode.YNode().Style = s
}()
rnode.YNode().Style = yaml.FlowStyle | yaml.SingleQuotedStyle
*p.str, err = rnode.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{}
43 changes: 43 additions & 0 deletions pkg/lib/update/resource-merge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,17 @@
package update_test

import (
"os"
"path/filepath"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/kptdev/kpt/internal/testutil"
"github.com/kptdev/kpt/internal/testutil/pkgbuilder"
"github.com/kptdev/kpt/pkg/lib/update"
"github.com/kptdev/kpt/pkg/lib/update/updatetypes"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestUpdate_ResourceMerge(t *testing.T) {
Expand Down Expand Up @@ -301,3 +304,43 @@ func TestUpdate_ResourceMerge(t *testing.T) {
})
}
}

func TestKeepTaggedNull(t *testing.T) {
updater := update.ResourceMergeUpdater{}

testdata, err := filepath.Abs(filepath.Join(".", "testdata", "keep-tagged-null"))
require.NoError(t, err)

tmpDir := t.TempDir()
newDest := filepath.Join(tmpDir, "dest")
err = os.MkdirAll(newDest, 0755)
require.NoError(t, err)

oldDestFiles, err := os.ReadDir(filepath.Join(testdata, "dest"))
require.NoError(t, err)
for _, file := range oldDestFiles {
if file.Type().IsRegular() {
content, err := os.ReadFile(filepath.Join(testdata, "dest", file.Name()))
require.NoError(t, err)
err = os.WriteFile(filepath.Join(newDest, file.Name()), content, 0644)
require.NoError(t, err)
}
}

options := updatetypes.Options{
OriginPath: filepath.Join(testdata, "origin"),
UpdatedPath: filepath.Join(testdata, "updated"),
LocalPath: newDest,
}
Comment thread
tgzsolt marked this conversation as resolved.
err = updater.Update(options)
require.NoError(t, err)

destBytes, err := os.ReadFile(filepath.Join(newDest, "configmap.yaml"))
require.NoError(t, err)
expBytes, err := os.ReadFile(filepath.Join(testdata, "expected", "configmap.yaml"))
require.NoError(t, err)

if diff := cmp.Diff(expBytes, destBytes); diff != "" {
t.Errorf("unexpected result (-want, +got): %s", diff)
}
Comment on lines +338 to +345
}
21 changes: 21 additions & 0 deletions pkg/lib/update/testdata/keep-tagged-null/dest/Kptfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
apiVersion: kpt.dev/v1
kind: Kptfile
metadata:
name: test
annotations:
config.kubernetes.io/local-config: "true"
upstream:
type: git
git:
repo: https://github.com/droot/pkg-catalog.git
directory: basens
ref: basens/v0
upstreamLock:
type: git
git:
repo: https://github.com/droot/pkg-catalog.git
directory: basens
ref: basens/v0
commit: b3e1d439516a5e8d49adc0c82d3e95578570dbfa
info:
description: Test package.
11 changes: 11 additions & 0 deletions pkg/lib/update/testdata/keep-tagged-null/dest/configmap.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
replicas: 1
nullKey: null
unsetKey:
tildeKey: ~
emptyStringKey1: ""
emptyStringKey2: ''
21 changes: 21 additions & 0 deletions pkg/lib/update/testdata/keep-tagged-null/expected/Kptfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
apiVersion: kpt.dev/v1
kind: Kptfile
metadata:
name: test
annotations:
config.kubernetes.io/local-config: "true"
upstream:
type: git
git:
repo: https://github.com/droot/pkg-catalog.git
directory: basens
ref: basens/v0
upstreamLock:
type: git
git:
repo: https://github.com/droot/pkg-catalog.git
directory: basens
ref: basens/v0
commit: b3e1d439516a5e8d49adc0c82d3e95578570dbfa
info:
description: Test package.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
replicas: 1
nullKey: something
unsetKey: something
tildeKey: something
emptyStringKey1: "something"
emptyStringKey2: 'something'
missKey: something
8 changes: 8 additions & 0 deletions pkg/lib/update/testdata/keep-tagged-null/origin/Kptfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
apiVersion: kpt.dev/v1
kind: Kptfile
metadata:
name: test
annotations:
config.kubernetes.io/local-config: "true"
info:
description: Test package.
11 changes: 11 additions & 0 deletions pkg/lib/update/testdata/keep-tagged-null/origin/configmap.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
replicas: 1
nullKey: null
unsetKey:
tildeKey: ~
emptyStringKey1: ""
emptyStringKey2: ''
8 changes: 8 additions & 0 deletions pkg/lib/update/testdata/keep-tagged-null/updated/Kptfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
apiVersion: kpt.dev/v1
kind: Kptfile
metadata:
name: test
annotations:
config.kubernetes.io/local-config: "true"
info:
description: Test package.
12 changes: 12 additions & 0 deletions pkg/lib/update/testdata/keep-tagged-null/updated/configmap.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
replicas: 1
nullKey: something
unsetKey: something
tildeKey: something
emptyStringKey1: something
emptyStringKey2: something
missKey: something
Loading