diff --git a/CHANGELOG.md b/CHANGELOG.md index 32f6c393aae..64d5bbcdb4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,7 @@ Here is an overview of all new **experimental** features: ### Improvements - **General**: Prevent multiple ScaledObjects managing one HPA ([#6130](https://github.com/kedacore/keda/issues/6130)) +- **General**: Show full triggers'types and authentications'types in status ([#6187](https://github.com/kedacore/keda/issues/6187)) - **AWS CloudWatch Scaler**: Add support for ignoreNullValues ([#5352](https://github.com/kedacore/keda/issues/5352)) - **Elasticsearch Scaler**: Support Query at the Elasticsearch scaler ([#6216](https://github.com/kedacore/keda/issues/6216)) - **Etcd Scaler**: Add username and password support for etcd ([#6199](https://github.com/kedacore/keda/pull/6199)) diff --git a/apis/keda/v1alpha1/scaledjob_types.go b/apis/keda/v1alpha1/scaledjob_types.go index 366f22a14cd..deab161df6a 100644 --- a/apis/keda/v1alpha1/scaledjob_types.go +++ b/apis/keda/v1alpha1/scaledjob_types.go @@ -32,11 +32,11 @@ const ( // +kubebuilder:resource:path=scaledjobs,scope=Namespaced,shortName=sj // +kubebuilder:printcolumn:name="Min",type="integer",JSONPath=".spec.minReplicaCount" // +kubebuilder:printcolumn:name="Max",type="integer",JSONPath=".spec.maxReplicaCount" -// +kubebuilder:printcolumn:name="Triggers",type="string",JSONPath=".spec.triggers[*].type" -// +kubebuilder:printcolumn:name="Authentication",type="string",JSONPath=".spec.triggers[*].authenticationRef.name" // +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status" // +kubebuilder:printcolumn:name="Active",type="string",JSONPath=".status.conditions[?(@.type==\"Active\")].status" // +kubebuilder:printcolumn:name="Paused",type="string",JSONPath=".status.conditions[?(@.type==\"Paused\")].status" +// +kubebuilder:printcolumn:name="Triggers",type="string",JSONPath=".status.triggersTypes" +// +kubebuilder:printcolumn:name="Authentications",type="string",JSONPath=".status.authenticationsTypes" // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" // ScaledJob is the Schema for the scaledjobs API @@ -81,6 +81,10 @@ type ScaledJobStatus struct { Conditions Conditions `json:"conditions,omitempty"` // +optional Paused string `json:"Paused,omitempty"` + // +optional + TriggersTypes *string `json:"triggersTypes,omitempty"` + // +optional + AuthenticationsTypes *string `json:"authenticationsTypes,omitempty"` } // ScaledJobList contains a list of ScaledJob diff --git a/apis/keda/v1alpha1/scaledobject_types.go b/apis/keda/v1alpha1/scaledobject_types.go index f73de8e0a69..37c5e640963 100644 --- a/apis/keda/v1alpha1/scaledobject_types.go +++ b/apis/keda/v1alpha1/scaledobject_types.go @@ -33,12 +33,12 @@ import ( // +kubebuilder:printcolumn:name="ScaleTargetName",type="string",JSONPath=".spec.scaleTargetRef.name" // +kubebuilder:printcolumn:name="Min",type="integer",JSONPath=".spec.minReplicaCount" // +kubebuilder:printcolumn:name="Max",type="integer",JSONPath=".spec.maxReplicaCount" -// +kubebuilder:printcolumn:name="Triggers",type="string",JSONPath=".spec.triggers[*].type" -// +kubebuilder:printcolumn:name="Authentication",type="string",JSONPath=".spec.triggers[*].authenticationRef.name" // +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status" // +kubebuilder:printcolumn:name="Active",type="string",JSONPath=".status.conditions[?(@.type==\"Active\")].status" // +kubebuilder:printcolumn:name="Fallback",type="string",JSONPath=".status.conditions[?(@.type==\"Fallback\")].status" // +kubebuilder:printcolumn:name="Paused",type="string",JSONPath=".status.conditions[?(@.type==\"Paused\")].status" +// +kubebuilder:printcolumn:name="Triggers",type="string",JSONPath=".status.triggersTypes" +// +kubebuilder:printcolumn:name="Authentications",type="string",JSONPath=".status.authenticationsTypes" // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" // ScaledObject is a specification for a ScaledObject resource @@ -177,6 +177,10 @@ type ScaledObjectStatus struct { PausedReplicaCount *int32 `json:"pausedReplicaCount,omitempty"` // +optional HpaName string `json:"hpaName,omitempty"` + // +optional + TriggersTypes *string `json:"triggersTypes,omitempty"` + // +optional + AuthenticationsTypes *string `json:"authenticationsTypes,omitempty"` } // +kubebuilder:object:root=true diff --git a/apis/keda/v1alpha1/scaletriggers_types.go b/apis/keda/v1alpha1/scaletriggers_types.go index 584dfa8b614..78c411966f9 100644 --- a/apis/keda/v1alpha1/scaletriggers_types.go +++ b/apis/keda/v1alpha1/scaletriggers_types.go @@ -18,6 +18,8 @@ package v1alpha1 import ( "fmt" + "slices" + "strings" autoscalingv2 "k8s.io/api/autoscaling/v2" ) @@ -80,3 +82,18 @@ func ValidateTriggers(triggers []ScaleTriggers) error { return nil } + +// CombinedTriggersAndAuthenticationsTypes returns a comma separated string of all trigger types and authentication types +func CombinedTriggersAndAuthenticationsTypes(triggers []ScaleTriggers) (string, string) { + var triggersTypes []string + var authTypes []string + for _, trigger := range triggers { + if !slices.Contains(triggersTypes, trigger.Type) { + triggersTypes = append(triggersTypes, trigger.Type) + } + if trigger.AuthenticationRef != nil && !slices.Contains(authTypes, trigger.AuthenticationRef.Name) { + authTypes = append(authTypes, trigger.AuthenticationRef.Name) + } + } + return strings.Join(triggersTypes, ","), strings.Join(authTypes, ",") +} diff --git a/apis/keda/v1alpha1/zz_generated.deepcopy.go b/apis/keda/v1alpha1/zz_generated.deepcopy.go index a144aeb07d3..a6e01a22f4f 100755 --- a/apis/keda/v1alpha1/zz_generated.deepcopy.go +++ b/apis/keda/v1alpha1/zz_generated.deepcopy.go @@ -816,6 +816,16 @@ func (in *ScaledJobStatus) DeepCopyInto(out *ScaledJobStatus) { *out = make(Conditions, len(*in)) copy(*out, *in) } + if in.TriggersTypes != nil { + in, out := &in.TriggersTypes, &out.TriggersTypes + *out = new(string) + **out = **in + } + if in.AuthenticationsTypes != nil { + in, out := &in.AuthenticationsTypes, &out.AuthenticationsTypes + *out = new(string) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScaledJobStatus. @@ -1008,6 +1018,16 @@ func (in *ScaledObjectStatus) DeepCopyInto(out *ScaledObjectStatus) { *out = new(int32) **out = **in } + if in.TriggersTypes != nil { + in, out := &in.TriggersTypes, &out.TriggersTypes + *out = new(string) + **out = **in + } + if in.AuthenticationsTypes != nil { + in, out := &in.AuthenticationsTypes, &out.AuthenticationsTypes + *out = new(string) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScaledObjectStatus. diff --git a/config/crd/bases/keda.sh_scaledjobs.yaml b/config/crd/bases/keda.sh_scaledjobs.yaml index 5ccf72f2d46..1a7e6be6a9b 100644 --- a/config/crd/bases/keda.sh_scaledjobs.yaml +++ b/config/crd/bases/keda.sh_scaledjobs.yaml @@ -23,12 +23,6 @@ spec: - jsonPath: .spec.maxReplicaCount name: Max type: integer - - jsonPath: .spec.triggers[*].type - name: Triggers - type: string - - jsonPath: .spec.triggers[*].authenticationRef.name - name: Authentication - type: string - jsonPath: .status.conditions[?(@.type=="Ready")].status name: Ready type: string @@ -38,6 +32,12 @@ spec: - jsonPath: .status.conditions[?(@.type=="Paused")].status name: Paused type: string + - jsonPath: .status.triggersTypes + name: Triggers + type: string + - jsonPath: .status.authenticationsTypes + name: Authentications + type: string - jsonPath: .metadata.creationTimestamp name: Age type: date @@ -8028,6 +8028,8 @@ spec: properties: Paused: type: string + authenticationsTypes: + type: string conditions: description: Conditions an array representation to store multiple Conditions @@ -8055,6 +8057,8 @@ spec: lastActiveTime: format: date-time type: string + triggersTypes: + type: string type: object type: object served: true diff --git a/config/crd/bases/keda.sh_scaledobjects.yaml b/config/crd/bases/keda.sh_scaledobjects.yaml index c4ded761ccf..779831c2f80 100644 --- a/config/crd/bases/keda.sh_scaledobjects.yaml +++ b/config/crd/bases/keda.sh_scaledobjects.yaml @@ -29,12 +29,6 @@ spec: - jsonPath: .spec.maxReplicaCount name: Max type: integer - - jsonPath: .spec.triggers[*].type - name: Triggers - type: string - - jsonPath: .spec.triggers[*].authenticationRef.name - name: Authentication - type: string - jsonPath: .status.conditions[?(@.type=="Ready")].status name: Ready type: string @@ -47,6 +41,12 @@ spec: - jsonPath: .status.conditions[?(@.type=="Paused")].status name: Paused type: string + - jsonPath: .status.triggersTypes + name: Triggers + type: string + - jsonPath: .status.authenticationsTypes + name: Authentications + type: string - jsonPath: .metadata.creationTimestamp name: Age type: date @@ -309,6 +309,8 @@ spec: status: description: ScaledObjectStatus is the status for a ScaledObject resource properties: + authenticationsTypes: + type: string compositeScalerName: type: string conditions: @@ -387,6 +389,8 @@ spec: type: object scaleTargetKind: type: string + triggersTypes: + type: string type: object required: - spec diff --git a/controllers/keda/scaledjob_controller.go b/controllers/keda/scaledjob_controller.go index 4a145c7c024..0f75d5eabe8 100755 --- a/controllers/keda/scaledjob_controller.go +++ b/controllers/keda/scaledjob_controller.go @@ -191,6 +191,11 @@ func (r *ScaledJobReconciler) reconcileScaledJob(ctx context.Context, logger log return "ScaledJob doesn't have correct triggers specification", err } + err = r.updateStatusWithTriggersAndAuthsTypes(ctx, logger, scaledJob) + if err != nil { + return "Cannot update ScaledJob status with triggers'names and authentications'names", err + } + // nosemgrep: trailofbits.go.invalid-usage-of-modified-variable.invalid-usage-of-modified-variable msg, err := r.deletePreviousVersionScaleJobs(ctx, logger, scaledJob) if err != nil { @@ -404,3 +409,14 @@ func (r *ScaledJobReconciler) updateTriggerAuthenticationStatusOnDelete(ctx cont return triggerAuthenticationStatus }) } + +func (r *ScaledJobReconciler) updateStatusWithTriggersAndAuthsTypes(ctx context.Context, logger logr.Logger, scaledJob *kedav1alpha1.ScaledJob) error { + triggersTypes, authsTypes := kedav1alpha1.CombinedTriggersAndAuthenticationsTypes(scaledJob.Spec.Triggers) + status := scaledJob.Status.DeepCopy() + status.TriggersTypes = &triggersTypes + status.AuthenticationsTypes = &authsTypes + + logger.V(1).Info("Updating ScaledJob status with triggers and authentications types", "triggersTypes", triggersTypes, "authenticationsTypes", authsTypes) + + return kedastatus.UpdateScaledJobStatus(ctx, r.Client, logger, scaledJob, status) +} diff --git a/controllers/keda/scaledobject_controller.go b/controllers/keda/scaledobject_controller.go index b18c84ae61d..b25b50b46c3 100755 --- a/controllers/keda/scaledobject_controller.go +++ b/controllers/keda/scaledobject_controller.go @@ -282,6 +282,11 @@ func (r *ScaledObjectReconciler) reconcileScaledObject(ctx context.Context, logg return "ScaledObject doesn't have correct triggers specification", err } + err = r.updateStatusWithTriggersAndAuthsTypes(ctx, logger, scaledObject) + if err != nil { + return "Cannot update ScaledObject status with triggers'types and authentications'types", err + } + // Create a new HPA or update existing one according to ScaledObject newHPACreated, err := r.ensureHPAForScaledObjectExists(ctx, logger, scaledObject, &gvkr) if err != nil { @@ -621,3 +626,14 @@ func (r *ScaledObjectReconciler) updateTriggerAuthenticationStatusOnDelete(ctx c return triggerAuthenticationStatus }) } + +func (r *ScaledObjectReconciler) updateStatusWithTriggersAndAuthsTypes(ctx context.Context, logger logr.Logger, scaledObject *kedav1alpha1.ScaledObject) error { + triggersTypes, authsTypes := kedav1alpha1.CombinedTriggersAndAuthenticationsTypes(scaledObject.Spec.Triggers) + status := scaledObject.Status.DeepCopy() + status.TriggersTypes = &triggersTypes + status.AuthenticationsTypes = &authsTypes + + logger.V(1).Info("Updating ScaledObject status with triggers and authentications types", "triggersTypes", triggersTypes, "authenticationsTypes", authsTypes) + + return kedastatus.UpdateScaledObjectStatus(ctx, r.Client, logger, scaledObject, status) +} diff --git a/pkg/status/status.go b/pkg/status/status.go index 8c42190d5f7..bbd1c00dbaa 100755 --- a/pkg/status/status.go +++ b/pkg/status/status.go @@ -55,19 +55,35 @@ func SetStatusConditions(ctx context.Context, client runtimeclient.StatusClient, // UpdateScaledObjectStatus patches the given ScaledObject with the updated status passed to it or returns an error. func UpdateScaledObjectStatus(ctx context.Context, client runtimeclient.StatusClient, logger logr.Logger, scaledObject *kedav1alpha1.ScaledObject, status *kedav1alpha1.ScaledObjectStatus) error { + return updateObjectStatus(ctx, client, logger, scaledObject, status) +} + +// UpdateScaledJobStatus patches the given ScaledObject with the updated status passed to it or returns an error. +func UpdateScaledJobStatus(ctx context.Context, client runtimeclient.StatusClient, logger logr.Logger, scaledJob *kedav1alpha1.ScaledJob, status *kedav1alpha1.ScaledJobStatus) error { + return updateObjectStatus(ctx, client, logger, scaledJob, status) +} + +// updateObjectStatus patches the given ScaledObject with the updated status passed to it or returns an error. +func updateObjectStatus(ctx context.Context, client runtimeclient.StatusClient, logger logr.Logger, object interface{}, status interface{}) error { transform := func(runtimeObj runtimeclient.Object, target interface{}) error { - status, ok := target.(*kedav1alpha1.ScaledObjectStatus) - if !ok { - return fmt.Errorf("transform target is not kedav1alpha1.ScaledObjectStatus type %v", target) - } switch obj := runtimeObj.(type) { case *kedav1alpha1.ScaledObject: + status, ok := target.(*kedav1alpha1.ScaledObjectStatus) + if !ok { + return fmt.Errorf("transform target is not kedav1alpha1.ScaledObjectStatus type %v", target) + } + obj.Status = *status + case *kedav1alpha1.ScaledJob: + status, ok := target.(*kedav1alpha1.ScaledJobStatus) + if !ok { + return fmt.Errorf("transform target is not kedav1alpha1.ScaledJobStatus type %v", target) + } obj.Status = *status default: } return nil } - return TransformObject(ctx, client, logger, scaledObject, status, transform) + return TransformObject(ctx, client, logger, object, status, transform) } // getTriggerAuth returns TriggerAuthentication/ClusterTriggerAuthentication object and its status from AuthenticationRef or returns an error. diff --git a/tests/internals/status_update/status_update_test.go b/tests/internals/status_update/status_update_test.go new file mode 100644 index 00000000000..31e0d659b96 --- /dev/null +++ b/tests/internals/status_update/status_update_test.go @@ -0,0 +1,277 @@ +//go:build e2e +// +build e2e + +package status_update_test + +import ( + "fmt" + "testing" + + . "github.com/kedacore/keda/v2/tests/helper" +) + +const ( + testName = "status-update-test" +) + +var ( + testNamespace = fmt.Sprintf("%s-ns", testName) + deploymentName = fmt.Sprintf("%s-deployment", testName) + metricsServerDeploymentName = fmt.Sprintf("%s-metrics-server", testName) + servciceName = fmt.Sprintf("%s-service", testName) + triggerAuthName = fmt.Sprintf("%s-ta", testName) + scaledObjectName = fmt.Sprintf("%s-so", testName) + scaledJobName = fmt.Sprintf("%s-sj", testName) + secretName = fmt.Sprintf("%s-secret", testName) + metricsServerEndpoint = fmt.Sprintf("http://%s.%s.svc.cluster.local:8080/api/value", servciceName, testNamespace) + minReplicaCount = 0 + maxReplicaCount = 2 +) + +type templateData struct { + TestNamespace string + DeploymentName string + MetricsServerDeploymentName string + MetricsServerEndpoint string + ServciceName string + ScaledObjectName string + ScaledJobName string + TriggerAuthName string + SecretName string + MetricValue int + MinReplicaCount string + MaxReplicaCount string +} + +const ( + secretTemplate = `apiVersion: v1 +kind: Secret +metadata: + name: {{.SecretName}} + namespace: {{.TestNamespace}} +data: + AUTH_PASSWORD: U0VDUkVUCg== + AUTH_USERNAME: VVNFUgo= +` + + triggerAuthenticationTemplate = `apiVersion: keda.sh/v1alpha1 +kind: TriggerAuthentication +metadata: + name: {{.TriggerAuthName}} + namespace: {{.TestNamespace}} +spec: + secretTargetRef: + - parameter: username + name: {{.SecretName}} + key: AUTH_USERNAME + - parameter: password + name: {{.SecretName}} + key: AUTH_PASSWORD +` + + metricsServerdeploymentTemplate = ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{.MetricsServerDeploymentName}} + namespace: {{.TestNamespace}} + labels: + app: {{.MetricsServerDeploymentName}} +spec: + replicas: 1 + selector: + matchLabels: + app: {{.MetricsServerDeploymentName}} + template: + metadata: + labels: + app: {{.MetricsServerDeploymentName}} + spec: + containers: + - name: metrics + image: ghcr.io/kedacore/tests-metrics-api + ports: + - containerPort: 8080 + envFrom: + - secretRef: + name: {{.SecretName}} + imagePullPolicy: Always +` + + serviceTemplate = ` +apiVersion: v1 +kind: Service +metadata: + name: {{.ServciceName}} + namespace: {{.TestNamespace}} +spec: + selector: + app: {{.MetricsServerDeploymentName}} + ports: + - port: 8080 + targetPort: 8080 +` + + deploymentTemplate = ` +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: {{.DeploymentName}} + name: {{.DeploymentName}} + namespace: {{.TestNamespace}} +spec: + selector: + matchLabels: + app: {{.DeploymentName}} + replicas: 0 + template: + metadata: + labels: + app: {{.DeploymentName}} + spec: + containers: + - name: nginx + image: nginxinc/nginx-unprivileged + ports: + - containerPort: 80 +` + + scaledObjectTemplate = ` +apiVersion: keda.sh/v1alpha1 +kind: ScaledObject +metadata: + name: {{.ScaledObjectName}} + namespace: {{.TestNamespace}} + labels: + app: {{.DeploymentName}} +spec: + scaleTargetRef: + name: {{.DeploymentName}} + minReplicaCount: {{.MinReplicaCount}} + maxReplicaCount: {{.MaxReplicaCount}} + cooldownPeriod: 1 + triggers: + - type: metrics-api + metadata: + targetValue: "5" + activationTargetValue: "20" + url: "{{.MetricsServerEndpoint}}" + valueLocation: 'value' + authMode: "basic" + method: "query" + authenticationRef: + name: {{.TriggerAuthName}} + - type: corn + metadata: + timezone: Asia/Kolkata + start: 0 6 * * * + end: 0 8 * * * + desiredReplicas: "9" + - type: corn + metadata: + timezone: Asia/Kolkata + start: 0 22 * * * + end: 0 23 * * * + desiredReplicas: "9"` + + scaledJobTemplate = ` + apiVersion: keda.sh/v1alpha1 + kind: ScaledJob + metadata: + name: {{.ScaledJobName}} + namespace: {{.TestNamespace}} + spec: + jobTargetRef: + template: + spec: + containers: + - name: external-executor + image: busybox + command: + - sleep + - "30" + imagePullPolicy: IfNotPresent + restartPolicy: Never + backoffLimit: 1 + pollingInterval: 5 + minReplicaCount: {{.MinReplicaCount}} + maxReplicaCount: {{.MaxReplicaCount}} + successfulJobsHistoryLimit: 0 + failedJobsHistoryLimit: 0 + triggers: + - type: metrics-api + metadata: + targetValue: "5" + activationTargetValue: "20" + url: "{{.MetricsServerEndpoint}}" + valueLocation: 'value' + authMode: "basic" + method: "query" + authenticationRef: + name: {{.TriggerAuthName}} + - type: corn + metadata: + timezone: Asia/Kolkata + start: 0 6 * * * + end: 0 8 * * * + desiredReplicas: "9" + - type: corn + metadata: + timezone: Asia/Kolkata + start: 0 22 * * * + end: 0 23 * * * + desiredReplicas: "9"` +) + +func TestScaler(t *testing.T) { + // setup + t.Log("--- setting up ---") + // Create kubernetes resources + kc := GetKubernetesClient(t) + data, templates := getTemplateData() + + CreateKubernetesResources(t, kc, testNamespace, data, templates) + + // test + testTriggersAndAuthenticationsTypes(t) + + // cleanup + DeleteKubernetesResources(t, testNamespace, data, templates) +} + +func testTriggersAndAuthenticationsTypes(t *testing.T) { + otherparameter := `-o jsonpath="{.status.triggersTypes}"` + CheckKubectlGetResult(t, "ScaledObject", scaledObjectName, testNamespace, otherparameter, "metrics-api,corn") + otherparameter = `-o jsonpath="{.status.authenticationsTypes}"` + CheckKubectlGetResult(t, "ScaledObject", scaledObjectName, testNamespace, otherparameter, triggerAuthName) + otherparameter = `-o jsonpath="{.status.triggersTypes}"` + CheckKubectlGetResult(t, "ScaledJob", scaledJobName, testNamespace, otherparameter, "metrics-api,corn") + otherparameter = `-o jsonpath="{.status.authenticationsTypes}"` + CheckKubectlGetResult(t, "ScaledJob", scaledJobName, testNamespace, otherparameter, triggerAuthName) +} + +func getTemplateData() (templateData, []Template) { + return templateData{ + TestNamespace: testNamespace, + DeploymentName: deploymentName, + MetricsServerDeploymentName: metricsServerDeploymentName, + ServciceName: servciceName, + TriggerAuthName: triggerAuthName, + ScaledObjectName: scaledObjectName, + ScaledJobName: scaledJobName, + SecretName: secretName, + MetricsServerEndpoint: metricsServerEndpoint, + MinReplicaCount: fmt.Sprintf("%v", minReplicaCount), + MaxReplicaCount: fmt.Sprintf("%v", maxReplicaCount), + MetricValue: 0, + }, []Template{ + {Name: "secretTemplate", Config: secretTemplate}, + {Name: "metricsServerdeploymentTemplate", Config: metricsServerdeploymentTemplate}, + {Name: "serviceTemplate", Config: serviceTemplate}, + {Name: "triggerAuthenticationTemplate", Config: triggerAuthenticationTemplate}, + {Name: "deploymentTemplate", Config: deploymentTemplate}, + {Name: "scaledObjectTemplate", Config: scaledObjectTemplate}, + {Name: "scaledJobTemplate", Config: scaledJobTemplate}, + } +}