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 }} + + @@ -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' +}