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  }