mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-19 01:58:44 +08:00
Agentless resources (integration app servers, or tctl/gRPC-created) have no agent to heartbeat ComponentFeatures to presence/the unifiedresources aggregation layer. Compute from resource type instead of reading the spec field for these.
245 lines
7.6 KiB
Go
245 lines
7.6 KiB
Go
/*
|
|
* Teleport
|
|
* Copyright (C) 2025 Gravitational, Inc.
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
package componentfeatures
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"slices"
|
|
|
|
"github.com/coreos/go-semver/semver"
|
|
|
|
componentfeaturesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/componentfeatures/v1"
|
|
"github.com/gravitational/teleport/api/types"
|
|
"github.com/gravitational/teleport/api/utils/clientutils"
|
|
)
|
|
|
|
// New creates a new [componentfeaturesv1.ComponentFeatures] struct containing the provided FeatureIDs.
|
|
func New(features ...FeatureID) *componentfeaturesv1.ComponentFeatures {
|
|
out := &componentfeaturesv1.ComponentFeatures{}
|
|
seen := make(map[FeatureID]struct{})
|
|
for _, f := range features {
|
|
if _, exists := seen[f]; exists {
|
|
continue
|
|
}
|
|
seen[f] = struct{}{}
|
|
out.Features = append(out.Features, f.ToProto())
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Join copies, deduplicates, and combines [componentfeaturesv1.ComponentFeatures] sets into a single set
|
|
// containing all unique features.
|
|
func Join(sets ...*componentfeaturesv1.ComponentFeatures) *componentfeaturesv1.ComponentFeatures {
|
|
out := &componentfeaturesv1.ComponentFeatures{}
|
|
seen := make(map[componentfeaturesv1.ComponentFeatureID]struct{})
|
|
|
|
for _, fs := range sets {
|
|
if fs == nil || len(fs.GetFeatures()) == 0 {
|
|
continue
|
|
}
|
|
for _, f := range fs.GetFeatures() {
|
|
if _, exists := seen[f]; !exists {
|
|
seen[f] = struct{}{}
|
|
out.Features = append(out.Features, f)
|
|
}
|
|
}
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
// InAllSets reports whether a given [componentfeaturesv1.ComponentFeatureID] is
|
|
// present in *every* [componentfeaturesv1.ComponentFeatures] set.
|
|
//
|
|
// If no sets are provided, or any set is nil, it returns false.
|
|
func InAllSets(feature FeatureID, sets ...*componentfeaturesv1.ComponentFeatures) bool {
|
|
if len(sets) == 0 {
|
|
return false
|
|
}
|
|
proto := feature.ToProto()
|
|
|
|
for _, fs := range sets {
|
|
if fs == nil || len(fs.GetFeatures()) == 0 {
|
|
return false
|
|
}
|
|
|
|
found := false
|
|
for _, f := range fs.GetFeatures() {
|
|
if f == proto {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// Intersect returns a new [componentfeaturesv1.ComponentFeatures] containing only features present in all input
|
|
// [componentfeaturesv1.ComponentFeatures].
|
|
//
|
|
// If no sets are provided, or any set is nil or empty, it returns an empty [componentfeaturesv1.ComponentFeatures].
|
|
func Intersect(sets ...*componentfeaturesv1.ComponentFeatures) *componentfeaturesv1.ComponentFeatures {
|
|
out := &componentfeaturesv1.ComponentFeatures{}
|
|
|
|
if len(sets) == 0 {
|
|
return out
|
|
}
|
|
|
|
counts := make(map[componentfeaturesv1.ComponentFeatureID]int)
|
|
|
|
for _, fs := range sets {
|
|
if fs == nil || len(fs.GetFeatures()) == 0 {
|
|
return out
|
|
}
|
|
|
|
seenInThisSet := make(map[componentfeaturesv1.ComponentFeatureID]struct{})
|
|
for _, f := range fs.GetFeatures() {
|
|
if _, seen := seenInThisSet[f]; seen {
|
|
continue
|
|
}
|
|
seenInThisSet[f] = struct{}{}
|
|
counts[f]++
|
|
}
|
|
}
|
|
|
|
for f, c := range counts {
|
|
if c == len(sets) {
|
|
out.Features = append(out.Features, f)
|
|
}
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
// ToIntegers deduplicates and converts [componentfeaturesv1.ComponentFeatures] into a list of integers.
|
|
func ToIntegers(features *componentfeaturesv1.ComponentFeatures) []int {
|
|
out := make([]int, 0)
|
|
seen := make(map[componentfeaturesv1.ComponentFeatureID]struct{})
|
|
|
|
for _, f := range features.GetFeatures() {
|
|
if _, seen := seen[f]; seen {
|
|
continue
|
|
}
|
|
seen[f] = struct{}{}
|
|
out = append(out, int(f))
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
type AuthProxyServersLister interface {
|
|
GetProxies() ([]types.Server, error)
|
|
GetAuthServers() ([]types.Server, error)
|
|
ListProxyServers(ctx context.Context, pageSize int, pageToken string) ([]types.Server, string, error)
|
|
ListAuthServers(ctx context.Context, pageSize int, pageToken string) ([]types.Server, string, error)
|
|
}
|
|
|
|
// GetClusterAuthProxyServerFeatures fetches all Auth and Proxy servers in the cluster and returns
|
|
// the intersection of their supported ComponentFeatures.
|
|
func GetClusterAuthProxyServerFeatures(ctx context.Context, clt AuthProxyServersLister, logger *slog.Logger) *componentfeaturesv1.ComponentFeatures {
|
|
features := make([]*componentfeaturesv1.ComponentFeatures, 0)
|
|
|
|
allProxies, err := clientutils.CollectWithFallback(
|
|
ctx,
|
|
clt.ListProxyServers,
|
|
func(context.Context) ([]types.Server, error) {
|
|
//nolint:staticcheck // TODO(kiosion): DELETE IN 21.0.0
|
|
return clt.GetProxies()
|
|
},
|
|
)
|
|
if err != nil {
|
|
// If we fail to get proxies & can't be sure about feature support,
|
|
// intersecting on `nil` ensures any intersection of ComponentFeatures will be empty.
|
|
logger.ErrorContext(ctx, "Failed to get proxy servers to collect ComponentFeatures", "error", err)
|
|
features = append(features, nil)
|
|
} else {
|
|
for _, srv := range allProxies {
|
|
features = append(features, GetEffectiveServerFeatures(srv))
|
|
}
|
|
}
|
|
|
|
allAuthServers, err := clientutils.CollectWithFallback(
|
|
ctx,
|
|
clt.ListAuthServers,
|
|
func(context.Context) ([]types.Server, error) {
|
|
//nolint:staticcheck // TODO(kiosion): DELETE IN 21.0.0
|
|
return clt.GetAuthServers()
|
|
},
|
|
)
|
|
if err != nil {
|
|
logger.ErrorContext(ctx, "Failed to get auth servers to collect ComponentFeatures", "error", err)
|
|
features = append(features, nil)
|
|
} else {
|
|
for _, srv := range allAuthServers {
|
|
features = append(features, GetEffectiveServerFeatures(srv))
|
|
}
|
|
}
|
|
|
|
return Intersect(features...)
|
|
}
|
|
|
|
// versionedComponent is implemented by any server type that advertises a
|
|
// version and a set of component features.
|
|
//
|
|
// TODO(kiosion): DELETE in 20.0.0
|
|
type versionedComponent interface {
|
|
GetTeleportVersion() string
|
|
GetComponentFeatures() *componentfeaturesv1.ComponentFeatures
|
|
}
|
|
|
|
// GetEffectiveServerFeatures computes a server's effective feature support.
|
|
//
|
|
// "Agentless" resources with no backing agent process to heartbeat
|
|
// ComponentFeatures to presence have features computed from their type
|
|
// instead of read from their spec field:
|
|
// - AppServer with integration set (served directly by Proxy, not an
|
|
// app agent; see lib/web/app/transport.go).
|
|
//
|
|
// Agent-backed resources use the field from spec set by presence heartbeat,
|
|
// with version-gating applied to strip premature advertisements from servers <18.7.6.
|
|
//
|
|
// TODO(kiosion): DELETE version-gating logic in 20.0.0
|
|
func GetEffectiveServerFeatures(component versionedComponent) *componentfeaturesv1.ComponentFeatures {
|
|
// Agentless app servers: no heartbeat, compute from app type.
|
|
if appServer, ok := component.(types.AppServer); ok && appServer.GetApp().GetIntegration() != "" {
|
|
return ForAppServer(appServer)
|
|
}
|
|
// Agent-backed: read stored field with version-gating.
|
|
f := component.GetComponentFeatures()
|
|
if f == nil {
|
|
return f
|
|
}
|
|
ver, err := semver.NewVersion(component.GetTeleportVersion())
|
|
if err != nil {
|
|
return f
|
|
}
|
|
if ver.LessThan(semver.Version{Major: 18, Minor: 7, Patch: 6}) {
|
|
features := slices.DeleteFunc(slices.Clone(f.GetFeatures()), func(id componentfeaturesv1.ComponentFeatureID) bool {
|
|
return id == FeatureResourceConstraintsV1.ToProto()
|
|
})
|
|
return &componentfeaturesv1.ComponentFeatures{Features: features}
|
|
}
|
|
return f
|
|
}
|