/
instance.go
344 lines (280 loc) · 10.2 KB
/
instance.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
/*
Copyright 2022 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package types
import (
"slices"
"strings"
"time"
"github.com/coreos/go-semver/semver"
"github.com/gravitational/trace"
"github.com/gravitational/teleport/api/defaults"
"github.com/gravitational/teleport/api/utils"
)
// Match checks if the given instance appears to match this filter.
func (f InstanceFilter) Match(i Instance) bool {
if f.ServerID != "" && f.ServerID != i.GetName() {
return false
}
if f.Version != "" && f.Version != i.GetTeleportVersion() {
// TODO(fspmarshall): move some of the lib/versioncontrol helpers to
// the api package and finalize version matching syntax so that we
// can do normalization and wildcard matching.
return false
}
if fv, ok := parseVersionRelaxed(f.OlderThanVersion); ok {
if iv, ok := parseVersionRelaxed(i.GetTeleportVersion()); ok {
if !iv.LessThan(fv) {
return false
}
}
}
if fv, ok := parseVersionRelaxed(f.NewerThanVersion); ok {
iv, ok := parseVersionRelaxed(i.GetTeleportVersion())
if !ok {
// treat instances with invalid versions are less/older than
// valid versions.
return false
}
if !fv.LessThan(iv) {
return false
}
}
// if Services was specified, ensure instance has at least one of the listed services.
if len(f.Services) != 0 && slices.IndexFunc(f.Services, i.HasService) == -1 {
return false
}
if f.ExternalUpgrader != "" && f.ExternalUpgrader != i.GetExternalUpgrader() {
return false
}
// empty upgrader matches all, so we have a separate bool flag for
// specifically matching instances with no ext upgrader defined.
if f.NoExtUpgrader && i.GetExternalUpgrader() != "" {
return false
}
return true
}
// shorthandChars are expected characters in version shorthand (e.g. "1" or "1.0" are shorthand for "1.0.0").
const shorthandChars = "0123456789."
// normalizeVersionShorthand attempts to convert go-style semver into the stricter semver
// notation expected by coreos/go-semver.
func normalizeVersionShorthand(version string) string {
version = strings.TrimPrefix(version, "v")
for _, c := range version {
if !strings.ContainsRune(shorthandChars, c) {
return version
}
}
switch strings.Count(version, ".") {
case 0:
return version + ".0.0"
case 1:
return version + ".0"
default:
return version
}
}
// parseVersionRelaxed wraps standard semver parsing with shorthand normalization.
func parseVersionRelaxed(version string) (ver semver.Version, ok bool) {
if version == "" {
return semver.Version{}, false
}
if ver.Set(normalizeVersionShorthand(version)) != nil {
return semver.Version{}, false
}
return ver, true
}
// Instance describes the configuration/status of a unique teleport server identity. Each
// instance may be running one or more teleport services, and may have multiple processes
// associated with it.
type Instance interface {
Resource
// GetTeleportVersion gets the teleport version reported by the instance.
GetTeleportVersion() string
// GetServices gets the running services reported by the instance. This list is not
// guaranteed to consist only of valid teleport services. Invalid/unexpected services
// should be ignored.
GetServices() []SystemRole
// HasService checks if this instance advertises the specified service.
HasService(SystemRole) bool
// GetHostname gets the hostname reported by the instance.
GetHostname() string
// GetAuthID gets the server ID of the auth server that most recently reported
// having observed this instance.
GetAuthID() string
// GetLastSeen gets the most recent time that an auth server reported having
// seen this instance.
GetLastSeen() time.Time
// SetLastSeen sets the most recent time that an auth server reported having
// seen this instance. Generally, if this value is being updated, the caller
// should follow up by calling SyncLogAndResourceExpiry so that the control log
// and resource-level expiry values can be reevaluated.
SetLastSeen(time.Time)
// GetExternalUpgrader gets the upgrader value as represented in the most recent
// hello message from this instance. This value corresponds to the TELEPORT_EXT_UPGRADER
// env var that is set when agents are configured to export schedule values to external
// upgraders.
GetExternalUpgrader() string
// GetExternalUpgraderVersion gets the reported upgrader version. This value corresponds
// to the TELEPORT_EXT_UPGRADER_VERSION env var that is set when agents are configured.
GetExternalUpgraderVersion() string
// SyncLogAndResourceExpiry filters expired entries from the control log and updates
// the resource-level expiry. All calculations are performed relative to the value of
// the LastSeen field, and the supplied TTL is used only as a default. The actual TTL
// of an instance resource may be longer than the supplied TTL if one or more control
// log entries use a custom TTL.
SyncLogAndResourceExpiry(ttl time.Duration)
// GetControlLog gets the instance control log entries associated with this instance.
// The control log is a log of recent events related to an auth server's administration
// of an instance's state. Auth servers generally ensure that they have successfully
// written to the log *prior* to actually attempting the planned action. As a result,
// the log may contain things that never actually happened.
GetControlLog() []InstanceControlLogEntry
// AppendControlLog appends entries to the control log. The control log is sorted by time,
// so appends do not need to be performed in any particular order.
AppendControlLog(entries ...InstanceControlLogEntry)
// Clone performs a deep copy on this instance.
Clone() Instance
}
// NewInstance assembles a new instance resource.
func NewInstance(serverID string, spec InstanceSpecV1) (Instance, error) {
instance := &InstanceV1{
ResourceHeader: ResourceHeader{
Metadata: Metadata{
Name: serverID,
},
},
Spec: spec,
}
if err := instance.CheckAndSetDefaults(); err != nil {
return nil, trace.Wrap(err)
}
return instance, nil
}
func (i *InstanceV1) CheckAndSetDefaults() error {
i.setStaticFields()
if err := i.ResourceHeader.CheckAndSetDefaults(); err != nil {
return trace.Wrap(err)
}
if i.Version != V1 {
return trace.BadParameter("unsupported instance resource version: %s", i.Version)
}
if i.Kind != KindInstance {
return trace.BadParameter("unexpected resource kind: %q (expected %s)", i.Kind, KindInstance)
}
if i.Metadata.Namespace != "" && i.Metadata.Namespace != defaults.Namespace {
return trace.BadParameter("invalid namespace %q (namespaces are deprecated)", i.Metadata.Namespace)
}
return nil
}
func (i *InstanceV1) setStaticFields() {
if i.Version == "" {
i.Version = V1
}
if i.Kind == "" {
i.Kind = KindInstance
}
}
func (i *InstanceV1) SyncLogAndResourceExpiry(ttl time.Duration) {
// expire control log entries relative to LastSeen.
logExpiry := i.expireControlLog(i.Spec.LastSeen, ttl)
// calculate the default resource expiry.
resourceExpiry := i.Spec.LastSeen.Add(ttl)
// if one or more log entries want to outlive the default resource
// expiry, we bump the resource expiry to match.
if logExpiry.After(resourceExpiry) {
resourceExpiry = logExpiry
}
i.Metadata.SetExpiry(resourceExpiry.UTC())
}
func (i *InstanceV1) GetTeleportVersion() string {
return i.Spec.Version
}
func (i *InstanceV1) GetServices() []SystemRole {
return i.Spec.Services
}
func (i *InstanceV1) HasService(s SystemRole) bool {
return slices.Contains(i.Spec.Services, s)
}
func (i *InstanceV1) GetHostname() string {
return i.Spec.Hostname
}
func (i *InstanceV1) GetAuthID() string {
return i.Spec.AuthID
}
func (i *InstanceV1) GetLastSeen() time.Time {
return i.Spec.LastSeen
}
func (i *InstanceV1) SetLastSeen(t time.Time) {
i.Spec.LastSeen = t.UTC()
}
func (i *InstanceV1) GetExternalUpgrader() string {
return i.Spec.ExternalUpgrader
}
func (i *InstanceV1) GetExternalUpgraderVersion() string {
return i.Spec.ExternalUpgraderVersion
}
func (i *InstanceV1) GetControlLog() []InstanceControlLogEntry {
return i.Spec.ControlLog
}
func (i *InstanceV1) AppendControlLog(entries ...InstanceControlLogEntry) {
n := len(i.Spec.ControlLog)
i.Spec.ControlLog = append(i.Spec.ControlLog, entries...)
for idx, entry := range i.Spec.ControlLog[n:] {
// ensure that all provided timestamps are UTC (non-UTC timestamps can cause
// panics in proto logic).
i.Spec.ControlLog[idx].Time = entry.Time.UTC()
}
slices.SortFunc(i.Spec.ControlLog, func(a, b InstanceControlLogEntry) int {
return a.Time.Compare(b.Time)
})
}
// expireControlLog removes expired entries from the control log relative to the supplied
// "now" value. The supplied ttl is used as the default ttl for entries that do not specify
// a custom ttl value. The returned timestamp is the observed expiry that was furthest in
// the future.
func (i *InstanceV1) expireControlLog(now time.Time, ttl time.Duration) time.Time {
now = now.UTC()
filtered := i.Spec.ControlLog[:0]
var latestExpiry time.Time
for _, entry := range i.Spec.ControlLog {
entryTTL := entry.TTL
if entryTTL == 0 {
entryTTL = ttl
}
if entry.Time.IsZero() {
entry.Time = now
}
expiry := entry.Time.Add(entryTTL)
if now.After(expiry) {
continue
}
if expiry.After(latestExpiry) {
latestExpiry = expiry
}
filtered = append(filtered, entry)
}
// ensure that we don't preserve pointers in the now out of
// range portion of the control log by zeroing the diff.
for idx := len(filtered); idx < len(i.Spec.ControlLog); idx++ {
i.Spec.ControlLog[idx] = InstanceControlLogEntry{}
}
i.Spec.ControlLog = filtered
return latestExpiry
}
func (i *InstanceV1) Clone() Instance {
return utils.CloneProtoMsg(i)
}
func (e *InstanceControlLogEntry) Clone() InstanceControlLogEntry {
e.Time = e.Time.UTC()
return *utils.CloneProtoMsg(e)
}