| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | package program |
| 4 | |
| 5 | import ( |
| 6 | "errors" |
| 7 | "fmt" |
| 8 | "strings" |
| 9 | ) |
| 10 | |
| 11 | // PromotionMode defines how non-identity chart labels are selected. |
| 12 | type PromotionMode string |
| 13 | |
| 14 | const ( |
| 15 | PromotionModeAutoIntersection PromotionMode = "auto_intersection" |
| 16 | PromotionModeExplicitIntersection PromotionMode = "explicit_intersection" |
| 17 | ) |
| 18 | |
| 19 | // LabelPolicy describes chart-label promotion and conflict behavior in IR. |
| 20 | type LabelPolicy struct { |
| 21 | Mode PromotionMode |
| 22 | |
| 23 | // PromoteKeys is explicit allowlist for PromotionModeExplicitIntersection. |
| 24 | PromoteKeys []string |
| 25 | |
| 26 | // Exclusions contains compile-resolved label keys that must never be |
| 27 | // promoted (e.g. selector-constrained keys, dynamic dimension-name keys). |
| 28 | Exclusions LabelExclusions |
| 29 | |
| 30 | // Precedence controls deterministic same-key merge behavior. |
| 31 | Precedence LabelPrecedence |
| 32 | } |
| 33 | |
| 34 | // LabelExclusions stores precomputed exclusion sets used by chart labeling. |
| 35 | type LabelExclusions struct { |
| 36 | SelectorConstrainedKeys []string |
| 37 | DimensionKeyLabels []string |
| 38 | } |
| 39 | |
| 40 | // LabelPrecedence encodes the selected conflict policy. |
| 41 | type LabelPrecedence struct { |
| 42 | SelectedOverInstance bool |
| 43 | InstanceOverJob bool |
| 44 | ReservedImmutable bool |
| 45 | } |
| 46 | |
| 47 | // DefaultLabelPrecedence returns the phase-1 conflict policy baseline. |
| 48 | func DefaultLabelPrecedence() LabelPrecedence { |
| 49 | return LabelPrecedence{ |
| 50 | SelectedOverInstance: true, |
| 51 | InstanceOverJob: true, |
| 52 | ReservedImmutable: true, |
| 53 | } |
| 54 | } |
| 55 | |
| 56 | func validateLabelPolicy(policy LabelPolicy) error { |
| 57 | switch policy.Mode { |
| 58 | case PromotionModeAutoIntersection, PromotionModeExplicitIntersection: |
| 59 | return nil |
| 60 | default: |
| 61 | return fmt.Errorf("invalid promotion mode %q", policy.Mode) |
| 62 | } |
| 63 | } |
| 64 | |
| 65 | func validateInstanceLabelSelectors(selectors []InstanceLabelSelector) error { |
| 66 | if len(selectors) == 0 { |
| 67 | return nil |
| 68 | } |
| 69 | |
| 70 | hasPositive := false |
| 71 | var errs []error |
| 72 | for i, selector := range selectors { |
| 73 | switch { |
| 74 | case selector.IncludeAll: |
| 75 | if selector.Exclude || selector.Key != "" { |
| 76 | errs = append(errs, fmt.Errorf("instance selector[%d]: include-all selector must not set exclude or key", i)) |
| 77 | continue |
| 78 | } |
| 79 | hasPositive = true |
| 80 | case selector.Exclude: |
| 81 | if selector.Key == "" { |
| 82 | errs = append(errs, fmt.Errorf("instance selector[%d]: exclude selector key is required", i)) |
| 83 | continue |
| 84 | } |
| 85 | if strings.TrimSpace(selector.Key) != selector.Key { |
| 86 | errs = append(errs, fmt.Errorf("instance selector[%d]: exclude selector key must be trimmed", i)) |
| 87 | } |
| 88 | case selector.Key != "": |
| 89 | if strings.TrimSpace(selector.Key) != selector.Key { |
| 90 | errs = append(errs, fmt.Errorf("instance selector[%d]: selector key must be trimmed", i)) |
| 91 | } |
| 92 | hasPositive = true |
| 93 | default: |
| 94 | errs = append(errs, fmt.Errorf("instance selector[%d]: selector is empty", i)) |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | if !hasPositive { |
| 99 | errs = append(errs, fmt.Errorf("instance selectors must include at least one positive selector")) |
| 100 | } |
| 101 | return errors.Join(errs...) |
| 102 | } |
| 103 | |
| 104 | // InstanceLabelSelector is a normalized token from instances.by_labels. |
| 105 | type InstanceLabelSelector struct { |
| 106 | // IncludeAll corresponds to token "*". |
| 107 | IncludeAll bool |
| 108 | // Exclude corresponds to token "!<key>". |
| 109 | Exclude bool |
| 110 | // Key is used by explicit include or exclude token. |
| 111 | Key string |
| 112 | } |
| 113 | |
| 114 | func (p LabelPolicy) clone() LabelPolicy { |
| 115 | out := p |
| 116 | out.PromoteKeys = append([]string(nil), p.PromoteKeys...) |
| 117 | out.Exclusions = p.Exclusions.clone() |
| 118 | out.Precedence = p.Precedence |
| 119 | return out |
| 120 | } |
| 121 | |
| 122 | func (e LabelExclusions) clone() LabelExclusions { |
| 123 | out := e |
| 124 | out.SelectorConstrainedKeys = append([]string(nil), e.SelectorConstrainedKeys...) |
| 125 | out.DimensionKeyLabels = append([]string(nil), e.DimensionKeyLabels...) |
| 126 | return out |
| 127 | } |
| 128 | |
| 129 | func (s InstanceLabelSelector) clone() InstanceLabelSelector { |
| 130 | return s |
| 131 | } |