diff --git a/api/v1/runtimecomponent_types.go b/api/v1/runtimecomponent_types.go index 2e694dfc..ae9e526f 100644 --- a/api/v1/runtimecomponent_types.go +++ b/api/v1/runtimecomponent_types.go @@ -450,6 +450,8 @@ type RuntimeComponentStatus struct { // The reconciliation interval in seconds. ReconcileInterval *int32 `json:"reconcileInterval,omitempty"` + + TrackedAnnotations map[common.StatusTrackedAnnotationType][]string `json:"trackedAnnotations,omitempty"` } // Defines possible status conditions. @@ -801,6 +803,28 @@ func (s *RuntimeComponentStatus) SetBinding(r *corev1.LocalObjectReference) { s.Binding = r } +func (s *RuntimeComponentStatus) GetTrackedAnnotations() common.StatusTrackedAnnotations { + return s.TrackedAnnotations +} + +func (s *RuntimeComponentStatus) SetTrackedAnnotations(trackedAnnotations common.StatusTrackedAnnotations) { + s.TrackedAnnotations = trackedAnnotations +} + +func (s *RuntimeComponentStatus) GetTrackedAnnotation(annotationType common.StatusTrackedAnnotationType) []string { + if s.TrackedAnnotations == nil { + s.TrackedAnnotations = make(common.StatusTrackedAnnotations) + } + return s.TrackedAnnotations[annotationType] +} + +func (s *RuntimeComponentStatus) SetTrackedAnnotation(annotationType common.StatusTrackedAnnotationType, annotationKeys []string) { + if s.TrackedAnnotations == nil { + s.TrackedAnnotations = make(common.StatusTrackedAnnotations) + } + s.TrackedAnnotations[annotationType] = annotationKeys +} + // GetMinReplicas returns minimum replicas func (a *RuntimeComponentAutoScaling) GetMinReplicas() *int32 { return a.MinReplicas diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index 921a97e3..7e4ae2d5 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -735,6 +735,22 @@ func (in *RuntimeComponentStatus) DeepCopyInto(out *RuntimeComponentStatus) { *out = new(int32) **out = **in } + if in.TrackedAnnotations != nil { + in, out := &in.TrackedAnnotations, &out.TrackedAnnotations + *out = make(map[common.StatusTrackedAnnotationType][]string, len(*in)) + for key, val := range *in { + var outVal []string + if val == nil { + (*out)[key] = nil + } else { + inVal := (*in)[key] + in, out := &inVal, &outVal + *out = make([]string, len(*in)) + copy(*out, *in) + } + (*out)[key] = outVal + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RuntimeComponentStatus. diff --git a/api/v1beta2/runtimecomponent_types.go b/api/v1beta2/runtimecomponent_types.go index 273532c2..1521a622 100644 --- a/api/v1beta2/runtimecomponent_types.go +++ b/api/v1beta2/runtimecomponent_types.go @@ -670,6 +670,20 @@ func (s *RuntimeComponentStatus) SetBinding(r *corev1.LocalObjectReference) { s.Binding = r } +func (s *RuntimeComponentStatus) GetTrackedAnnotations() common.StatusTrackedAnnotations { + return nil +} + +func (s *RuntimeComponentStatus) SetTrackedAnnotations(trackedAnnotations common.StatusTrackedAnnotations) { +} + +func (s *RuntimeComponentStatus) GetTrackedAnnotation(annotationType common.StatusTrackedAnnotationType) []string { + return nil +} + +func (s *RuntimeComponentStatus) SetTrackedAnnotation(annotationType common.StatusTrackedAnnotationType, annotationKeys []string) { +} + // GetMinReplicas returns minimum replicas func (a *RuntimeComponentAutoScaling) GetMinReplicas() *int32 { return a.MinReplicas diff --git a/common/common.go b/common/common.go index a8f750e3..e1dbdb6c 100644 --- a/common/common.go +++ b/common/common.go @@ -1,6 +1,8 @@ package common import ( + "slices" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/intstr" ) @@ -59,3 +61,96 @@ func GetDefaultMicroProfileLivenessProbe(ba BaseComponent) *corev1.Probe { func GetComponentNameLabel(ba BaseComponent) string { return ba.GetGroupName() + "/name" } + +// GetComponentAnnotationsGetter returns the getter for annotationType annotations configured in the BaseComponent instance. +func GetComponentAnnotationsGetter(ba BaseComponent, annotationType StatusTrackedAnnotationType) func() map[string]string { + var annotationGetter func() map[string]string + annotationGetter = nil + switch annotationType { + case StatusTrackedAnnotationTypeGlobal: + annotationGetter = ba.GetAnnotations + case StatusTrackedAnnotationTypeDeployment: + if ba.GetDeployment() != nil { + annotationGetter = ba.GetDeployment().GetAnnotations + } + case StatusTrackedAnnotationTypeStatefulSet: + if ba.GetStatefulSet() != nil { + annotationGetter = ba.GetStatefulSet().GetAnnotations + } + case StatusTrackedAnnotationTypeService: + if ba.GetService() != nil { + annotationGetter = ba.GetService().GetAnnotations + } + case StatusTrackedAnnotationTypeRoute: + if ba.GetRoute() != nil { + annotationGetter = ba.GetRoute().GetAnnotations + } + } + return annotationGetter +} + +// SaveTrackedAnnotations stores current annotations configured in the BaseComponent into the BaseComponentStatus. +func SaveTrackedAnnotations(ba BaseComponent, annotationTypes ...StatusTrackedAnnotationType) { + for _, annotationType := range annotationTypes { + if annotationGetter := GetComponentAnnotationsGetter(ba, annotationType); annotationGetter != nil { + currentAnnotationKeys := []string{} + for key := range annotationGetter() { + currentAnnotationKeys = append(currentAnnotationKeys, key) + } + if len(currentAnnotationKeys) > 0 { + slices.Sort(currentAnnotationKeys) + ba.GetStatus().SetTrackedAnnotation(annotationType, currentAnnotationKeys) + } else { + ba.GetStatus().SetTrackedAnnotation(annotationType, nil) + } + } + } +} + +// DeleteMissingTrackedAnnotations returns the map of currentAnnotations after removing annotationType annotations +// that are no longer configured in BaseComponent instance but still found within the tracked annotations in BaseComponentStatus. +func DeleteMissingTrackedAnnotations(currentAnnotations map[string]string, ba BaseComponent, annotationTypes ...StatusTrackedAnnotationType) map[string]string { + missingTrackedAnnotations := FilterMissingTrackedAnnotations(ba, StatusTrackedAnnotationTypeGlobal, ba.GetAnnotations()) + for _, missingTrackedAnnotation := range missingTrackedAnnotations { + delete(currentAnnotations, missingTrackedAnnotation) + } + for _, annotationType := range annotationTypes { + if annotationType != StatusTrackedAnnotationTypeGlobal { + if annotationGetter := GetComponentAnnotationsGetter(ba, annotationType); annotationGetter != nil { + missingTrackedAnnotations := FilterMissingTrackedAnnotations(ba, annotationType, annotationGetter()) + for _, missingTrackedAnnotation := range missingTrackedAnnotations { + delete(currentAnnotations, missingTrackedAnnotation) + } + } + } + } + return currentAnnotations +} + +// FilterMissingTrackedAnnotations returns an array of tracked annotations that are no longer configured in the BaseComponent instance +// but still exist in the tracked annotations of BaseComponentStatus. +func FilterMissingTrackedAnnotations(ba BaseComponent, annotationType StatusTrackedAnnotationType, annotations map[string]string) []string { + if ba.GetStatus() == nil { + return []string{} + } + trackedAnnotations := ba.GetStatus().GetTrackedAnnotation(annotationType) + if len(trackedAnnotations) == 0 || annotations == nil { + return []string{} + } + for annotationKey := range annotations { + if slices.Contains(trackedAnnotations, annotationKey) { + // remove annotationKey from trackedAnnotations + deleteIndex := -1 + for i := range trackedAnnotations { + if trackedAnnotations[i] == annotationKey { + deleteIndex = i + break + } + } + if deleteIndex != -1 { + trackedAnnotations = append(trackedAnnotations[:deleteIndex], trackedAnnotations[deleteIndex+1:]...) + } + } + } + return trackedAnnotations +} diff --git a/common/types.go b/common/types.go index 42c54ff2..d87e5951 100644 --- a/common/types.go +++ b/common/types.go @@ -18,6 +18,10 @@ type StatusEndpointScope string type StatusReferences map[string]string +type StatusTrackedAnnotationType string + +type StatusTrackedAnnotations map[StatusTrackedAnnotationType][]string + const ( StatusReferenceCertSecretName = "svcCertSecretName" StatusReferencePullSecretName = "saPullSecretName" @@ -90,6 +94,11 @@ type BaseComponentStatus interface { UnsetReconcileInterval() GetLatestTransitionTime() *metav1.Time + + GetTrackedAnnotations() StatusTrackedAnnotations + SetTrackedAnnotations(StatusTrackedAnnotations) + GetTrackedAnnotation(StatusTrackedAnnotationType) []string + SetTrackedAnnotation(StatusTrackedAnnotationType, []string) } const ( @@ -105,6 +114,13 @@ const ( // Status Endpoint Scopes StatusEndpointScopeExternal StatusEndpointScope = "External" StatusEndpointScopeInternal StatusEndpointScope = "Internal" + + // Status Tracked Annotation Types + StatusTrackedAnnotationTypeGlobal StatusTrackedAnnotationType = "global" + StatusTrackedAnnotationTypeDeployment StatusTrackedAnnotationType = "deployment" + StatusTrackedAnnotationTypeStatefulSet StatusTrackedAnnotationType = "statefulset" + StatusTrackedAnnotationTypeService StatusTrackedAnnotationType = "service" + StatusTrackedAnnotationTypeRoute StatusTrackedAnnotationType = "route" ) // BaseComponentAutoscaling represents basic HPA configuration diff --git a/config/crd/bases/rc.app.stacks_runtimecomponents.yaml b/config/crd/bases/rc.app.stacks_runtimecomponents.yaml index 3eb6cf95..081c0c56 100644 --- a/config/crd/bases/rc.app.stacks_runtimecomponents.yaml +++ b/config/crd/bases/rc.app.stacks_runtimecomponents.yaml @@ -8865,6 +8865,12 @@ spec: additionalProperties: type: string type: object + trackedAnnotations: + additionalProperties: + items: + type: string + type: array + type: object versions: properties: reconciled: diff --git a/internal/controller/runtimecomponent_controller.go b/internal/controller/runtimecomponent_controller.go index 59c0546a..f979187f 100644 --- a/internal/controller/runtimecomponent_controller.go +++ b/internal/controller/runtimecomponent_controller.go @@ -592,7 +592,8 @@ func (r *RuntimeComponentReconciler) SetupWithManager(mgr ctrl.Manager) error { pred := predicate.Funcs{ UpdateFunc: func(e event.UpdateEvent) bool { // Ignore updates to CR status in which case metadata.Generation does not change - return e.ObjectOld.GetGeneration() != e.ObjectNew.GetGeneration() && (isClusterWide || watchNamespacesMap[e.ObjectNew.GetNamespace()]) + return (e.ObjectOld.GetGeneration() != e.ObjectNew.GetGeneration() && (isClusterWide || watchNamespacesMap[e.ObjectNew.GetNamespace()])) || + (e.ObjectOld.GetGeneration() == e.ObjectNew.GetGeneration() && !appstacksutils.AnnotationsEqual(e.ObjectOld.GetAnnotations(), e.ObjectNew.GetAnnotations())) }, CreateFunc: func(e event.CreateEvent) bool { return isClusterWide || watchNamespacesMap[e.Object.GetNamespace()] diff --git a/utils/reconciler.go b/utils/reconciler.go index 6cc11fdf..d8beb325 100644 --- a/utils/reconciler.go +++ b/utils/reconciler.go @@ -384,6 +384,14 @@ func (r *ReconcilerBase) ManageSuccess(conditionType common.StatusConditionType, } } + // Track annotations that were defined in the CR into status + common.SaveTrackedAnnotations(ba, + common.StatusTrackedAnnotationTypeGlobal, + common.StatusTrackedAnnotationTypeDeployment, + common.StatusTrackedAnnotationTypeStatefulSet, + common.StatusTrackedAnnotationTypeService, + common.StatusTrackedAnnotationTypeRoute) + err := r.UpdateStatus(ba.(client.Object)) if err != nil { log.Error(err, "Unable to update status") diff --git a/utils/utils.go b/utils/utils.go index 5005717e..66bb6879 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "os" + "slices" "sort" "strconv" "strings" @@ -45,10 +46,41 @@ const RCOOperandVersion = "1.4.4" var APIVersionNotFoundError = errors.New("APIVersion is not available") +func AnnotationsEqual(a1, a2 map[string]string) bool { + if a1 == nil && a2 == nil { + return true + } + if a1 == nil { + return false + } + if a2 == nil { + return false + } + if len(a1) != len(a2) { + return false + } + for k1, v1 := range a1 { + if a2[k1] != v1 { + return false + } + } + return true +} + +func FilterMapByKeys(values map[string]string, keys []string) map[string]string { + for k := range values { + if !slices.Contains(keys, k) { + delete(values, k) + } + } + return values +} + // CustomizeDeployment ... func CustomizeDeployment(deploy *appsv1.Deployment, ba common.BaseComponent) { obj := ba.(metav1.Object) deploy.Labels = ba.GetLabels() + deploy.Annotations = common.DeleteMissingTrackedAnnotations(deploy.Annotations, ba, common.StatusTrackedAnnotationTypeDeployment) deploy.Annotations = MergeMaps(deploy.Annotations, ba.GetAnnotations()) if ba.GetAutoscaling() == nil { @@ -79,6 +111,7 @@ func CustomizeDeployment(deploy *appsv1.Deployment, ba common.BaseComponent) { func CustomizeStatefulSet(statefulSet *appsv1.StatefulSet, ba common.BaseComponent) { obj := ba.(metav1.Object) statefulSet.Labels = ba.GetLabels() + statefulSet.Annotations = common.DeleteMissingTrackedAnnotations(statefulSet.Annotations, ba, common.StatusTrackedAnnotationTypeStatefulSet) statefulSet.Annotations = MergeMaps(statefulSet.Annotations, ba.GetAnnotations()) if ba.GetAutoscaling() == nil { @@ -110,6 +143,7 @@ func CustomizeStatefulSet(statefulSet *appsv1.StatefulSet, ba common.BaseCompone func CustomizeRoute(route *routev1.Route, ba common.BaseComponent, key string, crt string, ca string, destCACert string) { obj := ba.(metav1.Object) route.Labels = ba.GetLabels() + route.Annotations = common.DeleteMissingTrackedAnnotations(route.Annotations, ba, common.StatusTrackedAnnotationTypeRoute) route.Annotations = MergeMaps(route.Annotations, ba.GetAnnotations()) if ba.GetRoute() != nil { @@ -198,6 +232,7 @@ func ErrorIsNoMatchesForKind(err error, kind string, version string) bool { func CustomizeService(svc *corev1.Service, ba common.BaseComponent) { obj := ba.(metav1.Object) svc.Labels = ba.GetLabels() + svc.Annotations = common.DeleteMissingTrackedAnnotations(svc.Annotations, ba, common.StatusTrackedAnnotationTypeService) CustomizeServiceAnnotations(svc) svc.Annotations = MergeMaps(svc.Annotations, ba.GetAnnotations()) @@ -344,6 +379,7 @@ func customizeProbeDefaults(config *corev1.Probe, defaultProbe *corev1.Probe) *c func CustomizeNetworkPolicy(networkPolicy *networkingv1.NetworkPolicy, isOpenShift bool, ba common.BaseComponent) { obj := ba.(metav1.Object) networkPolicy.Labels = ba.GetLabels() + networkPolicy.Annotations = common.DeleteMissingTrackedAnnotations(networkPolicy.Annotations, ba, common.StatusTrackedAnnotationTypeGlobal) networkPolicy.Annotations = MergeMaps(networkPolicy.Annotations, ba.GetAnnotations()) networkPolicy.Spec.PolicyTypes = []networkingv1.PolicyType{networkingv1.PolicyTypeIngress} @@ -649,6 +685,7 @@ func customizeAffinityArchitectures(affinity *corev1.Affinity, affinityConfig co func CustomizePodSpec(pts *corev1.PodTemplateSpec, ba common.BaseComponent) { obj := ba.(metav1.Object) pts.Labels = ba.GetLabels() + pts.Annotations = common.DeleteMissingTrackedAnnotations(pts.Annotations, ba, common.StatusTrackedAnnotationTypeDeployment, common.StatusTrackedAnnotationTypeStatefulSet) pts.Annotations = MergeMaps(pts.Annotations, ba.GetAnnotations()) // If they exist, add annotations from the StatefulSet or Deployment to the pods