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 }