This is an automated email from the ASF dual-hosted git repository.

robocanic pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/dubbo-admin.git


The following commit(s) were added to refs/heads/develop by this push:
     new d9ebf33a feat: unify and enhance the lifecycle of an instance (#1440)
d9ebf33a is described below

commit d9ebf33aedf967205f22c453882a612ab71cf46d
Author: LunaRain_079 <[email protected]>
AuthorDate: Sun Apr 12 19:22:42 2026 +0800

    feat: unify and enhance the lifecycle of an instance (#1440)
    
    * feat: add lifecycle state management and color coding for instance 
statuses
    
    * feat: enhance instance lifecycle management with state derivation and UI 
updates
    
    * feat: improve resource handling in informer with enhanced error reporting
    
    * feat: enhance instance lifecycle logging with detailed merge and delete 
events
    
    * feat: implement ResourceKeyProvider interface for consistent key 
generation in informers
    
    * refactor: refactor instance resource handling and key function resolution 
in informers
    
    * refactor: remove unused deployState and registerState columns from 
instance table
    
    * feat: enhance runtime instance retrieval with improved matching logic and 
fallback handling
    
    * feat: improve runtime instance identification with enhanced error logging 
and filtering
    
    * feat: add pod watch selector and RPC port identifiers for improved 
service configuration
    
    * feat: add refresh button and localization support for improved user 
interaction
    
    * fix: fix lint
    
    * feat: enhance runtime instance retrieval with improved fallback handling 
and logging for ambiguous matches
    
    * fix: enhance instance lifecycle and deployment state management with new 
types and improved data handling
---
 api/mesh/v1alpha1/runtime_instance_helper.go       |   6 +-
 pkg/console/model/application.go                   |  23 +--
 pkg/console/model/instance.go                      | 195 ++++++++++++++++-----
 pkg/console/service/application.go                 |   5 +-
 pkg/core/controller/informer.go                    |  42 +++--
 pkg/core/controller/listwatcher.go                 |   6 +
 pkg/core/discovery/subscriber/rpc_instance.go      |  61 ++++++-
 pkg/core/engine/component.go                       |  12 +-
 pkg/core/engine/subscriber/runtime_instance.go     | 139 ++++++++++++---
 .../resource/apis/mesh/v1alpha1/instance_helper.go |  55 ++++++
 .../kubernetes/listerwatcher/runtime_instance.go   | 105 ++++++++++-
 .../dubbo-samples-shop/dubbo-samples-shop-all.yaml |  22 +++
 release/kubernetes/dubbo-system/dubbo-admin.yaml   |  14 ++
 ui-vue3/src/base/constants.ts                      |  18 +-
 ui-vue3/src/base/i18n/en.ts                        |   2 +
 ui-vue3/src/base/i18n/zh.ts                        |   2 +
 ui-vue3/src/components/SearchTable.vue             |  11 +-
 .../views/resources/applications/tabs/instance.vue |  32 +++-
 ui-vue3/src/views/resources/instances/index.vue    |  43 +++--
 .../instances/slots/InstanceTabHeaderSlot.vue      |  39 ++++-
 .../src/views/resources/instances/tabs/detail.vue  |  18 +-
 21 files changed, 699 insertions(+), 151 deletions(-)

diff --git a/api/mesh/v1alpha1/runtime_instance_helper.go 
b/api/mesh/v1alpha1/runtime_instance_helper.go
index c69c002a..3bffb4cf 100644
--- a/api/mesh/v1alpha1/runtime_instance_helper.go
+++ b/api/mesh/v1alpha1/runtime_instance_helper.go
@@ -23,4 +23,8 @@ const (
        StartupProbe   = "startup"
 )
 
-const InstanceTerminating = "Terminating"
+const (
+       InstanceStarting    = "Starting"
+       InstanceCrashing    = "Crashing"
+       InstanceTerminating = "Terminating"
+)
diff --git a/pkg/console/model/application.go b/pkg/console/model/application.go
index d11943f6..ee70a038 100644
--- a/pkg/console/model/application.go
+++ b/pkg/console/model/application.go
@@ -131,17 +131,18 @@ func NewApplicationTabInstanceInfoReq() 
*ApplicationTabInstanceInfoReq {
 }
 
 type AppInstanceInfoResp struct {
-       AppName         string            `json:"appName"`
-       CreateTime      string            `json:"createTime"`
-       DeployState     string            `json:"deployState"`
-       DeployClusters  string            `json:"deployClusters"`
-       IP              string            `json:"ip"`
-       Labels          map[string]string `json:"labels"`
-       Name            string            `json:"name"`
-       RegisterCluster string            `json:"registerCluster"`
-       RegisterState   string            `json:"registerState"`
-       RegisterTime    string            `json:"registerTime"`
-       WorkloadName    string            `json:"workloadName"`
+       AppName         string                 `json:"appName"`
+       CreateTime      string                 `json:"createTime"`
+       LifecycleState  InstanceLifecycleState `json:"lifecycleState"`
+       DeployState     InstanceDeployState    `json:"deployState"`
+       DeployClusters  string                 `json:"deployClusters"`
+       IP              string                 `json:"ip"`
+       Labels          map[string]string      `json:"labels"`
+       Name            string                 `json:"name"`
+       RegisterCluster string                 `json:"registerCluster"`
+       RegisterState   InstanceRegisterState  `json:"registerState"`
+       RegisterTime    string                 `json:"registerTime"`
+       WorkloadName    string                 `json:"workloadName"`
 }
 
 type ApplicationServiceReq struct {
diff --git a/pkg/console/model/instance.go b/pkg/console/model/instance.go
index e933d779..907fea27 100644
--- a/pkg/console/model/instance.go
+++ b/pkg/console/model/instance.go
@@ -20,6 +20,7 @@ package model
 import (
        "github.com/duke-git/lancet/v2/strutil"
 
+       meshproto "github.com/apache/dubbo-admin/api/mesh/v1alpha1"
        "github.com/apache/dubbo-admin/pkg/config/app"
        meshresource 
"github.com/apache/dubbo-admin/pkg/core/resource/apis/mesh/v1alpha1"
        coremodel "github.com/apache/dubbo-admin/pkg/core/resource/model"
@@ -54,17 +55,18 @@ func NewSearchPaginationResult() *SearchPaginationResult {
 }
 
 type SearchInstanceResp struct {
-       Ip               string            `json:"ip"`
-       Name             string            `json:"name"`
-       WorkloadName     string            `json:"workloadName"`
-       AppName          string            `json:"appName"`
-       DeployState      string            `json:"deployState"`
-       DeployCluster    string            `json:"deployCluster"`
-       RegisterState    string            `json:"registerState"`
-       RegisterClusters []string          `json:"registerClusters"`
-       CreateTime       string            `json:"createTime"`
-       RegisterTime     string            `json:"registerTime"`
-       Labels           map[string]string `json:"labels"`
+       Ip               string                 `json:"ip"`
+       Name             string                 `json:"name"`
+       WorkloadName     string                 `json:"workloadName"`
+       AppName          string                 `json:"appName"`
+       LifecycleState   InstanceLifecycleState `json:"lifecycleState"`
+       DeployState      InstanceDeployState    `json:"deployState"`
+       DeployCluster    string                 `json:"deployCluster"`
+       RegisterState    InstanceRegisterState  `json:"registerState"`
+       RegisterClusters []string               `json:"registerClusters"`
+       CreateTime       string                 `json:"createTime"`
+       RegisterTime     string                 `json:"registerTime"`
+       Labels           map[string]string      `json:"labels"`
 }
 
 func NewSearchInstanceResp() *SearchInstanceResp {
@@ -85,13 +87,10 @@ func (r *SearchInstanceResp) 
FromInstanceResource(instanceResource *meshresource
        if cfg.Engine != nil && cfg.Engine.ID == instance.SourceEngine {
                r.DeployCluster = cfg.Engine.Name
        }
-       if r.RegisterTime != "" {
-               r.RegisterState = "Registered"
-       } else {
-               r.RegisterState = "UnRegistered"
-       }
+       r.RegisterState = DeriveInstanceRegisterState(instance)
        r.Labels = instance.Tags
-       r.DeployState = instance.DeployState
+       r.DeployState = DeriveInstanceDeployState(instance)
+       r.LifecycleState = DeriveInstanceLifecycleState(instance, 
r.DeployState, r.RegisterState)
        r.WorkloadName = instance.WorkloadName
        r.AppName = instance.AppName
        return r
@@ -104,23 +103,74 @@ type State struct {
        Value string `json:"value"`
 }
 
+// InstanceDeployState describes the runtime deployment state reported by the 
platform.
+type InstanceDeployState string
+
+const (
+       // InstanceDeployStateUnknown indicates the deployment state cannot be 
derived from runtime metadata.
+       InstanceDeployStateUnknown InstanceDeployState = "Unknown"
+       // InstanceDeployStatePending indicates the workload has been accepted 
but is not running yet.
+       InstanceDeployStatePending InstanceDeployState = "Pending"
+       // InstanceDeployStateStarting indicates the workload is running but 
not ready to serve.
+       InstanceDeployStateStarting InstanceDeployState = "Starting"
+       // InstanceDeployStateRunning indicates the workload is running and 
ready.
+       InstanceDeployStateRunning InstanceDeployState = "Running"
+       // InstanceDeployStateTerminating indicates the workload is shutting 
down.
+       InstanceDeployStateTerminating InstanceDeployState = "Terminating"
+       // InstanceDeployStateFailed indicates the workload has failed.
+       InstanceDeployStateFailed InstanceDeployState = "Failed"
+       // InstanceDeployStateSucceeded indicates the workload has completed 
successfully and exited.
+       InstanceDeployStateSucceeded InstanceDeployState = "Succeeded"
+       // InstanceDeployStateCrashing indicates the workload is repeatedly 
crashing or restarting.
+       InstanceDeployStateCrashing InstanceDeployState = "Crashing"
+)
+
+// InstanceRegisterState describes whether the instance is visible to the 
registry.
+type InstanceRegisterState string
+
+const (
+       // InstanceRegisterStateRegistered indicates the instance has been 
registered to the registry.
+       InstanceRegisterStateRegistered InstanceRegisterState = "Registered"
+       // InstanceRegisterStateUnregistered indicates the instance has not 
registered yet or has been removed.
+       InstanceRegisterStateUnregistered InstanceRegisterState = "UnRegistered"
+)
+
+// InstanceLifecycleState describes the user-facing lifecycle synthesized from 
deploy/register signals.
+type InstanceLifecycleState string
+
+const (
+       // InstanceLifecycleStateStarting indicates the instance is still 
warming up.
+       InstanceLifecycleStateStarting InstanceLifecycleState = "Starting"
+       // InstanceLifecycleStateServing indicates the instance is both running 
and registered.
+       InstanceLifecycleStateServing InstanceLifecycleState = "Serving"
+       // InstanceLifecycleStateDraining indicates the instance is running but 
has started unregistering.
+       InstanceLifecycleStateDraining InstanceLifecycleState = "Draining"
+       // InstanceLifecycleStateTerminating indicates the instance is shutting 
down.
+       InstanceLifecycleStateTerminating InstanceLifecycleState = "Terminating"
+       // InstanceLifecycleStateError indicates the instance is in an 
unexpected or failed state.
+       InstanceLifecycleStateError InstanceLifecycleState = "Error"
+       // InstanceLifecycleStateUnknown indicates the lifecycle cannot be 
inferred from current signals.
+       InstanceLifecycleStateUnknown InstanceLifecycleState = "Unknown"
+)
+
 type InstanceDetailResp struct {
-       RpcPort          int64             `json:"rpcPort"`
-       Ip               string            `json:"ip"`
-       AppName          string            `json:"appName"`
-       WorkloadName     string            `json:"workloadName"`
-       Labels           map[string]string `json:"labels"`
-       CreateTime       string            `json:"createTime"`
-       ReadyTime        string            `json:"readyTime"`
-       RegisterTime     string            `json:"registerTime"`
-       RegisterClusters []string          `json:"registerClusters"`
-       DeployCluster    string            `json:"deployCluster"`
-       DeployState      string            `json:"deployState"`
-       RegisterState    string            `json:"registerState"`
-       Node             string            `json:"node"`
-       Image            string            `json:"image"`
-       Probes           ProbeStruct       `json:"probes"`
-       Tags             map[string]string `json:"tags"`
+       RpcPort          int64                  `json:"rpcPort"`
+       Ip               string                 `json:"ip"`
+       AppName          string                 `json:"appName"`
+       WorkloadName     string                 `json:"workloadName"`
+       Labels           map[string]string      `json:"labels"`
+       CreateTime       string                 `json:"createTime"`
+       ReadyTime        string                 `json:"readyTime"`
+       RegisterTime     string                 `json:"registerTime"`
+       RegisterClusters []string               `json:"registerClusters"`
+       DeployCluster    string                 `json:"deployCluster"`
+       LifecycleState   InstanceLifecycleState `json:"lifecycleState"`
+       DeployState      InstanceDeployState    `json:"deployState"`
+       RegisterState    InstanceRegisterState  `json:"registerState"`
+       Node             string                 `json:"node"`
+       Image            string                 `json:"image"`
+       Probes           ProbeStruct            `json:"probes"`
+       Tags             map[string]string      `json:"tags"`
 }
 
 const (
@@ -158,16 +208,9 @@ func FromInstanceResource(res 
*meshresource.InstanceResource, cfg app.AdminConfi
        if cfg.Engine.ID == res.Spec.SourceEngine {
                r.DeployCluster = cfg.Engine.Name
        }
-       if strutil.IsNotBlank(instance.DeployState) {
-               r.DeployState = instance.DeployState
-       } else {
-               r.DeployState = "Unknown"
-       }
-       if strutil.IsBlank(r.RegisterTime) {
-               r.RegisterState = "UnRegistered"
-       } else {
-               r.RegisterState = "Registered"
-       }
+       r.DeployState = DeriveInstanceDeployState(instance)
+       r.RegisterState = DeriveInstanceRegisterState(instance)
+       r.LifecycleState = DeriveInstanceLifecycleState(instance, 
r.DeployState, r.RegisterState)
        r.Node = instance.Node
        r.Image = instance.Image
        r.Probes = ProbeStruct{}
@@ -196,3 +239,69 @@ func FromInstanceResource(res 
*meshresource.InstanceResource, cfg app.AdminConfi
        }
        return r
 }
+
+func DeriveInstanceDeployState(instance *meshproto.Instance) 
InstanceDeployState {
+       if instance == nil || strutil.IsBlank(instance.DeployState) {
+               return InstanceDeployStateUnknown
+       }
+       deployState := InstanceDeployState(instance.DeployState)
+       switch deployState {
+       case InstanceDeployStateRunning:
+               if !isPodReady(instance) {
+                       return InstanceDeployStateStarting
+               }
+               return InstanceDeployStateRunning
+       default:
+               return deployState
+       }
+}
+
+func DeriveInstanceRegisterState(instance *meshproto.Instance) 
InstanceRegisterState {
+       if instance == nil || strutil.IsBlank(instance.RegisterTime) {
+               return InstanceRegisterStateUnregistered
+       }
+       return InstanceRegisterStateRegistered
+}
+
+func DeriveInstanceLifecycleState(
+       instance *meshproto.Instance,
+       deployState InstanceDeployState,
+       registerState InstanceRegisterState,
+) InstanceLifecycleState {
+       switch deployState {
+       case InstanceDeployStateCrashing, InstanceDeployStateFailed, 
InstanceDeployStateUnknown, InstanceDeployStateSucceeded:
+               return InstanceLifecycleStateError
+       case InstanceDeployStateTerminating:
+               return InstanceLifecycleStateTerminating
+       }
+
+       if registerState == InstanceRegisterStateRegistered {
+               if deployState == InstanceDeployStateRunning {
+                       return InstanceLifecycleStateServing
+               }
+               return InstanceLifecycleStateError
+       }
+
+       if instance != nil && deployState == InstanceDeployStateRunning && 
strutil.IsNotBlank(instance.UnregisterTime) {
+               return InstanceLifecycleStateDraining
+       }
+
+       switch deployState {
+       case InstanceDeployStatePending, InstanceDeployStateStarting, 
InstanceDeployStateRunning:
+               return InstanceLifecycleStateStarting
+       default:
+               return InstanceLifecycleStateUnknown
+       }
+}
+
+func isPodReady(instance *meshproto.Instance) bool {
+       for _, condition := range instance.Conditions {
+               if condition == nil {
+                       continue
+               }
+               if condition.Type == "Ready" {
+                       return condition.Status == "True"
+               }
+       }
+       return false
+}
diff --git a/pkg/console/service/application.go 
b/pkg/console/service/application.go
index cd295e2b..9a5ee89d 100644
--- a/pkg/console/service/application.go
+++ b/pkg/console/service/application.go
@@ -95,7 +95,8 @@ func buildAppInstanceInfoResp(instanceRes 
*meshresource.InstanceResource, cfg ap
        resp.Name = instance.Name
        resp.AppName = instance.AppName
        resp.CreateTime = instance.CreateTime
-       resp.DeployState = instance.DeployState
+       resp.DeployState = model.DeriveInstanceDeployState(instance)
+       resp.LifecycleState = model.DeriveInstanceLifecycleState(instance, 
resp.DeployState, model.DeriveInstanceRegisterState(instance))
        if cfg.Engine.ID == instance.SourceEngine {
                resp.DeployClusters = cfg.Engine.Name
        }
@@ -104,7 +105,7 @@ func buildAppInstanceInfoResp(instanceRes 
*meshresource.InstanceResource, cfg ap
        if d := cfg.FindDiscovery(instanceRes.Mesh); d != nil {
                resp.RegisterCluster = d.Name
        }
-       resp.RegisterState = "Registered"
+       resp.RegisterState = model.DeriveInstanceRegisterState(instance)
        resp.RegisterTime = instance.RegisterTime
        resp.WorkloadName = instance.WorkloadName
        return resp
diff --git a/pkg/core/controller/informer.go b/pkg/core/controller/informer.go
index 92cdb693..cf0003e3 100644
--- a/pkg/core/controller/informer.go
+++ b/pkg/core/controller/informer.go
@@ -229,17 +229,10 @@ func (s *informer) HandleDeltas(obj interface{}, _ bool) 
error {
        }
        // from oldest to newest
        for _, d := range deltas {
-               var resource model.Resource
-               var object interface{}
-               if o, ok := d.Object.(cache.DeletedFinalStateUnknown); ok {
-                       object = o.Obj
-               } else {
-                       object = d.Object
-               }
-               resource, ok := object.(model.Resource)
-               if !ok {
-                       logger.Errorf("object from ListWatcher is not conformed 
to Resource, obj: %v", obj)
-                       return bizerror.NewAssertionError("Resource", 
reflect.TypeOf(obj).Name())
+               resource, err := s.toResource(d.Object)
+               if err != nil {
+                       logger.Errorf("object from ListWatcher is not conformed 
to Resource, obj: %v, err: %v", obj, err)
+                       return err
                }
                switch d.Type {
                case cache.Sync, cache.Replaced, cache.Added, cache.Updated:
@@ -257,6 +250,8 @@ func (s *informer) HandleDeltas(obj interface{}, _ bool) 
error {
                                s.EmitEvent(cache.Added, nil, resource)
                        }
                case cache.Deleted:
+                       logger.Infof("informer processing delete delta, 
resource kind: %s, key: %s",
+                               resource.ResourceKind().ToString(), 
resource.ResourceKey())
                        if err := s.indexer.Delete(resource); err != nil {
                                logger.Errorf("failed to delete resource from 
informer, cause %v, resource: %s,", err, resource.String())
                                return err
@@ -267,6 +262,31 @@ func (s *informer) HandleDeltas(obj interface{}, _ bool) 
error {
        return nil
 }
 
+func (s *informer) toResource(obj interface{}) (model.Resource, error) {
+       object := obj
+       if tombstone, ok := obj.(cache.DeletedFinalStateUnknown); ok {
+               logger.Debugf("informer resolved tombstone object during delete 
handling, key: %s", tombstone.Key)
+               object = tombstone.Obj
+       }
+       if resource, ok := object.(model.Resource); ok {
+               return resource, nil
+       }
+       if s.transform != nil {
+               transformed, err := s.transform(object)
+               if err != nil {
+                       return nil, err
+               }
+               if resource, ok := transformed.(model.Resource); ok {
+                       return resource, nil
+               }
+               object = transformed
+       }
+       if object == nil {
+               return nil, bizerror.NewAssertionError("Resource", "nil")
+       }
+       return nil, bizerror.NewAssertionError("Resource", 
reflect.TypeOf(object).Name())
+}
+
 // EmitEvent emits an event to the event bus.
 func (s *informer) EmitEvent(typ cache.DeltaType, oldObj model.Resource, 
newObj model.Resource) {
        event := events.NewResourceChangedEvent(typ, oldObj, newObj)
diff --git a/pkg/core/controller/listwatcher.go 
b/pkg/core/controller/listwatcher.go
index e90580f0..4978f011 100644
--- a/pkg/core/controller/listwatcher.go
+++ b/pkg/core/controller/listwatcher.go
@@ -31,3 +31,9 @@ type ResourceListerWatcher interface {
        // return nil if there is no need to transform, see 
cache.SharedInformer for detail
        TransformFunc() cache.TransformFunc
 }
+
+// ResourceKeyProvider can be optionally implemented by a 
ResourceListerWatcher when
+// raw watch objects need a key that is consistent with the transformed 
resource key.
+type ResourceKeyProvider interface {
+       KeyFunc() cache.KeyFunc
+}
diff --git a/pkg/core/discovery/subscriber/rpc_instance.go 
b/pkg/core/discovery/subscriber/rpc_instance.go
index 0343e5cf..4879d1b8 100644
--- a/pkg/core/discovery/subscriber/rpc_instance.go
+++ b/pkg/core/discovery/subscriber/rpc_instance.go
@@ -20,6 +20,7 @@ package subscriber
 import (
        "reflect"
 
+       "github.com/duke-git/lancet/v2/slice"
        "github.com/duke-git/lancet/v2/strutil"
        "k8s.io/client-go/tools/cache"
 
@@ -110,7 +111,12 @@ func (s *RPCInstanceEventSubscriber) 
processUpsert(rpcInstanceRes *meshresource.
        // We should merge the rpc info into it
        if instanceRes != nil {
                meshresource.MergeRPCInstanceIntoInstance(rpcInstanceRes, 
instanceRes)
-               return s.instanceStore.Update(instanceRes)
+               if err := s.instanceStore.Update(instanceRes); err != nil {
+                       return err
+               }
+               logger.Infof("instance lifecycle merged rpc source, instance: 
%s, rpc: %s, registerState: registered, deployState: %s",
+                       instanceRes.ResourceKey(), 
rpcInstanceRes.ResourceKey(), instanceRes.Spec.DeployState)
+               return nil
        }
        // Otherwise we can create a new instance resource by rpc instance
        instanceRes = meshresource.FromRPCInstance(rpcInstanceRes)
@@ -119,6 +125,8 @@ func (s *RPCInstanceEventSubscriber) 
processUpsert(rpcInstanceRes *meshresource.
                logger.Errorf("add instance resource failed, instance: %s, err: 
%s", instanceRes.ResourceKey(), err.Error())
                return err
        }
+       logger.Infof("instance lifecycle created rpc-only instance, instance: 
%s, rpc: %s, registerState: registered",
+               instanceRes.ResourceKey(), rpcInstanceRes.ResourceKey())
 
        instanceAddEvent := events.NewResourceChangedEvent(cache.Added, nil, 
instanceRes)
        s.eventEmitter.Send(instanceAddEvent)
@@ -135,10 +143,26 @@ func (s *RPCInstanceEventSubscriber) 
processDelete(rpcInstanceRes *meshresource.
                logger.Warnf("cannot find instance resource for rpc instance 
%s, skipped deleting instance", rpcInstanceRes.Name)
                return nil
        }
+       meshresource.ClearRPCInstanceFromInstance(instanceRes)
+       if meshresource.HasRuntimeInstanceSource(instanceRes) {
+               if err := s.instanceStore.Update(instanceRes); err != nil {
+                       logger.Errorf("update instance resource failed after 
rpc delete, instance: %s, err: %s",
+                               instanceRes.ResourceKey(), err.Error())
+                       return err
+               }
+               logger.Infof("instance lifecycle rpc source removed, keep 
instance by runtime source, instance: %s, rpc: %s, deployState: %s",
+                       instanceRes.ResourceKey(), 
rpcInstanceRes.ResourceKey(), instanceRes.Spec.DeployState)
+               instanceUpdateEvent := 
events.NewResourceChangedEvent(cache.Updated, instanceRes, instanceRes)
+               s.eventEmitter.Send(instanceUpdateEvent)
+               logger.Debugf("rpc instance delete trigger instance update 
event, event: %s", instanceUpdateEvent.String())
+               return nil
+       }
        if err := s.instanceStore.Delete(instanceRes); err != nil {
                logger.Errorf("delete instance resource failed, instance: %s, 
err: %s", instanceRes.ResourceKey(), err.Error())
                return err
        }
+       logger.Infof("instance lifecycle rpc source removed and no runtime 
source remains, deleted instance: %s, rpc: %s",
+               instanceRes.ResourceKey(), rpcInstanceRes.ResourceKey())
        instanceDeleteEvent := events.NewResourceChangedEvent(cache.Deleted, 
instanceRes, nil)
        s.eventEmitter.Send(instanceDeleteEvent)
        logger.Debugf("rpc instance delete trigger instance delete event, 
event: %s", instanceDeleteEvent.String())
@@ -173,7 +197,7 @@ func (s *RPCInstanceEventSubscriber) getRelatedInstanceRes(
 func (s *RPCInstanceEventSubscriber) 
findRelatedRuntimeInstanceAndMerge(instanceRes *meshresource.InstanceResource) {
        switch s.engineCfg.Type {
        case enginecfg.Kubernetes:
-               rtInstance := s.getRuntimeInstanceByIp(instanceRes.Spec.Ip)
+               rtInstance := s.getRuntimeInstanceForInstance(instanceRes)
                if rtInstance == nil {
                        logger.Warnf("cannot find runtime instance for instace 
%s, skipping merging", instanceRes.ResourceKey())
                        return
@@ -184,7 +208,8 @@ func (s *RPCInstanceEventSubscriber) 
findRelatedRuntimeInstanceAndMerge(instance
        }
 }
 
-func (s *RPCInstanceEventSubscriber) getRuntimeInstanceByIp(ip string) 
*meshresource.RuntimeInstanceResource {
+func (s *RPCInstanceEventSubscriber) getRuntimeInstanceForInstance(instanceRes 
*meshresource.InstanceResource) *meshresource.RuntimeInstanceResource {
+       ip := instanceRes.Spec.Ip
        resources, err := 
s.rtInstanceStore.ListByIndexes([]index.IndexCondition{
                {IndexName: index.ByRuntimeInstanceIPIndex, Value: ip, 
Operator: index.Equals},
        })
@@ -195,9 +220,31 @@ func (s *RPCInstanceEventSubscriber) 
getRuntimeInstanceByIp(ip string) *meshreso
        if len(resources) == 0 {
                return nil
        }
-       runtimeInstanceRes, ok := 
resources[0].(*meshresource.RuntimeInstanceResource)
-       if !ok {
-               return nil
+       candidates := make([]*meshresource.RuntimeInstanceResource, 0, 
len(resources))
+       for _, item := range resources {
+               res, ok := item.(*meshresource.RuntimeInstanceResource)
+               if !ok {
+                       continue
+               }
+               candidates = append(candidates, res)
+       }
+       // Prefer exact runtime identity match by app + rpcPort + mesh.
+       for _, candidate := range candidates {
+               if candidate.Spec == nil {
+                       continue
+               }
+               if candidate.Mesh == instanceRes.Mesh &&
+                       candidate.Spec.AppName == instanceRes.Spec.AppName &&
+                       candidate.Spec.RpcPort == instanceRes.Spec.RpcPort {
+                       return candidate
+               }
+       }
+       if len(candidates) > 1 {
+               keys := slice.Map(candidates, func(_ int, item 
*meshresource.RuntimeInstanceResource) string {
+                       return item.ResourceKey()
+               })
+               logger.Warnf("multiple runtime instances share same ip %s, 
fallback to first candidate, runtime keys: %v, target instance: %s",
+                       ip, keys, instanceRes.ResourceKey())
        }
-       return runtimeInstanceRes
+       return candidates[0]
 }
diff --git a/pkg/core/engine/component.go b/pkg/core/engine/component.go
index 88dc293e..8c893a9e 100644
--- a/pkg/core/engine/component.go
+++ b/pkg/core/engine/component.go
@@ -24,8 +24,6 @@ import (
        "reflect"
        "sync/atomic"
 
-       "k8s.io/client-go/tools/cache"
-
        "github.com/apache/dubbo-admin/pkg/common/bizerror"
        enginecfg "github.com/apache/dubbo-admin/pkg/config/engine"
        storecfg "github.com/apache/dubbo-admin/pkg/config/store"
@@ -37,6 +35,7 @@ import (
        meshresource 
"github.com/apache/dubbo-admin/pkg/core/resource/apis/mesh/v1alpha1"
        "github.com/apache/dubbo-admin/pkg/core/runtime"
        "github.com/apache/dubbo-admin/pkg/core/store"
+       "k8s.io/client-go/tools/cache"
 )
 
 func init() {
@@ -157,7 +156,7 @@ func (e *engineComponent) initInformers(cfg 
*enginecfg.Config, emitter events.Em
                if err != nil {
                        return fmt.Errorf("can not find store for resource kind 
%s, %w", rk, err)
                }
-               informer := controller.NewInformerWithOptions(lw, emitter, rs, 
cache.MetaNamespaceKeyFunc, controller.Options{ResyncPeriod: 0})
+               informer := controller.NewInformerWithOptions(lw, emitter, rs, 
resolveInformerKeyFunc(lw), controller.Options{ResyncPeriod: 0})
                if lw.TransformFunc() != nil {
                        err = informer.SetTransform(lw.TransformFunc())
                        if err != nil {
@@ -170,6 +169,13 @@ func (e *engineComponent) initInformers(cfg 
*enginecfg.Config, emitter events.Em
        return nil
 }
 
+func resolveInformerKeyFunc(lw controller.ResourceListerWatcher) cache.KeyFunc 
{
+       if provider, ok := lw.(controller.ResourceKeyProvider); ok {
+               return provider.KeyFunc()
+       }
+       return cache.MetaNamespaceKeyFunc
+}
+
 func (e *engineComponent) initSubscribers(eventbus events.EventBus) error {
        rs, err := e.storeRouter.ResourceKindRoute(meshresource.InstanceKind)
        if err != nil {
diff --git a/pkg/core/engine/subscriber/runtime_instance.go 
b/pkg/core/engine/subscriber/runtime_instance.go
index f7d38709..b39a0988 100644
--- a/pkg/core/engine/subscriber/runtime_instance.go
+++ b/pkg/core/engine/subscriber/runtime_instance.go
@@ -92,14 +92,7 @@ func (s *RuntimeInstanceEventSubscriber) ProcessEvent(event 
events.Event) error
 
 // processUpsert when runtime instance added or updated, we should add/update 
the corresponding instance resource
 func (s *RuntimeInstanceEventSubscriber) processUpsert(rtInstanceRes 
*meshresource.RuntimeInstanceResource) error {
-       var instanceResource *meshresource.InstanceResource
-       var err error
-       switch rtInstanceRes.Spec.SourceEngineType {
-       case string(enginecfg.Kubernetes):
-               instanceResource, err = s.getRelatedInstanceByIP(rtInstanceRes)
-       default:
-               instanceResource, err = 
s.getRelatedInstanceByName(rtInstanceRes)
-       }
+       instanceResource, err := s.getRelatedInstance(rtInstanceRes)
        if err != nil {
                return err
        }
@@ -107,12 +100,19 @@ func (s *RuntimeInstanceEventSubscriber) 
processUpsert(rtInstanceRes *meshresour
        // so we should merge the runtime info into it
        if instanceResource != nil {
                meshresource.MergeRuntimeInstanceIntoInstance(rtInstanceRes, 
instanceResource)
-               return s.instanceStore.Update(instanceResource)
+               if err = s.instanceStore.Update(instanceResource); err != nil {
+                       return err
+               }
+               logger.Infof("instance lifecycle merged runtime source, 
instance: %s, runtime: %s, deployState: %s, hasRPCSource: %t",
+                       instanceResource.ResourceKey(), 
rtInstanceRes.ResourceKey(), instanceResource.Spec.DeployState,
+                       meshresource.HasRPCInstanceSource(instanceResource))
+               return nil
        }
        // if instance resource does not exist, that is to say the rpc instance 
does not exist in remote registry.
        // we need to check whether the runtime instance resource is enough to 
create a new instance resource
        if !checkAttributesEnough(rtInstanceRes) {
-               logger.Warnf("cannot identify runtime instance %s to a dubbo 
instance, skipped creating new instance", rtInstanceRes.ResourceKey())
+               logger.Warnf("cannot identify runtime instance %s to a dubbo 
instance, skipped creating new instance, reason: %s",
+                       rtInstanceRes.ResourceKey(), 
runtimeIdentityMissingReason(rtInstanceRes))
                return nil
        }
        // if conditions met, we should create a new instance resource by 
runtime instance
@@ -121,6 +121,8 @@ func (s *RuntimeInstanceEventSubscriber) 
processUpsert(rtInstanceRes *meshresour
                logger.Errorf("add instance resource failed, instance: %s, err: 
%s", instanceRes.ResourceKey(), err.Error())
                return err
        }
+       logger.Infof("instance lifecycle created runtime-only instance, 
instance: %s, runtime: %s, deployState: %s",
+               instanceRes.ResourceKey(), rtInstanceRes.ResourceKey(), 
instanceRes.Spec.DeployState)
        instanceAddEvent := events.NewResourceChangedEvent(cache.Added, nil, 
instanceRes)
        s.eventEmitter.Send(instanceAddEvent)
        logger.Debugf("runtime instance upsert trigger instance add event, 
event: %s", instanceAddEvent.String())
@@ -129,14 +131,7 @@ func (s *RuntimeInstanceEventSubscriber) 
processUpsert(rtInstanceRes *meshresour
 
 // processDelete when runtime instance deleted, we should delete the 
corresponding instance resource
 func (s *RuntimeInstanceEventSubscriber) processDelete(rtInstanceRes 
*meshresource.RuntimeInstanceResource) error {
-       var instanceResource *meshresource.InstanceResource
-       var err error
-       switch rtInstanceRes.Spec.SourceEngineType {
-       case string(enginecfg.Kubernetes):
-               instanceResource, err = s.getRelatedInstanceByIP(rtInstanceRes)
-       default:
-               instanceResource, err = 
s.getRelatedInstanceByName(rtInstanceRes)
-       }
+       instanceResource, err := s.getRelatedInstance(rtInstanceRes)
        if err != nil {
                return err
        }
@@ -144,16 +139,76 @@ func (s *RuntimeInstanceEventSubscriber) 
processDelete(rtInstanceRes *meshresour
                logger.Warnf("cannot find instance resource by runtime instance 
%s, skipped deleting instance", rtInstanceRes.ResourceKey())
                return nil
        }
+       meshresource.ClearRuntimeInstanceFromInstance(instanceResource)
+       if meshresource.HasRPCInstanceSource(instanceResource) {
+               if err = s.instanceStore.Update(instanceResource); err != nil {
+                       logger.Errorf("update instance resource failed after 
runtime delete, instance: %s, err: %s",
+                               instanceResource.ResourceKey(), err.Error())
+                       return err
+               }
+               logger.Infof("instance lifecycle runtime source removed, keep 
instance by rpc source, instance: %s, runtime: %s, registerState: registered",
+                       instanceResource.ResourceKey(), 
rtInstanceRes.ResourceKey())
+               instanceUpdateEvent := 
events.NewResourceChangedEvent(cache.Updated, instanceResource, 
instanceResource)
+               s.eventEmitter.Send(instanceUpdateEvent)
+               logger.Debugf("runtime instance delete trigger instance update 
event, event: %s", instanceUpdateEvent.String())
+               return nil
+       }
        if err = s.instanceStore.Delete(instanceResource); err != nil {
                logger.Errorf("delete instance resource failed, instance: %s, 
err: %s", instanceResource.ResourceKey(), err.Error())
                return err
        }
+       logger.Infof("instance lifecycle runtime source removed and no rpc 
source remains, deleted instance: %s, runtime: %s",
+               instanceResource.ResourceKey(), rtInstanceRes.ResourceKey())
        instanceDeleteEvent := events.NewResourceChangedEvent(cache.Deleted, 
instanceResource, nil)
        s.eventEmitter.Send(instanceDeleteEvent)
        logger.Debugf("runtime instance delete trigger instance delete event, 
event: %s", instanceDeleteEvent.String())
        return nil
 }
 
+func (s *RuntimeInstanceEventSubscriber) getRelatedInstance(
+       rtInstanceRes *meshresource.RuntimeInstanceResource) 
(*meshresource.InstanceResource, error) {
+       if rtInstanceRes == nil || rtInstanceRes.Spec == nil {
+               return nil, nil
+       }
+       switch rtInstanceRes.Spec.SourceEngineType {
+       case string(enginecfg.Kubernetes):
+               return s.getRelatedKubernetesInstance(rtInstanceRes)
+       default:
+               return s.getRelatedInstanceByName(rtInstanceRes)
+       }
+}
+
+func (s *RuntimeInstanceEventSubscriber) getRelatedKubernetesInstance(
+       rtInstanceRes *meshresource.RuntimeInstanceResource) 
(*meshresource.InstanceResource, error) {
+       if rtInstanceRes == nil || rtInstanceRes.Spec == nil {
+               return nil, nil
+       }
+       if hasRuntimeIdentity(rtInstanceRes) {
+               instanceResName := 
meshresource.BuildInstanceResName(rtInstanceRes.Spec.AppName, 
rtInstanceRes.Spec.Ip, rtInstanceRes.Spec.RpcPort)
+               if strutil.IsNotBlank(rtInstanceRes.Mesh) && 
constants.DefaultMesh != rtInstanceRes.Mesh {
+                       res, exists, err := 
s.instanceStore.GetByKey(coremodel.BuildResourceKey(rtInstanceRes.Mesh, 
instanceResName))
+                       if err != nil {
+                               return nil, err
+                       }
+                       if exists {
+                               instanceRes, ok := 
res.(*meshresource.InstanceResource)
+                               if !ok {
+                                       return nil, 
bizerror.NewAssertionError("InstanceResource", reflect.TypeOf(res).Name())
+                               }
+                               return instanceRes, nil
+                       }
+               }
+               instanceRes, err := s.getRelatedInstanceByName(rtInstanceRes)
+               if err != nil {
+                       return nil, err
+               }
+               if instanceRes != nil {
+                       return instanceRes, nil
+               }
+       }
+       return s.getRelatedInstanceByIP(rtInstanceRes)
+}
+
 func (s *RuntimeInstanceEventSubscriber) getRelatedInstanceByName(
        rtInstanceRes *meshresource.RuntimeInstanceResource) 
(*meshresource.InstanceResource, error) {
        if rtInstanceRes.Spec == nil || 
strutil.IsBlank(rtInstanceRes.Spec.AppName) ||
@@ -171,21 +226,28 @@ func (s *RuntimeInstanceEventSubscriber) 
getRelatedInstanceByName(
                return nil, nil
        }
        instanceResList := make([]*meshresource.InstanceResource, 
len(resources))
+       filtered := make([]*meshresource.InstanceResource, 0, len(resources))
        for i, item := range resources {
                res, ok := item.(*meshresource.InstanceResource)
                if !ok {
                        return nil, 
bizerror.NewAssertionError("InstanceResource", reflect.TypeOf(item).Name())
                }
                instanceResList[i] = res
+               if strutil.IsBlank(rtInstanceRes.Mesh) || res.Mesh == 
rtInstanceRes.Mesh {
+                       filtered = append(filtered, res)
+               }
        }
-       if len(instanceResList) > 1 {
-               resKeys := slice.Map(instanceResList, func(index int, item 
*meshresource.InstanceResource) string {
+       if len(filtered) == 0 {
+               return nil, nil
+       }
+       if len(filtered) > 1 {
+               resKeys := slice.Map(filtered, func(index int, item 
*meshresource.InstanceResource) string {
                        return item.ResourceKey()
                })
                logger.Warnf("there are more than two instances which have the 
same name, instance keys: %s, name: %s", resKeys, instanceResName)
                return nil, nil
        }
-       return instanceResList[0], nil
+       return filtered[0], nil
 }
 
 func (s *RuntimeInstanceEventSubscriber) getRelatedInstanceByIP(
@@ -225,3 +287,38 @@ func checkAttributesEnough(rtInstanceRes 
*meshresource.RuntimeInstanceResource)
        }
        return true
 }
+
+func runtimeIdentityMissingReason(rtInstanceRes 
*meshresource.RuntimeInstanceResource) string {
+       if rtInstanceRes == nil {
+               return "runtime instance is nil"
+       }
+       if rtInstanceRes.Spec == nil {
+               return "runtime instance spec is nil"
+       }
+       if strutil.IsBlank(rtInstanceRes.Spec.AppName) {
+               return "appName is empty"
+       }
+       if strutil.IsBlank(rtInstanceRes.Spec.Ip) {
+               return "pod ip is empty"
+       }
+       if rtInstanceRes.Spec.RpcPort <= 0 {
+               return "rpcPort is not configured"
+       }
+       if strutil.IsBlank(rtInstanceRes.Mesh) {
+               return "mesh is empty"
+       }
+       if constants.DefaultMesh == rtInstanceRes.Mesh {
+               return "mesh is default, missing registry identifier"
+       }
+       return "unknown"
+}
+
+func hasRuntimeIdentity(rtInstanceRes *meshresource.RuntimeInstanceResource) 
bool {
+       if rtInstanceRes == nil || rtInstanceRes.Spec == nil {
+               return false
+       }
+       return strutil.IsNotBlank(rtInstanceRes.Spec.AppName) &&
+               strutil.IsNotBlank(rtInstanceRes.Spec.Ip) &&
+               rtInstanceRes.Spec.RpcPort > 0 &&
+               strutil.IsNotBlank(rtInstanceRes.Mesh)
+}
diff --git a/pkg/core/resource/apis/mesh/v1alpha1/instance_helper.go 
b/pkg/core/resource/apis/mesh/v1alpha1/instance_helper.go
index ffc72bab..f7b86c34 100644
--- a/pkg/core/resource/apis/mesh/v1alpha1/instance_helper.go
+++ b/pkg/core/resource/apis/mesh/v1alpha1/instance_helper.go
@@ -19,6 +19,9 @@ package v1alpha1
 
 import (
        "fmt"
+       "time"
+
+       "github.com/apache/dubbo-admin/pkg/common/constants"
 )
 
 func BuildInstanceResName(appName string, ip string, rpcPort int64) string {
@@ -79,3 +82,55 @@ func MergeRuntimeInstanceIntoInstance(
        instanceRes.Spec.Conditions = rtInstanceRes.Spec.Conditions
        instanceRes.Spec.SourceEngine = rtInstanceRes.Spec.SourceEngine
 }
+
+func ClearRPCInstanceFromInstance(instanceRes *InstanceResource) {
+       if instanceRes == nil || instanceRes.Spec == nil {
+               return
+       }
+       instanceRes.Spec.ReleaseVersion = ""
+       instanceRes.Spec.RegisterTime = ""
+       instanceRes.Spec.UnregisterTime = 
time.Now().Format(constants.TimeFormatStr)
+       instanceRes.Spec.Protocol = ""
+       instanceRes.Spec.Serialization = ""
+       instanceRes.Spec.PreferSerialization = ""
+       instanceRes.Spec.Tags = nil
+}
+
+func ClearRuntimeInstanceFromInstance(instanceRes *InstanceResource) {
+       if instanceRes == nil || instanceRes.Spec == nil {
+               return
+       }
+       instanceRes.Labels = nil
+       instanceRes.Spec.Image = ""
+       instanceRes.Spec.CreateTime = ""
+       instanceRes.Spec.StartTime = ""
+       instanceRes.Spec.ReadyTime = ""
+       instanceRes.Spec.DeployState = ""
+       instanceRes.Spec.WorkloadType = ""
+       instanceRes.Spec.WorkloadName = ""
+       instanceRes.Spec.Node = ""
+       instanceRes.Spec.Probes = nil
+       instanceRes.Spec.Conditions = nil
+       instanceRes.Spec.SourceEngine = ""
+}
+
+func HasRuntimeInstanceSource(instanceRes *InstanceResource) bool {
+       if instanceRes == nil || instanceRes.Spec == nil {
+               return false
+       }
+       return instanceRes.Spec.SourceEngine != "" ||
+               instanceRes.Spec.DeployState != "" ||
+               instanceRes.Spec.WorkloadName != "" ||
+               instanceRes.Spec.Node != "" ||
+               instanceRes.Spec.Image != "" ||
+               instanceRes.Spec.StartTime != "" ||
+               instanceRes.Spec.ReadyTime != "" ||
+               len(instanceRes.Spec.Conditions) > 0
+}
+
+func HasRPCInstanceSource(instanceRes *InstanceResource) bool {
+       if instanceRes == nil || instanceRes.Spec == nil {
+               return false
+       }
+       return instanceRes.Spec.RegisterTime != ""
+}
diff --git a/pkg/engine/kubernetes/listerwatcher/runtime_instance.go 
b/pkg/engine/kubernetes/listerwatcher/runtime_instance.go
index 725b9218..bc35f3fb 100644
--- a/pkg/engine/kubernetes/listerwatcher/runtime_instance.go
+++ b/pkg/engine/kubernetes/listerwatcher/runtime_instance.go
@@ -48,6 +48,7 @@ type PodListerWatcher struct {
 }
 
 var _ controller.ResourceListerWatcher = &PodListerWatcher{}
+var _ controller.ResourceKeyProvider = &PodListerWatcher{}
 
 func NewPodListWatcher(clientset *kubernetes.Clientset, cfg *enginecfg.Config) 
(*PodListerWatcher, error) {
        var selector fields.Selector
@@ -105,10 +106,7 @@ func (p *PodListerWatcher) TransformFunc() 
cache.TransformFunc {
                                readyTime = 
c.LastTransitionTime.Format(constants.TimeFormatStr)
                        }
                })
-               phase := string(pod.Status.Phase)
-               if pod.DeletionTimestamp != nil {
-                       phase = meshproto.InstanceTerminating
-               }
+               phase := derivePodPhase(pod)
                var workloadName string
                var workloadType string
                if len(pod.GetOwnerReferences()) > 0 {
@@ -173,6 +171,105 @@ func (p *PodListerWatcher) TransformFunc() 
cache.TransformFunc {
        }
 }
 
+func (p *PodListerWatcher) KeyFunc() cache.KeyFunc {
+       return p.resourceKeyFromObject
+}
+
+func (p *PodListerWatcher) resourceKeyFromObject(obj interface{}) (string, 
error) {
+       for {
+               tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
+               if !ok {
+                       break
+               }
+               obj = tombstone.Obj
+       }
+       switch o := obj.(type) {
+       case *v1.Pod:
+               return p.resourceKeyFromPod(o), nil
+       case *meshresource.RuntimeInstanceResource:
+               return o.ResourceKey(), nil
+       default:
+               if obj == nil {
+                       return "", bizerror.NewAssertionError("Pod", "nil")
+               }
+               return "", bizerror.NewAssertionError("Pod", 
reflect.TypeOf(obj).Name())
+       }
+}
+
+func (p *PodListerWatcher) resourceKeyFromPod(pod *v1.Pod) string {
+       return coremodel.BuildResourceKey(p.getDubboMesh(pod), pod.Name)
+}
+
+func derivePodPhase(pod *v1.Pod) string {
+       if pod == nil {
+               return "Unknown"
+       }
+       if pod.DeletionTimestamp != nil {
+               return meshproto.InstanceTerminating
+       }
+       if hasCrashingContainerStatus(pod.Status.InitContainerStatuses) || 
hasCrashingContainerStatus(pod.Status.ContainerStatuses) {
+               return meshproto.InstanceCrashing
+       }
+       switch pod.Status.Phase {
+       case v1.PodPending:
+               if hasStartingContainerStatus(pod.Status.InitContainerStatuses) 
|| hasStartingContainerStatus(pod.Status.ContainerStatuses) {
+                       return meshproto.InstanceStarting
+               }
+               return string(v1.PodPending)
+       case v1.PodRunning:
+               if !isPodReady(pod.Status.Conditions) {
+                       return meshproto.InstanceStarting
+               }
+               return string(v1.PodRunning)
+       case v1.PodFailed:
+               return string(v1.PodFailed)
+       case v1.PodSucceeded:
+               return string(v1.PodSucceeded)
+       case v1.PodUnknown:
+               return string(v1.PodUnknown)
+       default:
+               return string(pod.Status.Phase)
+       }
+}
+
+func isPodReady(conditions []v1.PodCondition) bool {
+       for _, condition := range conditions {
+               if condition.Type == v1.PodReady {
+                       return condition.Status == v1.ConditionTrue
+               }
+       }
+       return false
+}
+
+func hasStartingContainerStatus(statuses []v1.ContainerStatus) bool {
+       for _, status := range statuses {
+               if status.State.Waiting == nil {
+                       continue
+               }
+               switch status.State.Waiting.Reason {
+               case "ContainerCreating", "PodInitializing":
+                       return true
+               }
+       }
+       return false
+}
+
+func hasCrashingContainerStatus(statuses []v1.ContainerStatus) bool {
+       for _, status := range statuses {
+               if status.State.Waiting != nil {
+                       switch status.State.Waiting.Reason {
+                       case "CrashLoopBackOff", "ImagePullBackOff", 
"ErrImagePull", "RunContainerError",
+                               "CreateContainerConfigError", 
"CreateContainerError", "StartError":
+                               return true
+                       }
+               }
+               if status.State.Terminated != nil && 
status.State.Terminated.ExitCode != 0 {
+                       return true
+               }
+       }
+       return false
+}
+
 func (p *PodListerWatcher) getMainContainer(pod *v1.Pod) *v1.Container {
        containers := pod.Spec.Containers
        strategy := p.cfg.Properties.MainContainerChooseStrategy
diff --git a/release/kubernetes/dubbo-samples-shop/dubbo-samples-shop-all.yaml 
b/release/kubernetes/dubbo-samples-shop/dubbo-samples-shop-all.yaml
index 91a5961e..1bcdfdc3 100644
--- a/release/kubernetes/dubbo-samples-shop/dubbo-samples-shop-all.yaml
+++ b/release/kubernetes/dubbo-samples-shop/dubbo-samples-shop-all.yaml
@@ -114,6 +114,8 @@ spec:
         app: shop-order
         orderVersion: v1
         app-type: dubbo
+        org.apache.dubbo/dubbo-rpc-port: "20882"
+        org.apache.dubbo/registry: nacos2.5
     spec:
       containers:
         - name: shop-order
@@ -161,6 +163,8 @@ spec:
         app: shop-order
         orderVersion: v2
         app-type: dubbo
+        org.apache.dubbo/dubbo-rpc-port: "20883"
+        org.apache.dubbo/registry: nacos2.5
     spec:
       containers:
         - name: shop-order
@@ -205,6 +209,8 @@ spec:
       labels:
         app: shop-user
         app-type: dubbo
+        org.apache.dubbo/dubbo-rpc-port: "20884"
+        org.apache.dubbo/registry: nacos2.5
     spec:
       containers:
         - name: shop-user
@@ -250,6 +256,8 @@ spec:
         app: shop-detail
         detailVersion: v1
         app-type: dubbo
+        org.apache.dubbo/dubbo-rpc-port: "20885"
+        org.apache.dubbo/registry: nacos2.5
     spec:
       containers:
         - name: shop-detail
@@ -297,6 +305,8 @@ spec:
         app: shop-detail
         detailVersion: v2
         app-type: dubbo
+        org.apache.dubbo/dubbo-rpc-port: "20886"
+        org.apache.dubbo/registry: nacos2.5
     spec:
       containers:
         - name: shop-detail
@@ -344,6 +354,8 @@ spec:
         app: shop-comment
         commentVersion: v1
         app-type: dubbo
+        org.apache.dubbo/dubbo-rpc-port: "20887"
+        org.apache.dubbo/registry: nacos2.5
     spec:
       containers:
         - name: shop-comment
@@ -391,6 +403,8 @@ spec:
         app: shop-comment
         commentVersion: v2
         app-type: dubbo
+        org.apache.dubbo/dubbo-rpc-port: "20888"
+        org.apache.dubbo/registry: nacos2.5
     spec:
       containers:
         - name: shop-comment
@@ -434,6 +448,8 @@ spec:
       labels:
         app: shop-user
         app-type: dubbo
+        org.apache.dubbo/dubbo-rpc-port: "20892"
+        org.apache.dubbo/registry: nacos2.5
     spec:
       containers:
         - name: shop-user-gray
@@ -480,6 +496,8 @@ spec:
         app: shop-order
         orderVersion: v1
         app-type: dubbo
+        org.apache.dubbo/dubbo-rpc-port: "20891"
+        org.apache.dubbo/registry: nacos2.5
     spec:
       containers:
         - name: shop-order
@@ -525,6 +543,8 @@ spec:
         app: shop-detail
         detailVersion: v1
         app-type: dubbo
+        org.apache.dubbo/dubbo-rpc-port: "20890"
+        org.apache.dubbo/registry: nacos2.5
     spec:
       containers:
         - name: shop-detail
@@ -570,6 +590,8 @@ spec:
         app: shop-comment
         commentVersion: v1
         app-type: dubbo
+        org.apache.dubbo/dubbo-rpc-port: "20889"
+        org.apache.dubbo/registry: nacos2.5
     spec:
       containers:
         - name: shop-comment
diff --git a/release/kubernetes/dubbo-system/dubbo-admin.yaml 
b/release/kubernetes/dubbo-system/dubbo-admin.yaml
index 18faf418..54216c4f 100644
--- a/release/kubernetes/dubbo-system/dubbo-admin.yaml
+++ b/release/kubernetes/dubbo-system/dubbo-admin.yaml
@@ -113,6 +113,20 @@ data:
       id: default
       name: default
       type: kubernetes
+      properties:
+        podWatchSelector: metadata.namespace=dubbo-samples-shop
+        dubboAppIdentifier:
+          type: ByLabel
+          labelKey: app
+        dubboRPCPortIdentifier:
+          type: ByLabel
+          labelKey: org.apache.dubbo/dubbo-rpc-port
+        dubboDiscoveryIdentifier:
+          type: ByLabel
+          labelKey: org.apache.dubbo/registry
+        mainContainerChooseStrategy:
+          type: ByIndex
+          index: 0
 ---
 apiVersion: v1
 kind: Service
diff --git a/ui-vue3/src/base/constants.ts b/ui-vue3/src/base/constants.ts
index 166772ef..e1c865fe 100644
--- a/ui-vue3/src/base/constants.ts
+++ b/ui-vue3/src/base/constants.ts
@@ -57,8 +57,8 @@ export const PRIMARY_COLOR_T = (percent: string) => 
computed(() => PRIMARY_COLOR
 export const PRIMARY_COLOR_R = computed(() => 
getTextColorByBackground(PRIMARY_COLOR.value))
 
 export const INSTANCE_REGISTER_COLOR: { [key: string]: string } = {
-  HEALTHY: 'green',
-  REGISTED: 'green'
+  REGISTERED: 'green',
+  UNREGISTERED: 'default'
 }
 
 export const TAB_HEADER_TITLE: Component = {
@@ -83,7 +83,19 @@ export const TAB_HEADER_TITLE: Component = {
  */
 export const INSTANCE_DEPLOY_COLOR: { [key: string]: string } = {
   RUNNING: 'green',
+  STARTING: 'gold',
   PENDING: 'yellow',
   TERMINATING: 'red',
-  CRASHING: 'darkRed'
+  FAILED: 'red',
+  UNKNOWN: 'default',
+  CRASHING: 'red'
+}
+
+export const INSTANCE_LIFECYCLE_COLOR: { [key: string]: string } = {
+  STARTING: 'gold',
+  SERVING: 'green',
+  DRAINING: 'orange',
+  TERMINATING: 'red',
+  ERROR: 'red',
+  UNKNOWN: 'default'
 }
diff --git a/ui-vue3/src/base/i18n/en.ts b/ui-vue3/src/base/i18n/en.ts
index f81082e9..777c7dc9 100644
--- a/ui-vue3/src/base/i18n/en.ts
+++ b/ui-vue3/src/base/i18n/en.ts
@@ -172,6 +172,7 @@ const words: I18nType = {
     instanceName: 'InstanceName',
     ip: 'Ip',
     name: 'Name',
+    lifecycleState: 'Lifecycle State',
     deployState: 'Deploy State',
     deployCluster: 'Deploy Cluster',
     deployClusters: 'Deploy Clusters',
@@ -564,6 +565,7 @@ const words: I18nType = {
   dependentService: 'Dependent Service',
   submit: 'Submit',
   reset: 'Reset',
+  refresh: 'Refresh',
   router: {
     resource: {
       app: {
diff --git a/ui-vue3/src/base/i18n/zh.ts b/ui-vue3/src/base/i18n/zh.ts
index 3d1c4765..c382c0e9 100644
--- a/ui-vue3/src/base/i18n/zh.ts
+++ b/ui-vue3/src/base/i18n/zh.ts
@@ -194,6 +194,7 @@ const words: I18nType = {
     instanceIP: '实例IP',
     ip: 'IP',
     name: '实例名称',
+    lifecycleState: '生命周期状态',
     deployState: '部署状态',
     deployCluster: '部署集群',
     deployClusters: '部署集群',
@@ -554,6 +555,7 @@ const words: I18nType = {
   idx: '序号',
   submit: '提交',
   reset: '重置',
+  refresh: '刷新',
   router: {
     resource: {
       app: {
diff --git a/ui-vue3/src/components/SearchTable.vue 
b/ui-vue3/src/components/SearchTable.vue
index 5324e6a4..e67424df 100644
--- a/ui-vue3/src/components/SearchTable.vue
+++ b/ui-vue3/src/components/SearchTable.vue
@@ -68,7 +68,7 @@
           </a-form>
         </a-col>
         <a-col :span="6">
-          <a-flex style="justify-content: flex-end">
+          <a-flex style="justify-content: flex-end; align-items: center; gap: 
12px">
             <slot name="customOperation"></slot>
             <div class="common-tool" @click="commonTool.customColumns = 
!commonTool.customColumns">
               <div class="custom-column button">
@@ -221,22 +221,23 @@ function hideColumn(item: any) {
 
   .common-tool {
     margin-top: 5px;
-    width: 100px;
     cursor: pointer;
     position: relative;
+    flex: none;
 
     .button {
-      vertical-align: center;
+      display: flex;
+      align-items: center;
+      justify-content: center;
       line-height: 24px;
       font-size: 24px;
-      float: right;
 
       &:hover {
         color: v-bind('PRIMARY_COLOR');
       }
 
       svg {
-        margin-left: 10px;
+        margin-left: 0;
       }
     }
 
diff --git a/ui-vue3/src/views/resources/applications/tabs/instance.vue 
b/ui-vue3/src/views/resources/applications/tabs/instance.vue
index ed59e4e0..b75bad3a 100644
--- a/ui-vue3/src/views/resources/applications/tabs/instance.vue
+++ b/ui-vue3/src/views/resources/applications/tabs/instance.vue
@@ -39,13 +39,24 @@
           </a-tooltip>
         </template>
         <template v-if="column.dataIndex === 'deployState'">
-          <a-tag :color="INSTANCE_DEPLOY_COLOR[text.toUpperCase()]">{{ text 
}}</a-tag>
+          <a-tag :color="INSTANCE_DEPLOY_COLOR[(text || 
'UNKNOWN').toUpperCase()] || 'default'">
+            {{ text }}
+          </a-tag>
+        </template>
+        <template v-if="column.dataIndex === 'lifecycleState'">
+          <a-tag :color="INSTANCE_LIFECYCLE_COLOR[(text || 
'UNKNOWN').toUpperCase()] || 'default'">
+            {{ text }}
+          </a-tag>
         </template>
         <template v-if="column.dataIndex === 'deployClusters'">
           <a-tag>{{ text }}</a-tag>
         </template>
         <template v-if="column.dataIndex === 'registerState'">
-          <a-tag :color="INSTANCE_REGISTER_COLOR[text.toUpperCase()]">{{ text 
}}</a-tag>
+          <a-tag
+            :color="INSTANCE_REGISTER_COLOR[(text || 
'UNREGISTERED').toUpperCase()] || 'default'"
+          >
+            {{ text }}
+          </a-tag>
         </template>
         <template v-if="column.dataIndex === 'registerCluster'">
           <a-tag>{{ text }}</a-tag>
@@ -64,7 +75,12 @@
 
 <script setup lang="ts">
 import { onMounted, provide, reactive } from 'vue'
-import { INSTANCE_DEPLOY_COLOR, INSTANCE_REGISTER_COLOR, PRIMARY_COLOR } from 
'@/base/constants'
+import {
+  INSTANCE_DEPLOY_COLOR,
+  INSTANCE_LIFECYCLE_COLOR,
+  INSTANCE_REGISTER_COLOR,
+  PRIMARY_COLOR
+} from '@/base/constants'
 import { Icon } from '@iconify/vue'
 import SearchTable from '@/components/SearchTable.vue'
 import { SearchDomain } from '@/utils/SearchUtil'
@@ -126,6 +142,12 @@ const columns = [
     // sorter: true,
     width: 150
   },
+  {
+    title: 'instanceDomain.lifecycleState',
+    dataIndex: 'lifecycleState',
+    key: 'lifecycleState',
+    width: 150
+  },
   {
     title: 'instanceDomain.deployState',
     dataIndex: 'deployState',
@@ -203,7 +225,7 @@ function instanceInfo(params: any) {
 * on (pod) group_left(pod_ip)
 kube_pod_info{pod_ip="${ip}"}`)
       instance.cpu = isNumber(cpu) ? cpu.toFixed(3) + 'u' : cpu
-      instance.memory = bytesToHuman(mem)
+      instance.memory = isNumber(mem) ? bytesToHuman(mem) : mem
     })
   })
 }
@@ -255,7 +277,7 @@ onMounted(() => {
 })
 
 const viewDetail = (record: any) => {
-  router.push(`/resources/instances/detail/${record.name}/${record.ip}`)
+  
router.push(`/resources/instances/detail/${record.name}/${record.ip}/${record.appName}`)
 }
 
 provide(PROVIDE_INJECT_KEY.SEARCH_DOMAIN, searchDomain)
diff --git a/ui-vue3/src/views/resources/instances/index.vue 
b/ui-vue3/src/views/resources/instances/index.vue
index ae88eb39..6055b087 100644
--- a/ui-vue3/src/views/resources/instances/index.vue
+++ b/ui-vue3/src/views/resources/instances/index.vue
@@ -17,6 +17,18 @@
 <template>
   <div class="instances-container">
     <search-table :search-domain="searchDomain">
+      <template #customOperation>
+        <a-button
+          class="refresh-button"
+          :loading="searchDomain.table.loading"
+          @click="searchDomain.onSearch()"
+        >
+          <template #icon>
+            <Icon icon="material-symbols:refresh-rounded"></Icon>
+          </template>
+          {{ $t('refresh') }}
+        </a-button>
+      </template>
       <template #bodyCell="{ text, record, index, column }">
         <template v-if="column.dataIndex === 'name'">
           <a-tooltip :title="text">
@@ -43,18 +55,14 @@
           <span>{{ text }}</span>
         </template>
 
-        <template v-if="column.dataIndex === 'deployState'">
-          <a-tag :color="INSTANCE_DEPLOY_COLOR[text.toUpperCase()]">{{ text 
}}</a-tag>
-        </template>
-
-        <template v-if="column.dataIndex === 'deployCluster'">
-          <a-tag color="grey">
+        <template v-if="column.dataIndex === 'lifecycleState'">
+          <a-tag :color="INSTANCE_LIFECYCLE_COLOR[(text || 
'UNKNOWN').toUpperCase()] || 'default'">
             {{ text }}
           </a-tag>
         </template>
 
-        <template v-if="column.dataIndex === 'registerState'">
-          <a-tag :color="INSTANCE_REGISTER_COLOR[text.toUpperCase()]">
+        <template v-if="column.dataIndex === 'deployCluster'">
+          <a-tag color="grey">
             {{ text }}
           </a-tag>
         </template>
@@ -83,7 +91,7 @@ import { searchInstances } from '@/api/service/instance'
 import SearchTable from '@/components/SearchTable.vue'
 import { SearchDomain } from '@/utils/SearchUtil'
 import { PROVIDE_INJECT_KEY } from '@/base/enums/ProvideInject'
-import { INSTANCE_DEPLOY_COLOR, INSTANCE_REGISTER_COLOR, PRIMARY_COLOR } from 
'@/base/constants'
+import { INSTANCE_LIFECYCLE_COLOR, PRIMARY_COLOR } from '@/base/constants'
 import router from '@/router'
 import { Icon } from '@iconify/vue'
 import { queryMetrics } from '@/base/http/promQuery'
@@ -110,13 +118,11 @@ let columns = [
     width: 200
   },
   {
-    title: 'instanceDomain.deployState',
-    key: 'deployState',
-    dataIndex: 'deployState',
-    width: 120
-    // sorter: (a: any, b: any) => sortString(a.deployState, b.deployState)
+    title: 'instanceDomain.lifecycleState',
+    key: 'lifecycleState',
+    dataIndex: 'lifecycleState',
+    width: 130
   },
-
   {
     title: 'instanceDomain.deployCluster',
     key: 'deployCluster',
@@ -124,13 +130,6 @@ let columns = [
     // sorter: (a: any, b: any) => sortString(a.deployCluster, 
b.deployCluster),
     width: 120
   },
-  {
-    title: 'instanceDomain.registerState',
-    key: 'registerState',
-    dataIndex: 'registerState',
-    // sorter: (a: any, b: any) => sortString(a.registerState, 
b.registerState),
-    width: 120
-  },
   {
     title: 'instanceDomain.registerCluster',
     key: 'registerClusters',
diff --git 
a/ui-vue3/src/views/resources/instances/slots/InstanceTabHeaderSlot.vue 
b/ui-vue3/src/views/resources/instances/slots/InstanceTabHeaderSlot.vue
index bcfaade9..cf21a233 100644
--- a/ui-vue3/src/views/resources/instances/slots/InstanceTabHeaderSlot.vue
+++ b/ui-vue3/src/views/resources/instances/slots/InstanceTabHeaderSlot.vue
@@ -17,18 +17,53 @@
 <template>
   <!--      example like blow-->
   <div class="__container_AppTabHeaderSlot">
-    <a-row>
-      <a-col :span="12">
+    <a-row :gutter="12" align="middle">
+      <a-col flex="none">
         <span class="header-desc">{{ $t('instanceDomain.name') }}: {{ 
route.params?.name }}</span>
       </a-col>
+      <a-col flex="none">
+        <a-tag :color="lifecycleColor(instanceLifecycleState)">
+          {{ instanceLifecycleState }}
+        </a-tag>
+      </a-col>
     </a-row>
   </div>
 </template>
 
 <script lang="ts" setup>
+import { ref, watch } from 'vue'
 import { useRoute } from 'vue-router'
+import { getInstanceDetail } from '@/api/service/instance'
+import { INSTANCE_LIFECYCLE_COLOR } from '@/base/constants'
 
 const route = useRoute()
+const instanceLifecycleState = ref('Unknown')
+
+const lifecycleColor = (state?: string) => {
+  return INSTANCE_LIFECYCLE_COLOR[(state || 'UNKNOWN').toUpperCase()] || 
'default'
+}
+
+const fetchInstanceLifecycleState = async () => {
+  const instanceName = route.params?.name
+  if (!instanceName) {
+    instanceLifecycleState.value = 'Unknown'
+    return
+  }
+
+  try {
+    const { data } = await getInstanceDetail({
+      instanceName,
+      instanceIP: route.params?.pathId
+    })
+    instanceLifecycleState.value = data?.lifecycleState || 'Unknown'
+  } catch {
+    instanceLifecycleState.value = 'Unknown'
+  }
+}
+
+watch(() => [route.params?.name, route.params?.pathId], 
fetchInstanceLifecycleState, {
+  immediate: true
+})
 </script>
 <style lang="less" scoped>
 .__container_AppTabHeaderSlot {
diff --git a/ui-vue3/src/views/resources/instances/tabs/detail.vue 
b/ui-vue3/src/views/resources/instances/tabs/detail.vue
index 134144ea..495a359b 100644
--- a/ui-vue3/src/views/resources/instances/tabs/detail.vue
+++ b/ui-vue3/src/views/resources/instances/tabs/detail.vue
@@ -51,21 +51,13 @@
           <a-col :span="12">
             <a-card class="_detail" style="height: 100%">
               <a-descriptions class="description-column" :column="1">
-                <!-- deployState -->
                 <a-descriptions-item
                   :label="$t('instanceDomain.deployState')"
                   :labelStyle="{ fontWeight: 'bold' }"
                 >
-                  <a-typography-paragraph
-                    type="success"
-                    style=""
-                    v-if="instanceDetail?.deployState === 'Running'"
-                  >
-                    Running
-                  </a-typography-paragraph>
-                  <a-typography-paragraph type="danger" v-else>
+                  <a-tag :color="deployColor(instanceDetail?.deployState)">
                     {{ instanceDetail?.deployState }}
-                  </a-typography-paragraph>
+                  </a-tag>
                 </a-descriptions-item>
 
                 <!-- Start time -->
@@ -249,7 +241,7 @@ import { type ComponentInternalInstance, 
getCurrentInstance, onMounted, reactive
 import { CopyOutlined } from '@ant-design/icons-vue'
 import useClipboard from 'vue-clipboard3'
 import { message } from 'ant-design-vue'
-import { PRIMARY_COLOR, PRIMARY_COLOR_T } from '@/base/constants'
+import { INSTANCE_DEPLOY_COLOR, PRIMARY_COLOR, PRIMARY_COLOR_T } from 
'@/base/constants'
 import { getInstanceDetail } from '@/api/service/instance'
 import { useRoute, useRouter } from 'vue-router'
 import { formattedDate } from '@/utils/DateUtil'
@@ -296,6 +288,10 @@ function copyIt(v: string) {
 const isProbeOpen = (status: boolean) => {
   return status ? '开启' : '关闭'
 }
+
+const deployColor = (state?: string) => {
+  return INSTANCE_DEPLOY_COLOR[(state || 'UNKNOWN').toUpperCase()] || 'default'
+}
 </script>
 
 <style lang="less" scoped>

Reply via email to