github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/preflight/collect.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 preflight
    21  
    22  import (
    23  	"context"
    24  	"encoding/json"
    25  	"fmt"
    26  	"reflect"
    27  	"time"
    28  
    29  	"github.com/pkg/errors"
    30  	troubleshoot "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
    31  	pkgcollector "github.com/replicatedhq/troubleshoot/pkg/collect"
    32  	"github.com/replicatedhq/troubleshoot/pkg/logger"
    33  	"github.com/replicatedhq/troubleshoot/pkg/preflight"
    34  	"helm.sh/helm/v3/pkg/cli/values"
    35  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    36  	"k8s.io/apimachinery/pkg/labels"
    37  	"k8s.io/client-go/kubernetes"
    38  	"k8s.io/client-go/kubernetes/scheme"
    39  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    40  	"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
    41  
    42  	preflightv1beta2 "github.com/1aal/kubeblocks/externalapis/preflight/v1beta2"
    43  	kbcollector "github.com/1aal/kubeblocks/pkg/cli/preflight/collector"
    44  	viper "github.com/1aal/kubeblocks/pkg/viperx"
    45  )
    46  
    47  const (
    48  	StorageClassPath       = "cluster-resources/storage-classes.json"
    49  	StorageClassErrorsPath = "cluster-resources/storage-classes-errors.json"
    50  )
    51  
    52  func CollectPreflight(f cmdutil.Factory, helmOpts *values.Options, ctx context.Context, kbPreflight *preflightv1beta2.Preflight, kbHostPreflight *preflightv1beta2.HostPreflight, progressCh chan interface{}) ([]preflight.CollectResult, error) {
    53  	var (
    54  		collectResults []preflight.CollectResult
    55  		err            error
    56  	)
    57  	// deal with preflight
    58  	if kbPreflight != nil && (len(kbPreflight.Spec.ExtendCollectors) > 0 || len(kbPreflight.Spec.Collectors) > 0) {
    59  		res, err := CollectClusterData(ctx, kbPreflight, f, helmOpts, progressCh)
    60  		if err != nil {
    61  			return collectResults, errors.Wrap(err, "failed to collect data in cluster")
    62  		}
    63  		collectResults = append(collectResults, *res)
    64  	}
    65  	// deal with hostPreflight
    66  	if kbHostPreflight != nil {
    67  		if len(kbHostPreflight.Spec.ExtendCollectors) > 0 || len(kbHostPreflight.Spec.Collectors) > 0 {
    68  			res, err := CollectHostData(ctx, kbHostPreflight, progressCh)
    69  			if err != nil {
    70  				return collectResults, errors.Wrap(err, "failed to collect data from extend host")
    71  			}
    72  			collectResults = append(collectResults, *res)
    73  		}
    74  		if len(kbHostPreflight.Spec.RemoteCollectors) > 0 {
    75  			res, err := CollectRemoteData(ctx, kbHostPreflight, f, progressCh)
    76  			if err != nil {
    77  				return collectResults, errors.Wrap(err, "failed to collect data remotely")
    78  			}
    79  			collectResults = append(collectResults, *res)
    80  		}
    81  	}
    82  	return collectResults, err
    83  }
    84  
    85  // CollectHostData transforms the specs of hostPreflight to HostCollector, and sets the collectOpts
    86  func CollectHostData(ctx context.Context, hostPreflight *preflightv1beta2.HostPreflight, progressCh chan interface{}) (*preflight.CollectResult, error) {
    87  	collectOpts := preflight.CollectOpts{
    88  		ProgressChan: progressCh,
    89  	}
    90  	var collectors []pkgcollector.HostCollector
    91  	for _, collectSpec := range hostPreflight.Spec.Collectors {
    92  		collector, ok := pkgcollector.GetHostCollector(collectSpec, "")
    93  		if ok {
    94  			collectors = append(collectors, collector)
    95  		}
    96  	}
    97  	for _, kbCollector := range hostPreflight.Spec.ExtendCollectors {
    98  		collector, ok := kbcollector.GetExtendHostCollector(kbCollector, "")
    99  		if ok {
   100  			collectors = append(collectors, collector)
   101  		}
   102  	}
   103  	collectResults, err := CollectHost(ctx, collectOpts, collectors, hostPreflight)
   104  	if err != nil {
   105  		return nil, errors.Wrap(err, "failed to collect from extend host")
   106  	}
   107  	return &collectResults, nil
   108  }
   109  
   110  // CollectHost collects host data against by HostCollector, and returns the collected data which is encapsulated in CollectResult struct
   111  func CollectHost(ctx context.Context, opts preflight.CollectOpts, collectors []pkgcollector.HostCollector, hostPreflight *preflightv1beta2.HostPreflight) (preflight.CollectResult, error) {
   112  	allCollectedData := make(map[string][]byte)
   113  	collectResult := KBHostCollectResult{
   114  		HostCollectResult: preflight.HostCollectResult{
   115  			Collectors: collectors,
   116  			Context:    ctx,
   117  		},
   118  		AnalyzerSpecs:   hostPreflight.Spec.Analyzers,
   119  		KbAnalyzerSpecs: hostPreflight.Spec.ExtendAnalyzers,
   120  	}
   121  	for _, collector := range collectors {
   122  		isExcluded, _ := collector.IsExcluded()
   123  		if isExcluded {
   124  			continue
   125  		}
   126  		opts.ProgressChan <- fmt.Sprintf("[%s] Running collector...", collector.Title())
   127  		result, err := collector.Collect(opts.ProgressChan)
   128  		if err != nil {
   129  			opts.ProgressChan <- errors.Errorf("failed to run collector: %s: %v", collector.Title(), err)
   130  		}
   131  		for k, v := range result {
   132  			allCollectedData[k] = v
   133  		}
   134  	}
   135  	collectResult.AllCollectedData = allCollectedData
   136  	return collectResult, nil
   137  }
   138  
   139  // CollectClusterData transforms the specs of Preflight to Collector, and sets the collectOpts, such as restConfig, Namespace, and ProgressChan
   140  func CollectClusterData(ctx context.Context, kbPreflight *preflightv1beta2.Preflight, f cmdutil.Factory, helmOpts *values.Options, progressCh chan interface{}) (*preflight.CollectResult, error) {
   141  	var err error
   142  	v := viper.GetViper()
   143  
   144  	collectOpts := preflight.CollectOpts{
   145  		Namespace:              v.GetString("namespace"),
   146  		IgnorePermissionErrors: v.GetBool("collect-without-permissions"),
   147  		ProgressChan:           progressCh,
   148  	}
   149  
   150  	if collectOpts.KubernetesRestConfig, err = f.ToRESTConfig(); err != nil {
   151  		return nil, errors.Wrap(err, "failed to instantiate Kubernetes restconfig")
   152  	}
   153  
   154  	k8sClient, err := f.KubernetesClientSet()
   155  	if err != nil {
   156  		return nil, errors.Wrap(err, "failed to instantiate Kubernetes client")
   157  	}
   158  
   159  	if v.GetString("since") != "" || v.GetString("since-time") != "" {
   160  		err := ParseTimeFlags(v.GetString("since-time"), v.GetString("since"), kbPreflight.Spec.Collectors)
   161  		if err != nil {
   162  			return nil, err
   163  		}
   164  	}
   165  
   166  	collectSpecs := make([]*troubleshoot.Collect, 0, len(kbPreflight.Spec.Collectors))
   167  	collectSpecs = append(collectSpecs, kbPreflight.Spec.Collectors...)
   168  	collectSpecs = pkgcollector.EnsureCollectorInList(
   169  		collectSpecs, troubleshoot.Collect{ClusterInfo: &troubleshoot.ClusterInfo{}},
   170  	)
   171  	collectSpecs = pkgcollector.EnsureCollectorInList(
   172  		collectSpecs, troubleshoot.Collect{ClusterResources: &troubleshoot.ClusterResources{}},
   173  	)
   174  	collectSpecs = pkgcollector.DedupCollectors(collectSpecs)
   175  	collectSpecs = pkgcollector.EnsureClusterResourcesFirst(collectSpecs)
   176  
   177  	var collectors []pkgcollector.Collector
   178  	allCollectorsMap := make(map[reflect.Type][]pkgcollector.Collector)
   179  	for _, collectSpec := range collectSpecs {
   180  		if collectorInterface, ok := pkgcollector.GetCollector(collectSpec, "", collectOpts.Namespace, collectOpts.KubernetesRestConfig, k8sClient, nil); ok {
   181  			if collector, ok := collectorInterface.(pkgcollector.Collector); ok {
   182  				err := collector.CheckRBAC(ctx, collector, collectSpec, collectOpts.KubernetesRestConfig, collectOpts.Namespace)
   183  				if err != nil {
   184  					return nil, errors.Wrap(err, "failed to check RBAC for collectors")
   185  				}
   186  				collectorType := reflect.TypeOf(collector)
   187  				allCollectorsMap[collectorType] = append(allCollectorsMap[collectorType], collector)
   188  			}
   189  		}
   190  	}
   191  	// for _, collectSpec := range kbPreflight.Spec.ExtendCollectors {
   192  	//	// todo user defined cluster collector
   193  	// }
   194  
   195  	collectResults, err := CollectCluster(ctx, collectOpts, collectors, allCollectorsMap, kbPreflight, helmOpts, k8sClient)
   196  	return &collectResults, err
   197  }
   198  
   199  // CollectCluster collects cluster data against by Collector, and returns the collected data which is encapsulated in CollectResult struct
   200  func CollectCluster(ctx context.Context,
   201  	opts preflight.CollectOpts,
   202  	allCollectors []pkgcollector.Collector,
   203  	allCollectorsMap map[reflect.Type][]pkgcollector.Collector,
   204  	kbPreflight *preflightv1beta2.Preflight,
   205  	helmOpts *values.Options,
   206  	client *kubernetes.Clientset,
   207  ) (preflight.CollectResult, error) {
   208  	var foundForbidden bool
   209  	allCollectedData := make(map[string][]byte)
   210  	collectorList := map[string]preflight.CollectorStatus{}
   211  	for _, collectors := range allCollectorsMap {
   212  		if mergeCollector, ok := collectors[0].(pkgcollector.MergeableCollector); ok {
   213  			mergedCollectors, err := mergeCollector.Merge(collectors)
   214  			if err != nil {
   215  				msg := fmt.Sprintf("failed to merge collector: %s: %s", mergeCollector.Title(), err)
   216  				opts.ProgressChan <- msg
   217  			}
   218  			allCollectors = append(allCollectors, mergedCollectors...)
   219  		} else {
   220  			allCollectors = append(allCollectors, collectors...)
   221  		}
   222  
   223  		for _, collector := range collectors {
   224  			for _, e := range collector.GetRBACErrors() {
   225  				foundForbidden = true
   226  				opts.ProgressChan <- e
   227  			}
   228  
   229  			// generate a map of all collectors for atomic status messages
   230  			collectorList[collector.Title()] = preflight.CollectorStatus{
   231  				Status: "pending",
   232  			}
   233  		}
   234  	}
   235  
   236  	collectResult := KBClusterCollectResult{
   237  		ClusterCollectResult: preflight.ClusterCollectResult{
   238  			Collectors: allCollectors,
   239  			Context:    ctx,
   240  		},
   241  		AnalyzerSpecs:   kbPreflight.Spec.Analyzers,
   242  		KbAnalyzerSpecs: kbPreflight.Spec.ExtendAnalyzers,
   243  		HelmOptions:     helmOpts,
   244  	}
   245  
   246  	if foundForbidden && !opts.IgnorePermissionErrors {
   247  		// collectResult.IsRBACAllowed() = false
   248  		return collectResult, errors.New("insufficient permissions to run all collectors")
   249  	}
   250  
   251  	for i, collector := range allCollectors {
   252  		isExcluded, _ := collector.IsExcluded()
   253  		if isExcluded {
   254  			logger.Printf("Excluding %q collector", collector.Title())
   255  			continue
   256  		}
   257  
   258  		// skip collectors with RBAC errors unless its the ClusterResources collector
   259  		if collector.HasRBACErrors() {
   260  			if _, ok := collector.(*pkgcollector.CollectClusterResources); !ok {
   261  				opts.ProgressChan <- fmt.Sprintf("skipping collector %s with insufficient RBAC permissions", collector.Title())
   262  				opts.ProgressChan <- preflight.CollectProgress{
   263  					CurrentName:    collector.Title(),
   264  					CurrentStatus:  "skipped",
   265  					CompletedCount: i + 1,
   266  					TotalCount:     len(allCollectors),
   267  					Collectors:     collectorList,
   268  				}
   269  				continue
   270  			}
   271  		}
   272  
   273  		collectorList[collector.Title()] = preflight.CollectorStatus{
   274  			Status: "running",
   275  		}
   276  		opts.ProgressChan <- preflight.CollectProgress{
   277  			CurrentName:    collector.Title(),
   278  			CurrentStatus:  "running",
   279  			CompletedCount: i,
   280  			TotalCount:     len(allCollectors),
   281  			Collectors:     collectorList,
   282  		}
   283  
   284  		result, err := collector.Collect(opts.ProgressChan)
   285  		if err != nil {
   286  			collectorList[collector.Title()] = preflight.CollectorStatus{
   287  				Status: "failed",
   288  			}
   289  			opts.ProgressChan <- errors.Errorf("failed to run collector: %s: %v", collector.Title(), err)
   290  			opts.ProgressChan <- preflight.CollectProgress{
   291  				CurrentName:    collector.Title(),
   292  				CurrentStatus:  "failed",
   293  				CompletedCount: i + 1,
   294  				TotalCount:     len(allCollectors),
   295  				Collectors:     collectorList,
   296  			}
   297  			continue
   298  		}
   299  
   300  		collectorList[collector.Title()] = preflight.CollectorStatus{
   301  			Status: "completed",
   302  		}
   303  		opts.ProgressChan <- preflight.CollectProgress{
   304  			CurrentName:    collector.Title(),
   305  			CurrentStatus:  "completed",
   306  			CompletedCount: i + 1,
   307  			TotalCount:     len(allCollectors),
   308  			Collectors:     collectorList,
   309  		}
   310  
   311  		for k, v := range result {
   312  			allCollectedData[k] = v
   313  		}
   314  	}
   315  	retryErrorCausedByMetricsUnavailable(ctx, opts, client, allCollectedData)
   316  	collectResult.AllCollectedData = allCollectedData
   317  	return collectResult, nil
   318  }
   319  
   320  func retryErrorCausedByMetricsUnavailable(ctx context.Context, opts preflight.CollectOpts, client *kubernetes.Clientset, data map[string][]byte) {
   321  	handleStorageClassError(ctx, opts, client, data)
   322  }
   323  
   324  func handleStorageClassError(ctx context.Context, _ preflight.CollectOpts, client *kubernetes.Clientset, data map[string][]byte) {
   325  	storageClassError, ok := data[StorageClassErrorsPath]
   326  	if !ok {
   327  		return
   328  	}
   329  	var errorStrs []string
   330  	if err := json.Unmarshal(storageClassError, &errorStrs); err != nil || len(errorStrs) == 0 {
   331  		return
   332  	}
   333  	storageClasses, err := client.StorageV1().StorageClasses().List(ctx, metav1.ListOptions{})
   334  	if err != nil || storageClasses.Items == nil || len(storageClasses.Items) == 0 {
   335  		return
   336  	}
   337  	gvk, err := apiutil.GVKForObject(storageClasses, scheme.Scheme)
   338  	if err == nil {
   339  		storageClasses.GetObjectKind().SetGroupVersionKind(gvk)
   340  	}
   341  
   342  	for i, o := range storageClasses.Items {
   343  		gvk, err := apiutil.GVKForObject(&o, scheme.Scheme)
   344  		if err == nil {
   345  			storageClasses.Items[i].GetObjectKind().SetGroupVersionKind(gvk)
   346  		}
   347  	}
   348  
   349  	scBytes, err := json.MarshalIndent(storageClasses, "", "  ")
   350  	if err != nil {
   351  		return
   352  	}
   353  	data[StorageClassPath] = scBytes
   354  }
   355  
   356  func CollectRemoteData(ctx context.Context, preflightSpec *preflightv1beta2.HostPreflight, f cmdutil.Factory, progressCh chan interface{}) (*preflight.CollectResult, error) {
   357  	v := viper.GetViper()
   358  
   359  	restConfig, err := f.ToRESTConfig()
   360  	if err != nil {
   361  		return nil, errors.Wrap(err, "failed to convert kube flags to rest config")
   362  	}
   363  
   364  	labelSelector, err := labels.Parse(v.GetString("selector"))
   365  	if err != nil {
   366  		return nil, errors.Wrap(err, "unable to parse selector")
   367  	}
   368  
   369  	namespace := v.GetString("namespace")
   370  	if namespace == "" {
   371  		namespace = "default"
   372  	}
   373  
   374  	timeout := v.GetDuration("request-timeout")
   375  	if timeout == 0 {
   376  		timeout = 30 * time.Second
   377  	}
   378  
   379  	collectOpts := preflight.CollectOpts{
   380  		Namespace:              namespace,
   381  		IgnorePermissionErrors: v.GetBool("collect-without-permissions"),
   382  		ProgressChan:           progressCh,
   383  		KubernetesRestConfig:   restConfig,
   384  		Image:                  v.GetString("collector-image"),
   385  		PullPolicy:             v.GetString("collector-pullpolicy"),
   386  		LabelSelector:          labelSelector.String(),
   387  		Timeout:                timeout,
   388  	}
   389  
   390  	collectResults, err := preflight.CollectRemoteWithContext(ctx, collectOpts, ExtractHostPreflightSpec(preflightSpec))
   391  	if err != nil {
   392  		return nil, errors.Wrap(err, "failed to collect from remote")
   393  	}
   394  
   395  	return &collectResults, nil
   396  }
   397  
   398  func ParseTimeFlags(sinceTimeStr, sinceStr string, collectors []*troubleshoot.Collect) error {
   399  	var (
   400  		sinceTime time.Time
   401  		err       error
   402  	)
   403  	if sinceTimeStr != "" {
   404  		if sinceStr != "" {
   405  			return errors.Errorf("at most one of `sinceTime` or `since` may be specified")
   406  		}
   407  		sinceTime, err = time.Parse(time.RFC3339, sinceTimeStr)
   408  		if err != nil {
   409  			return errors.Wrap(err, "unable to parse --since-time flag")
   410  		}
   411  	} else {
   412  		parsedDuration, err := time.ParseDuration(sinceStr)
   413  		if err != nil {
   414  			return errors.Wrap(err, "unable to parse --since flag")
   415  		}
   416  		now := time.Now()
   417  		sinceTime = now.Add(0 - parsedDuration)
   418  	}
   419  	for _, collector := range collectors {
   420  		if collector.Logs != nil {
   421  			if collector.Logs.Limits == nil {
   422  				collector.Logs.Limits = new(troubleshoot.LogLimits)
   423  			}
   424  			collector.Logs.Limits.SinceTime = metav1.NewTime(sinceTime)
   425  		}
   426  	}
   427  	return nil
   428  }