-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathconf.go
293 lines (252 loc) · 7.28 KB
/
conf.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
// Package mute executes programs suppressing std streams if required
// license: MIT, see LICENSE for details.
// BurntSushi/toml module uses MIT license. see LICENSE for more details
package mute
import (
"fmt"
"io/ioutil"
"os"
"regexp"
"strconv"
"strings"
"github.com/BurntSushi/toml"
)
// Version is the program version
const Version string = "0.2.0"
// EnvConfig is the name of the environment variable to point to config file
const EnvConfig string = "MUTE_CONFIG"
// EnvExitCodes is the name of the environment variable to overwrite exit codes
const EnvExitCodes string = "MUTE_EXIT_CODES"
// EnvStdoutPattern is the name of the environment variable to overwrite stdout regex pattern
const EnvStdoutPattern string = "MUTE_STDOUT_PATTERN"
// ExitErrConf is exit code when config is invalid
const ExitErrConf = 126
// StdoutPattern hold regex pattern to match stdout with
type StdoutPattern struct {
Regexp *regexp.Regexp
}
// UnmarshalText reads the regex pattern from a byte slice
func (s *StdoutPattern) UnmarshalText(text []byte) error {
var err error
re, err := regexp.Compile(string(text))
s.Regexp = re
return err
}
// String return the regex pattern string
func (s *StdoutPattern) String() string {
return s.Regexp.String()
}
// NewStdoutPattern returns a pointer to a StdoutPattern object using the regex pattern string
func NewStdoutPattern(pattern string) *StdoutPattern {
var stdp StdoutPattern
var re *regexp.Regexp
re = regexp.MustCompile(pattern)
stdp = StdoutPattern{Regexp: re}
return &stdp
}
// Criterion is expected exit codes and stdout patterns to mute a process
type Criterion struct {
ExitCodes []int `toml:"exit_codes"`
StdoutPatterns []*StdoutPattern `toml:"stdout_patterns"`
}
// Criterion.String return a string desc to help debugging and inspecting data
// This string is not guaranteed to be structured nor accurate
func (c *Criterion) String() string {
var codes []string
for _, code := range c.ExitCodes {
codes = append(codes, strconv.Itoa(code))
}
return fmt.Sprintf("<Criterion codes=\"%s\" patterns_count=\"%d\">", strings.Join(codes, ","), len(c.StdoutPatterns))
}
// IsEmpty checks if a Criterion is empty (no exit codes, no patterns)
func (c *Criterion) IsEmpty() bool {
return len(c.ExitCodes) < 1 && len(c.StdoutPatterns) < 1
}
// Criteria is a list of Criterion that if a process matched any of, it'll be muted
type Criteria []*Criterion
// Conf is the mute configuration of default and per process criteria
type Conf struct {
Default Criteria
Commands map[string]Criteria
}
// ConfAccessError represents errors when accessing to Config files
type ConfAccessError struct {
err error
Path string
}
// NewCriterion returns pointer to Criterion with specified exit codes and regex patterns from strings
func NewCriterion(codes []int, patterns []string) *Criterion {
c := new(Criterion)
c.ExitCodes = codes
var stdp *StdoutPattern
for _, p := range patterns {
stdp = NewStdoutPattern(p)
c.StdoutPatterns = append(c.StdoutPatterns, stdp)
}
return c
}
// DefaultConf returns a Conf to use when there is no conf file to read
// It's a Conf with a Default Criteria to mute only successful
// runs (zero exit code)
func DefaultConf() *Conf {
criterion := NewCriterion([]int{0}, []string{})
conf := new(Conf)
crt := &conf.Default
crt.add(criterion)
return conf
}
// Criteria.add adds more Criterions to current Criteria
func (c *Criteria) add(items ...*Criterion) *Criteria {
*c = append(*c, items...)
return c
}
// codesContain search for a given exit code in a slice of exit codes
func codesContain(codes []int, code int) bool {
for _, item := range codes {
if item == code {
return true
}
}
return false
}
// stdoutPatternsContain searches through a slice of regex patterns for a given regex patterns
func stdoutPatternsContain(haystack []*StdoutPattern, needle *StdoutPattern) bool {
needleStr := needle.Regexp.String()
for _, item := range haystack {
if item.Regexp.String() == needleStr {
return true
}
}
return false
}
// Criterion.equal returns true of Criterions have the same exit codes and stdout patterns
func (c *Criterion) equal(c2 *Criterion) bool {
if len(c.ExitCodes) != len(c2.ExitCodes) || len(c.StdoutPatterns) != len(c2.StdoutPatterns) {
return false
}
for _, code := range c.ExitCodes {
if !codesContain(c2.ExitCodes, code) {
return false
}
}
for _, pattern := range c.StdoutPatterns {
if !stdoutPatternsContain(c2.StdoutPatterns, pattern) {
return false
}
}
return true
}
// Criteria.contains check if the criteria contains a given criterion
func (c *Criteria) contains(criterion *Criterion) bool {
for _, item := range *c {
if criterion.equal(item) {
return true
}
}
return false
}
// Criteria.equal check if criteria items are the same
func (c *Criteria) equal(c2 *Criteria) bool {
if len(*c) != len(*c2) {
return false
}
for _, item := range *c {
if !c2.contains(item) {
return false
}
}
return true
}
// Conf.equal check if Conf items are the same
func (c *Conf) equal(c2 *Conf) bool {
var c1Crt, c2Crt Criteria
var cmd string
var ok bool
if !c.Default.equal(&(c2.Default)) {
return false
}
if len(c.Commands) != len(c2.Commands) {
return false
}
for cmd, c1Crt = range c.Commands {
if c2Crt, ok = c2.Commands[cmd]; !ok {
return false
}
if !c1Crt.equal(&c2Crt) {
return false
}
}
return true
}
// IsEmpty determines if the Conf is empty
func (c *Conf) IsEmpty() bool {
return len(c.Default) < 1 && len(c.Commands) < 1
}
func (e ConfAccessError) Error() string {
return e.err.Error()
}
// ReadConfFile reads config file and returns Conf
func ReadConfFile(path string) (*Conf, error) {
var conf Conf
content, err := ioutil.ReadFile(path)
if err != nil {
return &conf, ConfAccessError{err: err, Path: path}
}
contentStr := string(content)
_, err = toml.Decode(contentStr, &conf)
return &conf, err
}
// ConfFromEnvStr returns a Conf populated by strings as accepted environment variables
// If the strings are empty, and empty Conf with no Criterion will be returned
func ConfFromEnvStr(exitCodesStr, pattern string) (*Conf, error) {
var err error
var exitCodes []int
var stdp StdoutPattern
var reg *regexp.Regexp
criterion := new(Criterion)
conf := new(Conf)
if exitCodesStr != "" {
codeStrList := strings.Split(exitCodesStr, ",")
for _, s := range codeStrList {
i, err := strconv.Atoi(s)
if err != nil {
return conf, err
}
exitCodes = append(exitCodes, i)
}
}
criterion.ExitCodes = exitCodes
if pattern != "" {
if reg, err = regexp.Compile(pattern); err != nil {
return conf, err
}
stdp = StdoutPattern{Regexp: reg}
criterion.StdoutPatterns = []*StdoutPattern{&stdp}
}
if !criterion.IsEmpty() {
conf.Default.add(criterion)
}
return conf, err
}
// GetCmdConf returns the Conf that the mute cmd will use based on env vars
func GetCmdConf() (*Conf, error) {
var conf *Conf
var err error
envExitCodes := os.Getenv(EnvExitCodes)
envPattern := os.Getenv(EnvStdoutPattern)
conf, err = ConfFromEnvStr(envExitCodes, envPattern)
if err != nil || !conf.IsEmpty() {
return conf, err
}
confPath := "/etc/mute.toml"
envConfPath, envConfSet := os.LookupEnv(EnvConfig)
if envConfSet {
confPath = envConfPath
if confPath == "" {
conf = DefaultConf()
return conf, err
}
}
conf, err = ReadConfFile(confPath)
return conf, err
}