2018-02-26 02:36:19 +00:00
|
|
|
package diff
|
|
|
|
|
|
|
|
|
|
import (
|
2018-05-16 22:31:22 +00:00
|
|
|
"encoding/json"
|
2018-03-01 07:36:00 +00:00
|
|
|
"errors"
|
2018-02-26 02:36:19 +00:00
|
|
|
"fmt"
|
|
|
|
|
|
2018-05-16 22:31:22 +00:00
|
|
|
log "github.com/sirupsen/logrus"
|
2018-02-26 02:36:19 +00:00
|
|
|
"github.com/yudai/gojsondiff"
|
|
|
|
|
"github.com/yudai/gojsondiff/formatter"
|
2018-05-16 22:31:22 +00:00
|
|
|
"k8s.io/api/core/v1"
|
2018-02-26 02:36:19 +00:00
|
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
2018-05-16 22:31:22 +00:00
|
|
|
"k8s.io/apimachinery/pkg/util/strategicpatch"
|
|
|
|
|
"k8s.io/kubernetes/pkg/apis/core"
|
|
|
|
|
"k8s.io/kubernetes/pkg/kubectl/scheme"
|
2018-07-24 23:37:12 +00:00
|
|
|
|
|
|
|
|
jsonutil "github.com/argoproj/argo-cd/util/json"
|
2018-02-26 02:36:19 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type DiffResult struct {
|
2018-03-01 07:36:00 +00:00
|
|
|
Diff gojsondiff.Diff
|
|
|
|
|
Modified bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type DiffResultList struct {
|
|
|
|
|
Diffs []DiffResult
|
|
|
|
|
Modified bool
|
2018-02-26 02:36:19 +00:00
|
|
|
}
|
|
|
|
|
|
2018-05-16 22:31:22 +00:00
|
|
|
// Diff performs a diff on two unstructured objects. If the live object happens to have a
|
|
|
|
|
// "kubectl.kubernetes.io/last-applied-configuration", then perform a three way diff.
|
|
|
|
|
func Diff(config, live *unstructured.Unstructured) *DiffResult {
|
2018-09-15 02:10:11 +00:00
|
|
|
if config != nil {
|
|
|
|
|
config = stripTypeInformation(config)
|
|
|
|
|
}
|
|
|
|
|
if live != nil {
|
|
|
|
|
live = stripTypeInformation(live)
|
|
|
|
|
}
|
2018-05-16 22:31:22 +00:00
|
|
|
orig := getLastAppliedConfigAnnotation(live)
|
2018-06-07 17:29:36 +00:00
|
|
|
if orig != nil && config != nil {
|
2018-09-15 02:10:11 +00:00
|
|
|
dr, err := ThreeWayDiff(orig, config, live)
|
|
|
|
|
if err == nil {
|
|
|
|
|
return dr
|
|
|
|
|
}
|
|
|
|
|
log.Warnf("three-way diff calculation failed: %v. Falling back to two-way diff", err)
|
2018-02-27 11:16:18 +00:00
|
|
|
}
|
2018-05-16 22:31:22 +00:00
|
|
|
return TwoWayDiff(config, live)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TwoWayDiff performs a normal two-way diff between two unstructured objects. Ignores extra fields
|
|
|
|
|
// in the live object.
|
2018-09-15 02:10:11 +00:00
|
|
|
// Inputs are assumed to be stripped of type information
|
2018-05-16 22:31:22 +00:00
|
|
|
func TwoWayDiff(config, live *unstructured.Unstructured) *DiffResult {
|
|
|
|
|
var configObj, liveObj map[string]interface{}
|
|
|
|
|
if config != nil {
|
|
|
|
|
configObj = config.Object
|
2018-02-27 11:16:18 +00:00
|
|
|
}
|
2018-05-16 22:31:22 +00:00
|
|
|
if live != nil {
|
2018-07-24 23:37:12 +00:00
|
|
|
liveObj = jsonutil.RemoveMapFields(configObj, live.Object)
|
2018-05-16 22:31:22 +00:00
|
|
|
}
|
|
|
|
|
gjDiff := gojsondiff.New().CompareObjects(configObj, liveObj)
|
2018-02-28 00:58:25 +00:00
|
|
|
dr := DiffResult{
|
|
|
|
|
Diff: gjDiff,
|
|
|
|
|
Modified: gjDiff.Modified(),
|
2018-02-26 02:36:19 +00:00
|
|
|
}
|
2018-02-28 00:58:25 +00:00
|
|
|
return &dr
|
2018-02-26 02:36:19 +00:00
|
|
|
}
|
|
|
|
|
|
2018-05-16 22:31:22 +00:00
|
|
|
// ThreeWayDiff performs a diff with the understanding of how to incorporate the
|
|
|
|
|
// last-applied-configuration annotation in the diff.
|
2018-09-15 02:10:11 +00:00
|
|
|
// Inputs are assumed to be stripped of type information
|
|
|
|
|
func ThreeWayDiff(orig, config, live *unstructured.Unstructured) (*DiffResult, error) {
|
2018-05-17 07:41:50 +00:00
|
|
|
orig = removeNamespaceAnnotation(orig)
|
2018-09-20 21:52:16 +00:00
|
|
|
config = removeNamespaceAnnotation(config)
|
2018-09-15 02:10:11 +00:00
|
|
|
// Remove defaulted fields from the live object.
|
|
|
|
|
// This subtracts any extra fields in the live object which are not present in last-applied-configuration.
|
|
|
|
|
// This is needed to perform a fair comparison when we send the objects to gojsondiff
|
|
|
|
|
live = &unstructured.Unstructured{Object: jsonutil.RemoveMapFields(orig.Object, live.Object)}
|
|
|
|
|
|
|
|
|
|
// 1. calculate a 3-way merge patch
|
|
|
|
|
patchBytes, err := threeWayMergePatch(orig, config, live)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. apply the patch against the live object
|
|
|
|
|
liveBytes, err := json.Marshal(live)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
versionedObject, err := scheme.Scheme.New(orig.GroupVersionKind())
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
patchedLiveBytes, err := strategicpatch.StrategicMergePatch(liveBytes, patchBytes, versionedObject)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
var patchedLive unstructured.Unstructured
|
|
|
|
|
err = json.Unmarshal(patchedLiveBytes, &patchedLive)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. diff the live object vs. the patched live object
|
|
|
|
|
gjDiff := gojsondiff.New().CompareObjects(patchedLive.Object, live.Object)
|
2018-05-16 22:31:22 +00:00
|
|
|
dr := DiffResult{
|
|
|
|
|
Diff: gjDiff,
|
|
|
|
|
Modified: gjDiff.Modified(),
|
|
|
|
|
}
|
2018-09-15 02:10:11 +00:00
|
|
|
return &dr, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// stripTypeInformation strips any type information (e.g. float64 vs. int) from the unstructured
|
|
|
|
|
// object by remarshalling the object. This is important for diffing since it will cause godiff
|
|
|
|
|
// to report a false difference.
|
|
|
|
|
func stripTypeInformation(un *unstructured.Unstructured) *unstructured.Unstructured {
|
|
|
|
|
unBytes, err := json.Marshal(un)
|
2018-05-16 22:31:22 +00:00
|
|
|
if err != nil {
|
2018-09-15 02:10:11 +00:00
|
|
|
panic(err)
|
2018-05-16 22:31:22 +00:00
|
|
|
}
|
2018-09-15 02:10:11 +00:00
|
|
|
var newUn unstructured.Unstructured
|
|
|
|
|
err = json.Unmarshal(unBytes, &newUn)
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(err)
|
2018-05-16 22:31:22 +00:00
|
|
|
}
|
2018-09-15 02:10:11 +00:00
|
|
|
return &newUn
|
2018-05-16 22:31:22 +00:00
|
|
|
}
|
|
|
|
|
|
2018-09-15 02:10:11 +00:00
|
|
|
// removeNamespaceAnnotation remove the namespace and an empty annotation map from the metadata.
|
|
|
|
|
// The namespace field is *always* present in live objects, but not necessarily present in config
|
|
|
|
|
// or last-applied. This results in a diff which we don't care about. We delete the two so that
|
|
|
|
|
// the diff is more relevant.
|
2018-05-17 07:41:50 +00:00
|
|
|
func removeNamespaceAnnotation(orig *unstructured.Unstructured) *unstructured.Unstructured {
|
|
|
|
|
orig = orig.DeepCopy()
|
|
|
|
|
if metadataIf, ok := orig.Object["metadata"]; ok {
|
|
|
|
|
metadata := metadataIf.(map[string]interface{})
|
|
|
|
|
delete(metadata, "namespace")
|
|
|
|
|
if annotationsIf, ok := metadata["annotations"]; ok {
|
|
|
|
|
annotation := annotationsIf.(map[string]interface{})
|
|
|
|
|
if len(annotation) == 0 {
|
|
|
|
|
delete(metadata, "annotations")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return orig
|
|
|
|
|
}
|
|
|
|
|
|
2018-05-16 22:31:22 +00:00
|
|
|
func threeWayMergePatch(orig, config, live *unstructured.Unstructured) ([]byte, error) {
|
|
|
|
|
origBytes, err := json.Marshal(orig.Object)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
configBytes, err := json.Marshal(config.Object)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
liveBytes, err := json.Marshal(live.Object)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
gvk := orig.GroupVersionKind()
|
|
|
|
|
versionedObject, err := scheme.Scheme.New(gvk)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
lookupPatchMeta, err := strategicpatch.NewPatchMetaFromStruct(versionedObject)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return strategicpatch.CreateThreeWayMergePatch(origBytes, configBytes, liveBytes, lookupPatchMeta, true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func getLastAppliedConfigAnnotation(live *unstructured.Unstructured) *unstructured.Unstructured {
|
|
|
|
|
if live == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
annots := live.GetAnnotations()
|
|
|
|
|
if annots == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
lastAppliedStr, ok := annots[v1.LastAppliedConfigAnnotation]
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
var obj unstructured.Unstructured
|
|
|
|
|
err := json.Unmarshal([]byte(lastAppliedStr), &obj)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Warnf("Failed to unmarshal %s in %s", core.LastAppliedConfigAnnotation, live.GetName())
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return &obj
|
|
|
|
|
}
|
|
|
|
|
|
2018-06-20 20:57:55 +00:00
|
|
|
// MatchObjectLists takes two possibly disjoint lists of Unstructured objects, and returns two new
|
|
|
|
|
// lists of equal lengths, filled out with nils from missing objects in the opposite list.
|
|
|
|
|
// These lists can then be passed into DiffArray for comparison
|
|
|
|
|
func MatchObjectLists(leftObjs, rightObjs []*unstructured.Unstructured) ([]*unstructured.Unstructured, []*unstructured.Unstructured) {
|
|
|
|
|
newLeftObjs := make([]*unstructured.Unstructured, 0)
|
|
|
|
|
newRightObjs := make([]*unstructured.Unstructured, 0)
|
|
|
|
|
|
|
|
|
|
for _, left := range leftObjs {
|
|
|
|
|
if left == nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
newLeftObjs = append(newLeftObjs, left)
|
|
|
|
|
right := objByKindName(rightObjs, left.GetKind(), left.GetName())
|
|
|
|
|
newRightObjs = append(newRightObjs, right)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, right := range rightObjs {
|
|
|
|
|
if right == nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
left := objByKindName(leftObjs, right.GetKind(), right.GetName())
|
|
|
|
|
if left != nil {
|
|
|
|
|
// object exists in both list. this object was already appended to both lists in the
|
|
|
|
|
// first for/loop
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
// if we get here, we found a right which doesn't exist in the left object list.
|
|
|
|
|
// append a nil to the left object list
|
|
|
|
|
newLeftObjs = append(newLeftObjs, nil)
|
|
|
|
|
newRightObjs = append(newRightObjs, right)
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
return newLeftObjs, newRightObjs
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func objByKindName(objs []*unstructured.Unstructured, kind, name string) *unstructured.Unstructured {
|
|
|
|
|
for _, obj := range objs {
|
|
|
|
|
if obj == nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if obj.GetKind() == kind && obj.GetName() == name {
|
|
|
|
|
return obj
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-26 02:36:19 +00:00
|
|
|
// DiffArray performs a diff on a list of unstructured objects. Objects are expected to match
|
|
|
|
|
// environments
|
2018-05-16 22:31:22 +00:00
|
|
|
func DiffArray(configArray, liveArray []*unstructured.Unstructured) (*DiffResultList, error) {
|
|
|
|
|
numItems := len(configArray)
|
|
|
|
|
if len(liveArray) != numItems {
|
2018-02-26 02:36:19 +00:00
|
|
|
return nil, fmt.Errorf("left and right arrays have mismatched lengths")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
diffResultList := DiffResultList{
|
|
|
|
|
Diffs: make([]DiffResult, numItems),
|
|
|
|
|
}
|
|
|
|
|
for i := 0; i < numItems; i++ {
|
2018-05-16 22:31:22 +00:00
|
|
|
config := configArray[i]
|
|
|
|
|
live := liveArray[i]
|
|
|
|
|
diffRes := Diff(config, live)
|
2018-02-26 02:36:19 +00:00
|
|
|
diffResultList.Diffs[i] = *diffRes
|
|
|
|
|
if diffRes.Modified {
|
|
|
|
|
diffResultList.Modified = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return &diffResultList, nil
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-01 07:36:00 +00:00
|
|
|
// ASCIIFormat returns the ASCII format of the diff
|
|
|
|
|
func (d *DiffResult) ASCIIFormat(left *unstructured.Unstructured, formatOpts formatter.AsciiFormatterConfig) (string, error) {
|
2018-02-28 00:58:25 +00:00
|
|
|
if !d.Diff.Modified() {
|
2018-03-01 07:36:00 +00:00
|
|
|
return "", nil
|
2018-02-26 02:36:19 +00:00
|
|
|
}
|
2018-03-01 07:36:00 +00:00
|
|
|
if left == nil {
|
|
|
|
|
return "", errors.New("Supplied nil left object")
|
2018-02-26 02:36:19 +00:00
|
|
|
}
|
2018-03-01 07:36:00 +00:00
|
|
|
asciiFmt := formatter.NewAsciiFormatter(left.Object, formatOpts)
|
|
|
|
|
return asciiFmt.Format(d.Diff)
|
|
|
|
|
}
|