github.com/oam-dev/kubevela@v1.9.11/pkg/velaql/providers/query/tree.go (about) 1 /* 2 Copyright 2021 The KubeVela Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package query 18 19 import ( 20 "context" 21 "encoding/json" 22 "fmt" 23 "os" 24 "sort" 25 "strconv" 26 "time" 27 28 appsv1 "k8s.io/api/apps/v1" 29 v12 "k8s.io/api/core/v1" 30 kerrors "k8s.io/apimachinery/pkg/api/errors" 31 "k8s.io/apimachinery/pkg/api/meta" 32 v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 33 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 34 "k8s.io/apimachinery/pkg/labels" 35 "k8s.io/apimachinery/pkg/runtime" 36 types2 "k8s.io/apimachinery/pkg/types" 37 "k8s.io/apimachinery/pkg/util/duration" 38 "k8s.io/klog/v2" 39 "k8s.io/kubectl/pkg/util/podutils" 40 41 "sigs.k8s.io/controller-runtime/pkg/client" 42 "sigs.k8s.io/yaml" 43 44 velatypes "github.com/oam-dev/kubevela/apis/types" 45 "github.com/oam-dev/kubevela/pkg/multicluster" 46 "github.com/oam-dev/kubevela/pkg/oam" 47 "github.com/oam-dev/kubevela/pkg/velaql/providers/query/types" 48 49 helmreleaseapi "github.com/fluxcd/helm-controller/api/v2beta1" 50 helmrepoapi "github.com/fluxcd/source-controller/api/v1beta2" 51 ) 52 53 const ( 54 // DefaultMaxDepth is the default max depth for query iterator 55 // check maxDepth function to get the customized val for max depth 56 DefaultMaxDepth = 5 57 ) 58 59 // relationshipKey is the configmap key of relationShip rule 60 var relationshipKey = "rules" 61 62 // RuleList the rule list 63 type RuleList []ChildrenResourcesRule 64 65 // GetRule get the rule by the resource type 66 func (rl *RuleList) GetRule(grt GroupResourceType) (*ChildrenResourcesRule, bool) { 67 for i, r := range *rl { 68 if r.GroupResourceType == grt { 69 return &(*rl)[i], true 70 } 71 } 72 return nil, false 73 } 74 75 // globalRule define the whole relationShip rule 76 var globalRule RuleList 77 78 func init() { 79 globalRule = append(globalRule, 80 ChildrenResourcesRule{ 81 GroupResourceType: GroupResourceType{Group: "apps", Kind: "Deployment"}, 82 SubResources: buildSubResources([]*SubResourceSelector{ 83 { 84 ResourceType: ResourceType{APIVersion: "apps/v1", Kind: "ReplicaSet"}, 85 listOptions: defaultWorkloadLabelListOption, 86 }, 87 }), 88 }, 89 ChildrenResourcesRule{ 90 SubResources: buildSubResources([]*SubResourceSelector{ 91 { 92 ResourceType: ResourceType{APIVersion: "v1", Kind: "Pod"}, 93 listOptions: defaultWorkloadLabelListOption, 94 }, 95 }), 96 GroupResourceType: GroupResourceType{Group: "apps", Kind: "ReplicaSet"}, 97 }, 98 ChildrenResourcesRule{ 99 SubResources: buildSubResources([]*SubResourceSelector{ 100 { 101 ResourceType: ResourceType{APIVersion: "v1", Kind: "Pod"}, 102 listOptions: defaultWorkloadLabelListOption, 103 }, 104 }), 105 GroupResourceType: GroupResourceType{Group: "apps", Kind: "StatefulSet"}, 106 }, 107 ChildrenResourcesRule{ 108 SubResources: buildSubResources([]*SubResourceSelector{ 109 { 110 ResourceType: ResourceType{APIVersion: "v1", Kind: "Pod"}, 111 listOptions: defaultWorkloadLabelListOption, 112 }, 113 }), 114 GroupResourceType: GroupResourceType{Group: "apps", Kind: "DaemonSet"}, 115 }, 116 ChildrenResourcesRule{ 117 GroupResourceType: GroupResourceType{Group: "batch", Kind: "Job"}, 118 SubResources: buildSubResources([]*SubResourceSelector{ 119 { 120 ResourceType: ResourceType{APIVersion: "v1", Kind: "Pod"}, 121 listOptions: defaultWorkloadLabelListOption, 122 }, 123 }), 124 }, 125 ChildrenResourcesRule{ 126 GroupResourceType: GroupResourceType{Group: "", Kind: "Service"}, 127 SubResources: buildSubResources([]*SubResourceSelector{ 128 { 129 ResourceType: ResourceType{APIVersion: "discovery.k8s.io/v1beta1", Kind: "EndpointSlice"}, 130 }, 131 { 132 ResourceType: ResourceType{APIVersion: "discovery.k8s.io/v1", Kind: "EndpointSlice"}, 133 }, 134 { 135 ResourceType: ResourceType{APIVersion: "v1", Kind: "Endpoints"}, 136 listOptions: service2EndpointListOption, 137 }, 138 }), 139 }, 140 ChildrenResourcesRule{ 141 GroupResourceType: GroupResourceType{Group: "helm.toolkit.fluxcd.io", Kind: "HelmRelease"}, 142 SubResources: buildSubResources([]*SubResourceSelector{ 143 { 144 ResourceType: ResourceType{APIVersion: "apps/v1", Kind: "Deployment"}, 145 }, 146 { 147 ResourceType: ResourceType{APIVersion: "apps/v1", Kind: "StatefulSet"}, 148 }, 149 { 150 ResourceType: ResourceType{APIVersion: "v1", Kind: "ConfigMap"}, 151 }, 152 { 153 ResourceType: ResourceType{APIVersion: "v1", Kind: "Secret"}, 154 }, 155 { 156 ResourceType: ResourceType{APIVersion: "v1", Kind: "Service"}, 157 }, 158 { 159 ResourceType: ResourceType{APIVersion: "v1", Kind: "PersistentVolumeClaim"}, 160 }, 161 { 162 ResourceType: ResourceType{APIVersion: "networking.k8s.io/v1", Kind: "Ingress"}, 163 }, 164 { 165 ResourceType: ResourceType{APIVersion: "gateway.networking.k8s.io/v1beta1", Kind: "HTTPRoute"}, 166 }, 167 { 168 ResourceType: ResourceType{APIVersion: "gateway.networking.k8s.io/v1beta1", Kind: "Gateway"}, 169 }, 170 { 171 ResourceType: ResourceType{APIVersion: "v1", Kind: "ServiceAccount"}, 172 }, 173 { 174 ResourceType: ResourceType{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "Role"}, 175 }, 176 { 177 ResourceType: ResourceType{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "RoleBinding"}, 178 }, 179 }), 180 DefaultGenListOptionFunc: helmRelease2AnyListOption, 181 DisableFilterByOwnerReference: true, 182 }, 183 ChildrenResourcesRule{ 184 GroupResourceType: GroupResourceType{Group: "kustomize.toolkit.fluxcd.io", Kind: "Kustomization"}, 185 SubResources: buildSubResources([]*SubResourceSelector{ 186 { 187 ResourceType: ResourceType{APIVersion: "apps/v1", Kind: "Deployment"}, 188 }, 189 { 190 ResourceType: ResourceType{APIVersion: "apps/v1", Kind: "StatefulSet"}, 191 }, 192 { 193 ResourceType: ResourceType{APIVersion: "v1", Kind: "ConfigMap"}, 194 }, 195 { 196 ResourceType: ResourceType{APIVersion: "v1", Kind: "Secret"}, 197 }, 198 { 199 ResourceType: ResourceType{APIVersion: "v1", Kind: "Service"}, 200 }, 201 { 202 ResourceType: ResourceType{APIVersion: "v1", Kind: "PersistentVolumeClaim"}, 203 }, 204 { 205 ResourceType: ResourceType{APIVersion: "networking.k8s.io/v1", Kind: "Ingress"}, 206 }, 207 { 208 ResourceType: ResourceType{APIVersion: "gateway.networking.k8s.io/v1beta1", Kind: "HTTPRoute"}, 209 }, 210 { 211 ResourceType: ResourceType{APIVersion: "gateway.networking.k8s.io/v1beta1", Kind: "Gateway"}, 212 }, 213 { 214 ResourceType: ResourceType{APIVersion: "v1", Kind: "ServiceAccount"}, 215 }, 216 { 217 ResourceType: ResourceType{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "Role"}, 218 }, 219 { 220 ResourceType: ResourceType{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "RoleBinding"}, 221 }, 222 }), 223 DefaultGenListOptionFunc: kustomization2AnyListOption, 224 DisableFilterByOwnerReference: true, 225 }, 226 ChildrenResourcesRule{ 227 SubResources: buildSubResources([]*SubResourceSelector{ 228 { 229 ResourceType: ResourceType{APIVersion: "batch/v1", Kind: "Job"}, 230 listOptions: cronJobLabelListOption, 231 }, 232 }), 233 GroupResourceType: GroupResourceType{Group: "batch", Kind: "CronJob"}, 234 }, 235 ) 236 } 237 238 // GroupResourceType define the parent resource type 239 type GroupResourceType struct { 240 Group string `json:"group"` 241 Kind string `json:"kind"` 242 } 243 244 // ResourceType define the children resource type 245 type ResourceType struct { 246 APIVersion string `json:"apiVersion,omitempty"` 247 Kind string `json:"kind,omitempty"` 248 } 249 250 // customRule define the customize rule created by user 251 type customRule struct { 252 ParentResourceType *GroupResourceType `json:"parentResourceType,omitempty"` 253 ChildrenResourceType []CustomSelector `json:"childrenResourceType,omitempty"` 254 } 255 256 // CustomSelector the custom resource selector configuration in configmap. support set the default label selector policy 257 type CustomSelector struct { 258 ResourceType `json:",inline"` 259 // defaultLabelSelector means read the label selector condition from the spec.selector. 260 DefaultLabelSelector bool `json:"defaultLabelSelector"` 261 } 262 263 // ChildrenResourcesRule define the relationShip between parentObject and children resource 264 type ChildrenResourcesRule struct { 265 // GroupResourceType the root resource type 266 GroupResourceType GroupResourceType 267 // every subResourceType can have a specified genListOptionFunc. 268 SubResources *SubResources 269 // if specified genListOptionFunc is nil will use use default genListOptionFunc to generate listOption. 270 DefaultGenListOptionFunc genListOptionFunc 271 // DisableFilterByOwnerReference means don't use parent resource's UID filter the result. 272 DisableFilterByOwnerReference bool 273 } 274 275 func buildSubResources(crs []*SubResourceSelector) *SubResources { 276 var cr SubResources = crs 277 return &cr 278 } 279 280 func buildSubResourceSelector(cus CustomSelector) *SubResourceSelector { 281 cr := SubResourceSelector{ 282 ResourceType: cus.ResourceType, 283 } 284 if cus.DefaultLabelSelector { 285 cr.listOptions = defaultWorkloadLabelListOption 286 } 287 return &cr 288 } 289 290 // SubResources the sub resource definitions 291 type SubResources []*SubResourceSelector 292 293 // Get get the sub resource by the resource type 294 func (c *SubResources) Get(rt ResourceType) *SubResourceSelector { 295 for _, r := range *c { 296 if r.ResourceType == rt { 297 return r 298 } 299 } 300 return nil 301 } 302 303 // Put add a sub resource to the list 304 func (c *SubResources) Put(cr *SubResourceSelector) { 305 *c = append(*c, cr) 306 } 307 308 // SubResourceSelector the sub resource selector configuration 309 type SubResourceSelector struct { 310 ResourceType 311 listOptions genListOptionFunc 312 } 313 314 type genListOptionFunc func(unstructured.Unstructured) (client.ListOptions, error) 315 316 // WorkloadUnstructured the workload unstructured, such as Deployment、Job、StatefulSet、ReplicaSet and DaemonSet 317 type WorkloadUnstructured struct { 318 unstructured.Unstructured 319 } 320 321 // GetSelector get the selector from the field path 322 func (w *WorkloadUnstructured) GetSelector(fields ...string) (labels.Selector, error) { 323 value, exist, err := unstructured.NestedFieldNoCopy(w.Object, fields...) 324 if err != nil { 325 return nil, err 326 } 327 if !exist { 328 return labels.Everything(), nil 329 } 330 if v, ok := value.(map[string]interface{}); ok { 331 var selector v1.LabelSelector 332 if err := runtime.DefaultUnstructuredConverter.FromUnstructured(v, &selector); err != nil { 333 return nil, err 334 } 335 return v1.LabelSelectorAsSelector(&selector) 336 } 337 return labels.Everything(), nil 338 } 339 340 func (w *WorkloadUnstructured) convertLabel2Selector(fields ...string) (labels.Selector, error) { 341 value, exist, err := unstructured.NestedFieldNoCopy(w.Object, fields...) 342 if err != nil { 343 return nil, err 344 } 345 if !exist { 346 return labels.Everything(), nil 347 } 348 if v, ok := value.(map[string]interface{}); ok { 349 var selector v1.LabelSelector 350 if err := runtime.DefaultUnstructuredConverter.FromUnstructured(v, &selector.MatchLabels); err != nil { 351 return nil, err 352 } 353 return v1.LabelSelectorAsSelector(&selector) 354 } 355 return labels.Everything(), nil 356 } 357 358 var defaultWorkloadLabelListOption genListOptionFunc = func(obj unstructured.Unstructured) (client.ListOptions, error) { 359 workload := WorkloadUnstructured{obj} 360 deploySelector, err := workload.GetSelector("spec", "selector") 361 if err != nil { 362 return client.ListOptions{}, err 363 } 364 return client.ListOptions{Namespace: obj.GetNamespace(), LabelSelector: deploySelector}, nil 365 } 366 367 var service2EndpointListOption = func(obj unstructured.Unstructured) (client.ListOptions, error) { 368 svc := v12.Service{} 369 err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &svc) 370 if err != nil { 371 return client.ListOptions{}, err 372 } 373 stsSelector, err := v1.LabelSelectorAsSelector(&v1.LabelSelector{MatchLabels: svc.Labels}) 374 if err != nil { 375 return client.ListOptions{}, err 376 } 377 return client.ListOptions{Namespace: svc.Namespace, LabelSelector: stsSelector}, nil 378 } 379 380 var cronJobLabelListOption = func(obj unstructured.Unstructured) (client.ListOptions, error) { 381 workload := WorkloadUnstructured{obj} 382 cronJobSelector, err := workload.convertLabel2Selector("spec", "jobTemplate", "metadata", "labels") 383 if err != nil { 384 return client.ListOptions{}, err 385 } 386 return client.ListOptions{Namespace: obj.GetNamespace(), LabelSelector: cronJobSelector}, nil 387 } 388 389 var helmRelease2AnyListOption = func(obj unstructured.Unstructured) (client.ListOptions, error) { 390 hrSelector, err := v1.LabelSelectorAsSelector(&v1.LabelSelector{MatchLabels: map[string]string{ 391 "helm.toolkit.fluxcd.io/name": obj.GetName(), 392 "helm.toolkit.fluxcd.io/namespace": obj.GetNamespace(), 393 }}) 394 if err != nil { 395 return client.ListOptions{}, err 396 } 397 return client.ListOptions{LabelSelector: hrSelector}, nil 398 } 399 400 var kustomization2AnyListOption = func(obj unstructured.Unstructured) (client.ListOptions, error) { 401 kusSelector, err := v1.LabelSelectorAsSelector(&v1.LabelSelector{MatchLabels: map[string]string{ 402 "kustomize.toolkit.fluxcd.io/name": obj.GetName(), 403 "kustomize.toolkit.fluxcd.io/namespace": obj.GetNamespace(), 404 }}) 405 if err != nil { 406 return client.ListOptions{}, err 407 } 408 return client.ListOptions{LabelSelector: kusSelector}, nil 409 } 410 411 type healthyCheckFunc func(obj unstructured.Unstructured) (*types.HealthStatus, error) 412 413 var checkPodStatus = func(obj unstructured.Unstructured) (*types.HealthStatus, error) { 414 var pod v12.Pod 415 err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &pod) 416 if err != nil { 417 return nil, fmt.Errorf("failed to convert unstructured Pod to typed: %w", err) 418 } 419 420 getFailMessage := func(ctr *v12.ContainerStatus) string { 421 if ctr.State.Terminated != nil { 422 if ctr.State.Terminated.Message != "" { 423 return ctr.State.Terminated.Message 424 } 425 if ctr.State.Terminated.Reason == "OOMKilled" { 426 return ctr.State.Terminated.Reason 427 } 428 if ctr.State.Terminated.ExitCode != 0 { 429 return fmt.Sprintf("container %q failed with exit code %d", ctr.Name, ctr.State.Terminated.ExitCode) 430 } 431 } 432 return "" 433 } 434 435 switch pod.Status.Phase { 436 case v12.PodSucceeded: 437 return &types.HealthStatus{ 438 Status: types.HealthStatusHealthy, 439 Reason: pod.Status.Reason, 440 Message: pod.Status.Message, 441 }, nil 442 case v12.PodRunning: 443 switch pod.Spec.RestartPolicy { 444 case v12.RestartPolicyAlways: 445 // if pod is ready, it is automatically healthy 446 if podutils.IsPodReady(&pod) { 447 return &types.HealthStatus{ 448 Status: types.HealthStatusHealthy, 449 Reason: "all containers are ready", 450 }, nil 451 } 452 // if it's not ready, check to see if any container terminated, if so, it's unhealthy 453 for _, ctrStatus := range pod.Status.ContainerStatuses { 454 if ctrStatus.LastTerminationState.Terminated != nil { 455 return &types.HealthStatus{ 456 Status: types.HealthStatusUnHealthy, 457 Reason: pod.Status.Reason, 458 Message: pod.Status.Message, 459 }, nil 460 } 461 } 462 // otherwise we are progressing towards a ready state 463 return &types.HealthStatus{ 464 Status: types.HealthStatusProgressing, 465 Reason: pod.Status.Reason, 466 Message: pod.Status.Message, 467 }, nil 468 case v12.RestartPolicyOnFailure, v12.RestartPolicyNever: 469 // pods set with a restart policy of OnFailure or Never, have a finite life. 470 // These pods are typically resource hooks. Thus, we consider these as Progressing 471 // instead of healthy. 472 return &types.HealthStatus{ 473 Status: types.HealthStatusProgressing, 474 Reason: pod.Status.Reason, 475 Message: pod.Status.Message, 476 }, nil 477 } 478 case v12.PodPending: 479 return &types.HealthStatus{ 480 Status: types.HealthStatusProgressing, 481 Message: pod.Status.Message, 482 }, nil 483 case v12.PodFailed: 484 if pod.Status.Message != "" { 485 // Pod has a nice error message. Use that. 486 return &types.HealthStatus{Status: types.HealthStatusUnHealthy, Message: pod.Status.Message}, nil 487 } 488 for _, ctr := range append(pod.Status.InitContainerStatuses, pod.Status.ContainerStatuses...) { 489 if msg := getFailMessage(ctr.DeepCopy()); msg != "" { 490 return &types.HealthStatus{Status: types.HealthStatusUnHealthy, Message: msg}, nil 491 } 492 } 493 return &types.HealthStatus{Status: types.HealthStatusUnHealthy, Message: ""}, nil 494 default: 495 } 496 return &types.HealthStatus{ 497 Status: types.HealthStatusUnKnown, 498 Reason: string(pod.Status.Phase), 499 Message: pod.Status.Message, 500 }, nil 501 } 502 503 var checkHelmReleaseStatus = func(obj unstructured.Unstructured) (*types.HealthStatus, error) { 504 helmRelease := &helmreleaseapi.HelmRelease{} 505 err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &helmRelease) 506 if err != nil { 507 return nil, fmt.Errorf("failed to convert unstructured helmRelease to typed: %w", err) 508 } 509 if len(helmRelease.Status.Conditions) != 0 { 510 for _, condition := range helmRelease.Status.Conditions { 511 if condition.Type == "Ready" { 512 if condition.Status == v1.ConditionTrue { 513 return &types.HealthStatus{ 514 Status: types.HealthStatusHealthy, 515 }, nil 516 } 517 return &types.HealthStatus{ 518 Status: types.HealthStatusUnHealthy, 519 Message: condition.Message, 520 }, nil 521 } 522 } 523 } 524 return &types.HealthStatus{ 525 Status: types.HealthStatusUnKnown, 526 }, nil 527 } 528 529 var checkHelmRepoStatus = func(obj unstructured.Unstructured) (*types.HealthStatus, error) { 530 helmRepo := helmrepoapi.HelmRepository{} 531 err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &helmRepo) 532 if err != nil { 533 return nil, fmt.Errorf("failed to convert unstructured helmRelease to typed: %w", err) 534 } 535 if len(helmRepo.Status.Conditions) != 0 { 536 for _, condition := range helmRepo.Status.Conditions { 537 if condition.Type == "Ready" { 538 if condition.Status == v1.ConditionTrue { 539 return &types.HealthStatus{ 540 Status: types.HealthStatusHealthy, 541 Message: condition.Message, 542 }, nil 543 } 544 return &types.HealthStatus{ 545 Status: types.HealthStatusUnHealthy, 546 Message: condition.Message, 547 }, nil 548 } 549 } 550 } 551 return &types.HealthStatus{ 552 Status: types.HealthStatusUnKnown, 553 }, nil 554 } 555 556 var checkReplicaSetStatus = func(obj unstructured.Unstructured) (*types.HealthStatus, error) { 557 replicaSet := appsv1.ReplicaSet{} 558 err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &replicaSet) 559 if err != nil { 560 return nil, fmt.Errorf("failed to convert unstructured ReplicaSet to typed: %w", err) 561 } 562 if replicaSet.Generation <= replicaSet.Status.ObservedGeneration { 563 cond := getAppsv1ReplicaSetCondition(replicaSet.Status, appsv1.ReplicaSetReplicaFailure) 564 if cond != nil && cond.Status == v12.ConditionTrue { 565 return &types.HealthStatus{ 566 Status: types.HealthStatusUnHealthy, 567 Reason: cond.Reason, 568 Message: cond.Message, 569 }, nil 570 } else if replicaSet.Spec.Replicas != nil && replicaSet.Status.AvailableReplicas < *replicaSet.Spec.Replicas { 571 return &types.HealthStatus{ 572 Status: types.HealthStatusProgressing, 573 Message: fmt.Sprintf("Waiting for rollout to finish: %d out of %d new replicas are available...", replicaSet.Status.AvailableReplicas, *replicaSet.Spec.Replicas), 574 }, nil 575 } 576 } else { 577 return &types.HealthStatus{ 578 Status: types.HealthStatusProgressing, 579 Message: "Waiting for rollout to finish: observed replica set generation less then desired generation", 580 }, nil 581 } 582 583 return &types.HealthStatus{ 584 Status: types.HealthStatusHealthy, 585 }, nil 586 } 587 588 func getAppsv1ReplicaSetCondition(status appsv1.ReplicaSetStatus, condType appsv1.ReplicaSetConditionType) *appsv1.ReplicaSetCondition { 589 for i := range status.Conditions { 590 c := status.Conditions[i] 591 if c.Type == condType { 592 return &c 593 } 594 } 595 return nil 596 } 597 598 var checkPVCHealthStatus = func(obj unstructured.Unstructured) (*types.HealthStatus, error) { 599 pvc := v12.PersistentVolumeClaim{} 600 err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &pvc) 601 if err != nil { 602 return nil, fmt.Errorf("failed to convert unstructured PVC to typed: %w", err) 603 } 604 var status types.HealthStatusCode 605 switch pvc.Status.Phase { 606 case v12.ClaimLost: 607 status = types.HealthStatusUnHealthy 608 case v12.ClaimPending: 609 status = types.HealthStatusProgressing 610 case v12.ClaimBound: 611 status = types.HealthStatusHealthy 612 default: 613 status = types.HealthStatusUnKnown 614 } 615 return &types.HealthStatus{Status: status}, nil 616 } 617 618 var checkServiceStatus = func(obj unstructured.Unstructured) (*types.HealthStatus, error) { 619 svc := v12.Service{} 620 err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &svc) 621 if err != nil { 622 return nil, fmt.Errorf("failed to convert unstructured service to typed: %w", err) 623 } 624 health := types.HealthStatus{Status: types.HealthStatusHealthy} 625 if svc.Spec.Type == v12.ServiceTypeLoadBalancer { 626 if len(svc.Status.LoadBalancer.Ingress) > 0 { 627 health.Status = types.HealthStatusHealthy 628 } else { 629 health.Status = types.HealthStatusProgressing 630 } 631 } 632 return &health, nil 633 } 634 635 // CheckResourceStatus return object status data 636 func CheckResourceStatus(obj unstructured.Unstructured) (*types.HealthStatus, error) { 637 group := obj.GroupVersionKind().Group 638 kind := obj.GroupVersionKind().Kind 639 var checkFunc healthyCheckFunc 640 switch group { 641 case "": 642 switch kind { 643 case "Pod": 644 checkFunc = checkPodStatus 645 case "Service": 646 checkFunc = checkServiceStatus 647 case "PersistentVolumeClaim": 648 checkFunc = checkPVCHealthStatus 649 } 650 case "apps": 651 switch kind { 652 case "ReplicaSet": 653 checkFunc = checkReplicaSetStatus 654 default: 655 } 656 case "helm.toolkit.fluxcd.io": 657 switch kind { 658 case "HelmRelease": 659 checkFunc = checkHelmReleaseStatus 660 default: 661 } 662 case "source.toolkit.fluxcd.io": 663 switch kind { 664 case "HelmRepository": 665 checkFunc = checkHelmRepoStatus 666 default: 667 } 668 default: 669 } 670 if checkFunc != nil { 671 return checkFunc(obj) 672 } 673 return &types.HealthStatus{Status: types.HealthStatusHealthy}, nil 674 } 675 676 type additionalInfoFunc func(obj unstructured.Unstructured) (map[string]interface{}, error) 677 678 func additionalInfo(obj unstructured.Unstructured) (map[string]interface{}, error) { 679 group := obj.GroupVersionKind().Group 680 kind := obj.GroupVersionKind().Kind 681 var infoFunc additionalInfoFunc 682 switch group { 683 case "": 684 switch kind { 685 case "Pod": 686 infoFunc = podAdditionalInfo 687 case "Service": 688 infoFunc = svcAdditionalInfo 689 } 690 case "apps": 691 switch kind { 692 case "Deployment": 693 infoFunc = deploymentAdditionalInfo 694 case "StatefulSet": 695 infoFunc = statefulSetAdditionalInfo 696 default: 697 } 698 default: 699 } 700 if infoFunc != nil { 701 return infoFunc(obj) 702 } 703 return nil, nil 704 } 705 706 func svcAdditionalInfo(obj unstructured.Unstructured) (map[string]interface{}, error) { 707 svc := v12.Service{} 708 err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &svc) 709 if err != nil { 710 return nil, fmt.Errorf("failed to convert unstructured svc to typed: %w", err) 711 } 712 if svc.Spec.Type == v12.ServiceTypeLoadBalancer { 713 var eip string 714 for _, ingress := range svc.Status.LoadBalancer.Ingress { 715 if len(ingress.IP) != 0 { 716 eip = ingress.IP 717 } 718 } 719 if len(eip) == 0 { 720 eip = "pending" 721 } 722 return map[string]interface{}{ 723 "EIP": eip, 724 }, nil 725 } 726 return nil, nil 727 } 728 729 // the logic of this func totaly copy from the source-code of kubernetes tableConvertor 730 // https://github.com/kubernetes/kubernetes/blob/ea0764452222146c47ec826977f49d7001b0ea8c/pkg/printers/internalversion/printers.go#L740 731 // The result is same with the output of kubectl. 732 // nolint 733 func podAdditionalInfo(obj unstructured.Unstructured) (map[string]interface{}, error) { 734 pod := v12.Pod{} 735 err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &pod) 736 if err != nil { 737 return nil, fmt.Errorf("failed to convert unstructured Pod to typed: %w", err) 738 } 739 740 hasPodReadyCondition := func(conditions []v12.PodCondition) bool { 741 for _, condition := range conditions { 742 if condition.Type == v12.PodReady && condition.Status == v12.ConditionTrue { 743 return true 744 } 745 } 746 return false 747 } 748 749 restarts := 0 750 totalContainers := len(pod.Spec.Containers) 751 readyContainers := 0 752 753 reason := string(pod.Status.Phase) 754 if pod.Status.Reason != "" { 755 reason = pod.Status.Reason 756 } 757 758 initializing := false 759 for i := range pod.Status.InitContainerStatuses { 760 container := pod.Status.InitContainerStatuses[i] 761 restarts += int(container.RestartCount) 762 switch { 763 case container.State.Terminated != nil && container.State.Terminated.ExitCode == 0: 764 continue 765 case container.State.Terminated != nil: 766 // initialization is failed 767 if len(container.State.Terminated.Reason) == 0 { 768 if container.State.Terminated.Signal != 0 { 769 reason = fmt.Sprintf("Init:Signal:%d", container.State.Terminated.Signal) 770 } else { 771 reason = fmt.Sprintf("Init:ExitCode:%d", container.State.Terminated.ExitCode) 772 } 773 } else { 774 reason = "Init:" + container.State.Terminated.Reason 775 } 776 initializing = true 777 case container.State.Waiting != nil && len(container.State.Waiting.Reason) > 0 && container.State.Waiting.Reason != "PodInitializing": 778 reason = "Init:" + container.State.Waiting.Reason 779 initializing = true 780 default: 781 reason = fmt.Sprintf("Init:%d/%d", i, len(pod.Spec.InitContainers)) 782 initializing = true 783 } 784 break 785 } 786 if !initializing { 787 restarts = 0 788 hasRunning := false 789 for i := len(pod.Status.ContainerStatuses) - 1; i >= 0; i-- { 790 container := pod.Status.ContainerStatuses[i] 791 792 restarts += int(container.RestartCount) 793 if container.State.Waiting != nil && container.State.Waiting.Reason != "" { 794 reason = container.State.Waiting.Reason 795 } else if container.State.Terminated != nil && container.State.Terminated.Reason != "" { 796 reason = container.State.Terminated.Reason 797 } else if container.State.Terminated != nil && container.State.Terminated.Reason == "" { 798 if container.State.Terminated.Signal != 0 { 799 reason = fmt.Sprintf("Signal:%d", container.State.Terminated.Signal) 800 } else { 801 reason = fmt.Sprintf("ExitCode:%d", container.State.Terminated.ExitCode) 802 } 803 } else if container.Ready && container.State.Running != nil { 804 hasRunning = true 805 readyContainers++ 806 } 807 } 808 809 // change pod status back to "Running" if there is at least one container still reporting as "Running" status 810 if reason == "Completed" && hasRunning { 811 if hasPodReadyCondition(pod.Status.Conditions) { 812 reason = "Running" 813 } else { 814 reason = "NotReady" 815 } 816 } 817 } 818 819 if pod.DeletionTimestamp != nil && pod.Status.Reason == "NodeLost" { 820 reason = "Unknown" 821 } else if pod.DeletionTimestamp != nil { 822 reason = "Terminating" 823 } 824 return map[string]interface{}{ 825 "Ready": fmt.Sprintf("%d/%d", readyContainers, totalContainers), 826 "Status": reason, 827 "Restarts": restarts, 828 "Age": translateTimestampSince(pod.CreationTimestamp), 829 }, nil 830 } 831 832 func deploymentAdditionalInfo(obj unstructured.Unstructured) (map[string]interface{}, error) { 833 deployment := appsv1.Deployment{} 834 err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &deployment) 835 if err != nil { 836 return nil, fmt.Errorf("failed to convert unstructured Deployment to typed: %w", err) 837 } 838 839 readyReplicas := deployment.Status.ReadyReplicas 840 desiredReplicas := deployment.Spec.Replicas 841 updatedReplicas := deployment.Status.UpdatedReplicas 842 availableReplicas := deployment.Status.AvailableReplicas 843 844 return map[string]interface{}{ 845 "Ready": fmt.Sprintf("%d/%d", readyReplicas, *desiredReplicas), 846 "Update": updatedReplicas, 847 "Available": availableReplicas, 848 "Age": translateTimestampSince(deployment.CreationTimestamp), 849 }, nil 850 } 851 852 func statefulSetAdditionalInfo(obj unstructured.Unstructured) (map[string]interface{}, error) { 853 statefulSet := appsv1.StatefulSet{} 854 err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &statefulSet) 855 if err != nil { 856 return nil, fmt.Errorf("failed to convert unstructured StatefulSet to typed: %w", err) 857 } 858 859 readyReplicas := statefulSet.Status.ReadyReplicas 860 desiredReplicas := statefulSet.Spec.Replicas 861 862 return map[string]interface{}{ 863 "Ready": fmt.Sprintf("%d/%d", readyReplicas, *desiredReplicas), 864 "Age": translateTimestampSince(statefulSet.CreationTimestamp), 865 }, nil 866 } 867 868 func fetchObjectWithResourceTreeNode(ctx context.Context, cluster string, k8sClient client.Client, resource types.ResourceTreeNode) (*unstructured.Unstructured, error) { 869 o := unstructured.Unstructured{} 870 o.SetAPIVersion(resource.APIVersion) 871 o.SetKind(resource.Kind) 872 o.SetNamespace(resource.Namespace) 873 o.SetName(resource.Name) 874 err := k8sClient.Get(multicluster.ContextWithClusterName(ctx, cluster), types2.NamespacedName{Namespace: resource.Namespace, Name: resource.Name}, &o) 875 if err != nil { 876 return nil, err 877 } 878 return &o, nil 879 } 880 881 func listItemByRule(clusterCTX context.Context, k8sClient client.Client, resource ResourceType, 882 parentObject unstructured.Unstructured, specifiedFunc genListOptionFunc, defaultFunc genListOptionFunc, disableFilterByOwner bool) ([]unstructured.Unstructured, error) { 883 884 itemList := unstructured.UnstructuredList{} 885 itemList.SetAPIVersion(resource.APIVersion) 886 itemList.SetKind(fmt.Sprintf("%sList", resource.Kind)) 887 var err error 888 if specifiedFunc == nil && defaultFunc == nil { 889 // if the relationShip between parent and child hasn't defined by any genListOption, list all subResource and filter by ownerReference UID 890 err = k8sClient.List(clusterCTX, &itemList) 891 if err != nil { 892 return nil, err 893 } 894 var res []unstructured.Unstructured 895 for _, item := range itemList.Items { 896 for _, reference := range item.GetOwnerReferences() { 897 if reference.UID == parentObject.GetUID() { 898 res = append(res, item) 899 } 900 } 901 } 902 sort.Slice(res, func(i, j int) bool { 903 return res[i].GetName() < res[j].GetName() 904 }) 905 return res, nil 906 } 907 var listOptions client.ListOptions 908 if specifiedFunc != nil { 909 // specified func will override the default func 910 listOptions, err = specifiedFunc(parentObject) 911 if err != nil { 912 return nil, err 913 } 914 } else { 915 listOptions, err = defaultFunc(parentObject) 916 if err != nil { 917 return nil, err 918 } 919 } 920 err = k8sClient.List(clusterCTX, &itemList, &listOptions) 921 if err != nil { 922 return nil, err 923 } 924 if !disableFilterByOwner { 925 var res []unstructured.Unstructured 926 for _, item := range itemList.Items { 927 if len(item.GetOwnerReferences()) == 0 { 928 res = append(res, item) 929 } 930 for _, reference := range item.GetOwnerReferences() { 931 if reference.UID == parentObject.GetUID() { 932 res = append(res, item) 933 } 934 } 935 } 936 return res, nil 937 } 938 sort.Slice(itemList.Items, func(i, j int) bool { 939 return itemList.Items[i].GetName() < itemList.Items[j].GetName() 940 }) 941 return itemList.Items, nil 942 } 943 944 func iterateListSubResources(ctx context.Context, cluster string, k8sClient client.Client, parentResource types.ResourceTreeNode, depth int, filter func(node types.ResourceTreeNode) bool) ([]*types.ResourceTreeNode, error) { 945 if depth > maxDepth() { 946 klog.Warningf("listing application resource tree has reached the max-depth %d parentObject is %v", depth, parentResource) 947 return nil, nil 948 } 949 parentObject, err := fetchObjectWithResourceTreeNode(ctx, cluster, k8sClient, parentResource) 950 if err != nil { 951 return nil, err 952 } 953 group := parentObject.GetObjectKind().GroupVersionKind().Group 954 kind := parentObject.GetObjectKind().GroupVersionKind().Kind 955 956 if rule, ok := globalRule.GetRule(GroupResourceType{Group: group, Kind: kind}); ok { 957 var resList []*types.ResourceTreeNode 958 for i := range *rule.SubResources { 959 resource := (*rule.SubResources)[i].ResourceType 960 specifiedFunc := (*rule.SubResources)[i].listOptions 961 962 clusterCTX := multicluster.ContextWithClusterName(ctx, cluster) 963 items, err := listItemByRule(clusterCTX, k8sClient, resource, *parentObject, specifiedFunc, rule.DefaultGenListOptionFunc, rule.DisableFilterByOwnerReference) 964 if err != nil { 965 if meta.IsNoMatchError(err) || runtime.IsNotRegisteredError(err) || kerrors.IsNotFound(err) { 966 klog.Warningf("ignore list resources: %s as %v", resource.Kind, err) 967 continue 968 } 969 return nil, err 970 } 971 for i, item := range items { 972 rtn := types.ResourceTreeNode{ 973 APIVersion: item.GetAPIVersion(), 974 Kind: item.GroupVersionKind().Kind, 975 Namespace: item.GetNamespace(), 976 Name: item.GetName(), 977 UID: item.GetUID(), 978 Cluster: cluster, 979 Object: items[i], 980 } 981 if _, ok := globalRule.GetRule(GroupResourceType{Group: item.GetObjectKind().GroupVersionKind().Group, Kind: item.GetObjectKind().GroupVersionKind().Kind}); ok { 982 childrenRes, err := iterateListSubResources(ctx, cluster, k8sClient, rtn, depth+1, filter) 983 if err != nil { 984 return nil, err 985 } 986 rtn.LeafNodes = childrenRes 987 } 988 if !filter(rtn) && len(rtn.LeafNodes) == 0 { 989 continue 990 } 991 healthStatus, err := CheckResourceStatus(item) 992 if err != nil { 993 return nil, err 994 } 995 rtn.HealthStatus = *healthStatus 996 addInfo, err := additionalInfo(item) 997 if err != nil { 998 return nil, err 999 } 1000 rtn.CreationTimestamp = item.GetCreationTimestamp().Time 1001 if !item.GetDeletionTimestamp().IsZero() { 1002 rtn.DeletionTimestamp = item.GetDeletionTimestamp().Time 1003 } 1004 rtn.AdditionalInfo = addInfo 1005 resList = append(resList, &rtn) 1006 } 1007 } 1008 return resList, nil 1009 } 1010 return nil, nil 1011 } 1012 1013 // mergeCustomRules merge user defined resource topology rules with the system ones 1014 func mergeCustomRules(ctx context.Context, k8sClient client.Client) error { 1015 rulesList := v12.ConfigMapList{} 1016 if err := k8sClient.List(ctx, &rulesList, client.InNamespace(velatypes.DefaultKubeVelaNS), client.HasLabels{oam.LabelResourceRules}); err != nil { 1017 return client.IgnoreNotFound(err) 1018 } 1019 for _, item := range rulesList.Items { 1020 ruleStr := item.Data[relationshipKey] 1021 var ( 1022 customRules []*customRule 1023 format string 1024 err error 1025 ) 1026 if item.Labels != nil { 1027 format = item.Labels[oam.LabelResourceRuleFormat] 1028 } 1029 switch format { 1030 case oam.ResourceTopologyFormatJSON: 1031 err = json.Unmarshal([]byte(ruleStr), &customRules) 1032 case oam.ResourceTopologyFormatYAML, "": 1033 err = yaml.Unmarshal([]byte(ruleStr), &customRules) 1034 } 1035 if err != nil { 1036 // don't let one miss-config configmap brake whole process 1037 klog.Errorf("relationship rule configmap %s miss config %v", item.Name, err) 1038 continue 1039 } 1040 for _, rule := range customRules { 1041 1042 if cResource, ok := globalRule.GetRule(*rule.ParentResourceType); ok { 1043 for i, resourceType := range rule.ChildrenResourceType { 1044 if cResource.SubResources.Get(resourceType.ResourceType) == nil { 1045 cResource.SubResources.Put(buildSubResourceSelector(rule.ChildrenResourceType[i])) 1046 } 1047 } 1048 } else { 1049 var subResources []*SubResourceSelector 1050 for i := range rule.ChildrenResourceType { 1051 subResources = append(subResources, buildSubResourceSelector(rule.ChildrenResourceType[i])) 1052 } 1053 globalRule = append(globalRule, ChildrenResourcesRule{ 1054 GroupResourceType: *rule.ParentResourceType, 1055 DefaultGenListOptionFunc: nil, 1056 SubResources: buildSubResources(subResources)}) 1057 } 1058 } 1059 } 1060 return nil 1061 } 1062 1063 func translateTimestampSince(timestamp v1.Time) string { 1064 if timestamp.IsZero() { 1065 return "<unknown>" 1066 } 1067 1068 return duration.HumanDuration(time.Since(timestamp.Time)) 1069 } 1070 1071 // check if max depth is provided or return the default max depth 1072 func maxDepth() int { 1073 maxDepth, err := strconv.Atoi(os.Getenv("KUBEVELA_QUERYTREE_MAXDEPTH")) 1074 if err != nil || maxDepth <= 0 { 1075 return DefaultMaxDepth 1076 } 1077 1078 return maxDepth 1079 }