master
go 220 lines 5.69 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package pipeline
4
5 import (
6 "testing"
7
8 "github.com/netdata/netdata/go/plugins/plugin/agent/discovery/sd/model"
9 "github.com/netdata/netdata/go/plugins/plugin/framework/confgroup"
10
11 "github.com/stretchr/testify/assert"
12 "github.com/stretchr/testify/require"
13 "gopkg.in/yaml.v2"
14 )
15
16 func TestServiceEngine_compose(t *testing.T) {
17 // Config A:
18 // - rule1: matches exactly Name == "mock1" and yields 1 config
19 // - rule2: matches Name == "mock2" or "mock3" and yields 2 configs (YAML list)
20 // - drop1: matches Name == "dropme" but has no config_template => hard drop (break)
21 // - rule3: another rule matching Name == "mock3" to validate multi-rule aggregation
22 configA := `
23 - id: "rule1"
24 match: '{{ eq .Name "mock1" }}'
25 config_template: |
26 name: {{ .Name }}-1
27 - id: "rule2"
28 match: '{{ or (eq .Name "mock2") (eq .Name "mock3") }}'
29 config_template: |
30 - name: {{ .Name }}-2
31 - name: {{ .Name }}-3
32 - id: "drop1"
33 match: '{{ eq .Name "dropme" }}'
34 - id: "rule3"
35 match: '{{ eq .Name "mock3" }}'
36 config_template: |
37 - name: {{ .Name }}-4
38 `
39
40 // Config B:
41 // - drop3: matches Name == "mock3" with no template => hard drop (break)
42 // - rule2/rule3 exist but must NOT be evaluated if drop3 matches
43 configB := `
44 - id: "rule1"
45 match: '{{ eq .Name "mock1" }}'
46 config_template: |
47 name: {{ .Name }}-1
48 - id: "drop3"
49 match: '{{ eq .Name "mock3" }}'
50 - id: "rule2"
51 match: '{{ or (eq .Name "mock2") (eq .Name "mock3") }}'
52 config_template: |
53 - name: {{ .Name }}-2
54 - name: {{ .Name }}-3
55 - id: "rule3"
56 match: '{{ eq .Name "mock3" }}'
57 config_template: |
58 - name: {{ .Name }}-4
59 `
60
61 type tc struct {
62 configYAML string
63 target model.Target
64 wantConfigs []confgroup.Config
65 }
66
67 tests := map[string]tc{
68 "no rules match": {
69 configYAML: configA,
70 target: newMockTarget("nothing"),
71 wantConfigs: nil,
72 },
73 "drop rule hit (no config_template)": {
74 configYAML: configA,
75 target: newMockTarget("dropme"),
76 wantConfigs: nil,
77 },
78 "one rule -> one config": {
79 configYAML: configA,
80 target: newMockTarget("mock1"),
81 wantConfigs: []confgroup.Config{
82 {"name": "mock1-1", "module": "rule1"},
83 },
84 },
85 "one rule -> two configs (YAML list)": {
86 configYAML: configA,
87 target: newMockTarget("mock2"),
88 wantConfigs: []confgroup.Config{
89 {"name": "mock2-2", "module": "rule2"},
90 {"name": "mock2-3", "module": "rule2"},
91 },
92 },
93 "multiple rules aggregated (no drop before)": {
94 configYAML: configA,
95 target: newMockTarget("mock3"),
96 wantConfigs: []confgroup.Config{
97 {"name": "mock3-2", "module": "rule2"},
98 {"name": "mock3-3", "module": "rule2"},
99 {"name": "mock3-4", "module": "rule3"},
100 },
101 },
102 "hard drop stops further evaluation": {
103 configYAML: configB,
104 target: newMockTarget("mock3"),
105 wantConfigs: nil, // drop3 matches first => break => rule2/rule3 ignored
106 },
107 }
108
109 for name, test := range tests {
110 t.Run(name, func(t *testing.T) {
111 var cfg []ServiceRuleConfig
112 err := yaml.Unmarshal([]byte(test.configYAML), &cfg)
113 require.NoErrorf(t, err, "yaml unmarshalling of services config")
114
115 svr, err := newServiceEngine(cfg)
116 require.NoErrorf(t, err, "service engine creation")
117
118 assert.Equal(t, test.wantConfigs, svr.compose(test.target))
119 })
120 }
121 }
122
123 func TestServiceEngine_composeHTTPItems(t *testing.T) {
124 tests := map[string]struct {
125 configYAML string
126 target model.Target
127 wantConfigs []confgroup.Config
128 }{
129 "full job pass-through preserves module": {
130 configYAML: `
131 - id: "passthrough"
132 match: '{{ true }}'
133 config_template: |
134 {{ .Item | toYaml }}
135 `,
136 target: &itemTarget{Item: map[string]any{
137 "module": "nginx",
138 "name": "local",
139 "url": "http://127.0.0.1/stub_status",
140 }},
141 wantConfigs: []confgroup.Config{
142 {"module": "nginx", "name": "local", "url": "http://127.0.0.1/stub_status"},
143 },
144 },
145 "full job pass-through preserves numeric fields": {
146 configYAML: `
147 - id: "passthrough"
148 match: '{{ true }}'
149 config_template: |
150 {{ .Item | toYaml }}
151 `,
152 target: &itemTarget{Item: map[string]any{
153 "module": "httpcheck",
154 "name": "api",
155 "url": "http://127.0.0.1/health",
156 "port": float64(80),
157 }},
158 wantConfigs: []confgroup.Config{
159 {"module": "httpcheck", "name": "api", "url": "http://127.0.0.1/health", "port": 80},
160 },
161 },
162 "endpoint object fills module from rule id": {
163 configYAML: `
164 - id: "httpcheck"
165 match: '{{ hasKey .Item "url" }}'
166 config_template: |
167 name: {{ .Item.name }}
168 url: {{ .Item.url }}
169 `,
170 target: &itemTarget{Item: map[string]any{
171 "name": "api",
172 "url": "http://127.0.0.1/health",
173 }},
174 wantConfigs: []confgroup.Config{
175 {"module": "httpcheck", "name": "api", "url": "http://127.0.0.1/health"},
176 },
177 },
178 "scalar endpoint string uses TUID": {
179 configYAML: `
180 - id: "httpcheck"
181 match: '{{ kindIs "string" .Item }}'
182 config_template: |
183 name: {{ .TUID }}
184 url: {{ .Item }}
185 `,
186 target: &itemTarget{Item: "http://127.0.0.1/health", tuid: "http_item_1"},
187 wantConfigs: []confgroup.Config{
188 {"module": "httpcheck", "name": "http_item_1", "url": "http://127.0.0.1/health"},
189 },
190 },
191 }
192
193 for name, test := range tests {
194 t.Run(name, func(t *testing.T) {
195 var cfg []ServiceRuleConfig
196 err := yaml.Unmarshal([]byte(test.configYAML), &cfg)
197 require.NoErrorf(t, err, "yaml unmarshalling of services config")
198
199 svr, err := newServiceEngine(cfg)
200 require.NoErrorf(t, err, "service engine creation")
201
202 assert.Equal(t, test.wantConfigs, svr.compose(test.target))
203 })
204 }
205 }
206
207 type itemTarget struct {
208 model.Base
209 Item any
210 tuid string
211 }
212
213 func (t itemTarget) TUID() string {
214 if t.tuid != "" {
215 return t.tuid
216 }
217 return "item"
218 }
219
220 func (t itemTarget) Hash() uint64 { return 1 }