cloudflare/pint
Publicmirrored from https://github.com/cloudflare/pintAvailable
internal/parser/strict.go
210lines · modecode
| 1 | package parser |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "fmt" |
| 6 | "strings" |
| 7 | |
| 8 | "github.com/prometheus/common/model" |
| 9 | "gopkg.in/yaml.v3" |
| 10 | ) |
| 11 | |
| 12 | // https://github.com/go-yaml/yaml/blob/v3.0.1/resolve.go#L70-L81 |
| 13 | const ( |
| 14 | nullTag = "!!null" |
| 15 | boolTag = "!!bool" |
| 16 | strTag = "!!str" |
| 17 | intTag = "!!int" |
| 18 | floatTag = "!!float" |
| 19 | timestampTag = "!!timestamp" |
| 20 | seqTag = "!!seq" |
| 21 | mapTag = "!!map" |
| 22 | binaryTag = "!!binary" |
| 23 | mergeTag = "!!merge" |
| 24 | ) |
| 25 | |
| 26 | func describeTag(tag string) string { |
| 27 | switch tag { |
| 28 | case strTag: |
| 29 | return "string" |
| 30 | case intTag: |
| 31 | return "integer" |
| 32 | case seqTag: |
| 33 | return "list" |
| 34 | case mapTag: |
| 35 | return "mapping" |
| 36 | case binaryTag: |
| 37 | return "binary data" |
| 38 | default: |
| 39 | return strings.TrimLeft(tag, "!") |
| 40 | } |
| 41 | } |
| 42 | |
| 43 | func parseGroups(content []byte, doc *yaml.Node) (rules []Rule, err ParseError) { |
| 44 | names := map[string]struct{}{} |
| 45 | |
| 46 | for _, node := range unpackNodes(doc) { |
| 47 | if !isTag(node.ShortTag(), mapTag) { |
| 48 | return nil, ParseError{ |
| 49 | Line: node.Line, |
| 50 | Err: fmt.Errorf("top level field must be a groups key, got %s", describeTag(node.ShortTag())), |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | for _, entry := range mappingNodes(node) { |
| 55 | if entry.key.ShortTag() != strTag { |
| 56 | return nil, ParseError{ |
| 57 | Line: entry.key.Line, |
| 58 | Err: fmt.Errorf("groups key must be a %s, got a %s", describeTag(strTag), describeTag(entry.key.ShortTag())), |
| 59 | } |
| 60 | } |
| 61 | if entry.key.Value != "groups" { |
| 62 | return nil, ParseError{ |
| 63 | Line: entry.key.Line, |
| 64 | Err: fmt.Errorf("unexpected key %s", entry.key.Value), |
| 65 | } |
| 66 | } |
| 67 | if !isTag(entry.val.ShortTag(), seqTag) { |
| 68 | return nil, ParseError{ |
| 69 | Line: entry.key.Line, |
| 70 | Err: fmt.Errorf("groups value must be a %s, got %s", describeTag(seqTag), describeTag(entry.val.ShortTag())), |
| 71 | } |
| 72 | } |
| 73 | for _, group := range unpackNodes(entry.val) { |
| 74 | name, r, err := parseGroup(content, group) |
| 75 | if err.Err != nil { |
| 76 | return rules, err |
| 77 | } |
| 78 | if _, ok := names[name]; ok { |
| 79 | return nil, ParseError{ |
| 80 | Line: group.Line, |
| 81 | Err: errors.New("duplicated group name"), |
| 82 | } |
| 83 | } |
| 84 | names[name] = struct{}{} |
| 85 | rules = append(rules, r...) |
| 86 | } |
| 87 | } |
| 88 | } |
| 89 | return rules, ParseError{} |
| 90 | } |
| 91 | |
| 92 | func parseGroup(content []byte, group *yaml.Node) (name string, rules []Rule, err ParseError) { |
| 93 | if !isTag(group.ShortTag(), mapTag) { |
| 94 | return "", nil, ParseError{ |
| 95 | Line: group.Line, |
| 96 | Err: fmt.Errorf("group must be a %s, got %s", describeTag(mapTag), describeTag(group.ShortTag())), |
| 97 | } |
| 98 | } |
| 99 | |
| 100 | setKeys := make(map[string]struct{}, len(group.Content)) |
| 101 | |
| 102 | for _, entry := range mappingNodes(group) { |
| 103 | switch entry.key.Value { |
| 104 | case "name": |
| 105 | if entry.val.Kind != yaml.ScalarNode || entry.val.ShortTag() != strTag { |
| 106 | return "", nil, ParseError{ |
| 107 | Line: entry.key.Line, |
| 108 | Err: fmt.Errorf("group name must be a %s, got %s", describeTag(strTag), describeTag(entry.val.ShortTag())), |
| 109 | } |
| 110 | } |
| 111 | if entry.val.Value == "" { |
| 112 | return "", nil, ParseError{ |
| 113 | Line: entry.key.Line, |
| 114 | Err: errors.New("group name cannot be empty"), |
| 115 | } |
| 116 | } |
| 117 | name = entry.val.Value |
| 118 | case "interval", "query_offset": |
| 119 | if entry.val.Kind != yaml.ScalarNode || entry.val.ShortTag() != strTag { |
| 120 | return "", nil, ParseError{ |
| 121 | Line: entry.key.Line, |
| 122 | Err: fmt.Errorf("group %s must be a %s, got %s", entry.key.Value, describeTag(strTag), describeTag(entry.val.ShortTag())), |
| 123 | } |
| 124 | } |
| 125 | if _, err := model.ParseDuration(entry.val.Value); err != nil { |
| 126 | return "", nil, ParseError{ |
| 127 | Line: entry.key.Line, |
| 128 | Err: fmt.Errorf("invalid %s value: %w", entry.key.Value, err), |
| 129 | } |
| 130 | } |
| 131 | case "limit": |
| 132 | if entry.val.Kind != yaml.ScalarNode || entry.val.ShortTag() != intTag { |
| 133 | return "", nil, ParseError{ |
| 134 | Line: entry.key.Line, |
| 135 | Err: fmt.Errorf("group limit must be a %s, got %s", describeTag(intTag), describeTag(entry.val.ShortTag())), |
| 136 | } |
| 137 | } |
| 138 | case "rules": |
| 139 | if !isTag(entry.val.ShortTag(), seqTag) { |
| 140 | return "", nil, ParseError{ |
| 141 | Line: entry.key.Line, |
| 142 | Err: fmt.Errorf("rules must be a %s, got %s", describeTag(seqTag), describeTag(entry.val.ShortTag())), |
| 143 | } |
| 144 | } |
| 145 | for _, rule := range unpackNodes(entry.val) { |
| 146 | r, err := parseRuleStrict(content, rule) |
| 147 | if err.Err != nil { |
| 148 | return "", nil, err |
| 149 | } |
| 150 | rules = append(rules, r) |
| 151 | } |
| 152 | default: |
| 153 | return "", nil, ParseError{ |
| 154 | Line: entry.key.Line, |
| 155 | Err: fmt.Errorf("invalid group key %s", entry.key.Value), |
| 156 | } |
| 157 | } |
| 158 | |
| 159 | if _, ok := setKeys[entry.key.Value]; ok { |
| 160 | return "", nil, ParseError{ |
| 161 | Line: entry.key.Line, |
| 162 | Err: fmt.Errorf("duplicated key %s", entry.key.Value), |
| 163 | } |
| 164 | } |
| 165 | setKeys[entry.key.Value] = struct{}{} |
| 166 | } |
| 167 | |
| 168 | if _, ok := setKeys["rules"]; ok { |
| 169 | if _, ok := setKeys["name"]; !ok { |
| 170 | return "", nil, ParseError{ |
| 171 | Line: group.Line, |
| 172 | Err: errors.New("incomplete group definition, name is required and must be set"), |
| 173 | } |
| 174 | } |
| 175 | } |
| 176 | |
| 177 | return name, rules, ParseError{} |
| 178 | } |
| 179 | |
| 180 | func parseRuleStrict(content []byte, rule *yaml.Node) (Rule, ParseError) { |
| 181 | if !isTag(rule.ShortTag(), mapTag) { |
| 182 | return Rule{}, ParseError{ |
| 183 | Line: rule.Line, |
| 184 | Err: fmt.Errorf("rule definion must be a %s, got %s", describeTag(mapTag), describeTag(rule.ShortTag())), |
| 185 | } |
| 186 | } |
| 187 | |
| 188 | for i, node := range unpackNodes(rule) { |
| 189 | if i%2 != 0 { |
| 190 | continue |
| 191 | } |
| 192 | switch node.Value { |
| 193 | case "record": |
| 194 | case "alert": |
| 195 | case "expr": |
| 196 | case "for": |
| 197 | case "keep_firing_for": |
| 198 | case "labels": |
| 199 | case "annotations": |
| 200 | default: |
| 201 | return Rule{}, ParseError{ |
| 202 | Line: node.Line, |
| 203 | Err: fmt.Errorf("invalid rule key %s", node.Value), |
| 204 | } |
| 205 | } |
| 206 | } |
| 207 | |
| 208 | r, _ := parseRule(content, rule, 0) |
| 209 | return r, ParseError{} |
| 210 | } |
| 211 | |