@cryptotaxi247 / netdata-1 / commits / 1952857a4

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