dyncfg vnodes improvements (#19207)
Ilya Mashchenko committed
Dec 14, 2024 at 22:55 UTC
1952857a417a85a78dd90a5de1daded9b21309f5
6 files changed
+159
-82
src/go/plugin/go.d/agent/jobmgr/dyncfg.go
+1
-1
@@ -59,7 +59,7 @@ func (m *Manager) dyncfgConfig(fn functions.Function) {
59
switch id := fn.Args[0]; true {
60
case strings.HasPrefix(id, dyncfgCollectorIDPrefix):
61
m.dyncfgCollectorExec(fn)
62
- case strings.HasPrefix(id, dyncfgVnodeIDPrefix):
62
+ case strings.HasPrefix(id, dyncfgVnodeID):
63
m.dyncfgVnodeExec(fn)
64
default:
65
m.dyncfgRespf(fn, 503, "unknown function '%s' (%s).", fn.Name, id)
src/go/plugin/go.d/agent/jobmgr/dyncfg_vnode.go
+96
-39
@@ -18,33 +18,30 @@ import (
18
"github.com/netdata/netdata/go/plugins/plugin/go.d/agent/vnodes"
19
)
20
21
-//go:embed "vnode_config_schema.json"
22
-var vnodeConfigSchema string
23
-
21
const (
25
- dyncfgVnodeIDPrefix = "go.d:vnode:"
26
- dyncfgVnodePath = "/vnodes/vnode"
22
+ dyncfgVnodeID = "go.d:vnode"
23
+ dyncfgVnodePath = "/collectors/vnodes"
24
)
25
26
func (m *Manager) dyncfgVnodeModuleCreate() {
27
m.api.CONFIGCREATE(netdataapi.ConfigOpts{
31
- ID: fmt.Sprintf("%s%s", dyncfgVnodeIDPrefix, "vnode"),
28
+ ID: dyncfgVnodeID,
29
Status: dyncfgAccepted.String(),
30
ConfigType: "template",
31
Path: dyncfgVnodePath,
32
SourceType: "internal",
33
Source: "internal",
37
- SupportedCommands: "add schema userconfig",
34
+ SupportedCommands: "add schema userconfig test",
35
})
36
}
37
38
func (m *Manager) dyncfgVnodeJobCreate(cfg *vnodes.VirtualNode, status dyncfgStatus) {
42
- cmds := "userconfig schema get update"
39
+ cmds := "userconfig schema get update test"
40
if cfg.SourceType == confgroup.TypeDyncfg {
41
cmds += " remove"
42
}
43
m.api.CONFIGCREATE(netdataapi.ConfigOpts{
47
- ID: fmt.Sprintf("%s%s:%s", dyncfgVnodeIDPrefix, "vnode", cfg.Name),
44
+ ID: fmt.Sprintf("%s:%s", dyncfgVnodeID, cfg.Name),
45
Status: status.String(),
46
ConfigType: "job",
47
Path: dyncfgVnodePath,
@@ -62,13 +59,7 @@ func (m *Manager) dyncfgVnodeExec(fn functions.Function) {
59
m.dyncfgVnodeUserconfig(fn)
60
return
61
case "schema":
65
- m.dyncfgRespPayloadJSON(fn, vnodeConfigSchema)
66
- return
67
- case "get":
68
- m.dyncfgVnodeGet(fn)
69
- return
70
- case "add":
71
- m.dyncfgVnodeAdd(fn)
62
+ m.dyncfgRespPayloadJSON(fn, vnodes.ConfigSchema)
63
return
64
}
65
@@ -83,6 +74,12 @@ func (m *Manager) dyncfgVnodeSeqExec(fn functions.Function) {
74
action := strings.ToLower(fn.Args[1])
75
76
switch action {
77
+ case "test":
78
+ m.dyncfgVnodeTest(fn)
79
+ case "get":
80
+ m.dyncfgVnodeGet(fn)
81
+ case "add":
82
+ m.dyncfgVnodeAdd(fn)
83
case "update":
84
m.dyncfgVnodeUpdate(fn)
85
case "remove":
@@ -94,7 +91,8 @@ func (m *Manager) dyncfgVnodeSeqExec(fn functions.Function) {
91
}
92
93
func (m *Manager) dyncfgVnodeGet(fn functions.Function) {
97
- name := strings.TrimPrefix(fn.Args[0], fmt.Sprintf("%svnode:", dyncfgVnodeIDPrefix))
94
+ id := fn.Args[0]
95
+ name := strings.TrimPrefix(id, dyncfgVnodeID+":")
96
97
cfg, ok := m.Vnodes[name]
98
if !ok {
@@ -141,12 +139,7 @@ func (m *Manager) dyncfgVnodeAdd(fn functions.Function) {
139
return
140
}
141
144
- cfg.Name = name
145
- if cfg.Hostname == "" {
146
- cfg.Hostname = name
147
- }
148
- cfg.SourceType = confgroup.TypeDyncfg
149
- cfg.Source = "type=dyncfg"
142
+ dyncfgUpdateVnodeConfig(cfg, name)
143
144
m.Vnodes[name] = cfg
145
@@ -156,7 +149,7 @@ func (m *Manager) dyncfgVnodeAdd(fn functions.Function) {
149
150
func (m *Manager) dyncfgVnodeRemove(fn functions.Function) {
151
id := fn.Args[0]
159
- name := strings.TrimPrefix(id, fmt.Sprintf("%svnode:", dyncfgVnodeIDPrefix))
152
+ name := strings.TrimPrefix(id, dyncfgVnodeID+":")
153
154
vnode, ok := m.Vnodes[name]
155
if !ok {
@@ -170,18 +163,9 @@ func (m *Manager) dyncfgVnodeRemove(fn functions.Function) {
163
return
164
}
165
173
- var s strings.Builder
174
- for _, ecfg := range m.exposedConfigs.items {
175
- if ecfg.cfg.Vnode() == vnode.Name {
176
- if s.Len() > 0 {
177
- s.WriteString(", ")
178
- }
179
- s.WriteString(fmt.Sprintf("%s:%s", ecfg.cfg.Module(), ecfg.cfg.Name()))
180
- }
181
- }
182
- if s.Len() > 0 {
183
- m.Warningf("dyncfg: remove: vnode %s has running jobs (%s)", name, s.String())
184
- m.dyncfgRespf(fn, 404, "The specified vnode '%s' has running jobs (%s).", name, s.String())
166
+ if s := m.dyncfgVnodeAffectedJobs(vnode.Name); s != "" {
167
+ m.Warningf("dyncfg: remove: vnode %s has running jobs (%s)", name, s)
168
+ m.dyncfgRespf(fn, 404, "The specified vnode '%s' has running jobs (%s).", name, s)
169
return
170
}
171
@@ -190,10 +174,49 @@ func (m *Manager) dyncfgVnodeRemove(fn functions.Function) {
174
m.dyncfgRespf(fn, 200, "")
175
}
176
177
+func (m *Manager) dyncfgVnodeTest(fn functions.Function) {
178
+ id := fn.Args[0]
179
+ name := strings.TrimPrefix(id, dyncfgVnodeID+":")
180
+
181
+ orig, ok := m.Vnodes[name]
182
+ if !ok {
183
+ m.Warningf("dyncfg: test: vnode %s not found", name)
184
+ m.dyncfgRespf(fn, 404, "The specified vnode '%s' is not registered.", name)
185
+ return
186
+ }
187
+
188
+ cfg, err := vnodeConfigFromPayload(fn)
189
+ if err != nil {
190
+ m.Warningf("dyncfg: test: vnode: failed to create config from payload: %v", err)
191
+ m.dyncfgRespf(fn, 400, "Invalid configuration format. Failed to create configuration from payload: %v.", err)
192
+ return
193
+ }
194
+
195
+ if err := uuid.Validate(cfg.GUID); err != nil {
196
+ m.Warningf("dyncfg: test: vnode job %s: invalid guid: %v", name, err)
197
+ m.dyncfgRespf(fn, 400, "Failed to create configuration from payload. Invalid guid format: %v.", err)
198
+ return
199
+ }
200
+
201
+ dyncfgUpdateVnodeConfig(cfg, name)
202
+
203
+ if orig.Equal(cfg) {
204
+ m.dyncfgRespf(fn, 202, "Configuration unchanged.")
205
+ return
206
+ }
207
+
208
+ if s := m.dyncfgVnodeAffectedJobs(cfg.Name); s != "" {
209
+ m.dyncfgRespf(fn, 202, "Updated configuration will affect: %s.", s)
210
+ } else {
211
+ m.dyncfgRespf(fn, 202, "No jobs will be affected by this change.")
212
+ }
213
+}
214
+
215
func (m *Manager) dyncfgVnodeUpdate(fn functions.Function) {
194
- name := strings.TrimPrefix(fn.Args[0], fmt.Sprintf("%svnode:", dyncfgVnodeIDPrefix))
216
+ id := fn.Args[0]
217
+ name := strings.TrimPrefix(id, dyncfgVnodeID+":")
218
196
- _, ok := m.Vnodes[name]
219
+ orig, ok := m.Vnodes[name]
220
if !ok {
221
m.Warningf("dyncfg: remove: vnode %s not found", name)
222
m.dyncfgRespf(fn, 404, "The specified vnode '%s' is not registered.", name)
@@ -207,6 +230,19 @@ func (m *Manager) dyncfgVnodeUpdate(fn functions.Function) {
230
return
231
}
232
233
+ if err := uuid.Validate(cfg.GUID); err != nil {
234
+ m.Warningf("dyncfg: update: vnode job %s: invalid guid: %v", name, err)
235
+ m.dyncfgRespf(fn, 400, "Failed to create configuration from payload. Invalid guid format: %v.", err)
236
+ return
237
+ }
238
+
239
+ dyncfgUpdateVnodeConfig(cfg, name)
240
+
241
+ if orig.Equal(cfg) {
242
+ m.dyncfgRespf(fn, 202, "")
243
+ return
244
+ }
245
+
246
m.Vnodes[name] = cfg
247
248
m.runningJobs.forEach(func(_ string, job *module.Job) {
@@ -214,7 +250,6 @@ func (m *Manager) dyncfgVnodeUpdate(fn functions.Function) {
250
job.UpdateVnode(cfg)
251
}
252
})
217
-
253
m.dyncfgRespf(fn, 202, "")
254
m.dyncfgVnodeJobCreate(cfg, dyncfgRunning)
255
}
@@ -230,6 +265,28 @@ func (m *Manager) dyncfgVnodeUserconfig(fn functions.Function) {
265
m.dyncfgRespPayloadYAML(fn, string(bs))
266
}
267
268
+func (m *Manager) dyncfgVnodeAffectedJobs(vnode string) string {
269
+ var s strings.Builder
270
+ for _, ecfg := range m.exposedConfigs.items {
271
+ if ecfg.cfg.Vnode() == vnode {
272
+ if s.Len() > 0 {
273
+ s.WriteString(", ")
274
+ }
275
+ s.WriteString(fmt.Sprintf("%s:%s", ecfg.cfg.Module(), ecfg.cfg.Name()))
276
+ }
277
+ }
278
+ return s.String()
279
+}
280
+
281
+func dyncfgUpdateVnodeConfig(cfg *vnodes.VirtualNode, name string) {
282
+ cfg.Name = name
283
+ if cfg.Hostname == "" {
284
+ cfg.Hostname = name
285
+ }
286
+ cfg.SourceType = confgroup.TypeDyncfg
287
+ cfg.Source = "type=dyncfg"
288
+}
289
+
290
func vnodeConfigFromPayload(fn functions.Function) (*vnodes.VirtualNode, error) {
291
var cfg vnodes.VirtualNode
292
src/go/plugin/go.d/agent/jobmgr/manager.go
+1
-1
@@ -157,7 +157,7 @@ func (m *Manager) run() {
157
switch id := fn.Args[0]; true {
158
case strings.HasPrefix(id, dyncfgCollectorIDPrefix):
159
m.dyncfgCollectorSeqExec(fn)
160
- case strings.HasPrefix(id, dyncfgVnodeIDPrefix):
160
+ case strings.HasPrefix(id, dyncfgVnodeID):
161
m.dyncfgVnodeSeqExec(fn)
162
default:
163
m.dyncfgRespf(fn, 503, "unknown function '%s' (%s).", fn.Name, id)
src/go/plugin/go.d/agent/jobmgr/vnode_config_schema.json
deleted
-41
@@ -1,41 +0,0 @@
1
-{
2
- "jsonSchema": {
3
- "$schema": "http://json-schema.org/draft-07/schema#",
4
- "title": "Virtual Node configuration.",
5
- "type": "object",
6
- "properties": {
7
- "guid": {
8
- "title": "GUID",
9
- "description": "Uniquely identifies the node.",
10
- "type": "string"
11
- },
12
- "hostname": {
13
- "title": "Hostname",
14
- "description": "The hostname of the virtual node.",
15
- "type": "string"
16
- },
17
- "labels": {
18
- "title": "Labels",
19
- "description": "Additional key-value pairs to associate with the Virtual Node.",
20
- "type": [
21
- "object",
22
- "null"
23
- ],
24
- "additionalProperties": {
25
- "type": "string"
26
- }
27
- }
28
- },
29
- "required": [
30
- "guid"
31
- ],
32
- "patternProperties": {
33
- "^name$": {}
34
- }
35
- },
36
- "uiSchema": {
37
- "uiOptions": {
38
- "fullPage": true
39
- }
40
- }
41
-}
src/go/plugin/go.d/agent/vnodes/config_schema.json
new
+50
@@ -0,0 +1,50 @@
1
+{
2
+ "jsonSchema": {
3
+ "$schema": "http://json-schema.org/draft-07/schema#",
4
+ "title": "Virtual Node configuration.",
5
+ "type": "object",
6
+ "properties": {
7
+ "hostname": {
8
+ "title": "Hostname",
9
+ "description": "The hostname that identifies this virtual node in the Netdata UI and API. Must be unique within your monitoring environment.",
10
+ "type": "string"
11
+ },
12
+ "guid": {
13
+ "title": "GUID",
14
+ "description": "Uniquely identifies the node. Must be in [UUID format](https://www.guidgen.com/). Changing this value will result in creating a **new Virtual Node** - the existing Virtual Node and its history will not be modified.",
15
+ "type": "string"
16
+ },
17
+ "labels": {
18
+ "title": "Labels",
19
+ "description": "Optional key-value pairs that help categorize and organize virtual nodes.",
20
+ "type": [
21
+ "object",
22
+ "null"
23
+ ],
24
+ "additionalProperties": {
25
+ "type": "string"
26
+ }
27
+ }
28
+ },
29
+ "required": [
30
+ "guid"
31
+ ],
32
+ "patternProperties": {
33
+ "^name$": {}
34
+ }
35
+ },
36
+ "uiSchema": {
37
+ "uiOptions": {
38
+ "fullPage": true
39
+ },
40
+ "hostname": {
41
+ "ui:placeholder": "If not specified, the job name will be used as the hostname."
42
+ },
43
+ "guid": {
44
+ "ui:help": "Generate a UUID using the command line: `uuidgen` on macOS/Linux or `[guid]::NewGuid()` in PowerShell."
45
+ },
46
+ "labels": {
47
+ "ui:help": "Labels can be used for filtering and grouping in the Netdata UI. Common labels include environment, region, service, team, etc."
48
+ }
49
+ }
50
+}
src/go/plugin/go.d/agent/vnodes/vnodes.go
+11
@@ -3,6 +3,7 @@
3
package vnodes
4
5
import (
6
+ _ "embed"
7
"fmt"
8
"io"
9
"io/fs"
@@ -18,6 +19,9 @@ import (
19
"github.com/netdata/netdata/go/plugins/logger"
20
)
21
22
+//go:embed "config_schema.json"
23
+var ConfigSchema string
24
+
25
var log = logger.New().With(
26
slog.String("component", "vnodes"),
27
)
@@ -54,6 +58,13 @@ func (v *VirtualNode) Copy() *VirtualNode {
58
}
59
}
60
61
+func (v *VirtualNode) Equal(vn *VirtualNode) bool {
62
+ return v.Name == vn.Name &&
63
+ v.Hostname == vn.Hostname &&
64
+ v.GUID == vn.GUID &&
65
+ maps.Equal(v.Labels, vn.Labels)
66
+}
67
+
68
func readConfDir(dir string) map[string]*VirtualNode {
69
vnodes := make(map[string]*VirtualNode)
70