Skip to content

Commit

Permalink
Enhance ODLM create/update logic by adding hash values (#1086)
Browse files Browse the repository at this point in the history
* hash comparison and deep merge all the resources

Signed-off-by: YuChen <[email protected]>

* update hash number

Signed-off-by: YuChen <[email protected]>

---------

Signed-off-by: YuChen <[email protected]>
  • Loading branch information
YCShen1010 authored Sep 30, 2024
1 parent 2d8d9d3 commit f611358
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 28 deletions.
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
80 changes: 55 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,56 @@ 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)

r.EnsureAnnotation(existingK8sRes, newAnnotations)
r.EnsureLabel(existingK8sRes, newLabels)
if err := r.setOwnerReferences(ctx, &existingK8sRes, ownerReferences); err != nil {
// 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
}
newAnnotations = util.AddHashAnnotation(&existingRes, constant.K8sHashedData, templateHash, newAnnotations)

} 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(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 +1185,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 +1283,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 +1294,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
4 changes: 4 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,12 @@ 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)
}

return changedCRDecoded
}

Expand Down
33 changes: 30 additions & 3 deletions controllers/util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/discovery"
"k8s.io/client-go/util/jsonpath"

constant "github.com/IBM/operand-deployment-lifecycle-manager/v4/controllers/constant"
)

type TemplateValueRef struct {
Expand Down Expand Up @@ -154,14 +156,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

0 comments on commit f611358

Please sign in to comment.