diff --git a/README.md b/README.md index 44adbb2..beea9ca 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,11 @@ You first need to create a WorkUtilsClient. It requires passing `*runtime.Scheme ```go import( - "github.com/ohkinozomu/workutils" - "k8s.io/client-go/kubernetes/scheme" + "github.com/ohkinozomu/workutils" + "k8s.io/client-go/kubernetes/scheme" ) - client := workutils.NewWorkUtilsClient(scheme.Scheme) + client := workutils.NewWorkUtilsClient(scheme.Scheme) ``` If resources not included in `scheme.Scheme` are present in ManifestWork, `AddToScheme` for those resources is necessary. @@ -46,4 +46,6 @@ func (client *WorkUtilsClient) Add(work workapiv1.ManifestWork, obj runtime.Obje func (client *WorkUtilsClient) Update(work workapiv1.ManifestWork, obj runtime.Object) (workapiv1.ManifestWork, error) func (client *WorkUtilsClient) Remove(work workapiv1.ManifestWork, resource Resource) (workapiv1.ManifestWork, error) + +func (client *WorkUtilsClient) Diff(old workapiv1.ManifestWork, new workapiv1.ManifestWork) (added []runtime.Object, removed []runtime.Object, updated []runtime.Object, err error) ``` \ No newline at end of file diff --git a/common.go b/common.go index 05c92e4..3bb204f 100644 --- a/common.go +++ b/common.go @@ -43,6 +43,31 @@ func getNamespaceFromObject(obj runtime.Object) (string, error) { return accessor.GetNamespace(), nil } +func getResourceFromObject(obj runtime.Object) (Resource, error) { + group, version, kind, err := getGVKFromObject(obj) + if err != nil { + return Resource{}, err + } + + name, err := getNameFromObject(obj) + if err != nil { + return Resource{}, err + } + + namespace, err := getNamespaceFromObject(obj) + if err != nil { + return Resource{}, err + } + + return Resource{ + Group: group, + Version: version, + Kind: kind, + Name: name, + Namespace: namespace, + }, nil +} + func stringToRawExtension(manifest string) (runtime.RawExtension, error) { obj, _, err := decode([]byte(manifest), scheme.Scheme) if err != nil { diff --git a/diff.go b/diff.go new file mode 100644 index 0000000..a905178 --- /dev/null +++ b/diff.go @@ -0,0 +1,79 @@ +package workutils + +import ( + "bytes" + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + workapiv1 "open-cluster-management.io/api/work/v1" +) + +func (client *WorkUtilsClient) Diff(old workapiv1.ManifestWork, new workapiv1.ManifestWork) (added []runtime.Object, removed []runtime.Object, updated []runtime.Object, err error) { + oldMap := make(map[string]runtime.Object) + for _, manifest := range old.Spec.Workload.Manifests { + obj, _, err := decode(manifest.Raw, client.Scheme) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to decode manifest in old: %v", err) + } + key, err := manifestKey(obj) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to get manifest key: %v", err) + } + oldMap[key] = obj + } + + newMap := make(map[string]runtime.Object) + for _, manifest := range new.Spec.Workload.Manifests { + obj, _, err := decode(manifest.Raw, client.Scheme) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to decode manifest in new: %v", err) + } + key, err := manifestKey(obj) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to get manifest key: %v", err) + } + newMap[key] = obj + } + + for key, obj1 := range oldMap { + if obj2, found := newMap[key]; found { + e, err := equal(client.Scheme, obj1, obj2) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to compare objects: %v", err) + } + if !e { + updated = append(updated, obj2) + } + delete(newMap, key) + } else { + removed = append(removed, obj1) + } + } + + for _, obj := range newMap { + added = append(added, obj) + } + + return added, removed, updated, nil +} + +func manifestKey(obj runtime.Object) (string, error) { + r, err := getResourceFromObject(obj) + if err != nil { + return "", err + } + key := fmt.Sprintf("%s/%s/%s/%s/%s", r.Group, r.Version, r.Kind, r.Namespace, r.Name) + return key, nil +} + +func equal(scheme *runtime.Scheme, obj1, obj2 runtime.Object) (bool, error) { + raw1, err := objToRawExtension(obj1, scheme) + if err != nil { + return false, err + } + raw2, err := objToRawExtension(obj2, scheme) + if err != nil { + return false, err + } + return bytes.Equal(raw1.Raw, raw2.Raw), nil +} diff --git a/diff_test.go b/diff_test.go new file mode 100644 index 0000000..b4d8816 --- /dev/null +++ b/diff_test.go @@ -0,0 +1,106 @@ +package workutils + +import ( + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/scheme" + workapiv1 "open-cluster-management.io/api/work/v1" +) + +func TestDiff(t *testing.T) { + client := &WorkUtilsClient{ + Scheme: scheme.Scheme, + } + + manifest1 := ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: config1 + namespace: default +data: + key1: value1 +` + + manifest2 := ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: config2 + namespace: default +data: + key2: value2 +` + + manifest3 := ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: config1 + namespace: default +data: + key1: updatedValue +` + + manifest4 := ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: config3 + namespace: default +data: + key3: value3 +` + + raw1, err := stringToRawExtension(manifest1) + require.NoError(t, err) + + raw2, err := stringToRawExtension(manifest2) + require.NoError(t, err) + + raw3, err := stringToRawExtension(manifest3) + require.NoError(t, err) + + raw4, err := stringToRawExtension(manifest4) + require.NoError(t, err) + + work1 := workapiv1.ManifestWork{ + Spec: workapiv1.ManifestWorkSpec{ + Workload: workapiv1.ManifestsTemplate{ + Manifests: []workapiv1.Manifest{{ + RawExtension: raw1, + }, { + RawExtension: raw4, + }}, + }, + }, + } + work2 := workapiv1.ManifestWork{ + Spec: workapiv1.ManifestWorkSpec{ + Workload: workapiv1.ManifestsTemplate{ + Manifests: []workapiv1.Manifest{{ + RawExtension: raw2, + }, { + RawExtension: raw3, + }}, + }, + }, + } + + added, removed, updated, err := client.Diff(work1, work2) + require.NoError(t, err) + + o2, _, err := decode([]byte(manifest2), scheme.Scheme) + require.NoError(t, err) + require.Equal(t, []runtime.Object{o2}, added) + + o4, _, err := decode([]byte(manifest4), scheme.Scheme) + require.NoError(t, err) + require.Equal(t, []runtime.Object{o4}, removed) + + o3, _, err := decode([]byte(manifest3), scheme.Scheme) + require.NoError(t, err) + require.Equal(t, []runtime.Object{o3}, updated) +}