Repository: kubevela/kubevela

Issue:
Duplicated Statuses Across Components
**Describe the bug**
The new status update code (in master) has an issue where statuses are being copied between components. The issue is intermittent but frequent - affecting any app with >1 component. 

**To Reproduce**
Deploy an app with 2+ components.
`vela status <app>` will intermittently display the same status information across multiple components - most notable if you include unique IDs, or use different components. 

**Expected behavior**
In my test app (using an internal test component) I would expect to see:
```
Services:

  - Name: kubevela-metrics-dashboard  
    Cluster: local
    Namespace: default
    Type: datadog-dashboard
    Health: ✅ 
      Message: Synced! ID: xxx-xxx-xxx
      Status Details: 
        id: xxx-xxx-xxx
        url: https://app.datadoghq.com/dashboard/xxx-xxx-xxx
    No trait applied

  - Name: app-health-monitor  
    Cluster: local
    Namespace: default
    Type: datadog-monitor
    Health: ✅ 
      Message: Synced! ID: 12345678
      Status Details: 
        id: 12345678
        url: https://app.datadoghq.com/monitors/12345678
    No trait applied
```

but instead I will often get:
```
Services:

  - Name: kubevela-metrics-dashboard  
    Cluster: local
    Namespace: default
    Type: datadog-dashboard
    Health: ✅ 
      Message: Synced! ID: xxx-xxx-xxx
      Status Details: 
        url: https://app.datadoghq.com/dashboard/xxx-xxx-xxx
        id: xxx-xxx-xxx
    No trait applied

  - Name: app-health-monitor  
    Cluster: local
    Namespace: default
    Type: datadog-monitor
    Health: ✅ 
      Message: Synced! ID: xxx-xxx-xxx
      Status Details: 
        id: xxx-xxx-xxx
        url: https://app.datadoghq.com/dashboard/xxx-xxx-xxx
    No trait applied
```

Note the `/dashboard` url in the monitor component status.

**KubeVela Version**
master (unreleased)

**Cluster information**
N/A

**Additional context**
<!--
Add any other context about the problem here.
-->


Discussion:
@briankane: Have a fix - PR going up shortly@briankane: Fixed in https://github.com/kubevela/kubevela/pull/6886

Solution patch:
diff --git a/pkg/controller/core.oam.dev/v1beta1/application/application_controller.go b/pkg/controller/core.oam.dev/v1beta1/application/application_controller.go
index 938905e2..697ebd99 100644
--- a/pkg/controller/core.oam.dev/v1beta1/application/application_controller.go
+++ b/pkg/controller/core.oam.dev/v1beta1/application/application_controller.go
@@ -64,6 +64,7 @@ import (
 	"github.com/oam-dev/kubevela/pkg/resourcekeeper"
 	"github.com/oam-dev/kubevela/pkg/resourcetracker"
 	"github.com/oam-dev/kubevela/pkg/workflow"
+	oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/types"
 	"github.com/oam-dev/kubevela/version"
 )
 
@@ -698,21 +699,34 @@ func setVelaVersion(app *v1beta1.Application) {
 func evalStatus(ctx monitorContext.Context, handler *AppHandler, appFile *appfile.Appfile, appParser *appfile.Parser) bool {
 	healthCheck := handler.checkComponentHealth(appParser, appFile)
 	if !hasHealthCheckPolicy(appFile.ParsedPolicies) {
-		for idx, svc := range handler.services {
-			for _, component := range handler.app.Spec.Components {
-				_, status, _, _, err := healthCheck(ctx, component, nil, svc.Cluster, svc.Namespace)
-				if err != nil {
-					ctx.Error(err, "Failed to collect health status")
-				} else if status != nil {
-					handler.services[idx].Healthy = status.Healthy
-					handler.services[idx].Message = status.Message
-					handler.services[idx].Details = status.Details
-					handler.services[idx].Traits = status.Traits
-				}
-			}
+		// Build component map once for efficient lookup
+		componentMap := make(map[string]common.ApplicationComponent, len(handler.app.Spec.Components))
+		for _, component := range handler.app.Spec.Components {
+			componentMap[component.Name] = component
 		}
+
+		applyComponentHealthToServices(ctx, handler, componentMap, healthCheck)
 		handler.app.Status.Services = handler.services
 		return isHealthy(handler.services)
 	}
 	return true
 }
+
+// applyComponentHealthToServices updates each service's health status by matching it to its corresponding component.
+// Components are matched to services by name using the provided map for O(1) lookup performance.
+func applyComponentHealthToServices(ctx monitorContext.Context, handler *AppHandler, componentMap map[string]common.ApplicationComponent, healthCheck oamprovidertypes.ComponentHealthCheck) {
+	// Iterate services and lookup matching component from the map
+	for idx, svc := range handler.services {
+		if component, exists := componentMap[svc.Name]; exists {
+			_, status, _, _, err := healthCheck(ctx, component, nil, svc.Cluster, svc.Namespace)
+			if err != nil {
+				ctx.Error(err, "Failed to collect health status")
+			} else if status != nil {
+				handler.services[idx].Healthy = status.Healthy
+				handler.services[idx].Message = status.Message
+				handler.services[idx].Details = status.Details
+				handler.services[idx].Traits = status.Traits
+			}
+		}
+	}
+}



Test patch:
diff --git a/pkg/controller/core.oam.dev/v1beta1/application/evalstatus_test.go b/pkg/controller/core.oam.dev/v1beta1/application/evalstatus_test.go
new file mode 100644
index 00000000..9d0c7b03
--- /dev/null
+++ b/pkg/controller/core.oam.dev/v1beta1/application/evalstatus_test.go
@@ -0,0 +1,183 @@
+/*
+Copyright 2024 The KubeVela Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package application
+
+import (
+	"context"
+	"fmt"
+	"testing"
+
+	"cuelang.org/go/cue"
+	"github.com/stretchr/testify/assert"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime"
+
+	monitorContext "github.com/kubevela/pkg/monitor/context"
+
+	"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
+	"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
+)
+
+func Test_applyComponentHealthToServices(t *testing.T) {
+	tests := []struct {
+		name            string
+		components      []common.ApplicationComponent
+		services        []common.ApplicationComponentStatus
+		healthCheckFunc func(string) *common.ApplicationComponentStatus
+		verifyFunc      func(*testing.T, []common.ApplicationComponentStatus)
+	}{
+		{
+			name: "each service gets its matching component's health status",
+			components: []common.ApplicationComponent{
+				{Name: "frontend", Type: "webservice", Properties: &runtime.RawExtension{Raw: []byte(`{}`)}},
+				{Name: "backend", Type: "webservice", Properties: &runtime.RawExtension{Raw: []byte(`{}`)}},
+				{Name: "database", Type: "webservice", Properties: &runtime.RawExtension{Raw: []byte(`{}`)}},
+			},
+			services: []common.ApplicationComponentStatus{
+				{Name: "frontend", Namespace: "default", Cluster: "local"},
+				{Name: "backend", Namespace: "default", Cluster: "local"},
+				{Name: "database", Namespace: "default", Cluster: "local"},
+			},
+			healthCheckFunc: func(name string) *common.ApplicationComponentStatus {
+				switch name {
+				case "frontend":
+					return &common.ApplicationComponentStatus{
+						Healthy: true,
+						Message: "frontend is healthy",
+					}
+				case "backend":
+					return &common.ApplicationComponentStatus{
+						Healthy: false,
+						Message: "backend is unhealthy",
+					}
+				case "database":
+					return &common.ApplicationComponentStatus{
+						Healthy: true,
+						Message: "database is healthy",
+					}
+				default:
+					return nil
+				}
+			},
+			verifyFunc: func(t *testing.T, services []common.ApplicationComponentStatus) {
+				assert.True(t, services[0].Healthy, "frontend service should be healthy")
+				assert.Equal(t, "frontend is healthy", services[0].Message)
+				assert.False(t, services[1].Healthy, "backend service should be unhealthy")
+				assert.Equal(t, "backend is unhealthy", services[1].Message)
+				assert.True(t, services[2].Healthy, "database service should be healthy")
+				assert.Equal(t, "database is healthy", services[2].Message)
+			},
+		},
+		{
+			name: "unmatched services remain unchanged",
+			components: []common.ApplicationComponent{
+				{Name: "app", Type: "webservice", Properties: &runtime.RawExtension{Raw: []byte(`{}`)}},
+			},
+			services: []common.ApplicationComponentStatus{
+				{Name: "app", Namespace: "default", Cluster: "local"},
+				{Name: "orphan", Namespace: "default", Cluster: "local", Healthy: true, Message: "pre-existing"},
+			},
+			healthCheckFunc: func(name string) *common.ApplicationComponentStatus {
+				if name == "app" {
+					return &common.ApplicationComponentStatus{
+						Healthy: false,
+						Message: "app checked",
+					}
+				}
+				return nil
+			},
+			verifyFunc: func(t *testing.T, services []common.ApplicationComponentStatus) {
+				assert.False(t, services[0].Healthy, "app service should be unhealthy")
+				assert.Equal(t, "app checked", services[0].Message)
+				assert.True(t, services[1].Healthy, "orphan service should remain healthy")
+				assert.Equal(t, "pre-existing", services[1].Message)
+			},
+		},
+		{
+			name: "performance test - 100 components and services",
+			components: func() []common.ApplicationComponent {
+				comps := make([]common.ApplicationComponent, 100)
+				for i := 0; i < 100; i++ {
+					comps[i] = common.ApplicationComponent{
+						Name:       fmt.Sprintf("comp-%d", i),
+						Type:       "webservice",
+						Properties: &runtime.RawExtension{Raw: []byte(`{}`)},
+					}
+				}
+				return comps
+			}(),
+			services: func() []common.ApplicationComponentStatus {
+				svcs := make([]common.ApplicationComponentStatus, 100)
+				for i := 0; i < 100; i++ {
+					svcs[i] = common.ApplicationComponentStatus{
+						Name:      fmt.Sprintf("comp-%d", i),
+						Namespace: "default",
+						Cluster:   "local",
+					}
+				}
+				return svcs
+			}(),
+			healthCheckFunc: func(name string) *common.ApplicationComponentStatus {
+				return &common.ApplicationComponentStatus{
+					Healthy: true,
+					Message: name + " is healthy",
+				}
+			},
+			verifyFunc: func(t *testing.T, services []common.ApplicationComponentStatus) {
+				for i, svc := range services {
+					expectedMsg := fmt.Sprintf("comp-%d is healthy", i)
+					assert.True(t, svc.Healthy, "Service %d should be healthy", i)
+					assert.Equal(t, expectedMsg, svc.Message, "Service %d message", i)
+				}
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ctx := monitorContext.NewTraceContext(context.Background(), "test")
+
+			app := &v1beta1.Application{
+				Spec: v1beta1.ApplicationSpec{
+					Components: tt.components,
+				},
+			}
+
+			handler := &AppHandler{
+				app:      app,
+				services: make([]common.ApplicationComponentStatus, len(tt.services)),
+			}
+			copy(handler.services, tt.services)
+
+			componentMap := make(map[string]common.ApplicationComponent, len(tt.components))
+			for _, component := range tt.components {
+				componentMap[component.Name] = component
+			}
+
+			mockHealthCheck := func(ctx context.Context, comp common.ApplicationComponent, patcher *cue.Value, clusterName string, overrideNamespace string) (bool, *common.ApplicationComponentStatus, *unstructured.Unstructured, []*unstructured.Unstructured, error) {
+				status := tt.healthCheckFunc(comp.Name)
+				return false, status, nil, nil, nil
+			}
+
+			applyComponentHealthToServices(ctx, handler, componentMap, mockHealthCheck)
+
+			if tt.verifyFunc != nil {
+				tt.verifyFunc(t, handler.services)
+			}
+		})
+	}
+}