Set recommended kubernetes labels on services and workflows (#482)

Motivation:
There's a need to select efficiently workflows and their respective
services. Having the common kubernetes labels allow a single selector:

   podSelector:
     matchExpressions:
       - { key: app.kubernetes.io/component, operator: In, values:
         ["data-index-service", "jobs-service", "serverless-workflow"] }

Modification:
Make the v1.Deployment for services and the deployment or knative
services to contain at common labels

Result:
A workflow deployment or knative serving labels:
    app.kubernetes.io/name: ${workflow name}
    app.kubernetes.io/component: serverless-workflow
    app.kubernetes.io/part-of: ${platform url set by status}
    app.kubernetes.io/managed-by: sonataflow-operator

Data index or Jobs services Deployment.v1 labels:

    app.kubernetes.io/name: ${service name}
    app.kubernetes.io/component: data-index-service|jobs-service
    app.kubernetes.io/part-of: ${platform name}
    app.kubernetes.io/managed-by: sonataflow-operator

Reference: https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/#labels

Signed-off-by: Roy Golan <rgolan@redhat.com>
diff --git a/Makefile b/Makefile
index 81eb505..f88a875 100644
--- a/Makefile
+++ b/Makefile
@@ -354,4 +354,4 @@
 
 .PHONY: delete-cluster
 delete-cluster: install-kind
-	kind delete cluster && docker rm -f kind-registry
\ No newline at end of file
+	kind delete cluster && docker rm -f kind-registry
diff --git a/bundle/manifests/sonataflow-operator.clusterserviceversion.yaml b/bundle/manifests/sonataflow-operator.clusterserviceversion.yaml
index 37ca5da..e581164 100644
--- a/bundle/manifests/sonataflow-operator.clusterserviceversion.yaml
+++ b/bundle/manifests/sonataflow-operator.clusterserviceversion.yaml
@@ -733,6 +733,7 @@
         serviceAccountName: sonataflow-operator-controller-manager
       deployments:
       - label:
+          app.kubernetes.io/name: sonataflow-operator
           control-plane: sonataflow-operator
         name: sonataflow-operator-controller-manager
         spec:
diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml
index 3a5d651..eb34274 100644
--- a/config/manager/manager.yaml
+++ b/config/manager/manager.yaml
@@ -12,6 +12,7 @@
   namespace: system
   labels:
     control-plane: sonataflow-operator
+    app.kubernetes.io/name: sonataflow-operator
 spec:
   selector:
     matchLabels:
diff --git a/controllers/platform/k8s.go b/controllers/platform/k8s.go
index 62d8b36..cbb6028 100644
--- a/controllers/platform/k8s.go
+++ b/controllers/platform/k8s.go
@@ -233,8 +233,11 @@
 
 func getLabels(platform *operatorapi.SonataFlowPlatform, psh services.PlatformServiceHandler) (map[string]string, map[string]string) {
 	lbl := map[string]string{
-		workflowproj.LabelApp:     platform.Name,
-		workflowproj.LabelService: psh.GetServiceName(),
+		workflowproj.LabelService:      psh.GetServiceName(),
+		workflowproj.LabelK8SName:      psh.GetContainerName(),
+		workflowproj.LabelK8SComponent: psh.GetServiceName(),
+		workflowproj.LabelK8SPartOF:    platform.Name,
+		workflowproj.LabelK8SManagedBy: "sonataflow-operator",
 	}
 	selectorLbl := map[string]string{
 		workflowproj.LabelService: psh.GetServiceName(),
diff --git a/controllers/profiles/common/mutate_visitors.go b/controllers/profiles/common/mutate_visitors.go
index 426154e..0978438 100644
--- a/controllers/profiles/common/mutate_visitors.go
+++ b/controllers/profiles/common/mutate_visitors.go
@@ -21,12 +21,17 @@
 
 import (
 	"context"
+	"maps"
+	"reflect"
+	"slices"
 
 	"github.com/apache/incubator-kie-kogito-serverless-operator/controllers/discovery"
 	"github.com/apache/incubator-kie-kogito-serverless-operator/controllers/profiles/common/properties"
 	"github.com/imdario/mergo"
 	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	servingv1 "knative.dev/serving/pkg/apis/serving/v1"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
@@ -86,7 +91,28 @@
 			if err != nil {
 				return err
 			}
-			return EnsureDeployment(original.(*appsv1.Deployment), object.(*appsv1.Deployment))
+			src := original.(*appsv1.Deployment)
+			dst := object.(*appsv1.Deployment)
+			// merge new and old labels, but prevent overriding to keep exiting immutable selector working.
+			mergo.Merge(&dst.ObjectMeta.Labels, src.ObjectMeta.Labels, mergo.WithAppendSlice)
+			// to prevent furhter merge conflcts set the same lables on both src and dst
+			src.ObjectMeta.Labels = dst.ObjectMeta.Labels
+			if !maps.Equal(dst.Spec.Selector.MatchLabels, src.Spec.Selector.MatchLabels) {
+				// mutating selector labels is not supported so to prevent merge conflicts we set src and dst
+				// values to be identical
+				src.Spec.Selector.MatchLabels = dst.Spec.Selector.MatchLabels
+			}
+			if !slices.EqualFunc(
+				dst.Spec.Selector.MatchExpressions,
+				src.Spec.Selector.MatchExpressions,
+				func(lsr1, lsr2 metav1.LabelSelectorRequirement) bool {
+					return reflect.DeepEqual(lsr1, lsr2)
+				}) {
+				// mutating selector matchExpressions is not supported so to prevent merge conflicts we set src and dst
+				// values to be identical
+				src.Spec.Selector.MatchExpressions = dst.Spec.Selector.MatchExpressions
+			}
+			return EnsureDeployment(src, dst)
 		}
 	}
 }
diff --git a/controllers/profiles/common/object_creators.go b/controllers/profiles/common/object_creators.go
index c1ac670..8b72a23 100644
--- a/controllers/profiles/common/object_creators.go
+++ b/controllers/profiles/common/object_creators.go
@@ -86,7 +86,7 @@
 		Spec: appsv1.DeploymentSpec{
 			Replicas: getReplicasOrDefault(workflow),
 			Selector: &metav1.LabelSelector{
-				MatchLabels: lbl,
+				MatchLabels: workflowproj.GetSelectorLabels(workflow),
 			},
 			Template: corev1.PodTemplateSpec{
 				ObjectMeta: metav1.ObjectMeta{
diff --git a/controllers/profiles/common/object_creators_test.go b/controllers/profiles/common/object_creators_test.go
index 5057b21..074547e 100644
--- a/controllers/profiles/common/object_creators_test.go
+++ b/controllers/profiles/common/object_creators_test.go
@@ -39,6 +39,8 @@
 	"github.com/apache/incubator-kie-kogito-serverless-operator/workflowproj"
 )
 
+const platformName = "test-platform"
+
 func Test_ensureWorkflowPropertiesConfigMapMutator(t *testing.T) {
 	workflow := test.GetBaseSonataFlowWithDevProfile(t.Name())
 	platform := test.GetBasePlatform()
@@ -192,7 +194,12 @@
 	assert.NotEmpty(t, reflectSinkBinding.Spec.Sink)
 	assert.Equal(t, reflectSinkBinding.Spec.Sink.Ref.Kind, "Broker")
 	assert.NotNil(t, reflectSinkBinding.GetLabels())
-	assert.Equal(t, reflectSinkBinding.ObjectMeta.Labels, map[string]string{"app": "vet", "sonataflow.org/workflow-app": "vet"})
+	assert.Equal(t, reflectSinkBinding.ObjectMeta.Labels, map[string]string{
+		"sonataflow.org/workflow-app":  "vet",
+		"app.kubernetes.io/name":       "vet",
+		"app.kubernetes.io/component":  "serverless-workflow",
+		"app.kubernetes.io/managed-by": "sonataflow-operator",
+	})
 }
 
 func Test_ensureWorkflowTriggersAreCreated(t *testing.T) {
@@ -206,7 +213,12 @@
 	for _, trigger := range triggers {
 		assert.Contains(t, []string{"vet-vetappointmentrequestreceived-trigger", "vet-vetappointmentinfo-trigger"}, trigger.GetName())
 		assert.NotNil(t, trigger.GetLabels())
-		assert.Equal(t, trigger.GetLabels(), map[string]string{"app": "vet", "sonataflow.org/workflow-app": "vet"})
+		assert.Equal(t, trigger.GetLabels(), map[string]string{
+			"sonataflow.org/workflow-app":  "vet",
+			"app.kubernetes.io/name":       "vet",
+			"app.kubernetes.io/component":  "serverless-workflow",
+			"app.kubernetes.io/managed-by": "sonataflow-operator",
+		})
 	}
 }
 
diff --git a/controllers/profiles/dev/object_creators_dev_test.go b/controllers/profiles/dev/object_creators_dev_test.go
index 25fdc8e..e9daec3 100644
--- a/controllers/profiles/dev/object_creators_dev_test.go
+++ b/controllers/profiles/dev/object_creators_dev_test.go
@@ -43,5 +43,11 @@
 	assert.Equal(t, reflectService.Spec.Type, v1.ServiceTypeNodePort)
 	assert.NotNil(t, reflectService.ObjectMeta)
 	assert.NotNil(t, reflectService.ObjectMeta.Labels)
-	assert.Equal(t, reflectService.ObjectMeta.Labels, map[string]string{"test": "test", "app": "greeting", "sonataflow.org/workflow-app": "greeting"})
+	assert.Equal(t, reflectService.ObjectMeta.Labels, map[string]string{
+		"test":                         "test",
+		"sonataflow.org/workflow-app":  "greeting",
+		"app.kubernetes.io/name":       "greeting",
+		"app.kubernetes.io/component":  "serverless-workflow",
+		"app.kubernetes.io/managed-by": "sonataflow-operator",
+	})
 }
diff --git a/controllers/profiles/dev/status_enricher_dev.go b/controllers/profiles/dev/status_enricher_dev.go
index 1ecedaf..abab339 100644
--- a/controllers/profiles/dev/status_enricher_dev.go
+++ b/controllers/profiles/dev/status_enricher_dev.go
@@ -55,7 +55,7 @@
 			podList := &v1.PodList{}
 			opts := []client.ListOption{
 				client.InNamespace(workflow.Namespace),
-				client.MatchingLabels{workflowproj.LabelApp: labels[workflowproj.LabelApp]},
+				client.MatchingLabels{workflowproj.LabelK8SName: labels[workflowproj.LabelK8SName]},
 			}
 			err := c.List(ctx, podList, opts...)
 			if err != nil {
diff --git a/controllers/profiles/gitops/profile_gitops_test.go b/controllers/profiles/gitops/profile_gitops_test.go
index 051ea57..0a600ca 100644
--- a/controllers/profiles/gitops/profile_gitops_test.go
+++ b/controllers/profiles/gitops/profile_gitops_test.go
@@ -64,5 +64,12 @@
 
 	assert.NotNil(t, deployment.ObjectMeta)
 	assert.NotNil(t, deployment.ObjectMeta.Labels)
-	assert.Equal(t, deployment.ObjectMeta.Labels, map[string]string{"test": "test", "app": "simple", "sonataflow.org/workflow-app": "simple"})
+	assert.Equal(t, deployment.ObjectMeta.Labels, map[string]string{
+		"test":                         "test",
+		"sonataflow.org/workflow-app":  "simple",
+		"app.kubernetes.io/name":       "simple",
+		"app.kubernetes.io/component":  "serverless-workflow",
+		"app.kubernetes.io/managed-by": "sonataflow-operator",
+		"app.kubernetes.io/part-of":    "sonataflow-platform",
+	})
 }
diff --git a/controllers/profiles/preview/profile_preview_test.go b/controllers/profiles/preview/profile_preview_test.go
index 22146cc..ad1a1ed 100644
--- a/controllers/profiles/preview/profile_preview_test.go
+++ b/controllers/profiles/preview/profile_preview_test.go
@@ -63,7 +63,13 @@
 	assert.Len(t, deployment.Spec.Template.Spec.Containers[0].VolumeMounts, 1)
 	assert.NotNil(t, deployment.ObjectMeta)
 	assert.NotNil(t, deployment.ObjectMeta.Labels)
-	assert.Equal(t, deployment.ObjectMeta.Labels, map[string]string{"test": "test", "app": "greeting", "sonataflow.org/workflow-app": "greeting"})
+	assert.Equal(t, deployment.ObjectMeta.Labels, map[string]string{
+		"test":                         "test",
+		"sonataflow.org/workflow-app":  "greeting",
+		"app.kubernetes.io/name":       "greeting",
+		"app.kubernetes.io/component":  "serverless-workflow",
+		"app.kubernetes.io/managed-by": "sonataflow-operator",
+	})
 }
 
 func Test_reconcilerProdBuildConditions(t *testing.T) {
diff --git a/operator.yaml b/operator.yaml
index 325d30c..aed4104 100644
--- a/operator.yaml
+++ b/operator.yaml
@@ -27076,6 +27076,7 @@
 kind: Deployment
 metadata:
   labels:
+    app.kubernetes.io/name: sonataflow-operator
     control-plane: sonataflow-operator
   name: sonataflow-operator-controller-manager
   namespace: sonataflow-operator-system
diff --git a/test/e2e/platform_test.go b/test/e2e/platform_test.go
index c2c86d0..511503b 100644
--- a/test/e2e/platform_test.go
+++ b/test/e2e/platform_test.go
@@ -84,12 +84,12 @@
 			By("Wait for SonataFlowPlatform CR to complete deployment")
 			// wait for service deployments to be ready
 			EventuallyWithOffset(1, func() error {
-				cmd = exec.Command("kubectl", "wait", "pod", "-n", targetNamespace, "-l", "app=sonataflow-platform", "--for", "condition=Ready", "--timeout=5s")
+				cmd = exec.Command("kubectl", "wait", "pod", "-n", targetNamespace, "-l", "app.kubernetes.io/name in (jobs-service,data-index-service)", "--for", "condition=Ready", "--timeout=5s")
 				_, err = utils.Run(cmd)
 				return err
 			}, 20*time.Minute, 5).Should(Succeed())
 			By("Evaluate status of service's health endpoint")
-			cmd = exec.Command("kubectl", "get", "pod", "-l", "app=sonataflow-platform", "-n", targetNamespace, "-ojsonpath={.items[*].metadata.name}")
+			cmd = exec.Command("kubectl", "get", "pod", "-l", "app.kubernetes.io/name in (jobs-service,data-index-service)", "-n", targetNamespace, "-ojsonpath={.items[*].metadata.name}")
 			output, err := utils.Run(cmd)
 			Expect(err).NotTo(HaveOccurred())
 			// remove the last CR that is added by default as the last character of the string.
@@ -140,12 +140,12 @@
 		By("Wait for SonatatFlowPlatform CR to complete deployment")
 		// wait for service deployments to be ready
 		EventuallyWithOffset(1, func() error {
-			cmd = exec.Command("kubectl", "wait", "pod", "-n", targetNamespace, "-l", "app=sonataflow-platform", "--for", "condition=Ready", "--timeout=5s")
+			cmd = exec.Command("kubectl", "wait", "pod", "-n", targetNamespace, "-l", "app.kubernetes.io/name in (jobs-service,data-index-service)", "--for", "condition=Ready", "--timeout=5s")
 			_, err = utils.Run(cmd)
 			return err
 		}, 10*time.Minute, 5).Should(Succeed())
 		By("Evaluate status of all service's health endpoint")
-		cmd = exec.Command("kubectl", "get", "pod", "-l", "app=sonataflow-platform", "-n", targetNamespace, "-ojsonpath={.items[*].metadata.name}")
+		cmd = exec.Command("kubectl", "get", "pod", "-l", "app.kubernetes.io/name in (jobs-service,data-index-service)", "-n", targetNamespace, "-ojsonpath={.items[*].metadata.name}")
 		output, err := utils.Run(cmd)
 		Expect(err).NotTo(HaveOccurred())
 		for _, pn := range strings.Split(string(output), " ") {
diff --git a/test/testdata/sonataflow.org_v1alpha08_sonataflow-simpleops.yaml b/test/testdata/sonataflow.org_v1alpha08_sonataflow-simpleops.yaml
index 48711f5..6b47425 100644
--- a/test/testdata/sonataflow.org_v1alpha08_sonataflow-simpleops.yaml
+++ b/test/testdata/sonataflow.org_v1alpha08_sonataflow-simpleops.yaml
@@ -21,7 +21,6 @@
     sonataflow.org/version: 0.0.1
   labels:
     test: test
-    app: not-simple
 spec:
   podTemplate:
     container:
diff --git a/workflowproj/operator.go b/workflowproj/operator.go
index 2383f34..4c7bed2 100644
--- a/workflowproj/operator.go
+++ b/workflowproj/operator.go
@@ -35,12 +35,14 @@
 	// ApplicationPropertiesFileName is the default application properties file name holding user properties
 	ApplicationPropertiesFileName      = "application.properties"
 	workflowManagedConfigMapNameSuffix = "-managed-props"
-	// LabelApp key to use among object selectors, "app" is used among k8s applications to group objects in some UI consoles
-	LabelApp = "app"
 	// LabelService key to use among object selectors
 	LabelService = metadata.Domain + "/service"
 	// LabelWorkflow specialized label managed by the controller
-	LabelWorkflow = metadata.Domain + "/workflow-app"
+	LabelWorkflow     = metadata.Domain + "/workflow-app"
+	LabelK8SName      = "app.kubernetes.io/name"
+	LabelK8SComponent = "app.kubernetes.io/component"
+	LabelK8SPartOF    = "app.kubernetes.io/part-of"
+	LabelK8SManagedBy = "app.kubernetes.io/managed-by"
 )
 
 // SetTypeToObject sets the Kind and ApiVersion to a given object since the default constructor won't do it.
@@ -84,10 +86,22 @@
 
 // GetDefaultLabels gets the default labels based on the given workflow.
 func GetDefaultLabels(workflow *operatorapi.SonataFlow) map[string]string {
-	return map[string]string{
-		LabelApp:      workflow.Name,
-		LabelWorkflow: workflow.Name,
+	labels := map[string]string{
+		LabelWorkflow:     workflow.Name,
+		LabelK8SName:      workflow.Name,
+		LabelK8SComponent: "serverless-workflow",
+		LabelK8SManagedBy: "sonataflow-operator",
 	}
+	if workflow.Status.Platform != nil {
+		labels[LabelK8SPartOF] = workflow.Status.Platform.Name
+	}
+	return labels
+
+}
+func GetSelectorLabels(workflow *operatorapi.SonataFlow) map[string]string {
+	labels := GetDefaultLabels(workflow)
+	delete(labels, LabelK8SPartOF)
+	return labels
 }
 
 // SetMergedLabels adds the merged labels to the given object.
diff --git a/workflowproj/workflowproj_test.go b/workflowproj/workflowproj_test.go
index 2570802..81dc4bb 100644
--- a/workflowproj/workflowproj_test.go
+++ b/workflowproj/workflowproj_test.go
@@ -78,7 +78,12 @@
 	assert.NoError(t, err)
 	assert.NotNil(t, proj.Workflow)
 	assert.NotNil(t, proj.Workflow.ObjectMeta)
-	assert.Equal(t, proj.Workflow.ObjectMeta.Labels, map[string]string{"app": "hello", "sonataflow.org/workflow-app": "hello"})
+	assert.Equal(t, proj.Workflow.ObjectMeta.Labels, map[string]string{
+		"sonataflow.org/workflow-app":  "hello",
+		"app.kubernetes.io/name":       "hello",
+		"app.kubernetes.io/component":  "serverless-workflow",
+		"app.kubernetes.io/managed-by": "sonataflow-operator",
+	})
 	assert.NotNil(t, proj.Properties)
 	assert.NotEmpty(t, proj.Resources)
 	assert.Equal(t, "hello", proj.Workflow.Name)