/
dockerfinder.go
188 lines (168 loc) · 5.38 KB
/
dockerfinder.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
package cgroup
import (
"errors"
"fmt"
"regexp"
"strings"
)
const (
// A token to match an entire path component in a "/" delimited path
wildcardToken = "*"
// A regex expression that expresses wildcardToken
regexpWildcard = "[^\\/]*"
// A token to match, and extract as a container ID, an entire path component in a
// "/" delimited path
containerIDToken = "<id>"
// A regex expression that expresses containerIDToken
regexpContainerID = "([^\\/]*)"
// index for slice returned by FindStringSubmatch
submatchIndex = 1
)
// ContainerIDFinder finds a container id from a cgroup entry.
type ContainerIDFinder interface {
// FindContainerID returns a container id and true if the known pattern is matched, false otherwise.
FindContainerID(cgroup string) (containerID string, found bool)
}
func newContainerIDFinder(pattern string) (ContainerIDFinder, error) {
idTokenCount := 0
elems := strings.Split(pattern, "/")
for i, e := range elems {
switch e {
case wildcardToken:
elems[i] = regexpWildcard
case containerIDToken:
idTokenCount++
elems[i] = regexpContainerID
default:
elems[i] = regexp.QuoteMeta(e)
}
}
if idTokenCount != 1 {
return nil, fmt.Errorf("pattern %q must contain the container id token %q exactly once", pattern, containerIDToken)
}
pattern = "^" + strings.Join(elems, "/") + "$"
re, err := regexp.Compile(pattern)
if err != nil {
return nil, fmt.Errorf("failed to create container id fetcher: %w", err)
}
return &containerIDFinder{
re: re,
}, nil
}
// NewContainerIDFinder returns a new ContainerIDFinder.
//
// The patterns provided should use the Tokens defined in this package in order
// to describe how a container id should be extracted from a cgroup entry. The
// given patterns MUST NOT be ambiguous and an error will be returned if multiple
// patterns can match the same input. An example of invalid input:
//
// "/a/b/<id>"
// "/*/b/<id>"
//
// Examples:
//
// "/docker/<id>"
// "/my.slice/*/<id>/*"
//
// Note: The pattern provided is *not* a regular expression. It is a simplified matching
// language that enforces a forward slash-delimited schema.
func NewContainerIDFinder(patterns []string) (ContainerIDFinder, error) {
if len(patterns) < 1 {
return nil, errors.New("dockerfinder: at least 1 pattern must be supplied")
}
if ambiguousPatterns := findAmbiguousPatterns(patterns); len(ambiguousPatterns) != 0 {
return nil, fmt.Errorf("dockerfinder: patterns must not be ambiguous: %q", ambiguousPatterns)
}
var finders []ContainerIDFinder
for _, pattern := range patterns {
finder, err := newContainerIDFinder(pattern)
if err != nil {
return nil, err
}
finders = append(finders, finder)
}
return &containerIDFinders{
finders: finders,
}, nil
}
type containerIDFinder struct {
re *regexp.Regexp
}
func (f *containerIDFinder) FindContainerID(cgroup string) (string, bool) {
matches := f.re.FindStringSubmatch(cgroup)
if len(matches) == 0 {
return "", false
}
return matches[submatchIndex], true
}
type containerIDFinders struct {
finders []ContainerIDFinder
}
func (f *containerIDFinders) FindContainerID(cgroup string) (string, bool) {
for _, finder := range f.finders {
id, ok := finder.FindContainerID(cgroup)
if ok {
return id, ok
}
}
return "", false
}
// There must be exactly 0 or 1 pattern that matches a given input. Enforcing
// this at startup, instead of at runtime (e.g. in `FindContainerID`) ensures that
// a bad configuration is found immediately during rollout, rather than once a
// specific cgroup input is encountered.
//
// Given the restricted grammar of wildcardToken and containerIDToken and
// the goal of protecting a user from invalid configuration, detecting ambiguous patterns
// is done as follows:
//
// 1. If the number of path components in two patterns differ, they cannot match identical inputs.
// This assertions follows from the path focused grammar and the fact that the regex
// wildcards (regexpWildcard and regexpContainerID) cannot match "/".
// 2. If the number of path components in two patterns are the same, we test "component
// equivalence" at each index. wildcardToken and containerIDToken are equivalent to
// any other, otherwise, the two components at an index are directly compared.
// From this and the fact the regex wildcards cannot match "/" follows that a single
// non-equivalent path component means the two patterns cannot match the same inputs.
func findAmbiguousPatterns(patterns []string) []string {
p := patterns[0]
rest := patterns[1:]
foundPatterns := make(map[string]struct{})
// generate all combinations except for equivalent
// index combinations which will always match.
for len(rest) > 0 {
for _, p2 := range rest {
if equivalentPatterns(p, p2) {
foundPatterns[p] = struct{}{}
foundPatterns[p2] = struct{}{}
}
}
p = rest[0]
rest = rest[1:]
}
out := make([]string, 0, len(foundPatterns))
for foundPattern := range foundPatterns {
out = append(out, foundPattern)
}
return out
}
func equivalentPatterns(a, b string) bool {
if a == b {
return true
}
aComponents := strings.Split(a, "/")
bComponents := strings.Split(b, "/")
if len(aComponents) != len(bComponents) {
return false
}
for i, comp := range aComponents {
switch {
case comp == bComponents[i]:
case comp == wildcardToken || bComponents[i] == wildcardToken:
case comp == containerIDToken || bComponents[i] == containerIDToken:
default:
return false
}
}
return true
}