master
go 323 lines 9.82 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package vnodectl
4
5 import (
6 "encoding/json"
7 "fmt"
8 "strings"
9
10 "github.com/google/uuid"
11 "github.com/netdata/netdata/go/plugins/plugin/framework/confgroup"
12 "github.com/netdata/netdata/go/plugins/plugin/framework/dyncfg"
13 "github.com/netdata/netdata/go/plugins/plugin/framework/vnodes"
14 "gopkg.in/yaml.v2"
15 )
16
17 func dyncfgVnodeModCmds() string {
18 return dyncfg.JoinCommands(
19 dyncfg.CommandAdd,
20 dyncfg.CommandSchema,
21 dyncfg.CommandUserconfig,
22 dyncfg.CommandTest,
23 )
24 }
25
26 func dyncfgVnodeJobCmds(isDyncfgJob bool) string {
27 cmds := []dyncfg.Command{
28 dyncfg.CommandUserconfig,
29 dyncfg.CommandSchema,
30 dyncfg.CommandGet,
31 dyncfg.CommandUpdate,
32 dyncfg.CommandTest,
33 }
34 if isDyncfgJob {
35 cmds = append(cmds, dyncfg.CommandRemove)
36 }
37 return dyncfg.JoinCommands(cmds...)
38 }
39
40 func (c *Controller) SeqExec(fn dyncfg.Function) {
41 switch fn.Command() {
42 case dyncfg.CommandSchema:
43 c.dyncfgCmdSchema(fn)
44 case dyncfg.CommandUserconfig:
45 c.dyncfgCmdUserconfig(fn)
46 case dyncfg.CommandGet:
47 c.dyncfgCmdGet(fn)
48 case dyncfg.CommandAdd:
49 c.dyncfgCmdAdd(fn)
50 case dyncfg.CommandUpdate:
51 c.dyncfgCmdUpdate(fn)
52 case dyncfg.CommandRemove:
53 c.dyncfgCmdRemove(fn)
54 case dyncfg.CommandTest:
55 c.dyncfgCmdTest(fn)
56 default:
57 c.Warningf("dyncfg: function '%s' command '%s' not implemented", fn.Fn().Name, fn.Command())
58 c.api.SendCodef(fn, 501, "Function '%s' command '%s' is not implemented.", fn.Fn().Name, fn.Command())
59 }
60 }
61
62 func (c *Controller) dyncfgCmdSchema(fn dyncfg.Function) {
63 c.api.SendJSON(fn, vnodes.ConfigSchema)
64 }
65
66 func (c *Controller) dyncfgCmdUserconfig(fn dyncfg.Function) {
67 bs, err := dyncfgVnodeUserconfigFromPayload(fn)
68 if err != nil {
69 c.Warningf("dyncfg: %s: vnode: failed to create config from payload: %v", dyncfg.CommandUserconfig, err)
70 c.api.SendCodef(fn, 400, "Invalid configuration format. Failed to create configuration from payload: %v.", err)
71 return
72 }
73
74 c.api.SendYAML(fn, string(bs))
75 }
76
77 func (c *Controller) dyncfgCmdGet(fn dyncfg.Function) {
78 name := strings.TrimPrefix(fn.ID(), c.Prefix()+":")
79 cfg, ok := c.Lookup(name)
80 if !ok {
81 c.Warningf("dyncfg: %s: vnode %s not found", dyncfg.CommandGet, name)
82 c.api.SendCodef(fn, 404, "The specified vnode '%s' is not registered.", name)
83 return
84 }
85
86 bs, err := json.Marshal(cfg)
87 if err != nil {
88 c.Warningf("dyncfg: %s: vnode job %s failed to json marshal config: %v", dyncfg.CommandGet, name, err)
89 c.api.SendCodef(fn, 500, "Failed to convert configuration into JSON: %v.", err)
90 return
91 }
92
93 c.api.SendJSON(fn, string(bs))
94 }
95
96 func (c *Controller) dyncfgCmdAdd(fn dyncfg.Function) {
97 if err := fn.ValidateArgs(3); err != nil {
98 c.Warningf("dyncfg: %s: %v", dyncfg.CommandAdd, err)
99 c.api.SendCodef(fn, 400, "%v", err)
100 return
101 }
102 if !fn.HasPayload() {
103 c.Warningf("dyncfg: %s: vnode job %s missing configuration payload.", dyncfg.CommandAdd, fn.JobName())
104 c.api.SendCodef(fn, 400, "Missing configuration payload.")
105 return
106 }
107
108 name := fn.JobName()
109 if name == "" {
110 c.Warningf("dyncfg: %s: missing vnode name", dyncfg.CommandAdd)
111 c.api.SendCodef(fn, 400, "Missing vnode name.")
112 return
113 }
114 if err := dyncfg.JobNameRuleAllowDots(name); err != nil {
115 c.Warningf("dyncfg: %s: unacceptable vnode name '%s': %v", dyncfg.CommandAdd, name, err)
116 c.api.SendCodef(fn, 400, "Unacceptable vnode name '%s': %v.", name, err)
117 return
118 }
119 cfg, err := dyncfgVnodeConfigFromPayload(fn)
120 if err != nil {
121 c.Warningf("dyncfg: %s: vnode job %s: failed to create config from payload: %v", dyncfg.CommandAdd, name, err)
122 c.api.SendCodef(fn, 400, "Failed to create configuration from payload. Invalid configuration format: %v.", err)
123 return
124 }
125 if err := uuid.Validate(cfg.GUID); err != nil {
126 c.Warningf("dyncfg: %s: vnode job %s: invalid guid: %v", dyncfg.CommandAdd, name, err)
127 c.api.SendCodef(fn, 400, "Failed to create configuration from payload. Invalid guid format: %v.", err)
128 return
129 }
130
131 dyncfgUpdateVnodeConfig(cfg, name, fn)
132 if err := c.verifyVnodeUnique(cfg); err != nil {
133 c.Warningf("dyncfg: %s: vnode job %s: %v", dyncfg.CommandAdd, name, err)
134 c.api.SendCodef(fn, 400, "Failed to create configuration from payload: %v.", err)
135 return
136 }
137
138 if orig, ok := c.Lookup(name); ok && sameStoredVnode(orig, cfg) {
139 c.api.SendCodef(fn, 202, "")
140 c.createJob(cfg, dyncfg.StatusRunning)
141 return
142 }
143
144 if _, err := c.store.Upsert(cfg); err != nil {
145 c.Warningf("dyncfg: %s: vnode job %s: %v", dyncfg.CommandAdd, name, err)
146 c.api.SendCodef(fn, 400, "Failed to update vnode configuration: %v.", err)
147 return
148 }
149
150 c.applyUpdate(name, cfg)
151 c.api.SendCodef(fn, 202, "")
152 c.createJob(cfg, dyncfg.StatusRunning)
153 }
154
155 func (c *Controller) dyncfgCmdUpdate(fn dyncfg.Function) {
156 name := strings.TrimPrefix(fn.ID(), c.Prefix()+":")
157 orig, ok := c.Lookup(name)
158 if !ok {
159 c.Warningf("dyncfg: %s: vnode %s not found", dyncfg.CommandUpdate, name)
160 c.api.SendCodef(fn, 404, "The specified vnode '%s' is not registered.", name)
161 return
162 }
163
164 cfg, err := dyncfgVnodeConfigFromPayload(fn)
165 if err != nil {
166 c.Warningf("dyncfg: %s: vnode: failed to create config from payload: %v", dyncfg.CommandUpdate, err)
167 c.api.SendCodef(fn, 400, "Invalid configuration format. Failed to create configuration from payload: %v.", err)
168 return
169 }
170 if err := uuid.Validate(cfg.GUID); err != nil {
171 c.Warningf("dyncfg: %s: vnode job %s: invalid guid: %v", dyncfg.CommandUpdate, name, err)
172 c.api.SendCodef(fn, 400, "Failed to create configuration from payload. Invalid guid format: %v.", err)
173 return
174 }
175
176 dyncfgUpdateVnodeConfig(cfg, name, fn)
177 if err := c.verifyVnodeUnique(cfg); err != nil {
178 c.Warningf("dyncfg: %s: vnode job %s: %v", dyncfg.CommandUpdate, name, err)
179 c.api.SendCodef(fn, 400, "Failed to create configuration from payload: %v.", err)
180 return
181 }
182 if sameStoredVnode(orig, cfg) {
183 c.api.SendCodef(fn, 202, "")
184 return
185 }
186
187 if _, err := c.store.Upsert(cfg); err != nil {
188 c.Warningf("dyncfg: %s: vnode job %s: %v", dyncfg.CommandUpdate, name, err)
189 c.api.SendCodef(fn, 400, "Failed to update vnode configuration: %v.", err)
190 return
191 }
192
193 c.applyUpdate(name, cfg)
194 c.api.SendCodef(fn, 202, "")
195 c.createJob(cfg, dyncfg.StatusRunning)
196 }
197
198 func (c *Controller) dyncfgCmdRemove(fn dyncfg.Function) {
199 name := strings.TrimPrefix(fn.ID(), c.Prefix()+":")
200 vnode, ok := c.Lookup(name)
201 if !ok {
202 c.Warningf("dyncfg: %s: vnode %s not found", dyncfg.CommandRemove, name)
203 c.api.SendCodef(fn, 404, "The specified vnode '%s' is not registered.", name)
204 return
205 }
206 if vnode.SourceType != confgroup.TypeDyncfg {
207 c.Warningf("dyncfg: %s: module vnode %s: can not remove vnode of type %s", dyncfg.CommandRemove, vnode.Name, vnode.SourceType)
208 c.api.SendCodef(fn, 405, "Removing vnode of type '%s' is not supported. Only 'dyncfg' vnodes can be removed.", vnode.SourceType)
209 return
210 }
211
212 if affected := c.affectedJobsFor(vnode.Name); affected != "" {
213 c.Warningf("dyncfg: %s: vnode %s is referenced by configs (%s)", dyncfg.CommandRemove, name, affected)
214 c.api.SendCodef(fn, 409, "The specified vnode '%s' is referenced by configs (%s).", name, affected)
215 return
216 }
217
218 c.store.Remove(name)
219 c.api.ConfigDelete(fn.ID())
220 c.api.SendCodef(fn, 200, "")
221 }
222
223 func (c *Controller) dyncfgCmdTest(fn dyncfg.Function) {
224 if err := fn.ValidateArgs(3); err != nil {
225 c.Warningf("dyncfg: %s: %v", dyncfg.CommandTest, err)
226 c.api.SendCodef(fn, 400, "%v", err)
227 return
228 }
229
230 name := fn.JobName()
231 cfg, err := dyncfgVnodeConfigFromPayload(fn)
232 if err != nil {
233 c.Warningf("dyncfg: %s: vnode: failed to create config from payload: %v", dyncfg.CommandTest, err)
234 c.api.SendCodef(fn, 400, "Invalid configuration format. Failed to create configuration from payload: %v.", err)
235 return
236 }
237 if err := uuid.Validate(cfg.GUID); err != nil {
238 c.Warningf("dyncfg: %s: vnode job %s: invalid guid: %v", dyncfg.CommandTest, name, err)
239 c.api.SendCodef(fn, 400, "Failed to create configuration from payload. Invalid guid format: %v.", err)
240 return
241 }
242
243 dyncfgUpdateVnodeConfig(cfg, name, fn)
244 if err := c.verifyVnodeUnique(cfg); err != nil {
245 c.Warningf("dyncfg: %s: vnode job %s: %v", dyncfg.CommandTest, name, err)
246 c.api.SendCodef(fn, 400, "Failed to create configuration from payload: %v.", err)
247 return
248 }
249
250 if affected := c.affectedJobsFor(cfg.Name); affected != "" {
251 c.api.SendCodef(fn, 202, "Updated configuration will affect configs: %s.", affected)
252 return
253 }
254 c.api.SendCodef(fn, 202, "No configs will be affected by this change.")
255 }
256
257 func (c *Controller) affectedJobsFor(vnode string) string {
258 if c.affectedJobs == nil {
259 return ""
260 }
261 return strings.Join(c.affectedJobs(vnode), ", ")
262 }
263
264 func (c *Controller) applyUpdate(name string, cfg *vnodes.VirtualNode) {
265 if c.applyVnodeUpdate != nil {
266 c.applyVnodeUpdate(name, cfg)
267 }
268 }
269
270 func (c *Controller) verifyVnodeUnique(newCfg *vnodes.VirtualNode) error {
271 var err error
272 c.store.ForEach(func(cfg *vnodes.VirtualNode) bool {
273 if cfg.Name == newCfg.Name {
274 return true
275 }
276 if cfg.Hostname == newCfg.Hostname {
277 err = fmt.Errorf("duplicate virtual node hostname detected (job '%s')", cfg.Name)
278 return false
279 }
280 if cfg.GUID == newCfg.GUID {
281 err = fmt.Errorf("duplicate virtual node guid detected (job '%s')", cfg.Name)
282 return false
283 }
284 return true
285 })
286 return err
287 }
288
289 func dyncfgUpdateVnodeConfig(cfg *vnodes.VirtualNode, name string, fn dyncfg.Function) {
290 cfg.SourceType = confgroup.TypeDyncfg
291 cfg.Source = fn.Source()
292 cfg.Name = name
293 if cfg.Hostname == "" {
294 cfg.Hostname = name
295 }
296 }
297
298 func dyncfgVnodeConfigFromPayload(fn dyncfg.Function) (*vnodes.VirtualNode, error) {
299 var cfg vnodes.VirtualNode
300 if err := fn.UnmarshalPayload(&cfg); err != nil {
301 return nil, err
302 }
303 return &cfg, nil
304 }
305
306 func dyncfgVnodeUserconfigFromPayload(fn dyncfg.Function) ([]byte, error) {
307 cfg, err := dyncfgVnodeConfigFromPayload(fn)
308 if err != nil {
309 return nil, err
310 }
311
312 name := fn.JobName()
313 if name == "" {
314 name = "test"
315 }
316 dyncfgUpdateVnodeConfig(cfg, name, fn)
317
318 bs, err := yaml.Marshal([]any{cfg})
319 if err != nil {
320 return nil, err
321 }
322 return bs, nil
323 }