forked from openpitrix/libconfd
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathresource_processor.go
426 lines (361 loc) · 10.4 KB
/
resource_processor.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
// Copyright confd. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE-confd file.
package libconfd
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"strconv"
"strings"
"text/template"
)
type TemplateResourceProcessor struct {
TemplateResource
path string
client BackendClient
store *KVStore
stageFile *os.File
templateFunc *TemplateFunc
funcMap template.FuncMap
keepStageFile bool
lastIndex uint64
syncOnly bool
noop bool
}
func MakeAllTemplateResourceProcessor(
config *Config, client BackendClient,
) (
[]*TemplateResourceProcessor,
error,
) {
logger.Debug("Loading template resources from confdir " + config.ConfDir)
tcs, paths, err := ListTemplateResource(config.GetConfigDir())
if err != nil {
if len(paths) == 0 {
logger.Warning("Found no templates")
return nil, fmt.Errorf("Found no templates")
} else {
logger.Warning(err) // skip error
}
}
templates := make([]*TemplateResourceProcessor, len(paths))
for i, p := range paths {
templates[i] = NewTemplateResourceProcessor(
p, config, client, tcs[i],
)
}
return templates, nil
}
// NewTemplateResourceProcessor creates a NewTemplateResourceProcessor.
func NewTemplateResourceProcessor(
path string, config *Config, client BackendClient, res *TemplateResource,
) *TemplateResourceProcessor {
logger.Debug("Loading template resource from " + path)
tr := TemplateResourceProcessor{
TemplateResource: *res,
}
tr.path = path
tr.client = client
tr.store = NewKVStore()
tr.keepStageFile = config.KeepStageFile
tr.syncOnly = config.SyncOnly
tr.noop = config.Noop
if config.ConfDir != "" {
if s := tr.Dest; !filepath.IsAbs(s) {
os.MkdirAll(config.GetDefaultTemplateOutputDir(), 0744)
tr.Dest = filepath.Join(config.GetDefaultTemplateOutputDir(), s)
tr.Dest = filepath.Clean(tr.Dest)
}
}
if config.Prefix != "" {
tr.Prefix = config.Prefix
}
if !strings.HasPrefix(tr.Prefix, "/") {
tr.Prefix = "/" + tr.Prefix
}
if len(config.PGPPrivateKey) > 0 {
tr.PGPPrivateKey = append([]byte{}, config.PGPPrivateKey...)
}
if tr.Uid == -1 {
tr.Uid = os.Geteuid()
}
if tr.Gid == -1 {
tr.Gid = os.Getegid()
}
tr.templateFunc = NewTemplateFunc(tr.store, tr.PGPPrivateKey)
tr.funcMap = tr.templateFunc.FuncMap
if !filepath.IsAbs(tr.Src) {
tr.Src = filepath.Join(config.GetTemplateDir(), tr.Src)
}
// replace ${LIBCONFD_CONFDIR}
tr.Dest = strings.Replace(tr.Dest, `${LIBCONFD_CONFDIR}`, config.ConfDir, -1)
tr.CheckCmd = strings.Replace(tr.CheckCmd, `${LIBCONFD_CONFDIR}`, config.ConfDir, -1)
tr.ReloadCmd = strings.Replace(tr.ReloadCmd, `${LIBCONFD_CONFDIR}`, config.ConfDir, -1)
return &tr
}
// process is a convenience function that wraps calls to the three main tasks
// required to keep local configuration files in sync. First we gather vars
// from the store, then we stage a candidate configuration file, and finally sync
// things up.
// It returns an error if any.
func (p *TemplateResourceProcessor) Process(call *Call) (err error) {
if fn := call.Config.HookOnError; fn != nil {
defer func() {
if err != nil {
fn(p.path, err)
}
}()
}
if len(call.Config.FuncMap) > 0 {
for k, fn := range call.Config.FuncMap {
p.funcMap[k] = fn
}
}
if fn := call.Config.FuncMapUpdater; fn != nil {
fn(p.funcMap, p.templateFunc)
}
if err := p.setFileMode(call); err != nil {
logger.Error(err)
return err
}
if err := p.setVars(call); err != nil {
logger.Error(err)
return err
}
if err := p.createStageFile(call); err != nil {
logger.Error(err)
return err
}
if err := p.sync(call); err != nil {
logger.Error(err)
return err
}
return nil
}
// setFileMode sets the FileMode.
func (p *TemplateResourceProcessor) setFileMode(call *Call) error {
if p.Mode == "" {
if fi, err := os.Stat(p.Dest); err == nil {
p.FileMode = fi.Mode()
} else {
p.FileMode = 0644
}
} else {
mode, err := strconv.ParseUint(p.Mode, 0, 32)
if err != nil {
return err
}
p.FileMode = os.FileMode(mode)
}
return nil
}
// setVars sets the Vars for template resource.
func (p *TemplateResourceProcessor) setVars(call *Call) error {
logger.Debugln("prefix:", p.Prefix)
absKeys := p.getAbsKeys()
logger.Debugf("absKeys: %#v\n", absKeys)
if fn := call.Config.HookAbsKeyAdjuster; fn != nil {
for i, key := range absKeys {
absKeys[i] = fn(key)
}
}
values, err := p.client.GetValues(absKeys)
if err != nil {
return err
}
logger.Debugf("GetValues: %#v\n", values)
p.store.Purge()
for k, v := range values {
p.store.Set(path.Join("/", strings.TrimPrefix(k, p.Prefix)), v)
}
return nil
}
// createStageFile stages the src configuration file by processing the src
// template and setting the desired owner, group, and mode. It also sets the
// StageFile for the template resource.
// It returns an error if any.
func (p *TemplateResourceProcessor) createStageFile(call *Call) error {
if fileNotExists(p.Src) {
err := errors.New("Missing template: " + p.Src)
logger.Error(err)
return err
}
tmpl, err := template.New(filepath.Base(p.Src)).Funcs(template.FuncMap(p.funcMap)).ParseFiles(p.Src)
if err != nil {
err := fmt.Errorf("Unable to process template %s, %s", p.Src, err)
logger.Error(err)
return err
}
// create TempFile in Dest directory to avoid cross-filesystem issues
temp, err := ioutil.TempFile(filepath.Dir(p.Dest), "."+filepath.Base(p.Dest))
if err != nil {
logger.Error(err)
return err
}
if err = tmpl.Execute(temp, nil); err != nil {
temp.Close()
os.Remove(temp.Name())
logger.Error(err)
return err
}
defer temp.Close()
// Set the owner, group, and mode on the stage file now to make it easier to
// compare against the destination configuration file later.
os.Chmod(temp.Name(), p.FileMode)
os.Chown(temp.Name(), p.Uid, p.Gid)
p.stageFile = temp
return nil
}
// sync compares the staged and dest config files and attempts to sync them
// if they differ. sync will run a config check command if set before
// overwriting the target config file. Finally, sync will run a reload command
// if set to have the application or service pick up the changes.
// It returns an error if any.
func (p *TemplateResourceProcessor) sync(call *Call) error {
staged := p.stageFile.Name()
if p.keepStageFile {
logger.Info("Keeping staged file: " + staged)
} else {
defer os.Remove(staged)
}
logger.Debug("Comparing candidate config to " + p.Dest)
isSame, err := p.checkSameConfig(staged, p.Dest)
if err != nil {
logger.Warning(err)
return err
}
if p.noop {
logger.Warning("Noop mode enabled. " + p.Dest + " will not be modified")
return nil
}
if isSame {
logger.Debug("Target config " + p.Dest + " in sync")
return nil
}
logger.Info("Target config " + p.Dest + " out of sync")
if !p.syncOnly && strings.TrimSpace(p.CheckCmd) != "" {
if err := p.doCheckCmd(call); err != nil {
return fmt.Errorf("Config check failed: %v", err)
}
}
logger.Debug("Overwriting target config " + p.Dest)
err = os.Rename(staged, p.Dest)
if err != nil {
logger.Debug("Rename failed - target is likely a mount. Trying to write instead")
if !strings.Contains(err.Error(), "device or resource busy") {
return err
}
// try to open the file and write to it
var contents []byte
var rerr error
contents, rerr = ioutil.ReadFile(staged)
if rerr != nil {
return rerr
}
err := ioutil.WriteFile(p.Dest, contents, p.FileMode)
// make sure owner and group match the temp file, in case the file was created with WriteFile
os.Chown(p.Dest, p.Uid, p.Gid)
if err != nil {
return err
}
}
if !p.syncOnly && strings.TrimSpace(p.ReloadCmd) != "" {
if err := p.doReloadCmd(call); err != nil {
return err
}
}
logger.Info("Target config " + p.Dest + " has been updated")
return nil
}
// check executes the check command to validate the staged config file. The
// command is modified so that any references to src template are substituted
// with a string representing the full path of the staged file. This allows the
// check to be run on the staged file before overwriting the destination config
// file.
// It returns nil if the check command returns 0 and there are no other errors.
func (p *TemplateResourceProcessor) doCheckCmd(call *Call) (err error) {
if fn := call.Config.HookOnCheckCmdError; fn != nil {
defer func() {
if err != nil {
fn(p.path, p.CheckCmd, err)
}
}()
}
var cmdBuffer bytes.Buffer
data := make(map[string]string)
data["src"] = p.stageFile.Name()
tmpl, err := template.New("checkcmd").Parse(p.CheckCmd)
if err != nil {
return err
}
if err := tmpl.Execute(&cmdBuffer, data); err != nil {
return err
}
return p.runCommand(cmdBuffer.String())
}
// reload executes the reload command.
// It returns nil if the reload command returns 0.
func (p *TemplateResourceProcessor) doReloadCmd(call *Call) (err error) {
if fn := call.Config.HookOnReloadCmdError; fn != nil {
defer func() {
if err != nil {
fn(p.path, p.ReloadCmd, err)
}
}()
}
return p.runCommand(p.ReloadCmd)
}
// runCommand is a shared function used by check and reload
// to run the given command and log its output.
// It returns nil if the given cmd returns 0.
// The command can be run on unix and windows.
func (_ *TemplateResourceProcessor) runCommand(cmd string) error {
cmd = strings.TrimSpace(cmd)
logger.Debug("TemplateResourceProcessor.runCommand: " + cmd)
if _LIBCONFD_GOOS != runtime.GOOS {
err := fmt.Errorf("cross GOOS(%s) donot support runCommand!", _LIBCONFD_GOOS)
logger.Error(err)
return err
}
var c *exec.Cmd
if runtime.GOOS == "windows" {
c = exec.Command("cmd", "/C", cmd)
} else {
c = exec.Command("/bin/sh", "-c", cmd)
}
output, err := c.CombinedOutput()
if err != nil {
logger.Errorf("%q", string(output))
return err
}
logger.Debugf("%q", string(output))
return nil
}
// checkSameConfig reports whether src and dest config files are equal.
// Two config files are equal when they have the same file contents and
// Unix permissions. The owner, group, and mode must match.
// It return false in other cases.
func (_ *TemplateResourceProcessor) checkSameConfig(src, dest string) (bool, error) {
d, err := readFileStat(dest)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
s, err := readFileStat(src)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
return d == s, nil
}