Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
hash comparison and deep merge all the resources
Signed-off-by: YuChen <[email protected]>
  • Loading branch information
YuChen committed Sep 29, 2024
commit ebbff11c13ccff978d54135b6ff9dca0af3c2233
6 changes: 6 additions & 0 deletions controllers/constant/constant.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ const (
//HashedData is the key for checking the checksum of data section
HashedData string = "hashedData"

//HashedData is the key for k8s Resource
K8sHashedData string = "operator.ibm.com/operand-depoyment-lifecycle-manager.hashedData"

//RouteHash is the key for hash value of route
RouteHash string = "operator.ibm.com/odlm.route.hashedData"

//DefaultRequestTimeout is the default timeout for kube request
DefaultRequestTimeout = 5 * time.Second

Expand Down
78 changes: 53 additions & 25 deletions controllers/operandrequest/reconcile_operand.go
Original file line number Diff line number Diff line change
Expand Up @@ -1014,14 +1014,26 @@ func (r *Reconciler) createK8sResource(ctx context.Context, k8sResTemplate unstr
k8sResTemplate.Object[k] = v
}

k8sResConfigBytes, err := json.Marshal(k8sResConfigDecoded)
if err != nil {
return errors.Wrap(err, "failed to marshal k8sResConfigDecoded")
}

// Caculate the hash number of the new created template
_, templateHash := util.CalculateResHashes(nil, k8sResConfigBytes)

newAnnotations = util.AddHashAnnotation(&k8sResTemplate, constant.K8sHashedData, templateHash, newAnnotations)

if kind == "Route" {
if host, found := k8sResConfigDecoded["spec"].(map[string]interface{})["host"].(string); found {
hostHash := util.CalculateHash(host)
hostHash := util.CalculateHash([]byte(host))

// if newAnnotations == nil {
// newAnnotations = make(map[string]string)
// }
// newAnnotations[constant.RouteHash] = hostHash
newAnnotations = util.AddHashAnnotation(&k8sResTemplate, constant.RouteHash, hostHash, newAnnotations)

if newAnnotations == nil {
newAnnotations = make(map[string]string)
}
newAnnotations["operator.ibm.com/odlm.route.hashedData"] = hostHash
} else {
klog.Warningf("spec.host not found in Route %s/%s", namespace, name)
}
Expand Down Expand Up @@ -1064,29 +1076,30 @@ func (r *Reconciler) updateK8sResource(ctx context.Context, existingK8sRes unstr
return errors.Wrap(err, "failed to update Route")
}

// update the annotations of the Route host if the host is changed
if k8sResConfig != nil {
k8sResConfigDecoded := make(map[string]interface{})
if k8sResConfigUnmarshalErr := json.Unmarshal(k8sResConfig.Raw, &k8sResConfigDecoded); k8sResConfigUnmarshalErr != nil {
return errors.Wrap(k8sResConfigUnmarshalErr, "failed to unmarshal k8s Resource Config")
}

if host, found := k8sResConfigDecoded["spec"].(map[string]interface{})["host"].(string); found {
hostHash := util.CalculateHash(host)
hostHash := util.CalculateHash([]byte(host))

if newAnnotations == nil {
newAnnotations = make(map[string]string)
}
newAnnotations["operator.ibm.com/odlm.route.hashedData"] = hostHash
newAnnotations[constant.RouteHash] = hostHash
} else {
klog.Warningf("spec.host not found in Route %s/%s", namespace, name)
}
}
}

// Update the k8s res
// Update the k8s resource
err := wait.PollImmediate(constant.DefaultCRFetchPeriod, constant.DefaultCRFetchTimeout, func() (bool, error) {

existingK8sRes := unstructured.Unstructured{
existingRes := unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": apiversion,
"kind": kind,
Expand All @@ -1096,40 +1109,54 @@ func (r *Reconciler) updateK8sResource(ctx context.Context, existingK8sRes unstr
err := r.Client.Get(ctx, types.NamespacedName{
Name: name,
Namespace: namespace,
}, &existingK8sRes)

}, &existingRes)
if err != nil {
return false, errors.Wrapf(err, "failed to get k8s resource -- Kind: %s, NamespacedName: %s/%s", kind, namespace, name)
}
resourceVersion := existingRes.GetResourceVersion()

if !r.CheckLabel(existingK8sRes, map[string]string{constant.OpreqLabel: "true"}) && (newLabels == nil || newLabels[constant.OpreqLabel] != "true") {
if !r.CheckLabel(existingRes, map[string]string{constant.OpreqLabel: "true"}) && (newLabels == nil || newLabels[constant.OpreqLabel] != "true") {
return true, nil
}

if k8sResConfig != nil {

// Convert existing k8s resource to string
existingK8sResRaw, err := json.Marshal(existingK8sRes.Object)
existingResRaw, err := json.Marshal(existingRes.Object)
if err != nil {
return false, errors.Wrapf(err, "failed to marshal existing k8s resource -- Kind: %s, NamespacedName: %s/%s", kind, namespace, name)
}

// Merge the existing CR and the CR from the OperandConfig
updatedExistingK8sRes := util.MergeCR(existingK8sResRaw, k8sResConfig.Raw)
// Update the existing k8s resource with the merged CR
existingK8sRes.Object = updatedExistingK8sRes
// Caculate the hash number of the new created template
existingHash, templateHash := util.CalculateResHashes(&existingRes, k8sResConfig.Raw)

// If the hash number of the existing k8s resource is different from the hash number of template, update the k8s resource
if existingHash != templateHash {
k8sResConfigDecoded := make(map[string]interface{})
k8sResConfigUnmarshalErr := json.Unmarshal(k8sResConfig.Raw, &k8sResConfigDecoded)
if k8sResConfigUnmarshalErr != nil {
klog.Errorf("failed to unmarshal k8s Resource Config: %v", k8sResConfigUnmarshalErr)
}
for k, v := range k8sResConfigDecoded {
existingRes.Object[k] = v
}
} else {
// If the hash number are the same, then do the deep merge
// Merge the existing CR and the CR from the OperandConfig
updatedExistingRes := util.MergeCR(existingResRaw, k8sResConfig.Raw)
// Update the existing k8s resource with the merged CR
existingRes.Object = updatedExistingRes
}

r.EnsureAnnotation(existingK8sRes, newAnnotations)
r.EnsureLabel(existingK8sRes, newLabels)
if err := r.setOwnerReferences(ctx, &existingK8sRes, ownerReferences); err != nil {
r.EnsureAnnotation(existingRes, newAnnotations)
r.EnsureLabel(existingRes, newLabels)
if err := r.setOwnerReferences(ctx, &existingRes, ownerReferences); err != nil {
return false, errors.Wrapf(err, "failed to set ownerReferences for k8s resource -- Kind: %s, NamespacedName: %s/%s", kind, namespace, name)
}

klog.Infof("updating k8s resource with apiversion: %s, kind: %s, %s/%s", apiversion, kind, namespace, name)

resourceVersion := existingK8sRes.GetResourceVersion()
err = r.Update(ctx, &existingK8sRes)

err = r.Update(ctx, &existingRes)
if err != nil {
return false, errors.Wrapf(err, "failed to update k8s resource -- Kind: %s, NamespacedName: %s/%s", kind, namespace, name)
}
Expand All @@ -1156,6 +1183,7 @@ func (r *Reconciler) updateK8sResource(ctx context.Context, existingK8sRes unstr
} else {
klog.Infof("No updates on k8s resource with apiversion: %s, kind: %s, %s/%s", apiversion, kind, namespace, name)
}

}
return true, nil
})
Expand Down Expand Up @@ -1253,7 +1281,7 @@ func (r *Reconciler) updateK8sRoute(ctx context.Context, existingK8sRes unstruct
}

existingAnnos := existingRes.GetAnnotations()
existingHostHash := existingAnnos["operator.ibm.com/odlm.route.hashedData"]
existingHostHash := existingAnnos[constant.RouteHash]

if k8sResConfig != nil {
k8sResConfigDecoded := make(map[string]interface{})
Expand All @@ -1264,7 +1292,7 @@ func (r *Reconciler) updateK8sRoute(ctx context.Context, existingK8sRes unstruct

// Read the host from the OperandConfig
if newHost, found := k8sResConfigDecoded["spec"].(map[string]interface{})["host"].(string); found {
newHostHash := util.CalculateHash(newHost)
newHostHash := util.CalculateHash([]byte(newHost))

// Only re-create the route if the custom host has been removed
if newHost == "" && existingHostHash != newHostHash {
Expand Down
37 changes: 37 additions & 0 deletions controllers/util/merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ func MergeCR(defaultCR, changedCR []byte) map[string]interface{} {
return make(map[string]interface{})
}

// Handle when only one CR is provided
defaultCRDecoded := make(map[string]interface{})
changedCRDecoded := make(map[string]interface{})
if len(defaultCR) != 0 && len(changedCR) == 0 {
Expand All @@ -52,9 +53,15 @@ func MergeCR(defaultCR, changedCR []byte) map[string]interface{} {
if changedCRUnmarshalErr != nil {
klog.Errorf("failed to unmarshal service spec: %v", changedCRUnmarshalErr)
}

// Merge both specs
for key := range defaultCRDecoded {
checkKeyBeforeMerging(key, defaultCRDecoded[key], changedCRDecoded[key], changedCRDecoded)
}

// Ensure labels and annotations are merged as well
mergeMetadata(defaultCRDecoded, changedCRDecoded)

return changedCRDecoded
}

Expand Down Expand Up @@ -96,3 +103,33 @@ func checkKeyBeforeMerging(key string, defaultMap interface{}, changedMap interf
}
}
}

// Helper function to merge metadata like labels and annotations
func mergeMetadata(defaultCRDecoded, changedCRDecoded map[string]interface{}) {
// Handle metadata section
if defaultMeta, ok := defaultCRDecoded["metadata"].(map[string]interface{}); ok {
if changedMeta, ok := changedCRDecoded["metadata"].(map[string]interface{}); ok {
// Merge labels
if defaultLabels, ok := defaultMeta["labels"].(map[string]interface{}); ok {
if changedLabels, ok := changedMeta["labels"].(map[string]interface{}); ok {
for key, value := range defaultLabels {
changedLabels[key] = value
}
} else {
changedMeta["labels"] = defaultLabels
}
}
if defaultAnnotations, ok := defaultMeta["annotations"].(map[string]interface{}); ok {
if changedAnnotations, ok := changedMeta["annotations"].(map[string]interface{}); ok {
for key, value := range defaultAnnotations {
changedAnnotations[key] = value
}
} else {
changedMeta["annotations"] = defaultAnnotations
}
}
} else {
changedCRDecoded["metadata"] = defaultMeta
}
}
}
32 changes: 29 additions & 3 deletions controllers/util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"sync"
"time"

constant "github.com/IBM/operand-deployment-lifecycle-manager/v4/controllers/constant"
ocproute "github.com/openshift/api/route/v1"
"golang.org/x/mod/semver"
corev1 "k8s.io/api/core/v1"
Expand Down Expand Up @@ -154,14 +155,39 @@ func StringSliceContentEqual(a, b []string) bool {
return true
}

func CalculateHash(input string) string {
if input == "" {
// CalculateHash calculates the hash value for single resource
func CalculateHash(input []byte) string {
if len(input) == 0 {
return ""
}
hashedData := sha256.Sum256([]byte(input))
hashedData := sha256.Sum256(input)
return hex.EncodeToString(hashedData[:7])
}

// CalculateResHashes calculates the hash for the existing cluster resource and the new template resource
func CalculateResHashes(fromCluster *unstructured.Unstructured, fromTemplate []byte) (string, string) {
templateHash := CalculateHash(fromTemplate)

if fromCluster != nil {
clusterAnnos := fromCluster.GetAnnotations()
clusterHash := ""
if clusterAnnos != nil {
clusterHash = clusterAnnos[constant.K8sHashedData]
}
return clusterHash, templateHash
}
return "", templateHash
}

// SetHashAnnotation sets the hash annotation in the object
func AddHashAnnotation(obj *unstructured.Unstructured, key, hash string, newAnnotations map[string]string) map[string]string {
if newAnnotations == nil {
newAnnotations = make(map[string]string)
}
newAnnotations[key] = hash
return newAnnotations
}

// WaitTimeout waits for the waitgroup for the specified max timeout.
// Returns true if waiting timed out.
func WaitTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {
Expand Down