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  }