github.com/oam-dev/kubevela@v1.9.11/references/cli/system.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 cli
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"fmt"
    23  	"strings"
    24  
    25  	"github.com/gosuri/uitable"
    26  	"github.com/oam-dev/cluster-gateway/pkg/generated/clientset/versioned"
    27  	"github.com/pkg/errors"
    28  	"github.com/spf13/cobra"
    29  	v1 "k8s.io/api/apps/v1"
    30  	corev1 "k8s.io/api/core/v1"
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	"k8s.io/apimachinery/pkg/labels"
    33  	"k8s.io/client-go/kubernetes"
    34  	"k8s.io/client-go/rest"
    35  	apiregistrationV1beta "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1beta1"
    36  	apiregistration "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/typed/apiregistration/v1beta1"
    37  	metrics "k8s.io/metrics/pkg/client/clientset/versioned"
    38  	"sigs.k8s.io/yaml"
    39  
    40  	"github.com/oam-dev/kubevela/apis/types"
    41  	"github.com/oam-dev/kubevela/pkg/multicluster"
    42  	"github.com/oam-dev/kubevela/pkg/utils/common"
    43  )
    44  
    45  const (
    46  	// FlagSpecify specifies the deployment name
    47  	FlagSpecify = "specify"
    48  	// FlagOutputFormat specifies the output format. One of: (wide | yaml)
    49  	FlagOutputFormat = "output"
    50  	// APIServiceName is the name of APIService
    51  	APIServiceName = "v1alpha1.cluster.core.oam.dev"
    52  	// UnknownMetric represent that we can't compute the metric data
    53  	UnknownMetric = "N/A"
    54  )
    55  
    56  // NewSystemCommand print system detail info
    57  func NewSystemCommand(c common.Args, order string) *cobra.Command {
    58  	cmd := &cobra.Command{
    59  		Use:   "system",
    60  		Short: "Manage system.",
    61  		Long:  "Manage system, including printing the system deployment information in vela-system namespace and diagnosing the system's health.",
    62  		Example: "# Check all deployments information in all namespaces with label app.kubernetes.io/name=vela-core :\n" +
    63  			"> vela system info\n" +
    64  			"# Specify a deployment name with a namespace to check detail information:\n" +
    65  			"> vela system info -s kubevela-vela-core -n vela-system\n" +
    66  			"# Diagnose the system's health:\n" +
    67  			"> vela system diagnose\n",
    68  		Annotations: map[string]string{
    69  			types.TagCommandType:  types.TypeSystem,
    70  			types.TagCommandOrder: order,
    71  		},
    72  	}
    73  	cmd.AddCommand(
    74  		NewSystemInfoCommand(c),
    75  		NewSystemDiagnoseCommand(c))
    76  	return cmd
    77  }
    78  
    79  // NewSystemInfoCommand prints system detail info
    80  func NewSystemInfoCommand(c common.Args) *cobra.Command {
    81  	cmd := &cobra.Command{
    82  		Use:   "info",
    83  		Short: "Print the system deployment detail information in all namespaces with label app.kubernetes.io/name=vela-core.",
    84  		Long:  "Print the system deployment detail information in all namespaces with label app.kubernetes.io/name=vela-core.",
    85  		Args:  cobra.ExactArgs(0),
    86  		RunE: func(cmd *cobra.Command, args []string) error {
    87  			// Get deploymentName from flag
    88  			deployName, err := cmd.Flags().GetString(FlagSpecify)
    89  			if err != nil {
    90  				return errors.Wrapf(err, "failed to get deployment name flag")
    91  			}
    92  			// Get output format from flag
    93  			outputFormat, err := cmd.Flags().GetString(FlagOutputFormat)
    94  			if err != nil {
    95  				return errors.Wrapf(err, "failed to get output format flag")
    96  			}
    97  			if outputFormat != "" {
    98  				outputFormatOptions := map[string]struct{}{
    99  					"wide": {},
   100  					"yaml": {},
   101  				}
   102  				if _, exist := outputFormatOptions[outputFormat]; !exist {
   103  					return errors.Errorf("Outputformat must in wide | yaml !")
   104  				}
   105  			}
   106  			// Get kube config
   107  			if outputFormat != "" {
   108  				outputFormatOptions := map[string]struct{}{
   109  					"wide": {},
   110  					"yaml": {},
   111  				}
   112  				if _, exist := outputFormatOptions[outputFormat]; !exist {
   113  					return errors.Errorf("Outputformat must in wide | yaml !")
   114  				}
   115  			}
   116  			// Get kube config
   117  			config, err := c.GetConfig()
   118  			if err != nil {
   119  				return err
   120  			}
   121  			// Get clientset
   122  			clientset, err := kubernetes.NewForConfig(config)
   123  			if err != nil {
   124  				return err
   125  			}
   126  			ctx, cancel := context.WithCancel(context.Background())
   127  			defer cancel()
   128  			// Get deploymentsClient in all namespace
   129  			deployments, err := clientset.AppsV1().Deployments(metav1.NamespaceAll).List(
   130  				ctx,
   131  				metav1.ListOptions{
   132  					LabelSelector: "app.kubernetes.io/name=vela-core",
   133  				},
   134  			)
   135  			if err != nil {
   136  				return err
   137  			}
   138  			if deployName != "" {
   139  				// DeployName is not empty, print the specified deployment's information
   140  				found := false
   141  				for _, deployment := range deployments.Items {
   142  					if deployment.Name == deployName {
   143  						table := SpecifiedFormatPrinter(deployment)
   144  						cmd.Println(table.String())
   145  						found = true
   146  						break
   147  					}
   148  				}
   149  				if !found {
   150  					return errors.Errorf("deployment \"%s\" not found", deployName)
   151  				}
   152  			} else {
   153  				// Get metrics clientset
   154  				mc, err := metrics.NewForConfig(config)
   155  				if err != nil {
   156  					return err
   157  				}
   158  				switch outputFormat {
   159  				case "":
   160  					table, err := NormalFormatPrinter(ctx, deployments, mc)
   161  					if err != nil {
   162  						return err
   163  					}
   164  					cmd.Println(table.String())
   165  				case "wide":
   166  					table, err := WideFormatPrinter(ctx, deployments, mc)
   167  					if err != nil {
   168  						return err
   169  					}
   170  					cmd.Println(table.String())
   171  				case "yaml":
   172  					str, err := YamlFormatPrinter(deployments)
   173  					if err != nil {
   174  						return err
   175  					}
   176  					cmd.Println(str)
   177  				}
   178  			}
   179  			return nil
   180  		},
   181  		Annotations: map[string]string{
   182  			types.TagCommandType: types.TypeSystem,
   183  		},
   184  	}
   185  	cmd.Flags().StringP(FlagSpecify, "s", "", "Specify the name of the deployment to check detail information. If empty, it will print all deployments information. Default to be empty.")
   186  	cmd.Flags().StringP(FlagOutputFormat, "o", "", "Specifies the output format. One of: (wide | yaml)")
   187  	return cmd
   188  }
   189  
   190  // SpecifiedFormatPrinter prints the specified deployment's information
   191  func SpecifiedFormatPrinter(deployment v1.Deployment) *uitable.Table {
   192  	table := newUITable().
   193  		AddRow("Name:", deployment.Name).
   194  		AddRow("Namespace:", deployment.Namespace).
   195  		AddRow("CreationTimestamp:", deployment.CreationTimestamp).
   196  		AddRow("Labels:", Map2Str(deployment.Labels)).
   197  		AddRow("Annotations:", Map2Str(deployment.Annotations)).
   198  		AddRow("Selector:", Map2Str(deployment.Spec.Selector.MatchLabels)).
   199  		AddRow("Image:", deployment.Spec.Template.Spec.Containers[0].Image).
   200  		AddRow("Args:", strings.Join(deployment.Spec.Template.Spec.Containers[0].Args, "\n")).
   201  		AddRow("Envs:", GetEnvVariable(deployment.Spec.Template.Spec.Containers[0].Env)).
   202  		AddRow("Limits:", CPUMem(deployment.Spec.Template.Spec.Containers[0].Resources.Limits)).
   203  		AddRow("Requests:", CPUMem(deployment.Spec.Template.Spec.Containers[0].Resources.Requests))
   204  	table.MaxColWidth = 120
   205  	return table
   206  }
   207  
   208  // CPUMem returns the upsage of cpu and memory
   209  func CPUMem(resourceList corev1.ResourceList) string {
   210  	b := new(bytes.Buffer)
   211  	fmt.Fprintf(b, "cpu=%s\n", resourceList.Cpu())
   212  	fmt.Fprintf(b, "memory=%s", resourceList.Memory())
   213  	return b.String()
   214  }
   215  
   216  // Map2Str converts map to string
   217  func Map2Str(m map[string]string) string {
   218  	b := new(bytes.Buffer)
   219  	for key, value := range m {
   220  		fmt.Fprintf(b, "%s=%s\n", key, value)
   221  	}
   222  	if len(b.String()) > 1 {
   223  		return b.String()[:len(b.String())-1]
   224  	}
   225  	return b.String()
   226  }
   227  
   228  // NormalFormatPrinter prints information in format of normal
   229  func NormalFormatPrinter(ctx context.Context, deployments *v1.DeploymentList, mc *metrics.Clientset) (*uitable.Table, error) {
   230  	table := newUITable().AddRow("NAME", "NAMESPACE", "READY PODS", "IMAGE", "CPU(cores)", "MEMORY(bytes)")
   231  	cpuMetricMap, memMetricMap := ComputeMetricByDeploymentName(ctx, deployments, mc)
   232  	for _, deploy := range deployments.Items {
   233  		table.AddRow(
   234  			deploy.Name,
   235  			deploy.Namespace,
   236  			fmt.Sprintf("%d/%d", deploy.Status.ReadyReplicas, deploy.Status.Replicas),
   237  			deploy.Spec.Template.Spec.Containers[0].Image,
   238  			cpuMetricMap[deploy.Name],
   239  			memMetricMap[deploy.Name],
   240  		)
   241  	}
   242  	return table, nil
   243  }
   244  
   245  // WideFormatPrinter prints information in format of wide
   246  func WideFormatPrinter(ctx context.Context, deployments *v1.DeploymentList, mc *metrics.Clientset) (*uitable.Table, error) {
   247  	table := newUITable().AddRow("NAME", "NAMESPACE", "READY PODS", "IMAGE", "CPU(cores)", "MEMORY(bytes)", "ARGS", "ENVS")
   248  	table.MaxColWidth = 100
   249  	cpuMetricMap, memMetricMap := ComputeMetricByDeploymentName(ctx, deployments, mc)
   250  	for _, deploy := range deployments.Items {
   251  		table.AddRow(
   252  			deploy.Name,
   253  			deploy.Namespace,
   254  			fmt.Sprintf("%d/%d", deploy.Status.ReadyReplicas, deploy.Status.Replicas),
   255  			deploy.Spec.Template.Spec.Containers[0].Image,
   256  			cpuMetricMap[deploy.Name],
   257  			memMetricMap[deploy.Name],
   258  			strings.Join(deploy.Spec.Template.Spec.Containers[0].Args, " "),
   259  			limitStringLength(GetEnvVariable(deploy.Spec.Template.Spec.Containers[0].Env), 180),
   260  		)
   261  	}
   262  	return table, nil
   263  }
   264  
   265  // YamlFormatPrinter prints information in format of yaml
   266  func YamlFormatPrinter(deployments *v1.DeploymentList) (string, error) {
   267  	str := ""
   268  	for _, deployment := range deployments.Items {
   269  		// Set ManagedFields to nil because it's too long to read
   270  		deployment.ManagedFields = nil
   271  		deploymentYaml, err := yaml.Marshal(deployment)
   272  		if err != nil {
   273  			return "", err
   274  		}
   275  		str += string(deploymentYaml)
   276  	}
   277  	return str, nil
   278  }
   279  
   280  // ComputeMetricByDeploymentName computes cpu and memory metric of deployment
   281  func ComputeMetricByDeploymentName(ctx context.Context, deployments *v1.DeploymentList, mc *metrics.Clientset) (cpuMetricMap, memMetricMap map[string]string) {
   282  	cpuMetricMap = make(map[string]string)
   283  	memMetricMap = make(map[string]string)
   284  	podMetricsList, err := mc.MetricsV1beta1().PodMetricses(metav1.NamespaceAll).List(
   285  		ctx,
   286  		metav1.ListOptions{},
   287  	)
   288  	if err != nil {
   289  		for _, deploy := range deployments.Items {
   290  			cpuMetricMap[deploy.Name] = UnknownMetric
   291  			memMetricMap[deploy.Name] = UnknownMetric
   292  		}
   293  		return
   294  	}
   295  
   296  	for _, deploy := range deployments.Items {
   297  		cpuUsage, memUsage := int64(0), int64(0)
   298  		for _, pod := range podMetricsList.Items {
   299  			if strings.HasPrefix(pod.Name, deploy.Name) {
   300  				for _, container := range pod.Containers {
   301  					cpuUsage += container.Usage.Cpu().MilliValue()
   302  					memUsage += container.Usage.Memory().Value() / (1024 * 1024)
   303  				}
   304  			}
   305  		}
   306  		cpuMetricMap[deploy.Name] = fmt.Sprintf("%dm", cpuUsage)
   307  		memMetricMap[deploy.Name] = fmt.Sprintf("%dMi", memUsage)
   308  	}
   309  	return
   310  }
   311  
   312  // GetEnvVariable gets the environment variables
   313  func GetEnvVariable(envList []corev1.EnvVar) (envStr string) {
   314  	for _, env := range envList {
   315  		envStr += fmt.Sprintf("%s=%s ", env.Name, env.Value)
   316  	}
   317  	if len(envStr) == 0 {
   318  		return "-"
   319  	}
   320  	return
   321  }
   322  
   323  // NewSystemDiagnoseCommand create command to help user to diagnose system's health
   324  func NewSystemDiagnoseCommand(c common.Args) *cobra.Command {
   325  	cmd := &cobra.Command{
   326  		Use:   "diagnose",
   327  		Short: "Diagnoses system problems.",
   328  		Long:  "Diagnoses system problems.",
   329  		RunE: func(cmd *cobra.Command, args []string) error {
   330  			// Diagnose clusters' health
   331  			fmt.Println("------------------------------------------------------")
   332  			fmt.Println("Diagnosing health of clusters...")
   333  			k8sClient, err := c.GetClient()
   334  			if err != nil {
   335  				return errors.Wrapf(err, "failed to get k8s client")
   336  			}
   337  			clusters, err := multicluster.ListVirtualClusters(context.Background(), k8sClient)
   338  			if err != nil {
   339  				return errors.Wrap(err, "fail to get registered cluster")
   340  			}
   341  			// Get kube config
   342  			config, err := c.GetConfig()
   343  			if err != nil {
   344  				return err
   345  			}
   346  			for _, cluster := range clusters {
   347  				clusterName := cluster.Name
   348  				if clusterName == multicluster.ClusterLocalName {
   349  					continue
   350  				}
   351  				content, err := versioned.NewForConfigOrDie(config).ClusterV1alpha1().ClusterGateways().RESTClient(clusterName).Get().AbsPath("healthz").DoRaw(context.TODO())
   352  				if err != nil {
   353  					return errors.Wrapf(err, "failed connect cluster %s", clusterName)
   354  				}
   355  				cmd.Printf("Connect to cluster %s successfully.\n%s\n", clusterName, string(content))
   356  			}
   357  			fmt.Println("Result: Clusters are fine~")
   358  			fmt.Println("------------------------------------------------------")
   359  			// Diagnoses the link of hub APIServer to cluster-gateway
   360  			fmt.Println("------------------------------------------------------")
   361  			fmt.Println("Diagnosing the link of hub APIServer to cluster-gateway...")
   362  			// Get clientset
   363  			clientset, err := apiregistration.NewForConfig(config)
   364  			if err != nil {
   365  				return err
   366  			}
   367  			ctx, cancel := context.WithCancel(context.Background())
   368  			defer cancel()
   369  			apiService, err := clientset.APIServices().Get(ctx, APIServiceName, metav1.GetOptions{})
   370  			if err != nil {
   371  				return err
   372  			}
   373  			for _, condition := range apiService.Status.Conditions {
   374  				if condition.Type == "Available" {
   375  					if condition.Status != "True" {
   376  						cmd.Printf("APIService \"%s\" is not available! \nMessage: %s\n", APIServiceName, condition.Message)
   377  						return CheckAPIService(ctx, config, apiService)
   378  					}
   379  					cmd.Printf("APIService \"%s\" is available!\n", APIServiceName)
   380  				}
   381  			}
   382  			fmt.Println("Result: The link of hub APIServer to cluster-gateway is fine~")
   383  			fmt.Println("------------------------------------------------------")
   384  			// Todo: Diagnose others
   385  			return nil
   386  		},
   387  		Annotations: map[string]string{
   388  			types.TagCommandType: types.TypeSystem,
   389  		},
   390  	}
   391  	return cmd
   392  }
   393  
   394  // CheckAPIService checks the APIService
   395  func CheckAPIService(ctx context.Context, config *rest.Config, apiService *apiregistrationV1beta.APIService) error {
   396  	svcName := apiService.Spec.Service.Name
   397  	svcNamespace := apiService.Spec.Service.Namespace
   398  	clientset, err := kubernetes.NewForConfig(config)
   399  	if err != nil {
   400  		return err
   401  	}
   402  	svc, err := clientset.CoreV1().Services(svcNamespace).Get(ctx, svcName, metav1.GetOptions{})
   403  	if err != nil {
   404  		return err
   405  	}
   406  	set := labels.Set(svc.Spec.Selector)
   407  	listOptions := metav1.ListOptions{LabelSelector: set.AsSelector().String()}
   408  	pods, err := clientset.CoreV1().Pods(svcNamespace).List(ctx, listOptions)
   409  	if err != nil {
   410  		return err
   411  	}
   412  	if len(pods.Items) == 0 {
   413  		return errors.Errorf("No available pods in %s namespace with label %s.", svcNamespace, set.AsSelector().String())
   414  	}
   415  	for _, pod := range pods.Items {
   416  		for _, status := range pod.Status.ContainerStatuses {
   417  			if !status.Ready {
   418  				for _, condition := range pod.Status.Conditions {
   419  					if condition.Status != "True" {
   420  						return errors.Errorf("Pod %s is not ready. Condition \"%s\" status: %s.", pod.Name, condition.Type, condition.Status)
   421  					}
   422  				}
   423  				return errors.Errorf("Pod %s is not ready.", pod.Name)
   424  			}
   425  		}
   426  	}
   427  	return nil
   428  }