github.com/gravitational/teleport/api@v0.0.0-20240507183017-3110591cbafc/types/instance.go (about) 1 /* 2 Copyright 2022 Gravitational, Inc. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package types 18 19 import ( 20 "slices" 21 "strings" 22 "time" 23 24 "github.com/coreos/go-semver/semver" 25 "github.com/gravitational/trace" 26 27 "github.com/gravitational/teleport/api/defaults" 28 "github.com/gravitational/teleport/api/utils" 29 ) 30 31 // Match checks if the given instance appears to match this filter. 32 func (f InstanceFilter) Match(i Instance) bool { 33 if f.ServerID != "" && f.ServerID != i.GetName() { 34 return false 35 } 36 37 if f.Version != "" && f.Version != i.GetTeleportVersion() { 38 // TODO(fspmarshall): move some of the lib/versioncontrol helpers to 39 // the api package and finalize version matching syntax so that we 40 // can do normalization and wildcard matching. 41 return false 42 } 43 44 if fv, ok := parseVersionRelaxed(f.OlderThanVersion); ok { 45 if iv, ok := parseVersionRelaxed(i.GetTeleportVersion()); ok { 46 if !iv.LessThan(fv) { 47 return false 48 } 49 } 50 } 51 52 if fv, ok := parseVersionRelaxed(f.NewerThanVersion); ok { 53 iv, ok := parseVersionRelaxed(i.GetTeleportVersion()) 54 55 if !ok { 56 // treat instances with invalid versions are less/older than 57 // valid versions. 58 return false 59 } 60 61 if !fv.LessThan(iv) { 62 return false 63 } 64 } 65 66 // if Services was specified, ensure instance has at least one of the listed services. 67 if len(f.Services) != 0 && slices.IndexFunc(f.Services, i.HasService) == -1 { 68 return false 69 } 70 71 if f.ExternalUpgrader != "" && f.ExternalUpgrader != i.GetExternalUpgrader() { 72 return false 73 } 74 75 // empty upgrader matches all, so we have a separate bool flag for 76 // specifically matching instances with no ext upgrader defined. 77 if f.NoExtUpgrader && i.GetExternalUpgrader() != "" { 78 return false 79 } 80 81 return true 82 } 83 84 // shorthandChars are expected characters in version shorthand (e.g. "1" or "1.0" are shorthand for "1.0.0"). 85 const shorthandChars = "0123456789." 86 87 // normalizeVersionShorthand attempts to convert go-style semver into the stricter semver 88 // notation expected by coreos/go-semver. 89 func normalizeVersionShorthand(version string) string { 90 version = strings.TrimPrefix(version, "v") 91 for _, c := range version { 92 if !strings.ContainsRune(shorthandChars, c) { 93 return version 94 } 95 } 96 97 switch strings.Count(version, ".") { 98 case 0: 99 return version + ".0.0" 100 case 1: 101 return version + ".0" 102 default: 103 return version 104 } 105 } 106 107 // parseVersionRelaxed wraps standard semver parsing with shorthand normalization. 108 func parseVersionRelaxed(version string) (ver semver.Version, ok bool) { 109 if version == "" { 110 return semver.Version{}, false 111 } 112 113 if ver.Set(normalizeVersionShorthand(version)) != nil { 114 return semver.Version{}, false 115 } 116 117 return ver, true 118 } 119 120 // Instance describes the configuration/status of a unique teleport server identity. Each 121 // instance may be running one or more teleport services, and may have multiple processes 122 // associated with it. 123 type Instance interface { 124 Resource 125 126 // GetTeleportVersion gets the teleport version reported by the instance. 127 GetTeleportVersion() string 128 129 // GetServices gets the running services reported by the instance. This list is not 130 // guaranteed to consist only of valid teleport services. Invalid/unexpected services 131 // should be ignored. 132 GetServices() []SystemRole 133 134 // HasService checks if this instance advertises the specified service. 135 HasService(SystemRole) bool 136 137 // GetHostname gets the hostname reported by the instance. 138 GetHostname() string 139 140 // GetAuthID gets the server ID of the auth server that most recently reported 141 // having observed this instance. 142 GetAuthID() string 143 144 // GetLastSeen gets the most recent time that an auth server reported having 145 // seen this instance. 146 GetLastSeen() time.Time 147 148 // SetLastSeen sets the most recent time that an auth server reported having 149 // seen this instance. Generally, if this value is being updated, the caller 150 // should follow up by calling SyncLogAndResourceExpiry so that the control log 151 // and resource-level expiry values can be reevaluated. 152 SetLastSeen(time.Time) 153 154 // GetExternalUpgrader gets the upgrader value as represented in the most recent 155 // hello message from this instance. This value corresponds to the TELEPORT_EXT_UPGRADER 156 // env var that is set when agents are configured to export schedule values to external 157 // upgraders. 158 GetExternalUpgrader() string 159 160 // GetExternalUpgraderVersion gets the reported upgrader version. This value corresponds 161 // to the TELEPORT_EXT_UPGRADER_VERSION env var that is set when agents are configured. 162 GetExternalUpgraderVersion() string 163 164 // SyncLogAndResourceExpiry filters expired entries from the control log and updates 165 // the resource-level expiry. All calculations are performed relative to the value of 166 // the LastSeen field, and the supplied TTL is used only as a default. The actual TTL 167 // of an instance resource may be longer than the supplied TTL if one or more control 168 // log entries use a custom TTL. 169 SyncLogAndResourceExpiry(ttl time.Duration) 170 171 // GetControlLog gets the instance control log entries associated with this instance. 172 // The control log is a log of recent events related to an auth server's administration 173 // of an instance's state. Auth servers generally ensure that they have successfully 174 // written to the log *prior* to actually attempting the planned action. As a result, 175 // the log may contain things that never actually happened. 176 GetControlLog() []InstanceControlLogEntry 177 178 // AppendControlLog appends entries to the control log. The control log is sorted by time, 179 // so appends do not need to be performed in any particular order. 180 AppendControlLog(entries ...InstanceControlLogEntry) 181 182 // Clone performs a deep copy on this instance. 183 Clone() Instance 184 } 185 186 // NewInstance assembles a new instance resource. 187 func NewInstance(serverID string, spec InstanceSpecV1) (Instance, error) { 188 instance := &InstanceV1{ 189 ResourceHeader: ResourceHeader{ 190 Metadata: Metadata{ 191 Name: serverID, 192 }, 193 }, 194 Spec: spec, 195 } 196 if err := instance.CheckAndSetDefaults(); err != nil { 197 return nil, trace.Wrap(err) 198 } 199 return instance, nil 200 } 201 202 func (i *InstanceV1) CheckAndSetDefaults() error { 203 i.setStaticFields() 204 if err := i.ResourceHeader.CheckAndSetDefaults(); err != nil { 205 return trace.Wrap(err) 206 } 207 208 if i.Version != V1 { 209 return trace.BadParameter("unsupported instance resource version: %s", i.Version) 210 } 211 212 if i.Kind != KindInstance { 213 return trace.BadParameter("unexpected resource kind: %q (expected %s)", i.Kind, KindInstance) 214 } 215 216 if i.Metadata.Namespace != "" && i.Metadata.Namespace != defaults.Namespace { 217 return trace.BadParameter("invalid namespace %q (namespaces are deprecated)", i.Metadata.Namespace) 218 } 219 220 return nil 221 } 222 223 func (i *InstanceV1) setStaticFields() { 224 if i.Version == "" { 225 i.Version = V1 226 } 227 228 if i.Kind == "" { 229 i.Kind = KindInstance 230 } 231 } 232 233 func (i *InstanceV1) SyncLogAndResourceExpiry(ttl time.Duration) { 234 // expire control log entries relative to LastSeen. 235 logExpiry := i.expireControlLog(i.Spec.LastSeen, ttl) 236 237 // calculate the default resource expiry. 238 resourceExpiry := i.Spec.LastSeen.Add(ttl) 239 240 // if one or more log entries want to outlive the default resource 241 // expiry, we bump the resource expiry to match. 242 if logExpiry.After(resourceExpiry) { 243 resourceExpiry = logExpiry 244 } 245 246 i.Metadata.SetExpiry(resourceExpiry.UTC()) 247 } 248 249 func (i *InstanceV1) GetTeleportVersion() string { 250 return i.Spec.Version 251 } 252 253 func (i *InstanceV1) GetServices() []SystemRole { 254 return i.Spec.Services 255 } 256 257 func (i *InstanceV1) HasService(s SystemRole) bool { 258 return slices.Contains(i.Spec.Services, s) 259 } 260 261 func (i *InstanceV1) GetHostname() string { 262 return i.Spec.Hostname 263 } 264 265 func (i *InstanceV1) GetAuthID() string { 266 return i.Spec.AuthID 267 } 268 269 func (i *InstanceV1) GetLastSeen() time.Time { 270 return i.Spec.LastSeen 271 } 272 273 func (i *InstanceV1) SetLastSeen(t time.Time) { 274 i.Spec.LastSeen = t.UTC() 275 } 276 277 func (i *InstanceV1) GetExternalUpgrader() string { 278 return i.Spec.ExternalUpgrader 279 } 280 281 func (i *InstanceV1) GetExternalUpgraderVersion() string { 282 return i.Spec.ExternalUpgraderVersion 283 } 284 285 func (i *InstanceV1) GetControlLog() []InstanceControlLogEntry { 286 return i.Spec.ControlLog 287 } 288 289 func (i *InstanceV1) AppendControlLog(entries ...InstanceControlLogEntry) { 290 n := len(i.Spec.ControlLog) 291 i.Spec.ControlLog = append(i.Spec.ControlLog, entries...) 292 for idx, entry := range i.Spec.ControlLog[n:] { 293 // ensure that all provided timestamps are UTC (non-UTC timestamps can cause 294 // panics in proto logic). 295 i.Spec.ControlLog[idx].Time = entry.Time.UTC() 296 } 297 slices.SortFunc(i.Spec.ControlLog, func(a, b InstanceControlLogEntry) int { 298 return a.Time.Compare(b.Time) 299 }) 300 } 301 302 // expireControlLog removes expired entries from the control log relative to the supplied 303 // "now" value. The supplied ttl is used as the default ttl for entries that do not specify 304 // a custom ttl value. The returned timestamp is the observed expiry that was furthest in 305 // the future. 306 func (i *InstanceV1) expireControlLog(now time.Time, ttl time.Duration) time.Time { 307 now = now.UTC() 308 filtered := i.Spec.ControlLog[:0] 309 var latestExpiry time.Time 310 for _, entry := range i.Spec.ControlLog { 311 entryTTL := entry.TTL 312 if entryTTL == 0 { 313 entryTTL = ttl 314 } 315 if entry.Time.IsZero() { 316 entry.Time = now 317 } 318 expiry := entry.Time.Add(entryTTL) 319 if now.After(expiry) { 320 continue 321 } 322 323 if expiry.After(latestExpiry) { 324 latestExpiry = expiry 325 } 326 filtered = append(filtered, entry) 327 } 328 // ensure that we don't preserve pointers in the now out of 329 // range portion of the control log by zeroing the diff. 330 for idx := len(filtered); idx < len(i.Spec.ControlLog); idx++ { 331 i.Spec.ControlLog[idx] = InstanceControlLogEntry{} 332 } 333 i.Spec.ControlLog = filtered 334 return latestExpiry 335 } 336 337 func (i *InstanceV1) Clone() Instance { 338 return utils.CloneProtoMsg(i) 339 } 340 341 func (e *InstanceControlLogEntry) Clone() InstanceControlLogEntry { 342 e.Time = e.Time.UTC() 343 return *utils.CloneProtoMsg(e) 344 }