From f408893c2424e86e8090f9299004a0b08238d9ba Mon Sep 17 00:00:00 2001
From: Saramanda9988 <2074730050@qq.com>
Date: Sun, 15 Mar 2026 13:17:50 +0800
Subject: [PATCH 01/12] feat: add lifecycle state management and color coding
for instance statuses
---
pkg/console/model/instance.go | 86 +++++++++++++++----
pkg/core/discovery/subscriber/rpc_instance.go | 12 +++
.../engine/subscriber/runtime_instance.go | 12 +++
.../apis/mesh/v1alpha1/instance_helper.go | 55 ++++++++++++
ui-vue3/src/base/constants.ts | 18 +++-
ui-vue3/src/base/i18n/en.ts | 1 +
ui-vue3/src/base/i18n/zh.ts | 1 +
.../src/views/resources/instances/index.vue | 25 +++++-
.../views/resources/instances/tabs/detail.vue | 35 +++++---
9 files changed, 213 insertions(+), 32 deletions(-)
diff --git a/pkg/console/model/instance.go b/pkg/console/model/instance.go
index e933d7790..749a7422e 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"
@@ -58,6 +59,7 @@ type SearchInstanceResp struct {
Name string `json:"name"`
WorkloadName string `json:"workloadName"`
AppName string `json:"appName"`
+ LifecycleState string `json:"lifecycleState"`
DeployState string `json:"deployState"`
DeployCluster string `json:"deployCluster"`
RegisterState string `json:"registerState"`
@@ -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 = deriveRegisterState(instance)
r.Labels = instance.Tags
- r.DeployState = instance.DeployState
+ r.DeployState = deriveDeployState(instance)
+ r.LifecycleState = deriveLifecycleState(instance, r.DeployState, r.RegisterState)
r.WorkloadName = instance.WorkloadName
r.AppName = instance.AppName
return r
@@ -115,6 +114,7 @@ type InstanceDetailResp struct {
RegisterTime string `json:"registerTime"`
RegisterClusters []string `json:"registerClusters"`
DeployCluster string `json:"deployCluster"`
+ LifecycleState string `json:"lifecycleState"`
DeployState string `json:"deployState"`
RegisterState string `json:"registerState"`
Node string `json:"node"`
@@ -158,16 +158,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 = deriveDeployState(instance)
+ r.RegisterState = deriveRegisterState(instance)
+ r.LifecycleState = deriveLifecycleState(instance, r.DeployState, r.RegisterState)
r.Node = instance.Node
r.Image = instance.Image
r.Probes = ProbeStruct{}
@@ -196,3 +189,64 @@ func FromInstanceResource(res *meshresource.InstanceResource, cfg app.AdminConfi
}
return r
}
+
+func deriveDeployState(instance *meshproto.Instance) string {
+ if instance == nil || strutil.IsBlank(instance.DeployState) {
+ return "Unknown"
+ }
+ switch instance.DeployState {
+ case "Running":
+ if !isPodReady(instance) {
+ return "Starting"
+ }
+ return "Running"
+ default:
+ return instance.DeployState
+ }
+}
+
+func deriveRegisterState(instance *meshproto.Instance) string {
+ if instance == nil || strutil.IsBlank(instance.RegisterTime) {
+ return "UnRegistered"
+ }
+ return "Registered"
+}
+
+func deriveLifecycleState(instance *meshproto.Instance, deployState string, registerState string) string {
+ switch deployState {
+ case "Failed", "Unknown":
+ return "Error"
+ case "Terminating":
+ return "Terminating"
+ }
+
+ if registerState == "Registered" {
+ if deployState == "Running" {
+ return "Serving"
+ }
+ return "Error"
+ }
+
+ if deployState == "Running" && strutil.IsNotBlank(instance.UnregisterTime) {
+ return "Draining"
+ }
+
+ switch deployState {
+ case "Pending", "Starting", "Running":
+ return "Starting"
+ default:
+ return "Unknown"
+ }
+}
+
+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/core/discovery/subscriber/rpc_instance.go b/pkg/core/discovery/subscriber/rpc_instance.go
index e970239dd..a166c8144 100644
--- a/pkg/core/discovery/subscriber/rpc_instance.go
+++ b/pkg/core/discovery/subscriber/rpc_instance.go
@@ -135,6 +135,18 @@ 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
+ }
+ 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
diff --git a/pkg/core/engine/subscriber/runtime_instance.go b/pkg/core/engine/subscriber/runtime_instance.go
index 6038462c0..7a89c6199 100644
--- a/pkg/core/engine/subscriber/runtime_instance.go
+++ b/pkg/core/engine/subscriber/runtime_instance.go
@@ -144,6 +144,18 @@ 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
+ }
+ 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
diff --git a/pkg/core/resource/apis/mesh/v1alpha1/instance_helper.go b/pkg/core/resource/apis/mesh/v1alpha1/instance_helper.go
index ffc72babe..f7b86c346 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/ui-vue3/src/base/constants.ts b/ui-vue3/src/base/constants.ts
index 166772eff..e1c865fe9 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 f81082e98..d67f44902 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',
diff --git a/ui-vue3/src/base/i18n/zh.ts b/ui-vue3/src/base/i18n/zh.ts
index 3d1c47652..13adb99d0 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: '部署集群',
diff --git a/ui-vue3/src/views/resources/instances/index.vue b/ui-vue3/src/views/resources/instances/index.vue
index ae88eb398..f3dfcdafa 100644
--- a/ui-vue3/src/views/resources/instances/index.vue
+++ b/ui-vue3/src/views/resources/instances/index.vue
@@ -43,8 +43,16 @@
{{ text }}
+
+
+ {{ text }}
+
+
+
- {{ text }}
+
+ {{ text }}
+
@@ -54,7 +62,7 @@
-
+
{{ text }}
@@ -83,7 +91,12 @@ 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_DEPLOY_COLOR,
+ INSTANCE_LIFECYCLE_COLOR,
+ INSTANCE_REGISTER_COLOR,
+ PRIMARY_COLOR
+} from '@/base/constants'
import router from '@/router'
import { Icon } from '@iconify/vue'
import { queryMetrics } from '@/base/http/promQuery'
@@ -109,6 +122,12 @@ let columns = [
// sorter: (a: any, b: any) => sortString(a.ip, b.ip),
width: 200
},
+ {
+ title: 'instanceDomain.lifecycleState',
+ key: 'lifecycleState',
+ dataIndex: 'lifecycleState',
+ width: 130
+ },
{
title: 'instanceDomain.deployState',
key: 'deployState',
diff --git a/ui-vue3/src/views/resources/instances/tabs/detail.vue b/ui-vue3/src/views/resources/instances/tabs/detail.vue
index 134144ea7..f6bcc29ce 100644
--- a/ui-vue3/src/views/resources/instances/tabs/detail.vue
+++ b/ui-vue3/src/views/resources/instances/tabs/detail.vue
@@ -52,20 +52,22 @@
+
+
+ {{ instanceDetail?.lifecycleState }}
+
+
+
-
- Running
-
-
+
{{ instanceDetail?.deployState }}
-
+
@@ -249,7 +251,12 @@ 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,
+ INSTANCE_LIFECYCLE_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 +303,14 @@ function copyIt(v: string) {
const isProbeOpen = (status: boolean) => {
return status ? '开启' : '关闭'
}
+
+const deployColor = (state?: string) => {
+ return INSTANCE_DEPLOY_COLOR[(state || 'UNKNOWN').toUpperCase()] || 'default'
+}
+
+const lifecycleColor = (state?: string) => {
+ return INSTANCE_LIFECYCLE_COLOR[(state || 'UNKNOWN').toUpperCase()] || 'default'
+}