kartongips: implement proper diffing of aggregated ClusterRoles

For a while now we've had spurious diffs against Ceph on k0 because of
a ClusterRole with an aggregationRule.

The way these behave is that the config object has an empty rule list,
and instead populates an aggregationRule which combines other existing
ClusterRoles into that ClusterRole. The control plane then populates the
rule field when the object is read/acted on, which caused us to always
see a diff between the configuration of that ClusterRole.

This hacks together a hardcoded fix for this particular behaviour.
Porting kubecfg over to SSA would probably also fix this - but that's
too much work for now.

Change-Id: I357c1417d4023691e5809f1af23f58f364353388
diff --git a/cluster/tools/kartongips/pkg/kubecfg/diff.go b/cluster/tools/kartongips/pkg/kubecfg/diff.go
index f1136be..5d32b9e 100644
--- a/cluster/tools/kartongips/pkg/kubecfg/diff.go
+++ b/cluster/tools/kartongips/pkg/kubecfg/diff.go
@@ -91,6 +91,7 @@
 		liveObjObject := liveObj.Object
 		if c.DiffStrategy == "subset" {
 			liveObjObject = removeMapFields(obj.Object, liveObjObject)
+			liveObjObject = removeClusterRoleAggregatedRules(liveObjObject)
 		}
 
 		liveObjText, _ := json.MarshalIndent(liveObjObject, "", "  ")
@@ -211,6 +212,38 @@
 	return result
 }
 
+// removeClusterRoleAggregatedRules clears the rules field from live
+// ClusterRole objects which have an aggregationRule. This allows us to diff a
+// config object (which doesn't have these rules materialized) against a live
+// obejct (which does have these rules materialized) without spurious diffs.
+//
+// See the Aggregated ClusterRole section of the Kubernetes RBAC docuementation
+// for more information:
+//
+// https://kubernetes.io/docs/reference/access-authn-authz/rbac/#aggregated-clusterroles
+func removeClusterRoleAggregatedRules(live map[string]interface{}) map[string]interface{} {
+	if version, ok := live["apiVersion"].(string); !ok || version != "rbac.authorization.k8s.io/v1" {
+		return live
+	}
+
+	if kind, ok := live["kind"].(string); !ok || kind != "ClusterRole" {
+		return live
+	}
+
+	if _, ok := live["aggregationRule"].(map[string]interface{}); !ok {
+		return live
+	}
+
+	// Make copy of map.
+	res := make(map[string]interface{})
+	for k, v := range live {
+		res[k] = v
+	}
+	// Clear rules field.
+	res["rules"] = []interface{}{}
+	return res
+}
+
 func removeListFields(config, live []interface{}) []interface{} {
 	// If live is longer than config, then the extra elements at the end of the
 	// list will be returned as is so they appear in the diff.