master
go 243 lines 6.17 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package sd
4
5 import (
6 "bytes"
7 "context"
8 "errors"
9 "fmt"
10 "sync"
11 "testing"
12 "time"
13
14 "github.com/netdata/netdata/go/plugins/logger"
15 "github.com/netdata/netdata/go/plugins/pkg/netdataapi"
16 "github.com/netdata/netdata/go/plugins/pkg/safewriter"
17 "github.com/netdata/netdata/go/plugins/plugin/agent/discovery/sd/pipeline"
18 "github.com/netdata/netdata/go/plugins/plugin/framework/confgroup"
19 "github.com/netdata/netdata/go/plugins/plugin/framework/dyncfg"
20
21 "github.com/stretchr/testify/assert"
22 )
23
24 var lock = &sync.Mutex{}
25
26 const testPluginName = "test"
27
28 type discoverySim struct {
29 configs []confFile
30 wantPipelines []*mockPipeline
31 }
32
33 // discoverySimExt is an extended simulation that also checks exposed configs
34 type discoverySimExt struct {
35 configs []confFile
36 wantPipelines []*mockPipeline
37 wantExposedCount int
38 wantExposed []wantExposedCfg
39 }
40
41 type wantExposedCfg struct {
42 discovererType string
43 name string
44 sourceType string
45 status dyncfg.Status
46 }
47
48 func (sim *discoverySimExt) run(t *testing.T) {
49 fact := &mockFactory{}
50 var buf bytes.Buffer
51 mgr := &ServiceDiscovery{
52 Logger: logger.New(),
53 pluginName: testPluginName,
54 newPipeline: func(config pipeline.Config) (sdPipeline, error) {
55 return fact.create(config)
56 },
57 confProv: &mockConfigProvider{
58 confFiles: sim.configs,
59 ch: make(chan confFile),
60 },
61 dyncfgApi: dyncfg.NewResponder(netdataapi.New(safewriter.New(&buf))),
62 seen: dyncfg.NewSeenCache[sdConfig](),
63 exposed: dyncfg.NewExposedCache[sdConfig](),
64 discoverers: testDiscovererRegistry(),
65 // dyncfgCh is intentionally nil to trigger auto-enable in tests
66 }
67 mgr.sdCb = &sdCallbacks{sd: mgr}
68 mgr.handler = dyncfg.NewHandler(dyncfg.HandlerOpts[sdConfig]{
69 Logger: mgr.Logger,
70 API: mgr.dyncfgApi,
71 Seen: mgr.seen,
72 Exposed: mgr.exposed,
73 Callbacks: mgr.sdCb,
74
75 Path: fmt.Sprintf(dyncfgSDPath, testPluginName),
76 EnableFailCode: 422,
77 JobCommands: []dyncfg.Command{
78 dyncfg.CommandSchema,
79 dyncfg.CommandGet,
80 dyncfg.CommandEnable,
81 dyncfg.CommandDisable,
82 dyncfg.CommandUpdate,
83 dyncfg.CommandTest,
84 dyncfg.CommandUserconfig,
85 },
86 })
87
88 in := make(chan<- []*confgroup.Group)
89 done := make(chan struct{})
90 ctx, cancel := context.WithCancel(context.Background())
91
92 go func() { defer close(done); mgr.Run(ctx, in) }()
93
94 time.Sleep(time.Second * 3)
95
96 lock.Lock()
97 if sim.wantPipelines != nil {
98 assert.Equalf(t, sim.wantPipelines, fact.pipelines, "pipelines mismatch")
99 }
100 lock.Unlock()
101
102 cancel()
103
104 timeout := time.Second * 5
105 select {
106 case <-done:
107 case <-time.After(timeout):
108 t.Errorf("sd failed to exit in %s", timeout)
109 }
110
111 // Check exposed configs after SD goroutine has stopped (no race on entry.Status).
112 // Caches survive shutdown — StopAll only stops pipelines, doesn't clear caches.
113 assert.Equal(t, sim.wantExposedCount, mgr.exposed.Count(), "exposed configs count")
114 for _, want := range sim.wantExposed {
115 entry, ok := mgr.exposed.LookupByKey(want.discovererType + ":" + want.name)
116 if !assert.Truef(t, ok, "exposed config '%s:%s' not found", want.discovererType, want.name) {
117 continue
118 }
119 assert.Equal(t, want.sourceType, entry.Cfg.SourceType(), "exposed config '%s:%s' sourceType", want.discovererType, want.name)
120 assert.Equal(t, want.status, entry.Status, "exposed config '%s:%s' status", want.discovererType, want.name)
121 }
122 }
123
124 func (sim *discoverySim) run(t *testing.T) {
125 fact := &mockFactory{}
126 var buf bytes.Buffer
127 mgr := &ServiceDiscovery{
128 Logger: logger.New(),
129 pluginName: testPluginName,
130 newPipeline: func(config pipeline.Config) (sdPipeline, error) {
131 return fact.create(config)
132 },
133 confProv: &mockConfigProvider{
134 confFiles: sim.configs,
135 ch: make(chan confFile),
136 },
137 dyncfgApi: dyncfg.NewResponder(netdataapi.New(safewriter.New(&buf))),
138 seen: dyncfg.NewSeenCache[sdConfig](),
139 exposed: dyncfg.NewExposedCache[sdConfig](),
140 discoverers: testDiscovererRegistry(),
141 // dyncfgCh is intentionally nil to trigger auto-enable in tests
142 // (simulates terminal mode where netdata is not available)
143 }
144 mgr.sdCb = &sdCallbacks{sd: mgr}
145 mgr.handler = dyncfg.NewHandler(dyncfg.HandlerOpts[sdConfig]{
146 Logger: mgr.Logger,
147 API: mgr.dyncfgApi,
148 Seen: mgr.seen,
149 Exposed: mgr.exposed,
150 Callbacks: mgr.sdCb,
151
152 Path: fmt.Sprintf(dyncfgSDPath, testPluginName),
153 EnableFailCode: 422,
154 JobCommands: []dyncfg.Command{
155 dyncfg.CommandSchema,
156 dyncfg.CommandGet,
157 dyncfg.CommandEnable,
158 dyncfg.CommandDisable,
159 dyncfg.CommandUpdate,
160 dyncfg.CommandTest,
161 dyncfg.CommandUserconfig,
162 },
163 })
164
165 in := make(chan<- []*confgroup.Group)
166 done := make(chan struct{})
167 ctx, cancel := context.WithCancel(context.Background())
168
169 go func() { defer close(done); mgr.Run(ctx, in) }()
170
171 time.Sleep(time.Second * 3)
172
173 lock.Lock()
174 assert.Equalf(t, sim.wantPipelines, fact.pipelines, "before stop")
175 lock.Unlock()
176
177 cancel()
178
179 timeout := time.Second * 5
180
181 select {
182 case <-done:
183 lock.Lock()
184 for _, pl := range fact.pipelines {
185 assert.Truef(t, pl.stopped, "pipeline '%s' is not stopped after cancel()", pl.name)
186 }
187 lock.Unlock()
188 case <-time.After(timeout):
189 t.Errorf("sd failed to exit in %s", timeout)
190 }
191 }
192
193 type mockConfigProvider struct {
194 confFiles []confFile
195 ch chan confFile
196 }
197
198 func (m *mockConfigProvider) run(ctx context.Context) {
199 for _, conf := range m.confFiles {
200 select {
201 case <-ctx.Done():
202 return
203 case m.ch <- conf:
204 }
205 }
206 <-ctx.Done()
207 }
208
209 func (m *mockConfigProvider) configs() chan confFile {
210 return m.ch
211 }
212
213 type mockFactory struct {
214 pipelines []*mockPipeline
215 }
216
217 func (m *mockFactory) create(cfg pipeline.Config) (sdPipeline, error) {
218 lock.Lock()
219 defer lock.Unlock()
220
221 if cfg.Name == "invalid" || len(cfg.Services) == 0 {
222 return nil, errors.New("mock sdPipelineFactory.create() error")
223 }
224
225 pl := mockPipeline{name: cfg.Name}
226 m.pipelines = append(m.pipelines, &pl)
227
228 return &pl, nil
229 }
230
231 type mockPipeline struct {
232 name string
233 started bool
234 stopped bool
235 }
236
237 func (m *mockPipeline) Run(ctx context.Context, _ chan<- []*confgroup.Group) {
238 lock.Lock()
239 m.started = true
240 lock.Unlock()
241 defer func() { lock.Lock(); m.stopped = true; lock.Unlock() }()
242 <-ctx.Done()
243 }