github.com/kiali/kiali@v1.84.0/business/workloads.go (about) 1 package business 2 3 import ( 4 "bufio" 5 "context" 6 "encoding/json" 7 "fmt" 8 "io" 9 "net/http" 10 "regexp" 11 "sort" 12 "strconv" 13 "strings" 14 "sync" 15 "time" 16 17 "github.com/nitishm/engarde/pkg/parser" 18 osapps_v1 "github.com/openshift/api/apps/v1" 19 security_v1beta1 "istio.io/client-go/pkg/apis/security/v1beta1" 20 apps_v1 "k8s.io/api/apps/v1" 21 batch_v1 "k8s.io/api/batch/v1" 22 core_v1 "k8s.io/api/core/v1" 23 "k8s.io/apimachinery/pkg/api/errors" 24 meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 "k8s.io/apimachinery/pkg/labels" 26 27 "github.com/kiali/kiali/business/checkers" 28 "github.com/kiali/kiali/config" 29 "github.com/kiali/kiali/kubernetes" 30 "github.com/kiali/kiali/kubernetes/cache" 31 "github.com/kiali/kiali/log" 32 "github.com/kiali/kiali/models" 33 "github.com/kiali/kiali/observability" 34 "github.com/kiali/kiali/prometheus" 35 ) 36 37 func NewWorkloadService(userClients map[string]kubernetes.ClientInterface, prom prometheus.ClientInterface, cache cache.KialiCache, layer *Layer, config *config.Config) *WorkloadService { 38 excludedWorkloads := make(map[string]bool) 39 for _, w := range config.KubernetesConfig.ExcludeWorkloads { 40 excludedWorkloads[w] = true 41 } 42 43 return &WorkloadService{ 44 businessLayer: layer, 45 cache: cache, 46 config: config, 47 excludedWorkloads: excludedWorkloads, 48 prom: prom, 49 userClients: userClients, 50 } 51 } 52 53 // WorkloadService deals with fetching istio/kubernetes workloads related content and convert to kiali model 54 type WorkloadService struct { 55 // Careful not to call the workload service from here as that would be an infinite loop. 56 businessLayer *Layer 57 // The global kiali cache. This should be passed into the workload service rather than created inside of it. 58 cache cache.KialiCache 59 // The global kiali config. 60 config *config.Config 61 excludedWorkloads map[string]bool 62 prom prometheus.ClientInterface 63 userClients map[string]kubernetes.ClientInterface 64 } 65 66 type WorkloadCriteria struct { 67 Cluster string 68 Namespace string 69 WorkloadName string 70 WorkloadType string 71 IncludeIstioResources bool 72 IncludeServices bool 73 IncludeHealth bool 74 RateInterval string 75 QueryTime time.Time 76 } 77 78 // PodLog reports log entries 79 type PodLog struct { 80 Entries []LogEntry `json:"entries,omitempty"` 81 LinesTruncated bool `json:"linesTruncated,omitempty"` 82 } 83 84 // AccessLogEntry provides parsed info from a single proxy access log entry 85 type AccessLogEntry struct { 86 Timestamp string `json:"timestamp,omitempty"` 87 TimestampUnix int64 `json:"timestampUnix,omitempty"` 88 } 89 90 // LogEntry holds a single log entry 91 type LogEntry struct { 92 Message string `json:"message,omitempty"` 93 Severity string `json:"severity,omitempty"` 94 OriginalTime time.Time `json:"-"` 95 Timestamp string `json:"timestamp,omitempty"` 96 TimestampUnix int64 `json:"timestampUnix,omitempty"` 97 AccessLog *parser.AccessLog `json:"accessLog,omitempty"` 98 } 99 100 type filterOpts struct { 101 destWk string 102 destNs string 103 srcWk string 104 srcNs string 105 } 106 107 // LogOptions holds query parameter values 108 type LogOptions struct { 109 Duration *time.Duration 110 LogType models.LogType 111 MaxLines *int 112 core_v1.PodLogOptions 113 filter filterOpts 114 } 115 116 // Matches an ISO8601 full date 117 var severityRegexp = regexp.MustCompile(`(?i)ERROR|WARN|DEBUG|TRACE`) 118 119 func (in *WorkloadService) isWorkloadIncluded(workload string) bool { 120 if in.excludedWorkloads == nil { 121 return true 122 } 123 return !in.excludedWorkloads[workload] 124 } 125 126 // isWorkloadValid returns true if it is a known workload type and it is not configured as excluded 127 func (in *WorkloadService) isWorkloadValid(workloadType string) bool { 128 _, knownWorkloadType := controllerOrder[workloadType] 129 return knownWorkloadType && in.isWorkloadIncluded(workloadType) 130 } 131 132 // @TODO do validations per cluster 133 func (in *WorkloadService) getWorkloadValidations(authpolicies []*security_v1beta1.AuthorizationPolicy, workloadsPerNamespace map[string]models.WorkloadList) models.IstioValidations { 134 validations := checkers.WorkloadChecker{ 135 AuthorizationPolicies: authpolicies, 136 WorkloadsPerNamespace: workloadsPerNamespace, 137 }.Check() 138 139 return validations 140 } 141 142 // GetWorkloadList is the API handler to fetch the list of workloads in a given namespace. 143 func (in *WorkloadService) GetWorkloadList(ctx context.Context, criteria WorkloadCriteria) (models.WorkloadList, error) { 144 var end observability.EndFunc 145 ctx, end = observability.StartSpan(ctx, "GetWorkloadList", 146 observability.Attribute("package", "business"), 147 observability.Attribute("includeHealth", criteria.IncludeHealth), 148 observability.Attribute("includeIstioResources", criteria.IncludeIstioResources), 149 observability.Attribute("cluster", criteria.Cluster), 150 observability.Attribute("namespace", criteria.Namespace), 151 observability.Attribute("rateInterval", criteria.RateInterval), 152 observability.Attribute("queryTime", criteria.QueryTime), 153 ) 154 defer end() 155 156 namespace := criteria.Namespace 157 cluster := criteria.Cluster 158 159 workloadList := &models.WorkloadList{ 160 Namespace: namespace, 161 Workloads: []models.WorkloadListItem{}, 162 Validations: models.IstioValidations{}, 163 } 164 165 if _, ok := in.userClients[cluster]; !ok { 166 return *workloadList, fmt.Errorf("Cluster [%s] is not found or is not accessible for Kiali", cluster) 167 } 168 169 if _, err := in.businessLayer.Namespace.GetClusterNamespace(ctx, namespace, cluster); err != nil { 170 return *workloadList, err 171 } 172 173 var ws models.Workloads 174 // var authpolicies []*security_v1beta1.AuthorizationPolicy 175 var err error 176 177 nFetches := 1 178 if criteria.IncludeIstioResources { 179 nFetches = 2 180 } 181 182 wg := sync.WaitGroup{} 183 wg.Add(nFetches) 184 errChan := make(chan error, nFetches) 185 186 go func(ctx context.Context) { 187 defer wg.Done() 188 var err2 error 189 ws, err2 = in.fetchWorkloadsFromCluster(ctx, cluster, namespace, "") 190 if err2 != nil { 191 log.Errorf("Error fetching Workloads per namespace %s: %s", namespace, err2) 192 errChan <- err2 193 } 194 }(ctx) 195 196 istioConfigCriteria := IstioConfigCriteria{ 197 IncludeAuthorizationPolicies: true, 198 IncludeEnvoyFilters: true, 199 IncludeGateways: true, 200 IncludePeerAuthentications: true, 201 IncludeRequestAuthentications: true, 202 IncludeSidecars: true, 203 } 204 var istioConfigMap models.IstioConfigMap 205 206 if criteria.IncludeIstioResources { 207 go func(ctx context.Context) { 208 defer wg.Done() 209 var err2 error 210 istioConfigMap, err2 = in.businessLayer.IstioConfig.GetIstioConfigMap(ctx, namespace, istioConfigCriteria) 211 if err2 != nil { 212 log.Errorf("Error fetching Istio Config per namespace %s: %s", namespace, err2) 213 errChan <- err2 214 } 215 }(ctx) 216 } 217 218 wg.Wait() 219 if len(errChan) != 0 { 220 err = <-errChan 221 return *workloadList, err 222 } 223 224 for _, w := range ws { 225 wItem := &models.WorkloadListItem{Health: *models.EmptyWorkloadHealth()} 226 wItem.ParseWorkload(w) 227 if istioConfigList, ok := istioConfigMap[cluster]; ok && criteria.IncludeIstioResources { 228 wSelector := labels.Set(wItem.Labels).AsSelector().String() 229 wItem.IstioReferences = FilterUniqueIstioReferences(FilterWorkloadReferences(wSelector, istioConfigList)) 230 } 231 if criteria.IncludeHealth { 232 wItem.Health, err = in.businessLayer.Health.GetWorkloadHealth(ctx, namespace, cluster, wItem.Name, criteria.RateInterval, criteria.QueryTime, w) 233 if err != nil { 234 log.Errorf("Error fetching Health in namespace %s for workload %s: %s", namespace, wItem.Name, err) 235 } 236 } 237 wItem.Cluster = cluster 238 wItem.Namespace = namespace 239 workloadList.Workloads = append(workloadList.Workloads, *wItem) 240 } 241 242 for _, istioConfigList := range istioConfigMap { 243 // @TODO multi cluster validations 244 authpolicies := istioConfigList.AuthorizationPolicies 245 allWorkloads := map[string]models.WorkloadList{} 246 allWorkloads[namespace] = *workloadList 247 validations := in.getWorkloadValidations(authpolicies, allWorkloads) 248 validations.StripIgnoredChecks() 249 workloadList.Validations = workloadList.Validations.MergeValidations(validations) 250 } 251 252 return *workloadList, nil 253 } 254 255 func FilterWorkloadReferences(wSelector string, istioConfigList models.IstioConfigList) []*models.IstioValidationKey { 256 wkdReferences := make([]*models.IstioValidationKey, 0) 257 gwFiltered := kubernetes.FilterGatewaysBySelector(wSelector, istioConfigList.Gateways) 258 for _, g := range gwFiltered { 259 ref := models.BuildKey(g.Kind, g.Name, g.Namespace) 260 exist := false 261 for _, r := range wkdReferences { 262 exist = exist || *r == ref 263 } 264 if !exist { 265 wkdReferences = append(wkdReferences, &ref) 266 } 267 } 268 apFiltered := kubernetes.FilterAuthorizationPoliciesBySelector(wSelector, istioConfigList.AuthorizationPolicies) 269 for _, a := range apFiltered { 270 ref := models.BuildKey(a.Kind, a.Name, a.Namespace) 271 exist := false 272 for _, r := range wkdReferences { 273 exist = exist || *r == ref 274 } 275 if !exist { 276 wkdReferences = append(wkdReferences, &ref) 277 } 278 } 279 paFiltered := kubernetes.FilterPeerAuthenticationsBySelector(wSelector, istioConfigList.PeerAuthentications) 280 for _, p := range paFiltered { 281 ref := models.BuildKey(p.Kind, p.Name, p.Namespace) 282 exist := false 283 for _, r := range wkdReferences { 284 exist = exist || *r == ref 285 } 286 if !exist { 287 wkdReferences = append(wkdReferences, &ref) 288 } 289 } 290 scFiltered := kubernetes.FilterSidecarsBySelector(wSelector, istioConfigList.Sidecars) 291 for _, s := range scFiltered { 292 ref := models.BuildKey(s.Kind, s.Name, s.Namespace) 293 exist := false 294 for _, r := range wkdReferences { 295 exist = exist || *r == ref 296 } 297 if !exist { 298 wkdReferences = append(wkdReferences, &ref) 299 } 300 } 301 raFiltered := kubernetes.FilterRequestAuthenticationsBySelector(wSelector, istioConfigList.RequestAuthentications) 302 for _, ra := range raFiltered { 303 ref := models.BuildKey(ra.Kind, ra.Name, ra.Namespace) 304 exist := false 305 for _, r := range wkdReferences { 306 exist = exist || *r == ref 307 } 308 if !exist { 309 wkdReferences = append(wkdReferences, &ref) 310 } 311 } 312 efFiltered := kubernetes.FilterEnvoyFiltersBySelector(wSelector, istioConfigList.EnvoyFilters) 313 for _, ef := range efFiltered { 314 ref := models.BuildKey(ef.Kind, ef.Name, ef.Namespace) 315 exist := false 316 for _, r := range wkdReferences { 317 exist = exist || *r == ref 318 } 319 if !exist { 320 wkdReferences = append(wkdReferences, &ref) 321 } 322 } 323 return wkdReferences 324 } 325 326 func FilterUniqueIstioReferences(refs []*models.IstioValidationKey) []*models.IstioValidationKey { 327 refMap := make(map[models.IstioValidationKey]struct{}) 328 for _, ref := range refs { 329 if _, exist := refMap[*ref]; !exist { 330 refMap[*ref] = struct{}{} 331 } 332 } 333 filtered := make([]*models.IstioValidationKey, 0) 334 for k := range refMap { 335 filtered = append(filtered, &models.IstioValidationKey{ 336 ObjectType: k.ObjectType, 337 Name: k.Name, 338 Namespace: k.Namespace, 339 }) 340 } 341 return filtered 342 } 343 344 // GetWorkload is the API handler to fetch details of a specific workload. 345 // If includeServices is set true, the Workload will fetch all services related 346 func (in *WorkloadService) GetWorkload(ctx context.Context, criteria WorkloadCriteria) (*models.Workload, error) { 347 var end observability.EndFunc 348 ctx, end = observability.StartSpan(ctx, "GetWorkload", 349 observability.Attribute("package", "business"), 350 observability.Attribute("cluster", criteria.Cluster), 351 observability.Attribute("namespace", criteria.Namespace), 352 observability.Attribute("workloadName", criteria.WorkloadName), 353 observability.Attribute("workloadType", criteria.WorkloadType), 354 observability.Attribute("includeServices", criteria.IncludeServices), 355 observability.Attribute("rateInterval", criteria.RateInterval), 356 observability.Attribute("queryTime", criteria.QueryTime), 357 ) 358 defer end() 359 360 ns, err := in.businessLayer.Namespace.GetClusterNamespace(ctx, criteria.Namespace, criteria.Cluster) 361 if err != nil { 362 return nil, err 363 } 364 365 workload, err2 := in.fetchWorkload(ctx, criteria) 366 367 if err2 != nil { 368 return nil, err2 369 } 370 371 var runtimes []models.Runtime 372 wg := sync.WaitGroup{} 373 wg.Add(1) 374 go func() { 375 defer wg.Done() 376 conf := in.config 377 app := workload.Labels[conf.IstioLabels.AppLabelName] 378 version := workload.Labels[conf.IstioLabels.VersionLabelName] 379 runtimes = NewDashboardsService(ns, workload).GetCustomDashboardRefs(criteria.Namespace, app, version, workload.Pods) 380 }() 381 382 if criteria.IncludeServices { 383 var services *models.ServiceList 384 var err error 385 386 serviceCriteria := ServiceCriteria{ 387 Cluster: criteria.Cluster, 388 Namespace: criteria.Namespace, 389 ServiceSelector: labels.Set(workload.Labels).String(), 390 IncludeHealth: false, 391 IncludeOnlyDefinitions: true, 392 } 393 services, err = in.businessLayer.Svc.GetServiceList(ctx, serviceCriteria) 394 if err != nil { 395 return nil, err 396 } 397 workload.SetServices(services) 398 } 399 400 wg.Wait() 401 workload.Runtimes = runtimes 402 403 return workload, nil 404 } 405 406 func (in *WorkloadService) UpdateWorkload(ctx context.Context, cluster string, namespace string, workloadName string, workloadType string, includeServices bool, jsonPatch string, patchType string) (*models.Workload, error) { 407 var end observability.EndFunc 408 ctx, end = observability.StartSpan(ctx, "UpdateWorkload", 409 observability.Attribute("package", "business"), 410 observability.Attribute("cluster", cluster), 411 observability.Attribute("namespace", namespace), 412 observability.Attribute("workloadName", workloadName), 413 observability.Attribute("workloadType", workloadType), 414 observability.Attribute("includeServices", includeServices), 415 observability.Attribute("jsonPatch", jsonPatch), 416 observability.Attribute("patchType", patchType), 417 ) 418 defer end() 419 420 // Identify controller and apply patch to workload 421 err := in.updateWorkload(ctx, cluster, namespace, workloadName, workloadType, jsonPatch, patchType) 422 if err != nil { 423 return nil, err 424 } 425 426 // Cache is stopped after a Create/Update/Delete operation to force a refresh. 427 // Refresh once after all the updates have gone through since Update Workload will update 428 // every single workload type of that matches name/namespace and we only want to refresh once. 429 cache, err := kialiCache.GetKubeCache(cluster) 430 if err != nil { 431 return nil, err 432 } 433 cache.Refresh(namespace) 434 435 // After the update we fetch the whole workload 436 return in.GetWorkload(ctx, WorkloadCriteria{Cluster: cluster, Namespace: namespace, WorkloadName: workloadName, WorkloadType: workloadType, IncludeServices: includeServices}) 437 } 438 439 func (in *WorkloadService) GetPod(cluster, namespace, name string) (*models.Pod, error) { 440 k8s, ok := in.userClients[cluster] 441 if !ok { 442 return nil, fmt.Errorf("cluster [%s] is not found or is not accessible for Kiali", cluster) 443 } 444 445 // This isn't using the cache for some reason but it never has. 446 p, err := k8s.GetPod(namespace, name) 447 if err != nil { 448 return nil, err 449 } 450 451 pod := models.Pod{} 452 pod.Parse(p) 453 return &pod, nil 454 } 455 456 func (in *WorkloadService) BuildLogOptionsCriteria(container, duration string, logType models.LogType, sinceTime, maxLines string) (*LogOptions, error) { 457 opts := &LogOptions{} 458 opts.PodLogOptions = core_v1.PodLogOptions{Timestamps: true} 459 460 if container != "" { 461 opts.Container = container 462 } 463 464 if duration != "" { 465 duration, err := time.ParseDuration(duration) 466 if err != nil { 467 return nil, fmt.Errorf("invalid duration [%s]: %v", duration, err) 468 } 469 470 opts.Duration = &duration 471 } 472 473 opts.LogType = logType 474 475 if sinceTime != "" { 476 numTime, err := strconv.ParseInt(sinceTime, 10, 64) 477 if err != nil { 478 return nil, fmt.Errorf("invalid sinceTime [%s]: %v", sinceTime, err) 479 } 480 481 opts.SinceTime = &meta_v1.Time{Time: time.Unix(numTime, 0)} 482 } 483 484 if maxLines != "" { 485 if numLines, err := strconv.Atoi(maxLines); err == nil { 486 if numLines > 0 { 487 opts.MaxLines = &numLines 488 } 489 } else { 490 return nil, fmt.Errorf("invalid maxLines [%s]: %v", maxLines, err) 491 } 492 } 493 494 return opts, nil 495 } 496 497 func parseLogLine(line string, isProxy bool, engardeParser *parser.Parser) *LogEntry { 498 entry := LogEntry{ 499 Message: "", 500 Timestamp: "", 501 TimestampUnix: 0, 502 Severity: "INFO", 503 } 504 505 splitted := strings.SplitN(line, " ", 2) 506 if len(splitted) != 2 { 507 log.Debugf("Skipping unexpected log line [%s]", line) 508 return nil 509 } 510 511 // k8s promises RFC3339 or RFC3339Nano timestamp, ensure RFC3339 512 // Split by blanks, to get the miliseconds for sorting, try RFC3339Nano 513 entry.Timestamp = splitted[0] 514 515 entry.Message = strings.TrimSpace(splitted[1]) 516 if entry.Message == "" { 517 log.Debugf("Skipping empty log line [%s]", line) 518 return nil 519 } 520 521 // If we are past the requested time window then stop processing 522 parsedTimestamp, err := time.Parse(time.RFC3339Nano, entry.Timestamp) 523 entry.OriginalTime = parsedTimestamp 524 if err != nil { 525 log.Debugf("Failed to parse log timestamp (skipping) [%s], %s", entry.Timestamp, err.Error()) 526 return nil 527 } 528 529 severity := severityRegexp.FindString(line) 530 if severity != "" { 531 entry.Severity = strings.ToUpper(severity) 532 } 533 534 // If this is an istio access log, then parse it out. Prefer the access log time over the k8s time 535 // as it is the actual time as opposed to the k8s store time. 536 if isProxy { 537 al, err := engardeParser.Parse(entry.Message) 538 // engardeParser.Parse will not throw errors even if no fields 539 // were parsed out. Checking here that some fields were actually 540 // set before setting the AccessLog to an empty object. See issue #4346. 541 if err != nil || isAccessLogEmpty(al) { 542 if err != nil { 543 log.Debugf("AccessLog parse failure: %s", err.Error()) 544 } 545 // try to parse out the time manually 546 tokens := strings.SplitN(entry.Message, " ", 2) 547 timestampToken := strings.Trim(tokens[0], "[]") 548 t, err := time.Parse(time.RFC3339, timestampToken) 549 if err == nil { 550 parsedTimestamp = t 551 } 552 } else { 553 entry.AccessLog = al 554 t, err := time.Parse(time.RFC3339, al.Timestamp) 555 if err == nil { 556 parsedTimestamp = t 557 } 558 559 // clear accessLog fields we don't need in the returned JSON 560 entry.AccessLog.MixerStatus = "" 561 entry.AccessLog.OriginalMessage = "" 562 entry.AccessLog.ParseError = "" 563 } 564 } 565 566 // override the timestamp with a simpler format 567 timestamp := parseTimestamp(parsedTimestamp) 568 entry.Timestamp = timestamp 569 entry.TimestampUnix = parsedTimestamp.UnixMilli() 570 571 return &entry 572 } 573 574 func parseZtunnelLine(line string) *LogEntry { 575 entry := LogEntry{ 576 Message: "", 577 Timestamp: "", 578 TimestampUnix: 0, 579 Severity: "INFO", 580 } 581 582 splitted := strings.SplitN(line, " ", 2) 583 if len(splitted) != 2 { 584 log.Debugf("Skipping unexpected log line [%s]", line) 585 return nil 586 } 587 588 msgSplit := strings.Split(line, "\t") 589 590 if len(msgSplit) < 5 { 591 log.Debugf("Error splitting log line [%s]", line) 592 entry.Message = line 593 return &entry 594 } 595 596 entry.Message = msgSplit[4] 597 if entry.Message == "" { 598 log.Debugf("Skipping empty log line [%s]", line) 599 entry.Message = line 600 return &entry 601 } 602 603 // k8s promises RFC3339 or RFC3339Nano timestamp, ensure RFC3339 604 // Split by blanks, to get the milliseconds for sorting, try RFC3339Nano 605 ts := strings.Split(msgSplit[0], " ") // Sometime timestamp is duplicated 606 entry.Timestamp = ts[0] 607 608 // If we are past the requested time window then stop processing 609 parsedTimestamp, err := time.Parse(time.RFC3339Nano, entry.Timestamp) 610 entry.OriginalTime = parsedTimestamp 611 if err != nil { 612 log.Debugf("Failed to parse log timestamp (skipping) [%s], %s", entry.Timestamp, err.Error()) 613 return nil 614 } 615 616 if splitted[1] != "" { 617 entry.Severity = strings.ToUpper(splitted[1]) 618 } 619 620 // override the timestamp with a simpler format 621 timestamp := parseTimestamp(parsedTimestamp) 622 entry.Timestamp = timestamp 623 entry.TimestampUnix = parsedTimestamp.UnixMilli() 624 625 // Process some access log data 626 // More validations can be done. Data is in format direction=outbound 627 // Also, more data could be added? 628 al := parser.AccessLog{} 629 al.Timestamp = timestamp 630 if len(msgSplit) > 4 { 631 accessLog := strings.Split(msgSplit[4], " ") 632 for _, field := range accessLog { 633 parsed := strings.SplitN(field, "=", 2) 634 if len(parsed) == 2 { 635 parsed[1] = strings.Replace(parsed[1], "\"", "", -1) 636 switch parsed[0] { 637 case "src.identity": 638 al.UpstreamCluster = parsed[1] 639 case "duration": 640 al.Duration = parsed[1] 641 case "bytes_recv": 642 al.BytesReceived = parsed[1] 643 case "bytes_sent": 644 al.BytesSent = parsed[1] 645 case "dst.service": 646 al.RequestedServer = parsed[1] 647 case "error": 648 al.ParseError = parsed[1] 649 case "dst.addr": 650 al.UpstreamService = parsed[1] 651 case "src.addr": 652 al.DownstreamRemote = parsed[1] 653 } 654 } 655 } 656 } 657 658 entry.AccessLog = &al 659 660 return &entry 661 } 662 663 func parseTimestamp(parsedTimestamp time.Time) string { 664 precision := strings.Split(parsedTimestamp.String(), ".") 665 var milliseconds string 666 if len(precision) > 1 { 667 ms := precision[1] 668 milliseconds = ms[:3] 669 splittedms := strings.Fields(milliseconds) // This is needed to avoid invalid dates in ms like 200 670 milliseconds = splittedms[0] 671 } else { 672 milliseconds = "000" 673 } 674 675 timestamp := fmt.Sprintf("%d-%02d-%02d %02d:%02d:%02d.%s", 676 parsedTimestamp.Year(), parsedTimestamp.Month(), parsedTimestamp.Day(), 677 parsedTimestamp.Hour(), parsedTimestamp.Minute(), parsedTimestamp.Second(), milliseconds) 678 return timestamp 679 } 680 681 func isAccessLogEmpty(al *parser.AccessLog) bool { 682 if al == nil { 683 return true 684 } 685 686 return (al.Timestamp == "" && 687 al.Authority == "" && 688 al.BytesReceived == "" && 689 al.BytesSent == "" && 690 al.DownstreamLocal == "" && 691 al.DownstreamRemote == "" && 692 al.Duration == "" && 693 al.ForwardedFor == "" && 694 al.Method == "" && 695 al.MixerStatus == "" && 696 al.Protocol == "" && 697 al.RequestId == "" && 698 al.RequestedServer == "" && 699 al.ResponseFlags == "" && 700 al.RouteName == "" && 701 al.StatusCode == "" && 702 al.TcpServiceTime == "" && 703 al.UpstreamCluster == "" && 704 al.UpstreamFailureReason == "" && 705 al.UpstreamLocal == "" && 706 al.UpstreamService == "" && 707 al.UpstreamServiceTime == "" && 708 al.UriParam == "" && 709 al.UriPath == "" && 710 al.UserAgent == "") 711 } 712 713 func (in *WorkloadService) fetchWorkloads(ctx context.Context, namespace string, labelSelector string) (models.Workloads, error) { 714 allWls := models.Workloads{} 715 for c := range in.userClients { 716 ws, err := in.fetchWorkloadsFromCluster(ctx, c, namespace, labelSelector) 717 if err != nil { 718 if errors.IsNotFound(err) || errors.IsForbidden(err) { 719 // If a cluster is not found or not accessible, then we skip it 720 log.Debugf("Error while accessing to cluster [%s]: %s", c, err.Error()) 721 continue 722 } else { 723 // On any other error, abort and return the error. 724 return nil, err 725 } 726 } else { 727 allWls = append(allWls, ws...) 728 } 729 } 730 731 return allWls, nil 732 } 733 734 func (in *WorkloadService) fetchWorkloadsFromCluster(ctx context.Context, cluster string, namespace string, labelSelector string) (models.Workloads, error) { 735 var pods []core_v1.Pod 736 var repcon []core_v1.ReplicationController 737 var dep []apps_v1.Deployment 738 var repset []apps_v1.ReplicaSet 739 var depcon []osapps_v1.DeploymentConfig 740 var fulset []apps_v1.StatefulSet 741 var jbs []batch_v1.Job 742 var conjbs []batch_v1.CronJob 743 var daeset []apps_v1.DaemonSet 744 745 ws := models.Workloads{} 746 747 // Check if user has access to the namespace (RBAC) in cache scenarios and/or 748 // if namespace is accessible from Kiali (Deployment.AccessibleNamespaces) 749 if _, err := in.businessLayer.Namespace.GetClusterNamespace(ctx, namespace, cluster); err != nil { 750 return nil, err 751 } 752 753 userClient, ok := in.userClients[cluster] 754 if !ok { 755 return nil, fmt.Errorf("Cluster [%s] is not found or is not accessible for Kiali", cluster) 756 } 757 758 kubeCache := in.cache.GetKubeCaches()[cluster] 759 if kubeCache == nil { 760 return nil, fmt.Errorf("Cluster [%s] is not found or is not accessible for Kiali", cluster) 761 } 762 763 wg := sync.WaitGroup{} 764 wg.Add(9) 765 errChan := make(chan error, 9) 766 767 // Pods are always fetched 768 go func() { 769 defer wg.Done() 770 var err error 771 pods, err = kubeCache.GetPods(namespace, labelSelector) 772 if err != nil { 773 log.Errorf("Error fetching Pods per namespace %s: %s", namespace, err) 774 errChan <- err 775 } 776 }() 777 778 // Deployments are always fetched 779 go func() { 780 defer wg.Done() 781 var err error 782 dep, err = kubeCache.GetDeployments(namespace) 783 if err != nil { 784 log.Errorf("Error fetching Deployments per namespace %s: %s", namespace, err) 785 errChan <- err 786 } 787 }() 788 789 // ReplicaSets are always fetched 790 go func() { 791 defer wg.Done() 792 var err error 793 repset, err = kubeCache.GetReplicaSets(namespace) 794 if err != nil { 795 log.Errorf("Error fetching ReplicaSets per namespace %s: %s", namespace, err) 796 errChan <- err 797 } 798 }() 799 800 // ReplicaControllers are fetched only when included 801 go func() { 802 defer wg.Done() 803 804 var err error 805 if in.isWorkloadIncluded(kubernetes.ReplicationControllerType) { 806 // No Cache for ReplicationControllers 807 repcon, err = userClient.GetReplicationControllers(namespace) 808 if err != nil { 809 log.Errorf("Error fetching GetReplicationControllers per namespace %s: %s", namespace, err) 810 errChan <- err 811 } 812 } 813 }() 814 815 // DeploymentConfigs are fetched only when included 816 go func() { 817 defer wg.Done() 818 819 var err error 820 if userClient.IsOpenShift() && in.isWorkloadIncluded(kubernetes.DeploymentConfigType) { 821 // No cache for DeploymentConfigs 822 depcon, err = userClient.GetDeploymentConfigs(ctx, namespace) 823 if err != nil { 824 log.Errorf("Error fetching DeploymentConfigs per namespace %s: %s", namespace, err) 825 errChan <- err 826 } 827 } 828 }() 829 830 // StatefulSets are fetched only when included 831 go func() { 832 defer wg.Done() 833 834 var err error 835 if in.isWorkloadIncluded(kubernetes.StatefulSetType) { 836 fulset, err = kubeCache.GetStatefulSets(namespace) 837 if err != nil { 838 log.Errorf("Error fetching StatefulSets per namespace %s: %s", namespace, err) 839 errChan <- err 840 } 841 } 842 }() 843 844 // CronJobs are fetched only when included 845 go func() { 846 defer wg.Done() 847 848 var err error 849 if in.isWorkloadIncluded(kubernetes.CronJobType) { 850 // No cache for Cronjobs 851 conjbs, err = userClient.GetCronJobs(namespace) 852 if err != nil { 853 log.Errorf("Error fetching CronJobs per namespace %s: %s", namespace, err) 854 errChan <- err 855 } 856 } 857 }() 858 859 // Jobs are fetched only when included 860 go func() { 861 defer wg.Done() 862 863 var err error 864 if in.isWorkloadIncluded(kubernetes.JobType) { 865 // No cache for Jobs 866 jbs, err = userClient.GetJobs(namespace) 867 if err != nil { 868 log.Errorf("Error fetching Jobs per namespace %s: %s", namespace, err) 869 errChan <- err 870 } 871 } 872 }() 873 874 // DaemonSets are fetched only when included 875 go func() { 876 defer wg.Done() 877 878 var err error 879 if in.isWorkloadIncluded(kubernetes.DaemonSetType) { 880 daeset, err = kubeCache.GetDaemonSets(namespace) 881 if err != nil { 882 log.Errorf("Error fetching DaemonSets per namespace %s: %s", namespace, err) 883 } 884 } 885 }() 886 887 wg.Wait() 888 if len(errChan) != 0 { 889 err := <-errChan 890 return ws, err 891 } 892 893 // Key: name of controller; Value: type of controller 894 controllers := map[string]string{} 895 896 // Find controllers from pods 897 for _, pod := range pods { 898 if len(pod.OwnerReferences) != 0 { 899 for _, ref := range pod.OwnerReferences { 900 if ref.Controller != nil && *ref.Controller && in.isWorkloadIncluded(ref.Kind) { 901 if _, exist := controllers[ref.Name]; !exist { 902 controllers[ref.Name] = ref.Kind 903 } else { 904 if controllers[ref.Name] != ref.Kind { 905 controllers[ref.Name] = controllerPriority(controllers[ref.Name], ref.Kind) 906 } 907 } 908 } 909 } 910 } else { 911 if _, exist := controllers[pod.Name]; !exist { 912 // Pod without controller 913 controllers[pod.Name] = "Pod" 914 } 915 } 916 } 917 918 // Resolve ReplicaSets from Deployments 919 // Resolve ReplicationControllers from DeploymentConfigs 920 // Resolve Jobs from CronJobs 921 for controllerName, controllerType := range controllers { 922 if controllerType == kubernetes.ReplicaSetType { 923 found := false 924 iFound := -1 925 for i, rs := range repset { 926 if rs.Name == controllerName { 927 iFound = i 928 found = true 929 break 930 } 931 } 932 if found && len(repset[iFound].OwnerReferences) > 0 { 933 for _, ref := range repset[iFound].OwnerReferences { 934 if ref.Controller != nil && *ref.Controller { 935 if _, exist := controllers[ref.Name]; !exist { 936 // For valid owner controllers, delete the child ReplicaSet and add the parent controller, 937 // otherwise (for custom controllers), defer to the replica set. 938 if in.isWorkloadValid(ref.Kind) { 939 controllers[ref.Name] = ref.Kind 940 delete(controllers, controllerName) 941 } else { 942 log.Debugf("Add ReplicaSet to workload list for custom controller [%s][%s]", ref.Name, ref.Kind) 943 } 944 } else { 945 if controllers[ref.Name] != ref.Kind { 946 controllers[ref.Name] = controllerPriority(controllers[ref.Name], ref.Kind) 947 } 948 delete(controllers, controllerName) 949 } 950 } 951 } 952 } 953 } 954 if controllerType == kubernetes.ReplicationControllerType { 955 found := false 956 iFound := -1 957 for i, rc := range repcon { 958 if rc.Name == controllerName { 959 iFound = i 960 found = true 961 break 962 } 963 } 964 if found && len(repcon[iFound].OwnerReferences) > 0 { 965 for _, ref := range repcon[iFound].OwnerReferences { 966 if ref.Controller != nil && *ref.Controller { 967 // Delete the child ReplicationController and add the parent controller 968 if _, exist := controllers[ref.Name]; !exist { 969 controllers[ref.Name] = ref.Kind 970 } else { 971 if controllers[ref.Name] != ref.Kind { 972 controllers[ref.Name] = controllerPriority(controllers[ref.Name], ref.Kind) 973 } 974 } 975 delete(controllers, controllerName) 976 } 977 } 978 } 979 } 980 if controllerType == kubernetes.JobType { 981 found := false 982 iFound := -1 983 for i, jb := range jbs { 984 if jb.Name == controllerName { 985 iFound = i 986 found = true 987 break 988 } 989 } 990 if found && len(jbs[iFound].OwnerReferences) > 0 { 991 for _, ref := range jbs[iFound].OwnerReferences { 992 if ref.Controller != nil && *ref.Controller { 993 // Delete the child Job and add the parent controller 994 if _, exist := controllers[ref.Name]; !exist { 995 controllers[ref.Name] = ref.Kind 996 } else { 997 if controllers[ref.Name] != ref.Kind { 998 controllers[ref.Name] = controllerPriority(controllers[ref.Name], ref.Kind) 999 } 1000 } 1001 // Jobs are special as deleting CronJob parent doesn't delete children 1002 // So we need to check that parent exists before to delete children controller 1003 cnExist := false 1004 for _, cnj := range conjbs { 1005 if cnj.Name == ref.Name { 1006 cnExist = true 1007 break 1008 } 1009 } 1010 if cnExist { 1011 delete(controllers, controllerName) 1012 } 1013 } 1014 } 1015 } 1016 } 1017 } 1018 1019 // Cornercase, check for controllers without pods, to show them as a workload 1020 var selector labels.Selector 1021 var selErr error 1022 if labelSelector != "" { 1023 selector, selErr = labels.Parse(labelSelector) 1024 if selErr != nil { 1025 log.Errorf("%s can not be processed as selector: %v", labelSelector, selErr) 1026 } 1027 } 1028 for _, d := range dep { 1029 selectorCheck := true 1030 if selector != nil { 1031 selectorCheck = selector.Matches(labels.Set(d.Spec.Template.Labels)) 1032 } 1033 if _, exist := controllers[d.Name]; !exist && selectorCheck { 1034 controllers[d.Name] = "Deployment" 1035 } 1036 } 1037 for _, rs := range repset { 1038 selectorCheck := true 1039 if selector != nil { 1040 selectorCheck = selector.Matches(labels.Set(rs.Spec.Template.Labels)) 1041 } 1042 if _, exist := controllers[rs.Name]; !exist && len(rs.OwnerReferences) == 0 && selectorCheck { 1043 controllers[rs.Name] = "ReplicaSet" 1044 } 1045 } 1046 for _, dc := range depcon { 1047 selectorCheck := true 1048 if selector != nil { 1049 selectorCheck = selector.Matches(labels.Set(dc.Spec.Template.Labels)) 1050 } 1051 if _, exist := controllers[dc.Name]; !exist && selectorCheck { 1052 controllers[dc.Name] = "DeploymentConfig" 1053 } 1054 } 1055 for _, rc := range repcon { 1056 selectorCheck := true 1057 if selector != nil { 1058 selectorCheck = selector.Matches(labels.Set(rc.Spec.Template.Labels)) 1059 } 1060 if _, exist := controllers[rc.Name]; !exist && len(rc.OwnerReferences) == 0 && selectorCheck { 1061 controllers[rc.Name] = "ReplicationController" 1062 } 1063 } 1064 for _, fs := range fulset { 1065 selectorCheck := true 1066 if selector != nil { 1067 selectorCheck = selector.Matches(labels.Set(fs.Spec.Template.Labels)) 1068 } 1069 if _, exist := controllers[fs.Name]; !exist && selectorCheck { 1070 controllers[fs.Name] = "StatefulSet" 1071 } 1072 } 1073 for _, ds := range daeset { 1074 selectorCheck := true 1075 if selector != nil { 1076 selectorCheck = selector.Matches(labels.Set(ds.Spec.Template.Labels)) 1077 } 1078 if _, exist := controllers[ds.Name]; !exist && selectorCheck { 1079 controllers[ds.Name] = "DaemonSet" 1080 } 1081 } 1082 1083 // Build workloads from controllers 1084 var controllerNames []string 1085 for k := range controllers { 1086 controllerNames = append(controllerNames, k) 1087 } 1088 sort.Strings(controllerNames) 1089 for _, controllerName := range controllerNames { 1090 w := &models.Workload{ 1091 Pods: models.Pods{}, 1092 Services: []models.ServiceOverview{}, 1093 } 1094 w.Cluster = cluster 1095 w.Namespace = namespace 1096 controllerType := controllers[controllerName] 1097 // Flag to add a controller if it is found 1098 cnFound := true 1099 switch controllerType { 1100 case kubernetes.DeploymentType: 1101 found := false 1102 iFound := -1 1103 for i, dp := range dep { 1104 if dp.Name == controllerName { 1105 found = true 1106 iFound = i 1107 break 1108 } 1109 } 1110 if found { 1111 selector := labels.Set(dep[iFound].Spec.Template.Labels).AsSelector() 1112 w.SetPods(kubernetes.FilterPodsBySelector(selector, pods)) 1113 w.ParseDeployment(&dep[iFound]) 1114 } else { 1115 log.Errorf("Workload %s is not found as Deployment", controllerName) 1116 cnFound = false 1117 } 1118 case kubernetes.ReplicaSetType: 1119 found := false 1120 iFound := -1 1121 for i, rs := range repset { 1122 if rs.Name == controllerName { 1123 found = true 1124 iFound = i 1125 break 1126 } 1127 } 1128 if found { 1129 selector := labels.Set(repset[iFound].Spec.Template.Labels).AsSelector() 1130 w.SetPods(kubernetes.FilterPodsBySelector(selector, pods)) 1131 w.ParseReplicaSet(&repset[iFound]) 1132 } else { 1133 log.Errorf("Workload %s is not found as ReplicaSet", controllerName) 1134 cnFound = false 1135 } 1136 case kubernetes.ReplicationControllerType: 1137 found := false 1138 iFound := -1 1139 for i, rc := range repcon { 1140 if rc.Name == controllerName { 1141 found = true 1142 iFound = i 1143 break 1144 } 1145 } 1146 if found { 1147 selector := labels.Set(repcon[iFound].Spec.Template.Labels).AsSelector() 1148 w.SetPods(kubernetes.FilterPodsBySelector(selector, pods)) 1149 w.ParseReplicationController(&repcon[iFound]) 1150 } else { 1151 log.Errorf("Workload %s is not found as ReplicationController", controllerName) 1152 cnFound = false 1153 } 1154 case kubernetes.DeploymentConfigType: 1155 found := false 1156 iFound := -1 1157 for i, dc := range depcon { 1158 if dc.Name == controllerName { 1159 found = true 1160 iFound = i 1161 break 1162 } 1163 } 1164 if found { 1165 selector := labels.Set(depcon[iFound].Spec.Template.Labels).AsSelector() 1166 w.SetPods(kubernetes.FilterPodsBySelector(selector, pods)) 1167 w.ParseDeploymentConfig(&depcon[iFound]) 1168 } else { 1169 log.Errorf("Workload %s is not found as DeploymentConfig", controllerName) 1170 cnFound = false 1171 } 1172 case kubernetes.StatefulSetType: 1173 found := false 1174 iFound := -1 1175 for i, fs := range fulset { 1176 if fs.Name == controllerName { 1177 found = true 1178 iFound = i 1179 break 1180 } 1181 } 1182 if found { 1183 selector := labels.Set(fulset[iFound].Spec.Template.Labels).AsSelector() 1184 w.SetPods(kubernetes.FilterPodsBySelector(selector, pods)) 1185 w.ParseStatefulSet(&fulset[iFound]) 1186 } else { 1187 log.Errorf("Workload %s is not found as StatefulSet", controllerName) 1188 cnFound = false 1189 } 1190 case kubernetes.PodType: 1191 found := false 1192 iFound := -1 1193 for i, pod := range pods { 1194 if pod.Name == controllerName { 1195 found = true 1196 iFound = i 1197 break 1198 } 1199 } 1200 if found { 1201 w.SetPods([]core_v1.Pod{pods[iFound]}) 1202 w.ParsePod(&pods[iFound]) 1203 } else { 1204 log.Errorf("Workload %s is not found as Pod", controllerName) 1205 cnFound = false 1206 } 1207 case kubernetes.JobType: 1208 found := false 1209 iFound := -1 1210 for i, jb := range jbs { 1211 if jb.Name == controllerName { 1212 found = true 1213 iFound = i 1214 break 1215 } 1216 } 1217 if found { 1218 selector := labels.Set(jbs[iFound].Spec.Template.Labels).AsSelector() 1219 w.SetPods(kubernetes.FilterPodsBySelector(selector, pods)) 1220 w.ParseJob(&jbs[iFound]) 1221 } else { 1222 log.Errorf("Workload %s is not found as Job", controllerName) 1223 cnFound = false 1224 } 1225 case kubernetes.CronJobType: 1226 found := false 1227 iFound := -1 1228 for i, cjb := range conjbs { 1229 if cjb.Name == controllerName { 1230 found = true 1231 iFound = i 1232 break 1233 } 1234 } 1235 if found { 1236 selector := labels.Set(conjbs[iFound].Spec.JobTemplate.Spec.Template.Labels).AsSelector() 1237 w.SetPods(kubernetes.FilterPodsBySelector(selector, pods)) 1238 w.ParseCronJob(&conjbs[iFound]) 1239 } else { 1240 log.Warningf("Workload %s is not found as CronJob (CronJob could be deleted but children are still in the namespace)", controllerName) 1241 cnFound = false 1242 } 1243 case kubernetes.DaemonSetType: 1244 found := false 1245 iFound := -1 1246 for i, ds := range daeset { 1247 if ds.Name == controllerName { 1248 found = true 1249 iFound = i 1250 break 1251 } 1252 } 1253 if found { 1254 selector := labels.Set(daeset[iFound].Spec.Template.Labels).AsSelector() 1255 w.SetPods(kubernetes.FilterPodsBySelector(selector, pods)) 1256 w.ParseDaemonSet(&daeset[iFound]) 1257 } else { 1258 log.Errorf("Workload %s is not found as Deployment", controllerName) 1259 cnFound = false 1260 } 1261 default: 1262 // This covers the scenario of a custom controller without replicaset, controlling pods directly. 1263 // Note that a custom controller with replicaset(s) will return the replicaset(s) as the workloads. 1264 var cPods []core_v1.Pod 1265 cPods = kubernetes.FilterPodsByController(controllerName, controllerType, pods) 1266 if len(cPods) > 0 { 1267 w.ParsePods(controllerName, controllerType, cPods) 1268 log.Debugf("Workload %s of type %s has no ReplicaSet as a child controller (this may be ok, but is unusual)", controllerName, controllerType) 1269 } 1270 w.SetPods(cPods) 1271 } 1272 1273 // Add the Proxy Status to the workload 1274 for _, pod := range w.Pods { 1275 if pod.HasIstioSidecar() && !w.IsGateway() && config.Get().ExternalServices.Istio.IstioAPIEnabled { 1276 pod.ProxyStatus = in.businessLayer.ProxyStatus.GetPodProxyStatus(cluster, namespace, pod.Name) 1277 } 1278 } 1279 1280 if cnFound { 1281 ws = append(ws, w) 1282 } 1283 } 1284 return ws, nil 1285 } 1286 1287 func (in *WorkloadService) fetchWorkload(ctx context.Context, criteria WorkloadCriteria) (*models.Workload, error) { 1288 var pods []core_v1.Pod 1289 var repcon []core_v1.ReplicationController 1290 var dep *apps_v1.Deployment 1291 var repset []apps_v1.ReplicaSet 1292 var depcon *osapps_v1.DeploymentConfig 1293 var fulset *apps_v1.StatefulSet 1294 var jbs []batch_v1.Job 1295 var conjbs []batch_v1.CronJob 1296 var ds *apps_v1.DaemonSet 1297 1298 wl := &models.Workload{ 1299 WorkloadListItem: models.WorkloadListItem{ 1300 Cluster: criteria.Cluster, 1301 Namespace: criteria.Namespace, 1302 }, 1303 Pods: models.Pods{}, 1304 Services: []models.ServiceOverview{}, 1305 Runtimes: []models.Runtime{}, 1306 AdditionalDetails: []models.AdditionalItem{}, 1307 Health: *models.EmptyWorkloadHealth(), 1308 } 1309 1310 // Check if user has access to the namespace (RBAC) in cache scenarios and/or 1311 // if namespace is accessible from Kiali (Deployment.AccessibleNamespaces) 1312 if _, err := in.businessLayer.Namespace.GetClusterNamespace(ctx, criteria.Namespace, criteria.Cluster); err != nil { 1313 return nil, err 1314 } 1315 1316 // Flag used for custom controllers 1317 // i.e. a third party framework creates its own "Deployment" controller with extra features 1318 // on this case, Kiali will collect basic info from the ReplicaSet controller 1319 _, knownWorkloadType := controllerOrder[criteria.WorkloadType] 1320 1321 wg := sync.WaitGroup{} 1322 wg.Add(9) 1323 errChan := make(chan error, 9) 1324 1325 kialiCache, err := in.cache.GetKubeCache(criteria.Cluster) 1326 if err != nil { 1327 return nil, err 1328 } 1329 1330 client, ok := in.userClients[criteria.Cluster] 1331 if !ok { 1332 return nil, fmt.Errorf("no user client for cluster [%s]", criteria.Cluster) 1333 } 1334 1335 // Pods are always fetched for all workload types 1336 go func() { 1337 defer wg.Done() 1338 var err error 1339 pods, err = kialiCache.GetPods(criteria.Namespace, "") 1340 if err != nil { 1341 log.Errorf("Error fetching Pods per namespace %s: %s", criteria.Namespace, err) 1342 errChan <- err 1343 } 1344 }() 1345 1346 // fetch as Deployment when workloadType is Deployment or unspecified 1347 go func() { 1348 defer wg.Done() 1349 var err error 1350 1351 if criteria.WorkloadType != "" && criteria.WorkloadType != kubernetes.DeploymentType { 1352 return 1353 } 1354 dep, err = kialiCache.GetDeployment(criteria.Namespace, criteria.WorkloadName) 1355 if err != nil { 1356 if errors.IsNotFound(err) { 1357 dep = nil 1358 } else { 1359 log.Errorf("Error fetching Deployment per namespace %s and name %s: %s", criteria.Namespace, criteria.WorkloadName, err) 1360 errChan <- err 1361 } 1362 } 1363 }() 1364 1365 // fetch as ReplicaSet(s) when workloadType is ReplicaSet, unspecified, *or custom* 1366 go func() { 1367 defer wg.Done() 1368 1369 if criteria.WorkloadType != "" && criteria.WorkloadType != kubernetes.ReplicaSetType && knownWorkloadType { 1370 return 1371 } 1372 var err error 1373 repset, err = kialiCache.GetReplicaSets(criteria.Namespace) 1374 if err != nil { 1375 log.Errorf("Error fetching ReplicaSets per namespace %s: %s", criteria.Namespace, err) 1376 errChan <- err 1377 } 1378 }() 1379 1380 // fetch as ReplicationControllerType when included, and workloadType is ReplicationControllerType or unspecified 1381 go func() { 1382 defer wg.Done() 1383 1384 if criteria.WorkloadType != "" && criteria.WorkloadType != kubernetes.ReplicationControllerType { 1385 return 1386 } 1387 1388 var err error 1389 if in.isWorkloadIncluded(kubernetes.ReplicationControllerType) { 1390 // No cache for ReplicationControllers 1391 repcon, err = client.GetReplicationControllers(criteria.Namespace) 1392 if err != nil { 1393 log.Errorf("Error fetching GetReplicationControllers per namespace %s: %s", criteria.Namespace, err) 1394 errChan <- err 1395 } 1396 } 1397 }() 1398 1399 // fetch as DeploymentConfigType when included, and workloadType is DeploymentConfigType or unspecified 1400 go func() { 1401 defer wg.Done() 1402 1403 if criteria.WorkloadType != "" && criteria.WorkloadType != kubernetes.DeploymentConfigType { 1404 return 1405 } 1406 1407 var err error 1408 if client.IsOpenShift() && in.isWorkloadIncluded(kubernetes.DeploymentConfigType) { 1409 // No cache for deploymentConfigs 1410 depcon, err = client.GetDeploymentConfig(ctx, criteria.Namespace, criteria.WorkloadName) 1411 if err != nil { 1412 depcon = nil 1413 } 1414 } 1415 }() 1416 1417 // fetch as StatefulSetType when included, and workloadType is StatefulSetType or unspecified 1418 go func() { 1419 defer wg.Done() 1420 1421 if criteria.WorkloadType != "" && criteria.WorkloadType != kubernetes.StatefulSetType { 1422 return 1423 } 1424 1425 var err error 1426 if in.isWorkloadIncluded(kubernetes.StatefulSetType) { 1427 fulset, err = kialiCache.GetStatefulSet(criteria.Namespace, criteria.WorkloadName) 1428 if err != nil { 1429 fulset = nil 1430 } 1431 } 1432 }() 1433 1434 // fetch as CronJobType when included, and workloadType is CronJobType or unspecified 1435 go func() { 1436 defer wg.Done() 1437 1438 if criteria.WorkloadType != "" && criteria.WorkloadType != kubernetes.CronJobType { 1439 return 1440 } 1441 1442 var err error 1443 if in.isWorkloadIncluded(kubernetes.CronJobType) { 1444 // No cache for CronJobs 1445 conjbs, err = client.GetCronJobs(criteria.Namespace) 1446 if err != nil { 1447 log.Errorf("Error fetching CronJobs per namespace %s: %s", criteria.Namespace, err) 1448 errChan <- err 1449 } 1450 } 1451 }() 1452 1453 // fetch as JobType when included, and workloadType is JobType or unspecified 1454 go func() { 1455 defer wg.Done() 1456 1457 if criteria.WorkloadType != "" && criteria.WorkloadType != kubernetes.JobType { 1458 return 1459 } 1460 1461 var err error 1462 if in.isWorkloadIncluded(kubernetes.JobType) { 1463 // No cache for Jobs 1464 jbs, err = client.GetJobs(criteria.Namespace) 1465 if err != nil { 1466 log.Errorf("Error fetching Jobs per namespace %s: %s", criteria.Namespace, err) 1467 errChan <- err 1468 } 1469 } 1470 }() 1471 1472 // fetch as DaemonSetType when included, and workloadType is DaemonSetType or unspecified 1473 go func() { 1474 defer wg.Done() 1475 1476 if criteria.WorkloadType != "" && criteria.WorkloadType != kubernetes.DaemonSetType { 1477 return 1478 } 1479 1480 var err error 1481 if in.isWorkloadIncluded(kubernetes.DaemonSetType) { 1482 ds, err = kialiCache.GetDaemonSet(criteria.Namespace, criteria.WorkloadName) 1483 if err != nil { 1484 ds = nil 1485 } 1486 } 1487 }() 1488 1489 wg.Wait() 1490 if len(errChan) != 0 { 1491 err := <-errChan 1492 return wl, err 1493 } 1494 1495 // Key: name of controller; Value: type of controller 1496 controllers := map[string]string{} 1497 1498 // Find controllers from pods 1499 for _, pod := range pods { 1500 if len(pod.OwnerReferences) != 0 { 1501 for _, ref := range pod.OwnerReferences { 1502 if ref.Controller != nil && *ref.Controller && in.isWorkloadIncluded(ref.Kind) { 1503 if _, exist := controllers[ref.Name]; !exist { 1504 controllers[ref.Name] = ref.Kind 1505 } else { 1506 if controllers[ref.Name] != ref.Kind { 1507 controllers[ref.Name] = controllerPriority(controllers[ref.Name], ref.Kind) 1508 } 1509 } 1510 } 1511 } 1512 } else { 1513 if _, exist := controllers[pod.Name]; !exist { 1514 // Pod without controller 1515 controllers[pod.Name] = "Pod" 1516 } 1517 } 1518 } 1519 1520 // Resolve ReplicaSets from Deployments 1521 // Resolve ReplicationControllers from DeploymentConfigs 1522 // Resolve Jobs from CronJobs 1523 for controllerName, controllerType := range controllers { 1524 if controllerType == kubernetes.ReplicaSetType { 1525 found := false 1526 iFound := -1 1527 for i, rs := range repset { 1528 if rs.Name == controllerName { 1529 iFound = i 1530 found = true 1531 break 1532 } 1533 } 1534 if found && len(repset[iFound].OwnerReferences) > 0 { 1535 for _, ref := range repset[iFound].OwnerReferences { 1536 if ref.Controller != nil && *ref.Controller { 1537 // For valid owner controllers, delete the child ReplicaSet and add the parent controller, 1538 // otherwise (for custom controllers), defer to the replica set. 1539 if _, exist := controllers[ref.Name]; !exist { 1540 if in.isWorkloadValid(ref.Kind) { 1541 controllers[ref.Name] = ref.Kind 1542 delete(controllers, controllerName) 1543 } else { 1544 log.Debugf("Use ReplicaSet as workload for custom controller [%s][%s]", ref.Name, ref.Kind) 1545 } 1546 } else { 1547 if controllers[ref.Name] != ref.Kind { 1548 controllers[ref.Name] = controllerPriority(controllers[ref.Name], ref.Kind) 1549 } 1550 delete(controllers, controllerName) 1551 } 1552 } 1553 } 1554 } 1555 } 1556 if controllerType == kubernetes.ReplicationControllerType { 1557 found := false 1558 iFound := -1 1559 for i, rc := range repcon { 1560 if rc.Name == controllerName { 1561 iFound = i 1562 found = true 1563 break 1564 } 1565 } 1566 if found && len(repcon[iFound].OwnerReferences) > 0 { 1567 for _, ref := range repcon[iFound].OwnerReferences { 1568 if ref.Controller != nil && *ref.Controller { 1569 // Delete the child ReplicationController and add the parent controller 1570 if _, exist := controllers[ref.Name]; !exist { 1571 controllers[ref.Name] = ref.Kind 1572 } else { 1573 if controllers[ref.Name] != ref.Kind { 1574 controllers[ref.Name] = controllerPriority(controllers[ref.Name], ref.Kind) 1575 } 1576 } 1577 delete(controllers, controllerName) 1578 } 1579 } 1580 } 1581 } 1582 if controllerType == kubernetes.JobType { 1583 found := false 1584 iFound := -1 1585 for i, jb := range jbs { 1586 if jb.Name == controllerName { 1587 iFound = i 1588 found = true 1589 break 1590 } 1591 } 1592 if found && len(jbs[iFound].OwnerReferences) > 0 { 1593 for _, ref := range jbs[iFound].OwnerReferences { 1594 if ref.Controller != nil && *ref.Controller { 1595 // Delete the child Job and add the parent controller 1596 if _, exist := controllers[ref.Name]; !exist { 1597 controllers[ref.Name] = ref.Kind 1598 } else { 1599 if controllers[ref.Name] != ref.Kind { 1600 controllers[ref.Name] = controllerPriority(controllers[ref.Name], ref.Kind) 1601 } 1602 } 1603 // Jobs are special as deleting CronJob parent doesn't delete children 1604 // So we need to check that parent exists before to delete children controller 1605 cnExist := false 1606 for _, cnj := range conjbs { 1607 if cnj.Name == ref.Name { 1608 cnExist = true 1609 break 1610 } 1611 } 1612 if cnExist { 1613 delete(controllers, controllerName) 1614 } 1615 } 1616 } 1617 } 1618 } 1619 } 1620 1621 // Cornercase, check for controllers without pods, to show them as a workload 1622 if dep != nil { 1623 if _, exist := controllers[dep.Name]; !exist { 1624 controllers[dep.Name] = kubernetes.DeploymentType 1625 } 1626 } 1627 for _, rs := range repset { 1628 if _, exist := controllers[rs.Name]; !exist && len(rs.OwnerReferences) == 0 { 1629 controllers[rs.Name] = kubernetes.ReplicaSetType 1630 } 1631 } 1632 if depcon != nil { 1633 if _, exist := controllers[depcon.Name]; !exist { 1634 controllers[depcon.Name] = kubernetes.DeploymentConfigType 1635 } 1636 } 1637 for _, rc := range repcon { 1638 if _, exist := controllers[rc.Name]; !exist && len(rc.OwnerReferences) == 0 { 1639 controllers[rc.Name] = kubernetes.ReplicationControllerType 1640 } 1641 } 1642 if fulset != nil { 1643 if _, exist := controllers[fulset.Name]; !exist { 1644 controllers[fulset.Name] = kubernetes.StatefulSetType 1645 } 1646 } 1647 if ds != nil { 1648 if _, exist := controllers[ds.Name]; !exist { 1649 controllers[ds.Name] = kubernetes.DaemonSetType 1650 } 1651 } 1652 1653 // Build workload from controllers 1654 1655 if _, exist := controllers[criteria.WorkloadName]; exist { 1656 w := models.Workload{ 1657 WorkloadListItem: models.WorkloadListItem{ 1658 Cluster: criteria.Cluster, 1659 Namespace: criteria.Namespace, 1660 }, 1661 Pods: models.Pods{}, 1662 Services: []models.ServiceOverview{}, 1663 Runtimes: []models.Runtime{}, 1664 AdditionalDetails: []models.AdditionalItem{}, 1665 Health: *models.EmptyWorkloadHealth(), 1666 } 1667 1668 // We have a controller with criteria.workloadName but if criteria.WorkloadType is specified and does 1669 // not match then we may not yet have fetched the workload definition. 1670 // For known types: respect criteria.WorkloadType and return NotFound if necessary. 1671 // For custom types: fall through to the default handler and try to get the workload definition working 1672 // up from the pods or replicas sets. 1673 // see https://github.com/kiali/kiali/issues/3830 1674 discoveredControllerType := controllers[criteria.WorkloadName] 1675 controllerType := discoveredControllerType 1676 if criteria.WorkloadType != "" && discoveredControllerType != criteria.WorkloadType { 1677 controllerType = criteria.WorkloadType 1678 } 1679 1680 // Handle the known types... 1681 cnFound := true 1682 switch controllerType { 1683 case kubernetes.DeploymentType: 1684 if dep != nil && dep.Name == criteria.WorkloadName { 1685 selector := labels.Set(dep.Spec.Template.Labels).AsSelector() 1686 w.SetPods(kubernetes.FilterPodsBySelector(selector, pods)) 1687 w.ParseDeployment(dep) 1688 } else { 1689 log.Errorf("Workload %s is not found as Deployment", criteria.WorkloadName) 1690 cnFound = false 1691 } 1692 case kubernetes.ReplicaSetType: 1693 found := false 1694 iFound := -1 1695 for i, rs := range repset { 1696 if rs.Name == criteria.WorkloadName { 1697 found = true 1698 iFound = i 1699 break 1700 } 1701 } 1702 if found { 1703 selector := labels.Set(repset[iFound].Spec.Template.Labels).AsSelector() 1704 w.SetPods(kubernetes.FilterPodsBySelector(selector, pods)) 1705 w.ParseReplicaSet(&repset[iFound]) 1706 } else { 1707 log.Errorf("Workload %s is not found as ReplicaSet", criteria.WorkloadName) 1708 cnFound = false 1709 } 1710 case kubernetes.ReplicationControllerType: 1711 found := false 1712 iFound := -1 1713 for i, rc := range repcon { 1714 if rc.Name == criteria.WorkloadName { 1715 found = true 1716 iFound = i 1717 break 1718 } 1719 } 1720 if found { 1721 selector := labels.Set(repcon[iFound].Spec.Template.Labels).AsSelector() 1722 w.SetPods(kubernetes.FilterPodsBySelector(selector, pods)) 1723 w.ParseReplicationController(&repcon[iFound]) 1724 } else { 1725 log.Errorf("Workload %s is not found as ReplicationController", criteria.WorkloadName) 1726 cnFound = false 1727 } 1728 case kubernetes.DeploymentConfigType: 1729 if depcon != nil && depcon.Name == criteria.WorkloadName { 1730 selector := labels.Set(depcon.Spec.Template.Labels).AsSelector() 1731 w.SetPods(kubernetes.FilterPodsBySelector(selector, pods)) 1732 w.ParseDeploymentConfig(depcon) 1733 } else { 1734 log.Errorf("Workload %s is not found as DeploymentConfig", criteria.WorkloadName) 1735 cnFound = false 1736 } 1737 case kubernetes.StatefulSetType: 1738 if fulset != nil && fulset.Name == criteria.WorkloadName { 1739 selector := labels.Set(fulset.Spec.Template.Labels).AsSelector() 1740 w.SetPods(kubernetes.FilterPodsBySelector(selector, pods)) 1741 w.ParseStatefulSet(fulset) 1742 } else { 1743 log.Errorf("Workload %s is not found as StatefulSet", criteria.WorkloadName) 1744 cnFound = false 1745 } 1746 case kubernetes.PodType: 1747 found := false 1748 iFound := -1 1749 for i, pod := range pods { 1750 if pod.Name == criteria.WorkloadName { 1751 found = true 1752 iFound = i 1753 break 1754 } 1755 } 1756 if found { 1757 w.SetPods([]core_v1.Pod{pods[iFound]}) 1758 w.ParsePod(&pods[iFound]) 1759 } else { 1760 log.Errorf("Workload %s is not found as Pod", criteria.WorkloadName) 1761 cnFound = false 1762 } 1763 case kubernetes.JobType: 1764 found := false 1765 iFound := -1 1766 for i, jb := range jbs { 1767 if jb.Name == criteria.WorkloadName { 1768 found = true 1769 iFound = i 1770 break 1771 } 1772 } 1773 if found { 1774 selector := labels.Set(jbs[iFound].Spec.Template.Labels).AsSelector() 1775 w.SetPods(kubernetes.FilterPodsBySelector(selector, pods)) 1776 w.ParseJob(&jbs[iFound]) 1777 } else { 1778 log.Errorf("Workload %s is not found as Job", criteria.WorkloadName) 1779 cnFound = false 1780 } 1781 case kubernetes.CronJobType: 1782 found := false 1783 iFound := -1 1784 for i, cjb := range conjbs { 1785 if cjb.Name == criteria.WorkloadName { 1786 found = true 1787 iFound = i 1788 break 1789 } 1790 } 1791 if found { 1792 selector := labels.Set(conjbs[iFound].Spec.JobTemplate.Spec.Template.Labels).AsSelector() 1793 w.SetPods(kubernetes.FilterPodsBySelector(selector, pods)) 1794 w.ParseCronJob(&conjbs[iFound]) 1795 } else { 1796 log.Warningf("Workload %s is not found as CronJob (CronJob could be deleted but children are still in the namespace)", criteria.WorkloadName) 1797 cnFound = false 1798 } 1799 case kubernetes.DaemonSetType: 1800 if ds != nil && ds.Name == criteria.WorkloadName { 1801 selector := labels.Set(ds.Spec.Template.Labels).AsSelector() 1802 w.SetPods(kubernetes.FilterPodsBySelector(selector, pods)) 1803 w.ParseDaemonSet(ds) 1804 } else { 1805 log.Errorf("Workload %s is not found as DaemonSet", criteria.WorkloadName) 1806 cnFound = false 1807 } 1808 default: 1809 // Handle a custom type (criteria.WorkloadType is not a known type). 1810 // 1. Custom controller with replicaset 1811 // 2. Custom controller without replicaset controlling pods directly 1812 1813 // 1. use the controller type found in the Pod resolution and ignore the unknown criteria type 1814 var cPods []core_v1.Pod 1815 for _, rs := range repset { 1816 if discoveredControllerType == kubernetes.ReplicaSetType && criteria.WorkloadName == rs.Name { 1817 w.ParseReplicaSet(&rs) 1818 } else { 1819 rsOwnerRef := meta_v1.GetControllerOf(&rs.ObjectMeta) 1820 if rsOwnerRef != nil && rsOwnerRef.Name == criteria.WorkloadName && rsOwnerRef.Kind == discoveredControllerType { 1821 w.ParseReplicaSetParent(&rs, criteria.WorkloadName, discoveredControllerType) 1822 } else { 1823 continue 1824 } 1825 } 1826 for _, pod := range pods { 1827 if meta_v1.IsControlledBy(&pod, &rs) { 1828 cPods = append(cPods, pod) 1829 } 1830 } 1831 break 1832 } 1833 1834 // 2. If no pods we're found for a ReplicaSet type, it's possible the controller 1835 // is managing the pods itself i.e. the pod's have an owner ref directly to the controller type. 1836 if len(cPods) == 0 { 1837 cPods = kubernetes.FilterPodsByController(criteria.WorkloadName, discoveredControllerType, pods) 1838 if len(cPods) > 0 { 1839 w.ParsePods(criteria.WorkloadName, discoveredControllerType, cPods) 1840 log.Debugf("Workload %s of type %s has not a ReplicaSet as a child controller, it may need a revisit", criteria.WorkloadName, discoveredControllerType) 1841 } 1842 } 1843 1844 w.SetPods(cPods) 1845 } 1846 1847 // Add the Proxy Status to the workload 1848 for _, pod := range w.Pods { 1849 if pod.HasIstioSidecar() && !w.IsGateway() && config.Get().ExternalServices.Istio.IstioAPIEnabled { 1850 pod.ProxyStatus = in.businessLayer.ProxyStatus.GetPodProxyStatus(criteria.Cluster, criteria.Namespace, pod.Name) 1851 } 1852 // If Ambient is enabled for pod, check if has any Waypoint proxy 1853 if pod.AmbientEnabled() { 1854 w.WaypointWorkloads = in.getWaypointForWorkload(ctx, criteria.Namespace, w) 1855 } 1856 // If the pod is a waypoint proxy, check if it is attached to a namespace or to a service account, and get the affected workloads 1857 if pod.IsWaypoint() { 1858 // Get waypoint workloads from a namespace 1859 if pod.Labels["istio.io/gateway-name"] == "namespace" { 1860 w.WaypointWorkloads = append(w.WaypointWorkloads, in.listWaypointWorkloadsForNamespace(ctx, criteria.Namespace)...) 1861 } else { 1862 // Get waypoint workloads from a service account 1863 sa := pod.Annotations["istio.io/for-service-account"] 1864 w.WaypointWorkloads = append(w.WaypointWorkloads, in.listWaypointWorkloadsForSA(ctx, criteria.Namespace, sa)...) 1865 } 1866 } 1867 } 1868 1869 if cnFound { 1870 return &w, nil 1871 } 1872 } 1873 return wl, kubernetes.NewNotFound(criteria.WorkloadName, "Kiali", "Workload") 1874 } 1875 1876 // Get the Waypoint proxy for a workload 1877 func (in *WorkloadService) getWaypointForWorkload(ctx context.Context, namespace string, workload models.Workload) []models.Workload { 1878 wlist, err := in.fetchWorkloads(ctx, namespace, "") 1879 if err != nil { 1880 log.Errorf("Error fetching workloads") 1881 return nil 1882 } 1883 1884 var workloadslist []models.Workload 1885 // Get service Account name for each pod from the workload 1886 for _, wk := range wlist { 1887 if wk.Labels[config.WaypointLabel] == "istio.io-mesh-controller" { 1888 for _, pod := range wk.Pods { 1889 if pod.Labels["istio.io/gateway-name"] == "namespace" { 1890 workloadslist = append(workloadslist, *wk) 1891 break 1892 } else { 1893 // Get waypoint workloads from a service account 1894 sa := pod.Annotations["istio.io/for-service-account"] 1895 for _, workloadDef := range workload.Pods { 1896 if workloadDef.ServiceAccountName == sa { 1897 workloadslist = append(workloadslist, *wk) 1898 break 1899 } 1900 } 1901 1902 } 1903 } 1904 } 1905 } 1906 return workloadslist 1907 } 1908 1909 // Return the list of workloads binded to a service account, valid when the waypoint proxy is applied to a service account 1910 // TODO: This is scoped by namespace 1911 func (in *WorkloadService) listWaypointWorkloadsForSA(ctx context.Context, namespace string, sa string) []models.Workload { 1912 wlist, err := in.fetchWorkloads(ctx, namespace, "") 1913 if err != nil { 1914 log.Errorf("Error fetching workloads") 1915 } 1916 1917 var workloadslist []models.Workload 1918 // Get service Account name for each pod from the workload 1919 for _, workload := range wlist { 1920 if workload.Labels[config.WaypointLabel] != "istio.io-mesh-controller" { 1921 for _, pod := range workload.Pods { 1922 if pod.ServiceAccountName == sa { 1923 workloadslist = append(workloadslist, *workload) 1924 break 1925 1926 } 1927 } 1928 } 1929 } 1930 return workloadslist 1931 } 1932 1933 // Return the list of workloads when the waypoint proxy is applied per namespace 1934 func (in *WorkloadService) listWaypointWorkloadsForNamespace(ctx context.Context, namespace string) []models.Workload { 1935 wlist, err := in.fetchWorkloads(ctx, namespace, "") 1936 if err != nil { 1937 log.Errorf("Error fetching workloads") 1938 } 1939 1940 var workloadslist []models.Workload 1941 // Get service Account name for each pod from the workload 1942 for _, workload := range wlist { 1943 if workload.Labels[config.WaypointLabel] != "istio.io-mesh-controller" { 1944 workloadslist = append(workloadslist, *workload) 1945 } 1946 } 1947 return workloadslist 1948 } 1949 1950 func (in *WorkloadService) updateWorkload(ctx context.Context, cluster string, namespace string, workloadName string, workloadType string, jsonPatch string, patchType string) error { 1951 // Check if user has access to the namespace (RBAC) in cache scenarios and/or 1952 // if namespace is accessible from Kiali (Deployment.AccessibleNamespaces) 1953 if _, err := in.businessLayer.Namespace.GetClusterNamespace(ctx, namespace, cluster); err != nil { 1954 return err 1955 } 1956 1957 userClient, ok := in.userClients[cluster] 1958 if !ok { 1959 return fmt.Errorf("user client for cluster [%s] not found", cluster) 1960 } 1961 1962 workloadTypes := []string{ 1963 kubernetes.DeploymentType, 1964 kubernetes.ReplicaSetType, 1965 kubernetes.ReplicationControllerType, 1966 kubernetes.DeploymentConfigType, 1967 kubernetes.StatefulSetType, 1968 kubernetes.JobType, 1969 kubernetes.CronJobType, 1970 kubernetes.PodType, 1971 kubernetes.DaemonSetType, 1972 } 1973 1974 // workloadType is an optional parameter used to optimize the workload type fetch 1975 // By default workloads use only the "name" but not the pair "name,type". 1976 if workloadType != "" { 1977 found := false 1978 for _, wt := range workloadTypes { 1979 if workloadType == wt { 1980 found = true 1981 break 1982 } 1983 } 1984 if found { 1985 workloadTypes = []string{workloadType} 1986 } 1987 } 1988 1989 wg := sync.WaitGroup{} 1990 wg.Add(len(workloadTypes)) 1991 errChan := make(chan error, len(workloadTypes)) 1992 1993 for _, workloadType := range workloadTypes { 1994 go func(wkType string) { 1995 defer wg.Done() 1996 var err error 1997 if in.isWorkloadIncluded(wkType) { 1998 err = userClient.UpdateWorkload(namespace, workloadName, wkType, jsonPatch, patchType) 1999 } 2000 if err != nil { 2001 if !errors.IsNotFound(err) { 2002 log.Errorf("Error fetching %s per namespace %s and name %s: %s", wkType, namespace, workloadName, err) 2003 errChan <- err 2004 } 2005 } 2006 }(workloadType) 2007 } 2008 2009 wg.Wait() 2010 if len(errChan) != 0 { 2011 err := <-errChan 2012 return err 2013 } 2014 2015 return nil 2016 } 2017 2018 // KIALI-1730 2019 // This method is used to decide the priority of the controller in the cornercase when two controllers have same labels 2020 // on the selector. Note that this is a situation that user should control as it is described in the documentation: 2021 // https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors 2022 // But Istio only identifies one controller as workload (it doesn't note which one). 2023 // Kiali can select one on the list of workloads and other in the details and this should be consistent. 2024 var controllerOrder = map[string]int{ 2025 "Deployment": 6, 2026 "DeploymentConfig": 5, 2027 "ReplicaSet": 4, 2028 "ReplicationController": 3, 2029 "StatefulSet": 2, 2030 "Job": 1, 2031 "DaemonSet": 0, 2032 "Pod": -1, 2033 } 2034 2035 func controllerPriority(type1, type2 string) string { 2036 w1, e1 := controllerOrder[type1] 2037 if !e1 { 2038 log.Errorf("This controller %s is assigned in a Pod and it's not properly managed", type1) 2039 } 2040 w2, e2 := controllerOrder[type2] 2041 if !e2 { 2042 log.Errorf("This controller %s is assigned in a Pod and it's not properly managed", type2) 2043 } 2044 if w1 >= w2 { 2045 return type1 2046 } else { 2047 return type2 2048 } 2049 } 2050 2051 // GetWorkloadAppName returns the "Application" name (app label) that relates to a workload 2052 func (in *WorkloadService) GetWorkloadAppName(ctx context.Context, cluster, namespace, workload string) (string, error) { 2053 var end observability.EndFunc 2054 ctx, end = observability.StartSpan(ctx, "GetWorkloadAppName", 2055 observability.Attribute("package", "business"), 2056 observability.Attribute("cluster", cluster), 2057 observability.Attribute("namespace", namespace), 2058 observability.Attribute("workload", workload), 2059 ) 2060 defer end() 2061 2062 wkd, err := in.fetchWorkload(ctx, WorkloadCriteria{Cluster: cluster, Namespace: namespace, WorkloadName: workload, WorkloadType: ""}) 2063 if err != nil { 2064 return "", err 2065 } 2066 2067 appLabelName := in.config.IstioLabels.AppLabelName 2068 app := wkd.Labels[appLabelName] 2069 return app, nil 2070 } 2071 2072 // streamParsedLogs fetches logs from a container in a pod, parses and decorates each log line with some metadata (if possible) and 2073 // sends the processed lines to the client in JSON format. Results are sent as processing is performed, so in case of any error when 2074 // doing processing the JSON document will be truncated. 2075 func (in *WorkloadService) streamParsedLogs(cluster, namespace, name string, opts *LogOptions, w http.ResponseWriter) error { 2076 userClient, ok := in.userClients[cluster] 2077 if !ok { 2078 return fmt.Errorf("user client for cluster [%s] not found", cluster) 2079 } 2080 2081 var engardeParser *parser.Parser 2082 if opts.LogType == models.LogTypeProxy { 2083 engardeParser = parser.New(parser.IstioProxyAccessLogsPattern) 2084 } 2085 2086 k8sOpts := opts.PodLogOptions 2087 // the k8s API does not support "endTime/beforeTime". So for bounded time ranges we need to 2088 // discard the logs after sinceTime+duration 2089 isBounded := opts.Duration != nil 2090 2091 logsReader, err := userClient.StreamPodLogs(namespace, name, &k8sOpts) 2092 if err != nil { 2093 return err 2094 } 2095 2096 defer func() { 2097 e := logsReader.Close() 2098 if e != nil { 2099 log.Errorf("Error when closing the connection streaming logs of a pod: %s", e.Error()) 2100 } 2101 }() 2102 2103 bufferedReader := bufio.NewReader(logsReader) 2104 2105 var startTime *time.Time 2106 var endTime *time.Time 2107 if k8sOpts.SinceTime != nil { 2108 startTime = &k8sOpts.SinceTime.Time 2109 if isBounded { 2110 end := startTime.Add(*opts.Duration) 2111 endTime = &end 2112 } 2113 } 2114 2115 // To avoid high memory usage, the JSON will be written 2116 // to the HTTP Response as it's received from the cluster API. 2117 // That is, each log line is parsed, decorated with Kiali's metadata, 2118 // marshalled to JSON and immediately written to the HTTP Response. 2119 // This means that it is needed to push HTTP headers and start writing 2120 // the response body right now and any errors at the middle of the log 2121 // processing can no longer be informed to the client. So, starting 2122 // these lines, the best we can do if some error happens is to simply 2123 // log the error and stop/truncate the response, which will have the 2124 // effect of sending an incomplete JSON document that the browser will fail 2125 // to parse. Hopefully, the client/UI can catch the parsing error and 2126 // properly show an error message about the failure retrieving logs. 2127 w.Header().Set("Content-Type", "application/json") 2128 _, writeErr := w.Write([]byte("{\"entries\":[")) // This starts the JSON document 2129 if writeErr != nil { 2130 return writeErr 2131 } 2132 2133 firstEntry := true 2134 line, readErr := bufferedReader.ReadString('\n') 2135 linesWritten := 0 2136 for ; readErr == nil || (readErr == io.EOF && len(line) > 0); line, readErr = bufferedReader.ReadString('\n') { 2137 // Abort if we already reached the requested max-lines limit 2138 if opts.MaxLines != nil && linesWritten >= *opts.MaxLines { 2139 break 2140 } 2141 2142 var entry *LogEntry 2143 if opts.LogType == models.LogTypeZtunnel { 2144 entry = parseZtunnelLine(line) 2145 } else { 2146 entry = parseLogLine(line, opts.LogType == models.LogTypeProxy, engardeParser) 2147 } 2148 2149 if entry == nil { 2150 continue 2151 } 2152 2153 if opts.LogType == models.LogTypeZtunnel && !filterMatches(entry.Message, opts.filter) { 2154 continue 2155 } 2156 2157 // If we are past the requested time window then stop processing 2158 if startTime == nil { 2159 startTime = &entry.OriginalTime 2160 } 2161 2162 if isBounded { 2163 if endTime == nil { 2164 end := entry.OriginalTime.Add(*opts.Duration) 2165 endTime = &end 2166 } 2167 2168 if entry.OriginalTime.After(*endTime) { 2169 break 2170 } 2171 } 2172 2173 // Send to client the processed log line 2174 2175 response, err := json.Marshal(entry) 2176 if err != nil { 2177 // Remember that since the HTTP Response body is already being sent, 2178 // it is not possible to change the response code. So, log the error 2179 // and terminate early the response. 2180 log.Errorf("Error when marshalling JSON while streaming pod logs: %s", err.Error()) 2181 return nil 2182 } 2183 2184 if firstEntry { 2185 firstEntry = false 2186 } else { 2187 _, writeErr = w.Write([]byte{','}) 2188 if writeErr != nil { 2189 // Remember that since the HTTP Response body is already being sent, 2190 // it is not possible to change the response code. So, log the error 2191 // and terminate early the response. 2192 log.Errorf("Error when writing log entries separator: %s", writeErr.Error()) 2193 return nil 2194 } 2195 } 2196 2197 _, writeErr = w.Write(response) 2198 if writeErr != nil { 2199 log.Errorf("Error when writing a processed log entry while streaming pod logs: %s", writeErr.Error()) 2200 return nil 2201 } 2202 2203 linesWritten++ 2204 } 2205 2206 if readErr == nil && opts.MaxLines != nil && linesWritten >= *opts.MaxLines { 2207 // End the JSON document, setting the max-lines truncated flag 2208 _, writeErr = w.Write([]byte("], \"linesTruncated\": true}")) 2209 } else { 2210 // End the JSON document 2211 _, writeErr = w.Write([]byte("]}")) 2212 } 2213 if writeErr != nil { 2214 log.Errorf("Error when writing the outro of the JSON document while streaming pod logs: %s", err.Error()) 2215 } 2216 2217 return nil 2218 } 2219 2220 // StreamPodLogs streams pod logs to an HTTP Response given the provided options 2221 func (in *WorkloadService) StreamPodLogs(cluster, namespace, name string, opts *LogOptions, w http.ResponseWriter) error { 2222 2223 if opts.LogType == models.LogTypeZtunnel { 2224 // First, get ztunnel namespace and containers 2225 pods := in.cache.GetZtunnelPods(cluster) 2226 // This is needed for the K8S client 2227 opts.PodLogOptions.Container = models.IstioProxy 2228 // The ztunnel line should include the pod and the namespace 2229 fs := filterOpts{ 2230 destWk: fmt.Sprintf("dst.workload=\"%s\"", name), 2231 destNs: fmt.Sprintf("dst.namespace=\"%s\"", namespace), 2232 srcWk: fmt.Sprintf("src.workload=\"%s\"", name), 2233 srcNs: fmt.Sprintf("src.namespace=\"%s\"", namespace), 2234 } 2235 opts.filter = fs 2236 var streamErr error 2237 for _, pod := range pods { 2238 streamErr = in.streamParsedLogs(cluster, pod.Namespace, pod.Name, opts, w) 2239 } 2240 return streamErr 2241 } 2242 return in.streamParsedLogs(cluster, namespace, name, opts, w) 2243 } 2244 2245 // AND filter 2246 func filterMatches(line string, filter filterOpts) bool { 2247 if (strings.Contains(line, filter.destNs) && strings.Contains(line, filter.destWk)) || (strings.Contains(line, filter.srcNs) && strings.Contains(line, filter.srcWk)) { 2248 return true 2249 } 2250 return false 2251 }