diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a5918d1..b56e3e19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,7 @@ This changelog keeps track of work items that have been completed and are ready ### Fixes -- **General**: TODO ([#TODO](https://github.com/kedacore/http-add-on/issues/TODO)) +- **General**: Ensure operator is aware about changes on underlying ScaledObject ([#900](https://github.com/kedacore/http-add-on/issues/900)) ### Deprecations diff --git a/config/crd/bases/http.keda.sh_httpscaledobjects.yaml b/config/crd/bases/http.keda.sh_httpscaledobjects.yaml index 95b153c2..602f53c7 100644 --- a/config/crd/bases/http.keda.sh_httpscaledobjects.yaml +++ b/config/crd/bases/http.keda.sh_httpscaledobjects.yaml @@ -155,12 +155,6 @@ spec: type: description: Type of condition enum: - - Created - - Terminated - - Error - - Pending - - Terminating - - Unknown - Ready type: string required: diff --git a/operator/apis/http/v1alpha1/condition_types.go b/operator/apis/http/v1alpha1/condition_types.go new file mode 100644 index 00000000..114265d3 --- /dev/null +++ b/operator/apis/http/v1alpha1/condition_types.go @@ -0,0 +1,88 @@ +package v1alpha1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// +kubebuilder:validation:Enum=Ready + +// HTTPScaledObjectCreationStatus describes the creation status +// of the scaler's additional resources such as Services, Ingresses and Deployments +type HTTPScaledObjectCreationStatus string + +const ( + // Ready indicates the object is fully created + Ready HTTPScaledObjectCreationStatus = "Ready" +) + +// +kubebuilder:validation:Enum=ErrorCreatingAppScaledObject;AppScaledObjectCreated;TerminatingResources;AppScaledObjectTerminated;AppScaledObjectTerminationError;PendingCreation;HTTPScaledObjectIsReady; + +// HTTPScaledObjectConditionReason describes the reason why the condition transitioned +type HTTPScaledObjectConditionReason string + +const ( + ErrorCreatingAppScaledObject HTTPScaledObjectConditionReason = "ErrorCreatingAppScaledObject" + AppScaledObjectCreated HTTPScaledObjectConditionReason = "AppScaledObjectCreated" + TerminatingResources HTTPScaledObjectConditionReason = "TerminatingResources" + AppScaledObjectTerminated HTTPScaledObjectConditionReason = "AppScaledObjectTerminated" + AppScaledObjectTerminationError HTTPScaledObjectConditionReason = "AppScaledObjectTerminationError" + PendingCreation HTTPScaledObjectConditionReason = "PendingCreation" + HTTPScaledObjectIsReady HTTPScaledObjectConditionReason = "HTTPScaledObjectIsReady" +) + +// HTTPScaledObjectCondition stores the condition state +type HTTPScaledObjectCondition struct { + // Timestamp of the condition + // +optional + Timestamp string `json:"timestamp" description:"Timestamp of this condition"` + // Type of condition + // +required + Type HTTPScaledObjectCreationStatus `json:"type" description:"type of status condition"` + // Status of the condition, one of True, False, Unknown. + // +required + Status metav1.ConditionStatus `json:"status" description:"status of the condition, one of True, False, Unknown"` + // Reason for the condition's last transition. + // +optional + Reason HTTPScaledObjectConditionReason `json:"reason,omitempty" description:"one-word CamelCase reason for the condition's last transition"` + // Message indicating details about the transition. + // +optional + Message string `json:"message,omitempty" description:"human-readable message indicating details about last transition"` +} + +type Conditions []HTTPScaledObjectCondition + +// GetReadyCondition returns Condition of type Ready +func (c *Conditions) GetReadyCondition() HTTPScaledObjectCondition { + if *c == nil { + c = GetInitializedConditions() + } + return c.getCondition(Ready) +} + +// GetInitializedConditions returns Conditions initialized to the default -> Status: Unknown +func GetInitializedConditions() *Conditions { + return &Conditions{{Type: Ready, Status: metav1.ConditionUnknown}} +} + +// IsTrue is true if the condition is True +func (c *HTTPScaledObjectCondition) IsTrue() bool { + if c == nil { + return false + } + return c.Status == metav1.ConditionTrue +} + +// IsFalse is true if the condition is False +func (c *HTTPScaledObjectCondition) IsFalse() bool { + if c == nil { + return false + } + return c.Status == metav1.ConditionFalse +} + +func (c Conditions) getCondition(conditionType HTTPScaledObjectCreationStatus) HTTPScaledObjectCondition { + for i := range c { + if c[i].Type == conditionType { + return c[i] + } + } + return HTTPScaledObjectCondition{} +} diff --git a/operator/apis/http/v1alpha1/httpscaledobject_types.go b/operator/apis/http/v1alpha1/httpscaledobject_types.go index 8fbcb2a3..ef30600d 100644 --- a/operator/apis/http/v1alpha1/httpscaledobject_types.go +++ b/operator/apis/http/v1alpha1/httpscaledobject_types.go @@ -74,64 +74,6 @@ type HTTPScaledObjectSpec struct { CooldownPeriod *int32 `json:"scaledownPeriod,omitempty" description:"Cooldown period (seconds) for resources to scale down (Default 300)"` } -// +kubebuilder:validation:Enum=Created;Terminated;Error;Pending;Terminating;Unknown;Ready - -// HTTPScaledObjectCreationStatus describes the creation status -// of the scaler's additional resources such as Services, Ingresses and Deployments -type HTTPScaledObjectCreationStatus string - -const ( - // Created indicates the resource has been created - Created HTTPScaledObjectCreationStatus = "Created" - // Terminated indicates the resource has been terminated - Terminated HTTPScaledObjectCreationStatus = "Terminated" - // Error indicates the resource had an error - Error HTTPScaledObjectCreationStatus = "Error" - // Pending indicates the resource hasn't been created - Pending HTTPScaledObjectCreationStatus = "Pending" - // Terminating indicates that the resource is marked for deletion but hasn't - // been deleted yet - Terminating HTTPScaledObjectCreationStatus = "Terminating" - // Unknown indicates the status is unavailable - Unknown HTTPScaledObjectCreationStatus = "Unknown" - // Ready indicates the object is fully created - Ready HTTPScaledObjectCreationStatus = "Ready" -) - -// +kubebuilder:validation:Enum=ErrorCreatingAppScaledObject;AppScaledObjectCreated;TerminatingResources;AppScaledObjectTerminated;AppScaledObjectTerminationError;PendingCreation;HTTPScaledObjectIsReady; - -// HTTPScaledObjectConditionReason describes the reason why the condition transitioned -type HTTPScaledObjectConditionReason string - -const ( - ErrorCreatingAppScaledObject HTTPScaledObjectConditionReason = "ErrorCreatingAppScaledObject" - AppScaledObjectCreated HTTPScaledObjectConditionReason = "AppScaledObjectCreated" - TerminatingResources HTTPScaledObjectConditionReason = "TerminatingResources" - AppScaledObjectTerminated HTTPScaledObjectConditionReason = "AppScaledObjectTerminated" - AppScaledObjectTerminationError HTTPScaledObjectConditionReason = "AppScaledObjectTerminationError" - PendingCreation HTTPScaledObjectConditionReason = "PendingCreation" - HTTPScaledObjectIsReady HTTPScaledObjectConditionReason = "HTTPScaledObjectIsReady" -) - -// HTTPScaledObjectCondition stores the condition state -type HTTPScaledObjectCondition struct { - // Timestamp of the condition - // +optional - Timestamp string `json:"timestamp" description:"Timestamp of this condition"` - // Type of condition - // +required - Type HTTPScaledObjectCreationStatus `json:"type" description:"type of status condition"` - // Status of the condition, one of True, False, Unknown. - // +required - Status metav1.ConditionStatus `json:"status" description:"status of the condition, one of True, False, Unknown"` - // Reason for the condition's last transition. - // +optional - Reason HTTPScaledObjectConditionReason `json:"reason,omitempty" description:"one-word CamelCase reason for the condition's last transition"` - // Message indicating details about the transition. - // +optional - Message string `json:"message,omitempty" description:"human-readable message indicating details about last transition"` -} - // HTTPScaledObjectStatus defines the observed state of HTTPScaledObject type HTTPScaledObjectStatus struct { // TargetWorkload reflects details about the scaled workload. @@ -141,7 +83,7 @@ type HTTPScaledObjectStatus struct { // +optional TargetService string `json:"targetService,omitempty" description:"It reflects details about the scaled service"` // Conditions of the operator - Conditions []HTTPScaledObjectCondition `json:"conditions,omitempty" description:"List of auditable conditions of the operator"` + Conditions Conditions `json:"conditions,omitempty" description:"List of auditable conditions of the operator"` } // +genclient diff --git a/operator/apis/http/v1alpha1/zz_generated.deepcopy.go b/operator/apis/http/v1alpha1/zz_generated.deepcopy.go index 96be0193..0f713236 100644 --- a/operator/apis/http/v1alpha1/zz_generated.deepcopy.go +++ b/operator/apis/http/v1alpha1/zz_generated.deepcopy.go @@ -25,6 +25,25 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in Conditions) DeepCopyInto(out *Conditions) { + { + in := &in + *out = make(Conditions, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Conditions. +func (in Conditions) DeepCopy() Conditions { + if in == nil { + return nil + } + out := new(Conditions) + in.DeepCopyInto(out) + return *out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HTTPScaledObject) DeepCopyInto(out *HTTPScaledObject) { *out = *in @@ -145,7 +164,7 @@ func (in *HTTPScaledObjectStatus) DeepCopyInto(out *HTTPScaledObjectStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]HTTPScaledObjectCondition, len(*in)) + *out = make(Conditions, len(*in)) copy(*out, *in) } } diff --git a/operator/controllers/http/app.go b/operator/controllers/http/app.go index b62c6140..2240b1ed 100644 --- a/operator/controllers/http/app.go +++ b/operator/controllers/http/app.go @@ -37,7 +37,7 @@ func (r *HTTPScaledObjectReconciler) createOrUpdateApplicationResources( httpso, *SetMessage( CreateCondition( - v1alpha1.Pending, + v1alpha1.Ready, v1.ConditionUnknown, v1alpha1.PendingCreation, ), diff --git a/operator/controllers/http/httpscaledobject_controller.go b/operator/controllers/http/httpscaledobject_controller.go index abc54928..721f0752 100644 --- a/operator/controllers/http/httpscaledobject_controller.go +++ b/operator/controllers/http/httpscaledobject_controller.go @@ -22,6 +22,7 @@ import ( "fmt" "time" + kedav1alpha1 "github.com/kedacore/keda/v2/apis/keda/v1alpha1" appsv1 "k8s.io/api/apps/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -34,6 +35,7 @@ import ( httpv1alpha1 "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" "github.com/kedacore/http-add-on/operator/controllers/http/config" + "github.com/kedacore/http-add-on/operator/controllers/util" "github.com/kedacore/http-add-on/pkg/k8s" ) @@ -146,7 +148,20 @@ func (r *HTTPScaledObjectReconciler) Reconcile(ctx context.Context, req ctrl.Req // SetupWithManager sets up the controller with the Manager. func (r *HTTPScaledObjectReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). - For(&httpv1alpha1.HTTPScaledObject{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). + For(&httpv1alpha1.HTTPScaledObject{}, builder.WithPredicates( + predicate.Or( + predicate.GenerationChangedPredicate{}, + util.HTTPScaledObjectReadyConditionPredicate{}, + ), + )). + // Trigger a reconcile only when the ScaledObject spec,label or annotation changes. + // Ignore updates to ScaledObject status + Owns(&kedav1alpha1.ScaledObject{}, builder.WithPredicates( + predicate.Or( + predicate.LabelChangedPredicate{}, + predicate.AnnotationChangedPredicate{}, + util.ScaledObjectSpecChangedPredicate{}, + ))). Complete(r) } diff --git a/operator/controllers/http/scaled_object.go b/operator/controllers/http/scaled_object.go index 79e3c0d8..832c8f58 100644 --- a/operator/controllers/http/scaled_object.go +++ b/operator/controllers/http/scaled_object.go @@ -80,7 +80,7 @@ func (r *HTTPScaledObjectReconciler) createOrUpdateScaledObject( httpso, *SetMessage( CreateCondition( - httpv1alpha1.Error, + httpv1alpha1.Ready, v1.ConditionFalse, httpv1alpha1.ErrorCreatingAppScaledObject, ), @@ -97,7 +97,7 @@ func (r *HTTPScaledObjectReconciler) createOrUpdateScaledObject( httpso, *SetMessage( CreateCondition( - httpv1alpha1.Created, + httpv1alpha1.Ready, v1.ConditionTrue, httpv1alpha1.AppScaledObjectCreated, ), diff --git a/operator/controllers/http/scaled_object_test.go b/operator/controllers/http/scaled_object_test.go index 4535915b..93162f38 100644 --- a/operator/controllers/http/scaled_object_test.go +++ b/operator/controllers/http/scaled_object_test.go @@ -42,7 +42,7 @@ func TestCreateOrUpdateScaledObject(t *testing.T) { cond1ts, err := time.Parse(time.RFC3339, cond1.Timestamp) r.NoError(err) r.GreaterOrEqual(time.Since(cond1ts), time.Duration(0)) - r.Equal(v1alpha1.Created, cond1.Type) + r.Equal(v1alpha1.Ready, cond1.Type) r.Equal(metav1.ConditionTrue, cond1.Status) r.Equal(v1alpha1.AppScaledObjectCreated, cond1.Reason) diff --git a/operator/controllers/util/predicate.go b/operator/controllers/util/predicate.go new file mode 100644 index 00000000..f1df5734 --- /dev/null +++ b/operator/controllers/util/predicate.go @@ -0,0 +1,52 @@ +package util + +import ( + kedav1alpha1 "github.com/kedacore/keda/v2/apis/keda/v1alpha1" + "k8s.io/apimachinery/pkg/api/equality" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" +) + +type HTTPScaledObjectReadyConditionPredicate struct { + predicate.Funcs +} + +func (HTTPScaledObjectReadyConditionPredicate) Update(e event.UpdateEvent) bool { + if e.ObjectOld == nil || e.ObjectNew == nil { + return false + } + + var newReadyCondition, oldReadyCondition v1alpha1.HTTPScaledObjectCondition + + oldObj, ok := e.ObjectOld.(*v1alpha1.HTTPScaledObject) + if !ok { + return false + } + oldReadyCondition = oldObj.Status.Conditions.GetReadyCondition() + + newObj, ok := e.ObjectNew.(*v1alpha1.HTTPScaledObject) + if !ok { + return false + } + newReadyCondition = newObj.Status.Conditions.GetReadyCondition() + + // False/Unknown -> True + if !oldReadyCondition.IsTrue() && newReadyCondition.IsTrue() { + return true + } + + return false +} + +type ScaledObjectSpecChangedPredicate struct { + predicate.Funcs +} + +func (ScaledObjectSpecChangedPredicate) Update(e event.UpdateEvent) bool { + newObj := e.ObjectNew.(*kedav1alpha1.ScaledObject) + oldObj := e.ObjectOld.(*kedav1alpha1.ScaledObject) + + return !equality.Semantic.DeepDerivative(newObj.Spec, oldObj.Spec) +}