github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cluster/cluster.go (about) 1 /* 2 Copyright (C) 2022-2023 ApeCloud Co., Ltd 3 4 This file is part of KubeBlocks project 5 6 This program is free software: you can redistribute it and/or modify 7 it under the terms of the GNU Affero General Public License as published by 8 the Free Software Foundation, either version 3 of the License, or 9 (at your option) any later version. 10 11 This program is distributed in the hope that it will be useful 12 but WITHOUT ANY WARRANTY; without even the implied warranty of 13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 GNU Affero General Public License for more details. 15 16 You should have received a copy of the GNU Affero General Public License 17 along with this program. If not, see <http://www.gnu.org/licenses/>. 18 */ 19 20 package cluster 21 22 import ( 23 "context" 24 "fmt" 25 "strings" 26 27 corev1 "k8s.io/api/core/v1" 28 apierrors "k8s.io/apimachinery/pkg/api/errors" 29 "k8s.io/apimachinery/pkg/api/meta" 30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 "k8s.io/apimachinery/pkg/runtime" 32 "k8s.io/apimachinery/pkg/runtime/schema" 33 "k8s.io/client-go/dynamic" 34 clientset "k8s.io/client-go/kubernetes" 35 "k8s.io/client-go/kubernetes/scheme" 36 "k8s.io/kubectl/pkg/util/resource" 37 38 appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1" 39 dpv1alpha1 "github.com/1aal/kubeblocks/apis/dataprotection/v1alpha1" 40 "github.com/1aal/kubeblocks/pkg/cli/types" 41 "github.com/1aal/kubeblocks/pkg/cli/util" 42 "github.com/1aal/kubeblocks/pkg/constant" 43 dptypes "github.com/1aal/kubeblocks/pkg/dataprotection/types" 44 ) 45 46 // ConditionsError cluster displays this status on list cmd when the status of ApplyResources or ProvisioningStarted condition is "False". 47 const ConditionsError = "ConditionsError" 48 49 type GetOptions struct { 50 WithClusterDef bool 51 WithClusterVersion bool 52 WithConfigMap bool 53 WithPVC bool 54 WithService bool 55 WithSecret bool 56 WithPod bool 57 WithEvent bool 58 WithDataProtection bool 59 } 60 61 type ObjectsGetter struct { 62 Client clientset.Interface 63 Dynamic dynamic.Interface 64 Name string 65 Namespace string 66 GetOptions 67 } 68 69 func NewClusterObjects() *ClusterObjects { 70 return &ClusterObjects{ 71 Cluster: &appsv1alpha1.Cluster{}, 72 Nodes: []*corev1.Node{}, 73 } 74 } 75 76 func listResources[T any](dynamic dynamic.Interface, gvr schema.GroupVersionResource, ns string, opts metav1.ListOptions, items *[]T) error { 77 if *items == nil { 78 *items = []T{} 79 } 80 obj, err := dynamic.Resource(gvr).Namespace(ns).List(context.TODO(), opts) 81 if err != nil { 82 return err 83 } 84 for _, i := range obj.Items { 85 var object T 86 if err = runtime.DefaultUnstructuredConverter.FromUnstructured(i.Object, &object); err != nil { 87 return err 88 } 89 *items = append(*items, object) 90 } 91 return nil 92 } 93 94 // Get all kubernetes objects belonging to the database cluster 95 func (o *ObjectsGetter) Get() (*ClusterObjects, error) { 96 var err error 97 98 objs := NewClusterObjects() 99 ctx := context.TODO() 100 client := o.Client.CoreV1() 101 getResource := func(gvr schema.GroupVersionResource, name string, ns string, res interface{}) error { 102 obj, err := o.Dynamic.Resource(gvr).Namespace(ns).Get(ctx, name, metav1.GetOptions{}, "") 103 if err != nil { 104 return err 105 } 106 return runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, res) 107 } 108 109 listOpts := func() metav1.ListOptions { 110 return metav1.ListOptions{ 111 LabelSelector: fmt.Sprintf("%s=%s,%s=%s", 112 constant.AppInstanceLabelKey, o.Name, 113 constant.AppManagedByLabelKey, constant.AppName), 114 } 115 } 116 117 // get cluster 118 if err = getResource(types.ClusterGVR(), o.Name, o.Namespace, objs.Cluster); err != nil { 119 return nil, err 120 } 121 122 provisionCondition := meta.FindStatusCondition(objs.Cluster.Status.Conditions, appsv1alpha1.ConditionTypeProvisioningStarted) 123 if provisionCondition != nil && provisionCondition.Status == metav1.ConditionFalse { 124 objs.Cluster.Status.Phase = ConditionsError 125 } 126 127 applyResourcesCondition := meta.FindStatusCondition(objs.Cluster.Status.Conditions, appsv1alpha1.ConditionTypeApplyResources) 128 if applyResourcesCondition != nil && applyResourcesCondition.Status == metav1.ConditionFalse { 129 objs.Cluster.Status.Phase = ConditionsError 130 } 131 // get cluster definition 132 if o.WithClusterDef { 133 cd := &appsv1alpha1.ClusterDefinition{} 134 if err = getResource(types.ClusterDefGVR(), objs.Cluster.Spec.ClusterDefRef, "", cd); err != nil { 135 return nil, err 136 } 137 objs.ClusterDef = cd 138 } 139 140 // get cluster version 141 if o.WithClusterVersion { 142 v := &appsv1alpha1.ClusterVersion{} 143 if err = getResource(types.ClusterVersionGVR(), objs.Cluster.Spec.ClusterVersionRef, "", v); err != nil { 144 return nil, err 145 } 146 objs.ClusterVersion = v 147 } 148 149 // get services 150 if o.WithService { 151 if objs.Services, err = client.Services(o.Namespace).List(ctx, listOpts()); err != nil { 152 return nil, err 153 } 154 } 155 156 // get secrets 157 if o.WithSecret { 158 if objs.Secrets, err = client.Secrets(o.Namespace).List(ctx, listOpts()); err != nil { 159 return nil, err 160 } 161 } 162 163 // get configmaps 164 if o.WithConfigMap { 165 if objs.ConfigMaps, err = client.ConfigMaps(o.Namespace).List(ctx, listOpts()); err != nil { 166 return nil, err 167 } 168 } 169 170 // get PVCs 171 if o.WithPVC { 172 if objs.PVCs, err = client.PersistentVolumeClaims(o.Namespace).List(ctx, listOpts()); err != nil { 173 return nil, err 174 } 175 } 176 177 // get pods 178 if o.WithPod { 179 if objs.Pods, err = client.Pods(o.Namespace).List(ctx, listOpts()); err != nil { 180 return nil, err 181 } 182 var podList []corev1.Pod 183 // filter back-up job pod 184 for _, pod := range objs.Pods.Items { 185 labels := pod.GetLabels() 186 if labels[dptypes.BackupNameLabelKey] == "" { 187 podList = append(podList, pod) 188 } 189 } 190 objs.Pods.Items = podList 191 // get nodes where the pods are located 192 podLoop: 193 for _, pod := range objs.Pods.Items { 194 for _, node := range objs.Nodes { 195 if node.Name == pod.Spec.NodeName { 196 continue podLoop 197 } 198 } 199 200 nodeName := pod.Spec.NodeName 201 if len(nodeName) == 0 { 202 continue 203 } 204 205 node, err := client.Nodes().Get(ctx, nodeName, metav1.GetOptions{}) 206 if err != nil && !apierrors.IsNotFound(err) { 207 return nil, err 208 } 209 210 if node != nil { 211 objs.Nodes = append(objs.Nodes, node) 212 } 213 } 214 } 215 216 // get events 217 if o.WithEvent { 218 // get all events of cluster 219 if objs.Events, err = client.Events(o.Namespace).Search(scheme.Scheme, objs.Cluster); err != nil { 220 return nil, err 221 } 222 223 // get all events of pods 224 for _, pod := range objs.Pods.Items { 225 events, err := client.Events(o.Namespace).Search(scheme.Scheme, &pod) 226 if err != nil { 227 return nil, err 228 } 229 if objs.Events == nil { 230 objs.Events = events 231 } else { 232 objs.Events.Items = append(objs.Events.Items, events.Items...) 233 } 234 } 235 } 236 237 if o.WithDataProtection { 238 dpListOpts := metav1.ListOptions{ 239 LabelSelector: fmt.Sprintf("%s=%s", 240 constant.AppInstanceLabelKey, o.Name), 241 } 242 if err = listResources(o.Dynamic, types.BackupPolicyGVR(), o.Namespace, dpListOpts, &objs.BackupPolicies); err != nil { 243 return nil, err 244 } 245 if err = listResources(o.Dynamic, types.BackupScheduleGVR(), o.Namespace, dpListOpts, &objs.BackupSchedules); err != nil { 246 return nil, err 247 } 248 var backups []dpv1alpha1.Backup 249 if err = listResources(o.Dynamic, types.BackupGVR(), o.Namespace, dpListOpts, &backups); err != nil { 250 return nil, err 251 } 252 // filter backups with cluster uid for excluding same cluster name 253 for _, v := range backups { 254 sourceClusterUID := v.Labels[dptypes.ClusterUIDLabelKey] 255 if sourceClusterUID == "" || sourceClusterUID == string(objs.Cluster.UID) { 256 objs.Backups = append(objs.Backups, v) 257 } 258 } 259 } 260 return objs, nil 261 } 262 263 func (o *ClusterObjects) GetClusterInfo() *ClusterInfo { 264 c := o.Cluster 265 cluster := &ClusterInfo{ 266 Name: c.Name, 267 Namespace: c.Namespace, 268 ClusterVersion: c.Spec.ClusterVersionRef, 269 ClusterDefinition: c.Spec.ClusterDefRef, 270 TerminationPolicy: string(c.Spec.TerminationPolicy), 271 Status: string(c.Status.Phase), 272 CreatedTime: util.TimeFormat(&c.CreationTimestamp), 273 InternalEP: types.None, 274 ExternalEP: types.None, 275 Labels: util.CombineLabels(c.Labels), 276 } 277 278 if o.ClusterDef == nil { 279 return cluster 280 } 281 282 primaryComponent := FindClusterComp(o.Cluster, o.ClusterDef.Spec.ComponentDefs[0].Name) 283 internalEndpoints, externalEndpoints := GetComponentEndpoints(o.Services, primaryComponent) 284 if len(internalEndpoints) > 0 { 285 cluster.InternalEP = strings.Join(internalEndpoints, ",") 286 } 287 if len(externalEndpoints) > 0 { 288 cluster.ExternalEP = strings.Join(externalEndpoints, ",") 289 } 290 return cluster 291 } 292 293 func (o *ClusterObjects) GetComponentInfo() []*ComponentInfo { 294 var comps []*ComponentInfo 295 for _, c := range o.Cluster.Spec.ComponentSpecs { 296 // get all pods belonging to current component 297 var pods []corev1.Pod 298 for _, p := range o.Pods.Items { 299 if n, ok := p.Labels[constant.KBAppComponentLabelKey]; ok && n == c.Name { 300 pods = append(pods, p) 301 } 302 } 303 304 // current component has no derived pods 305 if len(pods) == 0 { 306 continue 307 } 308 309 image := types.None 310 if len(pods) > 0 { 311 image = pods[0].Spec.Containers[0].Image 312 } 313 314 running, waiting, succeeded, failed := util.GetPodStatus(pods) 315 comp := &ComponentInfo{ 316 Name: c.Name, 317 NameSpace: o.Cluster.Namespace, 318 Type: c.ComponentDefRef, 319 Cluster: o.Cluster.Name, 320 Replicas: fmt.Sprintf("%d / %d", c.Replicas, len(pods)), 321 Status: fmt.Sprintf("%d / %d / %d / %d ", running, waiting, succeeded, failed), 322 Image: image, 323 } 324 comp.CPU, comp.Memory = getResourceInfo(c.Resources.Requests, c.Resources.Limits) 325 comp.Storage = o.getStorageInfo(&c) 326 comps = append(comps, comp) 327 } 328 return comps 329 } 330 331 func (o *ClusterObjects) GetInstanceInfo() []*InstanceInfo { 332 var instances []*InstanceInfo 333 for _, pod := range o.Pods.Items { 334 instance := &InstanceInfo{ 335 Name: pod.Name, 336 Namespace: pod.Namespace, 337 Cluster: getLabelVal(pod.Labels, constant.AppInstanceLabelKey), 338 Component: getLabelVal(pod.Labels, constant.KBAppComponentLabelKey), 339 Status: string(pod.Status.Phase), 340 Role: getLabelVal(pod.Labels, constant.RoleLabelKey), 341 AccessMode: getLabelVal(pod.Labels, constant.ConsensusSetAccessModeLabelKey), 342 CreatedTime: util.TimeFormat(&pod.CreationTimestamp), 343 } 344 345 var component *appsv1alpha1.ClusterComponentSpec 346 for i, c := range o.Cluster.Spec.ComponentSpecs { 347 if c.Name == instance.Component { 348 component = &o.Cluster.Spec.ComponentSpecs[i] 349 } 350 } 351 instance.Storage = o.getStorageInfo(component) 352 getInstanceNodeInfo(o.Nodes, &pod, instance) 353 instance.CPU, instance.Memory = getResourceInfo(resource.PodRequestsAndLimits(&pod)) 354 instances = append(instances, instance) 355 } 356 return instances 357 } 358 359 func (o *ClusterObjects) getStorageInfo(component *appsv1alpha1.ClusterComponentSpec) []StorageInfo { 360 if component == nil { 361 return nil 362 } 363 364 getClassName := func(vcTpl *appsv1alpha1.ClusterComponentVolumeClaimTemplate) string { 365 if vcTpl.Spec.StorageClassName != nil { 366 return *vcTpl.Spec.StorageClassName 367 } 368 369 if o.PVCs == nil { 370 return types.None 371 } 372 373 // get storage class name from PVC 374 for _, pvc := range o.PVCs.Items { 375 labels := pvc.Labels 376 if len(labels) == 0 { 377 continue 378 } 379 380 if labels[constant.KBAppComponentLabelKey] != component.Name { 381 continue 382 } 383 384 if labels[constant.VolumeClaimTemplateNameLabelKey] != vcTpl.Name { 385 continue 386 } 387 if pvc.Spec.StorageClassName != nil { 388 return *pvc.Spec.StorageClassName 389 } else { 390 return types.None 391 } 392 } 393 394 return types.None 395 } 396 397 var infos []StorageInfo 398 for _, vcTpl := range component.VolumeClaimTemplates { 399 s := StorageInfo{ 400 Name: vcTpl.Name, 401 } 402 val := vcTpl.Spec.Resources.Requests[corev1.ResourceStorage] 403 s.StorageClass = getClassName(&vcTpl) 404 s.Size = val.String() 405 s.AccessMode = getAccessModes(vcTpl.Spec.AccessModes) 406 infos = append(infos, s) 407 } 408 return infos 409 } 410 411 func getInstanceNodeInfo(nodes []*corev1.Node, pod *corev1.Pod, i *InstanceInfo) { 412 i.Node, i.Region, i.AZ = types.None, types.None, types.None 413 if pod.Spec.NodeName == "" { 414 return 415 } 416 417 i.Node = strings.Join([]string{pod.Spec.NodeName, pod.Status.HostIP}, "/") 418 node := util.GetNodeByName(nodes, pod.Spec.NodeName) 419 if node == nil { 420 return 421 } 422 423 i.Region = getLabelVal(node.Labels, constant.RegionLabelKey) 424 i.AZ = getLabelVal(node.Labels, constant.ZoneLabelKey) 425 } 426 427 func getResourceInfo(reqs, limits corev1.ResourceList) (string, string) { 428 var cpu, mem string 429 names := []corev1.ResourceName{corev1.ResourceCPU, corev1.ResourceMemory} 430 for _, name := range names { 431 res := types.None 432 limit, req := limits[name], reqs[name] 433 434 // if request is empty and limit is not, set limit to request 435 if util.ResourceIsEmpty(&req) && !util.ResourceIsEmpty(&limit) { 436 req = limit 437 } 438 439 // if both limit and request are empty, only output none 440 if !util.ResourceIsEmpty(&limit) || !util.ResourceIsEmpty(&req) { 441 res = fmt.Sprintf("%s / %s", req.String(), limit.String()) 442 } 443 444 switch name { 445 case corev1.ResourceCPU: 446 cpu = res 447 case corev1.ResourceMemory: 448 mem = res 449 } 450 } 451 return cpu, mem 452 } 453 454 func getLabelVal(labels map[string]string, key string) string { 455 val := labels[key] 456 if len(val) == 0 { 457 return types.None 458 } 459 return val 460 } 461 462 func getAccessModes(modes []corev1.PersistentVolumeAccessMode) string { 463 modes = removeDuplicateAccessModes(modes) 464 var modesStr []string 465 if containsAccessMode(modes, corev1.ReadWriteOnce) { 466 modesStr = append(modesStr, "RWO") 467 } 468 if containsAccessMode(modes, corev1.ReadOnlyMany) { 469 modesStr = append(modesStr, "ROX") 470 } 471 if containsAccessMode(modes, corev1.ReadWriteMany) { 472 modesStr = append(modesStr, "RWX") 473 } 474 return strings.Join(modesStr, ",") 475 } 476 477 func removeDuplicateAccessModes(modes []corev1.PersistentVolumeAccessMode) []corev1.PersistentVolumeAccessMode { 478 var accessModes []corev1.PersistentVolumeAccessMode 479 for _, m := range modes { 480 if !containsAccessMode(accessModes, m) { 481 accessModes = append(accessModes, m) 482 } 483 } 484 return accessModes 485 } 486 487 func containsAccessMode(modes []corev1.PersistentVolumeAccessMode, mode corev1.PersistentVolumeAccessMode) bool { 488 for _, m := range modes { 489 if m == mode { 490 return true 491 } 492 } 493 return false 494 }