master
go 279 lines 5.97 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package httpsd
4
5 import (
6 "bytes"
7 "encoding/json"
8 "errors"
9 "fmt"
10 "hash/fnv"
11 "io"
12 "sort"
13 "strings"
14
15 "github.com/netdata/netdata/go/plugins/plugin/agent/discovery/sd/model"
16
17 "gopkg.in/yaml.v2"
18 )
19
20 type responseParser struct {
21 format string
22 }
23
24 func (p responseParser) parse(bs []byte, contentType string) ([]any, error) {
25 switch p.format {
26 case formatJSON:
27 return parseItemsJSON(bs)
28 case formatYAML:
29 return parseItemsYAML(bs)
30 case formatAuto:
31 return p.parseAuto(bs, contentType)
32 default:
33 return nil, fmt.Errorf("unsupported format %q", p.format)
34 }
35 }
36
37 func (p responseParser) parseAuto(bs []byte, contentType string) ([]any, error) {
38 switch detectContentTypeFormat(contentType) {
39 case formatJSON:
40 return parseItemsJSON(bs)
41 case formatYAML:
42 return parseItemsYAML(bs)
43 }
44
45 items, jsonErr := parseItemsJSON(bs)
46 if jsonErr == nil {
47 return items, nil
48 }
49
50 items, yamlErr := parseItemsYAML(bs)
51 if yamlErr == nil {
52 return items, nil
53 }
54
55 return nil, fmt.Errorf("parse response as json: %v; parse response as yaml: %v", jsonErr, yamlErr)
56 }
57
58 func detectContentTypeFormat(contentType string) string {
59 if i := strings.IndexByte(contentType, ';'); i >= 0 {
60 contentType = contentType[:i]
61 }
62 contentType = strings.TrimSpace(strings.ToLower(contentType))
63
64 switch {
65 case contentType == "application/json",
66 contentType == "text/json",
67 strings.HasSuffix(contentType, "+json"):
68 return formatJSON
69 case contentType == "application/yaml",
70 contentType == "application/x-yaml",
71 contentType == "text/yaml",
72 contentType == "text/x-yaml",
73 strings.HasSuffix(contentType, "+yaml"),
74 strings.HasSuffix(contentType, "+x-yaml"):
75 return formatYAML
76 default:
77 return ""
78 }
79 }
80
81 func parseItemsJSON(bs []byte) ([]any, error) {
82 dec := json.NewDecoder(bytes.NewReader(bs))
83
84 var data any
85 if err := dec.Decode(&data); err != nil {
86 return nil, err
87 }
88 var extra any
89 if err := dec.Decode(&extra); err == nil {
90 return nil, errors.New("multiple JSON values are not supported")
91 } else if !errors.Is(err, io.EOF) {
92 return nil, err
93 }
94
95 return extractItems(data)
96 }
97
98 func parseItemsYAML(bs []byte) ([]any, error) {
99 dec := yaml.NewDecoder(bytes.NewReader(bs))
100
101 var data any
102 if err := dec.Decode(&data); err != nil {
103 return nil, err
104 }
105 var extra any
106 if err := dec.Decode(&extra); err == nil {
107 return nil, errors.New("multiple YAML documents are not supported")
108 } else if !errors.Is(err, io.EOF) {
109 return nil, err
110 }
111
112 data, err := normalizeYAMLValue(data)
113 if err != nil {
114 return nil, err
115 }
116
117 return extractItems(data)
118 }
119
120 func extractItems(data any) ([]any, error) {
121 switch v := data.(type) {
122 case []any:
123 return normalizeItems(v)
124 case map[string]any:
125 items, ok := v["items"]
126 if !ok {
127 return nil, errors.New("unsupported response envelope: missing items field")
128 }
129 arr, ok := items.([]any)
130 if !ok {
131 return nil, fmt.Errorf("unsupported response envelope: items must be an array, got %T", items)
132 }
133 return normalizeItems(arr)
134 default:
135 return nil, fmt.Errorf("unsupported response format: expected array or object with items array, got %T", data)
136 }
137 }
138
139 func normalizeItems(items []any) ([]any, error) {
140 out := make([]any, 0, len(items))
141 for i, item := range items {
142 norm, err := normalizeItem(item)
143 if err != nil {
144 return nil, fmt.Errorf("item[%d]: %w", i, err)
145 }
146 out = append(out, norm)
147 }
148 return out, nil
149 }
150
151 func normalizeItem(item any) (any, error) {
152 switch v := item.(type) {
153 case map[string]any:
154 return normalizeMap(v)
155 case string:
156 return v, nil
157 default:
158 return nil, fmt.Errorf("unsupported item type %T", item)
159 }
160 }
161
162 func normalizeYAMLValue(v any) (any, error) {
163 switch vv := v.(type) {
164 case map[any]any:
165 m := make(map[string]any, len(vv))
166 for k, iv := range vv {
167 ks, ok := k.(string)
168 if !ok {
169 return nil, fmt.Errorf("yaml map key must be string, got %T", k)
170 }
171 norm, err := normalizeYAMLValue(iv)
172 if err != nil {
173 return nil, err
174 }
175 m[ks] = norm
176 }
177 return m, nil
178 case map[string]any:
179 return normalizeMap(vv)
180 case []any:
181 arr := make([]any, 0, len(vv))
182 for _, iv := range vv {
183 norm, err := normalizeYAMLValue(iv)
184 if err != nil {
185 return nil, err
186 }
187 arr = append(arr, norm)
188 }
189 return arr, nil
190 default:
191 return v, nil
192 }
193 }
194
195 func normalizeMap(src map[string]any) (map[string]any, error) {
196 m := make(map[string]any, len(src))
197 for k, v := range src {
198 norm, err := normalizeYAMLValue(v)
199 if err != nil {
200 return nil, err
201 }
202 m[k] = norm
203 }
204 return m, nil
205 }
206
207 func targetsFromItems(source string, items []any) ([]model.Target, error) {
208 targets := make([]model.Target, 0, len(items))
209 for i, item := range items {
210 canonical, err := canonicalJSON(item)
211 if err != nil {
212 return nil, fmt.Errorf("item[%d]: canonical encoding: %w", i, err)
213 }
214
215 tgt := &target{
216 label: itemLabel(item, canonical),
217 Item: item,
218 }
219 hash, err := model.CalcHash(struct {
220 Source string
221 Item string
222 }{
223 Source: source,
224 Item: string(canonical),
225 })
226 if err != nil {
227 return nil, fmt.Errorf("item[%d]: target hash: %w", i, err)
228 }
229 tgt.hash = hash
230 targets = append(targets, tgt)
231 }
232 return targets, nil
233 }
234
235 func itemLabel(item any, canonical []byte) string {
236 if m, ok := item.(map[string]any); ok {
237 if v, ok := m["name"].(string); ok {
238 if name := strings.TrimSpace(v); name != "" {
239 return name
240 }
241 }
242 }
243 return fmt.Sprintf("item-%x", hashBytes(canonical))
244 }
245
246 func canonicalJSON(v any) ([]byte, error) {
247 return json.Marshal(canonicalValue(v))
248 }
249
250 func canonicalValue(v any) any {
251 switch vv := v.(type) {
252 case map[string]any:
253 keys := make([]string, 0, len(vv))
254 for k := range vv {
255 keys = append(keys, k)
256 }
257 sort.Strings(keys)
258
259 m := make(map[string]any, len(vv))
260 for _, k := range keys {
261 m[k] = canonicalValue(vv[k])
262 }
263 return m
264 case []any:
265 arr := make([]any, 0, len(vv))
266 for _, iv := range vv {
267 arr = append(arr, canonicalValue(iv))
268 }
269 return arr
270 default:
271 return v
272 }
273 }
274
275 func hashBytes(bs []byte) uint64 {
276 h := fnv.New64a()
277 _, _ = h.Write(bs)
278 return h.Sum64()
279 }