// Copyright 2019 The Kubernetes Authors. // SPDX-License-Identifier: Apache-2.0 package merge import ( "encoding/json" "fmt" jsonpatch "github.com/evanphx/json-patch" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/mergepatch" "k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/client-go/kubernetes/scheme" "sigs.k8s.io/kustomize/api/resid" "sigs.k8s.io/kustomize/api/resmap" "sigs.k8s.io/kustomize/api/resource" ) type conflictDetector interface { hasConflict(patch1, patch2 *resource.Resource) (bool, error) findConflict( conflictingPatchIdx int, patches []*resource.Resource) (*resource.Resource, error) mergePatches(patch1, patch2 *resource.Resource) (*resource.Resource, error) } type jsonMergePatch struct { resourceFactory *resource.Factory } var _ conflictDetector = &jsonMergePatch{} func newJMPConflictDetector(rf *resource.Factory) conflictDetector { return &jsonMergePatch{resourceFactory: rf} } func (jmp *jsonMergePatch) hasConflict( patch1, patch2 *resource.Resource) (bool, error) { return mergepatch.HasConflicts(patch1.Map(), patch2.Map()) } func (jmp *jsonMergePatch) findConflict( conflictingPatchIdx int, patches []*resource.Resource) (*resource.Resource, error) { for i, patch := range patches { if i == conflictingPatchIdx { continue } if !patches[conflictingPatchIdx].OrgId().Equals(patch.OrgId()) { continue } conflict, err := mergepatch.HasConflicts( patch.Map(), patches[conflictingPatchIdx].Map()) if err != nil { return nil, err } if conflict { return patch, nil } } return nil, nil } func (jmp *jsonMergePatch) mergePatches( patch1, patch2 *resource.Resource) (*resource.Resource, error) { baseBytes, err := json.Marshal(patch1.Map()) if err != nil { return nil, err } patchBytes, err := json.Marshal(patch2.Map()) if err != nil { return nil, err } mergedBytes, err := jsonpatch.MergeMergePatches(baseBytes, patchBytes) if err != nil { return nil, err } mergedMap := make(map[string]interface{}) err = json.Unmarshal(mergedBytes, &mergedMap) return jmp.resourceFactory.FromMap(mergedMap), err } type strategicMergePatch struct { lookupPatchMeta strategicpatch.LookupPatchMeta rf *resource.Factory } var _ conflictDetector = &strategicMergePatch{} func newSMPConflictDetector( versionedObj runtime.Object, rf *resource.Factory) (conflictDetector, error) { lookupPatchMeta, err := strategicpatch.NewPatchMetaFromStruct(versionedObj) return &strategicMergePatch{lookupPatchMeta: lookupPatchMeta, rf: rf}, err } func (smp *strategicMergePatch) hasConflict( p1, p2 *resource.Resource) (bool, error) { return strategicpatch.MergingMapsHaveConflicts( p1.Map(), p2.Map(), smp.lookupPatchMeta) } func (smp *strategicMergePatch) findConflict( conflictingPatchIdx int, patches []*resource.Resource) (*resource.Resource, error) { for i, patch := range patches { if i == conflictingPatchIdx { continue } if !patches[conflictingPatchIdx].OrgId().Equals(patch.OrgId()) { continue } conflict, err := strategicpatch.MergingMapsHaveConflicts( patch.Map(), patches[conflictingPatchIdx].Map(), smp.lookupPatchMeta) if err != nil { return nil, err } if conflict { return patch, nil } } return nil, nil } func (smp *strategicMergePatch) mergePatches( patch1, patch2 *resource.Resource) (*resource.Resource, error) { if hasDeleteDirectiveMarker(patch2.Map()) { if hasDeleteDirectiveMarker(patch1.Map()) { return nil, fmt.Errorf( "cannot merge patches both containing '$patch: delete' directives") } patch1, patch2 = patch2, patch1 } mergeJSONMap, err := strategicpatch.MergeStrategicMergeMapPatchUsingLookupPatchMeta( smp.lookupPatchMeta, patch1.Map(), patch2.Map()) return smp.rf.FromMap(mergeJSONMap), err } type merginatorImpl struct { rf *resource.Factory } // NewMerginator returns a new implementation of resmap.Merginator. func NewMerginator(rf *resource.Factory) resmap.Merginator { return &merginatorImpl{rf: rf} } var _ resmap.Merginator = (*merginatorImpl)(nil) // Merge merges the incoming resources into a new resmap.ResMap. // Returns an error on conflict. func (m *merginatorImpl) Merge( patches []*resource.Resource) (resmap.ResMap, error) { rc := resmap.New() for ix, patch := range patches { id := patch.OrgId() existing := rc.GetMatchingResourcesByOriginalId(id.Equals) if len(existing) == 0 { rc.Append(patch) continue } if len(existing) > 1 { return nil, fmt.Errorf("self conflict in patches") } versionedObj, err := scheme.Scheme.New(toSchemaGvk(id.Gvk)) if err != nil && !runtime.IsNotRegisteredError(err) { return nil, err } var cd conflictDetector if err != nil { cd = newJMPConflictDetector(m.rf) } else { cd, err = newSMPConflictDetector(versionedObj, m.rf) if err != nil { return nil, err } } conflict, err := cd.hasConflict(existing[0], patch) if err != nil { return nil, err } if conflict { conflictingPatch, err := cd.findConflict(ix, patches) if err != nil { return nil, err } return nil, fmt.Errorf( "conflict between %#v and %#v", conflictingPatch.Map(), patch.Map()) } merged, err := cd.mergePatches(existing[0], patch) if err != nil { return nil, err } rc.Replace(merged) } return rc, nil } // toSchemaGvk converts to a schema.GroupVersionKind. func toSchemaGvk(x resid.Gvk) schema.GroupVersionKind { return schema.GroupVersionKind{ Group: x.Group, Version: x.Version, Kind: x.Kind, } } func hasDeleteDirectiveMarker(patch map[string]interface{}) bool { if v, ok := patch["$patch"]; ok && v == "delete" { return true } for _, v := range patch { switch typedV := v.(type) { case map[string]interface{}: if hasDeleteDirectiveMarker(typedV) { return true } case []interface{}: for _, sv := range typedV { typedE, ok := sv.(map[string]interface{}) if !ok { break } if hasDeleteDirectiveMarker(typedE) { return true } } } } return false }