package insights import ( "fmt" "regexp" "strings" "github.com/skyhook-io/radar/pkg/gitops" gitopstree "github.com/skyhook-io/radar/pkg/gitops/tree" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) var fluxQuotedReference = regexp.MustCompile("(?:([A-Z][A-Za-z0-9]*)[ \t]+)?['\"`]([^'\"`]+)['\"`]") // IsFluxBlockerCondition reports a Flux root condition saying an upstream // object blocks it. Its message is the root's own; only the linked ref and the // cause quoted from that object depend on reading the object. func IsFluxBlockerCondition(issue Issue) bool { if issue.Scope != ScopeCondition { return false } switch issue.Reason { case "SourceNotReady", "ArtifactFailed", "DependencyNotReady": return true } return false } // fluxFailureDetail is the dependency's own explanation of its failure. func fluxFailureDetail(node gitopstree.Node) string { if fetchFailed, _ := node.Data["fetchFailed"].(bool); fetchFailed { if detail := stringData(node.Data, "fetchFailedMessage"); detail != "" { return detail } } if stringData(node.Data, "readyStatus") == "False" { if detail := stringData(node.Data, "readyMessage"); detail != "" { return detail } } if stalled, _ := node.Data["stalled"].(bool); stalled { return stringData(node.Data, "stalledMessage") } return "" } func attachFluxBlocker(issue Issue, root *unstructured.Unstructured, tree *gitopstree.ResourceTree) Issue { if tree == nil { return issue } switch issue.Reason { case "SourceNotReady", "ArtifactFailed", "DependencyNotReady": default: return issue } direct := map[Ref]bool{} for _, source := range gitops.FluxSourceReferences(root) { if issue.Reason == "DependencyNotReady" { break } direct[Ref{Group: source.Group, Kind: source.Kind, Namespace: source.Namespace, Name: source.Name}] = true } // Inventory ownership can replace a dependsOn edge without erasing the declaration. deps, _, _ := unstructured.NestedSlice(root.Object, "spec", "dependsOn") for _, raw := range deps { dep, ok := raw.(map[string]any) if issue.Reason != "DependencyNotReady" || !ok || gitops.StringValue(dep["name"]) == "" { continue } direct[Ref{Group: tree.Root.Ref.Group, Kind: tree.Root.Ref.Kind, Namespace: firstNonEmpty(gitops.StringValue(dep["namespace"]), root.GetNamespace()), Name: gitops.StringValue(dep["name"])}] = true } quoted := fluxQuotedReference.FindAllStringSubmatch(issue.Message, -1) sourceMessage := strings.ToLower(issue.Message) missingArtifact := strings.HasPrefix(sourceMessage, "source artifact not found") || strings.HasPrefix(sourceMessage, "source is not ready, artifact not found") var notReadySources []Ref for _, node := range tree.Nodes { ref := refFromTree(node.Ref) if !direct[ref] || node.Role == gitopstree.RoleRoot || node.Remote { continue } for _, match := range quoted { if match[1] != "" && match[1] != "Source" && match[1] != ref.Kind { continue } bareName := match[2] == ref.Name && (match[1] == ref.Kind || match[1] == "Source" || issue.Reason == "DependencyNotReady") if bareName || match[2] == ref.Namespace+"/"+ref.Name || match[2] == ref.Kind+"/"+ref.Namespace+"/"+ref.Name { issue.Refs = append(issue.Refs, ref) break } } stalled, _ := node.Data["stalled"].(bool) if artifact, observed := node.Data["artifact"].(bool); observed && ref.Group == "source.toolkit.fluxcd.io" && (stringData(node.Data, "readyStatus") == "False" || stalled || missingArtifact && !artifact) { notReadySources = append(notReadySources, ref) } } // ArtifactFailed also covers local build-path errors; the reason alone does // not implicate a source: // https://github.com/fluxcd/kustomize-controller/blob/main/internal/controller/kustomization_controller.go if len(issue.Refs) == 0 && len(notReadySources) == 1 && issue.Reason != "DependencyNotReady" && (issue.Reason == "SourceNotReady" || strings.HasPrefix(sourceMessage, "source ") || strings.HasPrefix(sourceMessage, "could not get source object:")) { issue.Refs = notReadySources } // The root's message often stops at "artifact not found"; the linked // object's own error is the actual reason, so it becomes the cause. if len(issue.Refs) == 1 { for _, node := range tree.Nodes { if refFromTree(node.Ref) != issue.Refs[0] { continue } if detail := fluxFailureDetail(node); detail != "" && !strings.Contains(issue.Message, detail) { issue.Cause = fmt.Sprintf("%s %s: %s", node.Ref.Kind, node.Ref.Name, detail) } break } } if len(issue.Refs) > 0 { if issue.Reason == "DependencyNotReady" { issue.Action = "Open the referenced dependency and fix the prerequisite reported by the controller." } else { issue.Action = "Open the referenced source or chart and fix the reported fetch or artifact failure." } } return issue } func fluxSourceWarnings(tree *gitopstree.ResourceTree, issues []Issue) []Issue { if tree == nil { return nil } referenced := map[Ref]bool{} for _, issue := range issues { if issue.Scope == ScopeCondition { for _, ref := range issue.Refs { referenced[ref] = true } } } var out []Issue for _, node := range tree.Nodes { ref := refFromTree(node.Ref) relationship := stringData(node.Data, "relationship") if ref.Group != "source.toolkit.fluxcd.io" || referenced[ref] || (relationship != "source" && relationship != "chart source" && relationship != "chart reference") || node.Remote { continue } artifact, observed := node.Data["artifact"].(bool) stalled, _ := node.Data["stalled"].(bool) fetchFailed, _ := node.Data["fetchFailed"].(bool) if !observed || (stringData(node.Data, "readyStatus") != "False" && !stalled && !fetchFailed) { continue } // The consequence leads: the error is long and the band truncates. message := fmt.Sprintf("%s %s/%s can't fetch updates and has no artifact yet.", ref.Kind, ref.Namespace, ref.Name) if artifact { revision := stringData(node.Data, "revision") if revision == "" { revision = stringData(node.Data, "artifactRevision") } message = fmt.Sprintf("%s %s/%s can't fetch updates. Flux can keep applying the last fetched revision (%s), but new changes won't land until the fetch succeeds.", ref.Kind, ref.Namespace, ref.Name, revision) } out = append(out, Issue{Severity: SeverityWarning, Scope: ScopeTree, Reason: "SourceNotReady", Message: message, Cause: fluxFailureDetail(node), Refs: []Ref{ref}, Action: "Open the source or chart and fix the reported fetch failure."}) } return out } func unexplainedFluxDegraded(tree *gitopstree.ResourceTree, issues []Issue) int { referenced := map[Ref]bool{} for _, issue := range issues { if issue.Scope == ScopeCondition && issue.Severity == SeverityCritical || issue.Scope == ScopeTree && issue.Reason == "SourceNotReady" { for _, ref := range issue.Refs { referenced[ref] = true } } } count := tree.Summary.Degraded for _, node := range tree.Nodes { if !node.Dependency && (node.Role == gitopstree.RoleDeclared || node.Role == gitopstree.RoleGenerated) && (node.Health == "Degraded" || node.Health == "Missing") && referenced[refFromTree(node.Ref)] { count-- } } return count }