[vnet] leaf cluster support (#42600)

* [vnet] leaf cluster support

* add comments on clusterConfig fields

* add comment
This commit is contained in:
Nic Klaassen
2024-06-11 10:10:13 +00:00
committed by GitHub
parent 26217fee9f
commit 582ba28b69
9 changed files with 274 additions and 158 deletions
+5
View File
@@ -870,6 +870,11 @@ func (c *Client) VnetConfigServiceClient() vnet.VnetConfigServiceClient {
return vnet.NewVnetConfigServiceClient(c.conn)
}
// GetVnetConfig returns the singleton VnetConfig resource.
func (c *Client) GetVnetConfig(ctx context.Context) (*vnet.VnetConfig, error) {
return c.VnetConfigServiceClient().GetVnetConfig(ctx, &vnet.GetVnetConfigRequest{})
}
// Ping gets basic info about the auth server.
func (c *Client) Ping(ctx context.Context) (proto.PingResponse, error) {
rsp, err := c.grpc.Ping(ctx, &proto.PingRequest{})
+1
View File
@@ -1430,6 +1430,7 @@ type ClientI interface {
services.Integrations
services.KubeWaitingContainer
services.Notifications
services.VnetConfigGetter
types.Events
types.WebSessionsGetter
+7 -2
View File
@@ -22,10 +22,15 @@ import (
"github.com/gravitational/teleport/api/gen/proto/go/teleport/vnet/v1"
)
// VnetConfigService is an interface for the VnetConfig service.
type VnetConfigService interface {
// VnetConfigGetter is an interface for getting the cluster singleton VnetConfig.
type VnetConfigGetter interface {
// GetVnetConfig returns the singleton VnetConfig resource.
GetVnetConfig(context.Context) (*vnet.VnetConfig, error)
}
// VnetConfigService is an interface for the VnetConfig service.
type VnetConfigService interface {
VnetConfigGetter
// CreateVnetConfig does basic validation and creates a VnetConfig resource.
CreateVnetConfig(ctx context.Context, vnetConfig *vnet.VnetConfig) (*vnet.VnetConfig, error)
-11
View File
@@ -28,7 +28,6 @@ import (
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/gravitational/teleport"
vnetproto "github.com/gravitational/teleport/api/gen/proto/go/teleport/vnet/v1"
"github.com/gravitational/teleport/api/types"
prehogv1alpha "github.com/gravitational/teleport/gen/proto/go/prehog/v1alpha"
apiteleterm "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1"
@@ -360,16 +359,6 @@ func (p *appProvider) GetDialOptions(ctx context.Context, profileName string) (*
return dialOpts, nil
}
func (p *appProvider) GetVnetConfig(ctx context.Context, profileName, leafClusterName string) (*vnetproto.VnetConfig, error) {
clusterClient, err := p.getCachedClient(ctx, profileName, leafClusterName)
if err != nil {
return nil, trace.Wrap(err)
}
vnetConfigClient := clusterClient.AuthClient.VnetConfigServiceClient()
vnetConfig, err := vnetConfigClient.GetVnetConfig(ctx, &vnetproto.GetVnetConfigRequest{})
return vnetConfig, trace.Wrap(err)
}
// OnNewConnection submits a usage event once per appProvider lifetime.
// That is, if a user makes multiple connections to a single app, OnNewConnection submits a single
// event. This is to mimic how Connect submits events for its app gateways. This lets us compare
+90 -55
View File
@@ -21,6 +21,7 @@ import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"log/slog"
"net"
@@ -33,7 +34,6 @@ import (
"github.com/gravitational/teleport"
apiclient "github.com/gravitational/teleport/api/client"
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/gen/proto/go/teleport/vnet/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/auth/authclient"
"github.com/gravitational/teleport/lib/client"
@@ -61,9 +61,6 @@ type AppProvider interface {
// GetDialOptions returns ALPN dial options for the profile.
GetDialOptions(ctx context.Context, profileName string) (*DialOptions, error)
// GetVnetConfig returns the cluster VnetConfig resource.
GetVnetConfig(ctx context.Context, profileName, leafClusterName string) (*vnet.VnetConfig, error)
// OnNewConnection gets called whenever a new connection is about to be established through VNet.
// By the time OnNewConnection, VNet has already verified that the user holds a valid cert for the
// app.
@@ -120,7 +117,7 @@ func NewTCPAppResolver(appProvider AppProvider, opts ...tcpAppResolverOption) (*
opt(r)
}
r.clock = cmp.Or(r.clock, clockwork.NewRealClock())
r.clusterConfigCache = newClusterConfigCache(appProvider.GetVnetConfig, r.clock)
r.clusterConfigCache = newClusterConfigCache(appProvider.GetCachedClient, r.clock)
r.customDNSZoneChecker = newCustomDNSZoneValidator(r.lookupTXT)
return r, nil
}
@@ -155,63 +152,107 @@ func (r *TCPAppResolver) ResolveTCPHandler(ctx context.Context, fqdn string) (*T
// This is a query for the proxy address, which we'll never want to handle.
return nil, ErrNoTCPHandler
}
if match, err := r.fqdnMatchesProfile(ctx, profileName, fqdn); err != nil {
return nil, trace.Wrap(err)
} else if !match {
continue
}
slog := r.slog.With("profile", profileName, "fqdn", fqdn)
rootClient, err := r.appProvider.GetCachedClient(ctx, profileName, "")
clusterClient, err := r.clusterClientForAppFQDN(ctx, profileName, fqdn)
if err != nil {
// The user might be logged out from this one cluster (and retryWithRelogin isn't working). Don't
// return an error so that DNS resolution will be forwarded upstream instead of failing, to avoid
// breaking e.g. web app access (we don't know if this is a web or TCP app yet because we can't
// log in).
if errors.Is(err, errNoMatch) {
continue
}
// The user might be logged out from this one cluster (and retryWithRelogin isn't working). Log
// the error but don't return it so that DNS resolution will be forwarded upstream instead of
// failing, to avoid breaking e.g. web app access (we don't know if this is a web or TCP app yet
// because we can't log in).
slog.ErrorContext(ctx, "Failed to get teleport client.", "error", err)
continue
}
return r.resolveTCPHandlerForCluster(ctx, slog, rootClient.CurrentCluster(), profileName, "", fqdn)
leafClusterName := ""
if clusterClient.ClusterName() != profileName {
leafClusterName = clusterClient.ClusterName()
}
slog := r.slog.With("profile", profileName, "fqdn", fqdn, "leaf_cluster", leafClusterName)
return r.resolveTCPHandlerForCluster(ctx, slog, clusterClient, profileName, leafClusterName, fqdn)
}
// fqdn did not match any profile, forward the request upstream.
return nil, ErrNoTCPHandler
}
func (r *TCPAppResolver) fqdnMatchesProfile(ctx context.Context, profileName, fqdn string) (bool, error) {
if isSubdomain(fqdn, profileName) {
// The queried app fqdn is a subdomain of the proxy address, this is a match.
return true, nil
}
// Not a proxy address subdomain, must check custom DNS zones.
var errNoMatch = errors.New("cluster does not match queried FQDN")
// TODO(nklaassen): support leaf clusters.
vnetConfig, err := r.clusterConfigCache.getVnetConfig(ctx, profileName, "" /*leafClustername*/)
if err != nil {
// Good chance we're here because the user is not logged in to the profile.
r.slog.ErrorContext(ctx, "Failed to get VnetConfig, not checking custom DNS zones.", "profile", profileName, "error", err)
return false, nil
}
// TODO(nklaassen): support leaf clusters.
func (r *TCPAppResolver) clusterClientForAppFQDN(ctx context.Context, profileName, fqdn string) (ClusterClient, error) {
rootClient, err := r.appProvider.GetCachedClient(ctx, profileName, "")
if err != nil {
r.slog.ErrorContext(ctx, "Failed to get teleport client, not checking custom DNS zones.", "profile", profileName, "error", err)
return false, nil
r.slog.ErrorContext(ctx, "Failed to get root cluster client, apps in this cluster will not be resolved.", "profile", profileName, "error", err)
return nil, errNoMatch
}
clusterName := rootClient.ClusterName()
for _, zone := range vnetConfig.GetSpec().GetCustomDnsZones() {
if !isSubdomain(fqdn, zone.GetSuffix()) {
// The queried app fqdn is not a subdomain of this custom zone suffix, skip it.
if isSubdomain(fqdn, profileName) {
// The queried app fqdn is direct subdomain of this cluster proxy address.
return rootClient, nil
}
leafClusters, err := getLeafClusters(ctx, rootClient)
if err != nil {
// Good chance we're here because the user is not logged in to the profile.
r.slog.ErrorContext(ctx, "Failed to list leaf clusters, apps in this cluster will not be resolved.", "profile", profileName, "error", err)
return nil, errNoMatch
}
// Prefix with an empty string to represent the root cluster.
allClusters := append([]string{""}, leafClusters...)
for _, leafClusterName := range allClusters {
clusterClient, err := r.appProvider.GetCachedClient(ctx, profileName, leafClusterName)
if err != nil {
r.slog.ErrorContext(ctx, "Failed to get cluster client, apps in this cluster will not be resolved.", "profile", profileName, "leaf_cluster", leafClusterName, "error", err)
continue
}
// The queried app fqdn is a subdomain of this custom zone suffix. Check if the custom zone is valid.
if err := r.customDNSZoneChecker.validate(ctx, clusterName, zone.GetSuffix()); err != nil {
r.slog.ErrorContext(ctx, "Failed to validate custom DNS zone %q for cluster %q", "error", err)
return false, trace.Wrap(err, "validating custom DNS zone")
clusterConfig, err := r.clusterConfigCache.getClusterConfig(ctx, clusterClient)
if err != nil {
r.slog.ErrorContext(ctx, "Failed to get VnetConfig, apps in the cluster will not be resolved.", "profile", profileName, "leaf_cluster", leafClusterName, "error", err)
continue
}
for _, zone := range clusterConfig.dnsZones {
if !isSubdomain(fqdn, zone) {
// The queried app fqdn is not a subdomain of this zone, skip it.
continue
}
// Found a matching cluster.
if zone == clusterConfig.proxyPublicAddr {
// We don't need to validate a custom DNS zone if this is the proxy public address, this is a
// normal app public_addr.
return clusterClient, nil
}
// The queried app fqdn is a subdomain of this custom zone. Check if the zone is valid.
if err := r.customDNSZoneChecker.validate(ctx, clusterConfig.clusterName, zone); err != nil {
// Return an error here since the FQDN does match this custom zone, but the zone failed to
// validate.
return nil, trace.Wrap(err, "validating custom DNS zone %q matching queried FQDN %q", zone, fqdn)
}
return clusterClient, nil
}
}
return nil, errNoMatch
}
func getLeafClusters(ctx context.Context, rootClient ClusterClient) ([]string, error) {
var leafClusters []string
nextPage := ""
for {
remoteClusters, nextPage, err := rootClient.CurrentCluster().ListRemoteClusters(ctx, 0, nextPage)
if err != nil {
return nil, trace.Wrap(err)
}
for _, rc := range remoteClusters {
leafClusters = append(leafClusters, rc.GetName())
}
if nextPage == "" {
return leafClusters, nil
}
return true, nil
}
return false, nil
}
// resolveTCPHandlerForCluster takes a cluster client and resolves [fqdn] to a [TCPHandlerSpec] if a matching
@@ -221,13 +262,13 @@ func (r *TCPAppResolver) fqdnMatchesProfile(ctx context.Context, profileName, fq
func (r *TCPAppResolver) resolveTCPHandlerForCluster(
ctx context.Context,
slog *slog.Logger,
clt apiclient.GetResourcesClient,
clusterClient ClusterClient,
profileName, leafClusterName, fqdn string,
) (*TCPHandlerSpec, error) {
// An app public_addr could technically be full-qualified or not, match either way.
expr := fmt.Sprintf(`(resource.spec.public_addr == "%s" || resource.spec.public_addr == "%s") && hasPrefix(resource.spec.uri, "tcp://")`,
strings.TrimSuffix(fqdn, "."), fqdn)
resp, err := apiclient.GetResourcePage[types.AppServer](ctx, clt, &proto.ListResourcesRequest{
resp, err := apiclient.GetResourcePage[types.AppServer](ctx, clusterClient.CurrentCluster(), &proto.ListResourcesRequest{
ResourceType: types.KindAppServer,
PredicateExpression: expr,
Limit: 1,
@@ -248,19 +289,13 @@ func (r *TCPAppResolver) resolveTCPHandlerForCluster(
return nil, trace.Wrap(err)
}
var cidrRange string
vnetConfig, err := r.clusterConfigCache.getVnetConfig(ctx, profileName, leafClusterName)
switch {
case err == nil:
cidrRange = cmp.Or(vnetConfig.GetSpec().GetIpv4CidrRange(), defaultIPv4CIDRRange)
case trace.IsNotFound(err) || trace.IsNotImplemented(err):
cidrRange = defaultIPv4CIDRRange
default:
clusterConfig, err := r.clusterConfigCache.getClusterConfig(ctx, clusterClient)
if err != nil {
return nil, trace.Wrap(err)
}
return &TCPHandlerSpec{
IPv4CIDRRange: cidrRange,
IPv4CIDRRange: clusterConfig.ipv4CIDRRange,
TCPHandler: appHandler,
}, nil
}
+71 -34
View File
@@ -17,25 +17,38 @@
package vnet
import (
"cmp"
"context"
"net"
"strings"
"sync"
"time"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"golang.org/x/sync/singleflight"
"github.com/gravitational/teleport/api/gen/proto/go/teleport/vnet/v1"
)
type getClusterConfigFunc = func(ctx context.Context, profileName, leafClusterName string) (*vnet.VnetConfig, error)
type getClusterClientFunc = func(ctx context.Context, profileName, leafClusterName string) (ClusterClient, error)
type cacheEntry struct {
vnetConfig *vnet.VnetConfig
expires time.Time
type clusterConfig struct {
// clusterName is the name of the cluster as reported by Ping.
clusterName string
// proxyPublicAddr is the public address of the proxy as reported by Ping, with any ports removed, this is
// just the hostname. This is often but not always identical to the clusterName. For root clusters this
// will be the same as the profile name (the profile is named after the proxy public addr).
proxyPublicAddr string
// dnsZones is the list of DNS zones that are valid for this cluster, this includes proxyPublicAddr *and*
// any configured custom DNS zones for the cluster.
dnsZones []string
// ipv4CIDRRange is the CIDR range that IPv4 addresses should be assigned from for apps in this cluster.
ipv4CIDRRange string
// expires is the time at which this information should be considered stale and refetched. Stale data may
// be used if a subsequent fetch fails.
expires time.Time
}
func (e *cacheEntry) stale(clock clockwork.Clock) bool {
func (e *clusterConfig) stale(clock clockwork.Clock) bool {
return clock.Now().After(e.expires)
}
@@ -48,66 +61,90 @@ func (e *cacheEntry) stale(clock clockwork.Clock) bool {
type clusterConfigCache struct {
flightGroup singleflight.Group
clock clockwork.Clock
get getClusterConfigFunc
cache map[string]cacheEntry
getClient getClusterClientFunc
cache map[string]*clusterConfig
mu sync.RWMutex
}
func newClusterConfigCache(get getClusterConfigFunc, clock clockwork.Clock) *clusterConfigCache {
func newClusterConfigCache(getClient getClusterClientFunc, clock clockwork.Clock) *clusterConfigCache {
return &clusterConfigCache{
clock: clock,
get: get,
cache: make(map[string]cacheEntry),
clock: clock,
getClient: getClient,
cache: make(map[string]*clusterConfig),
}
}
func (c *clusterConfigCache) getVnetConfig(ctx context.Context, profileName, leafClusterName string) (*vnet.VnetConfig, error) {
k := clusterCacheKey(profileName, leafClusterName)
func (c *clusterConfigCache) getClusterConfig(ctx context.Context, clusterClient ClusterClient) (*clusterConfig, error) {
k := clusterClient.ClusterName()
// Use a singleflight.Group to avoid concurrent requests for the same cluster VnetConfig.
vnetConfig, err, _ := c.flightGroup.Do(k, func() (any, error) {
result, err, _ := c.flightGroup.Do(k, func() (any, error) {
// Check the cache inside flightGroup.Do to avoid the chance of immediate repeat calls to the cluster.
c.mu.RLock()
existingCacheEntry, existingCacheEntryFound := c.cache[k]
c.mu.RUnlock()
if existingCacheEntryFound && !existingCacheEntry.stale(c.clock) {
return existingCacheEntry.vnetConfig, nil
return existingCacheEntry, nil
}
vnetConfig, err := c.get(ctx, profileName, leafClusterName)
if trace.IsNotFound(err) || trace.IsNotImplemented(err) {
// Default to the empty config on NotFound or NotImplemented.
vnetConfig = &vnet.VnetConfig{}
err = nil
}
clusterConfig, err := c.getClusterConfigUncached(ctx, clusterClient)
if err != nil {
// It's better to return a stale cached VnetConfig than an error. The profile probably expired and
// we want to keep functioning until a relogin. We don't expect the VnetConfig to change very
// often.
if existingCacheEntryFound {
return existingCacheEntry.vnetConfig, nil
return existingCacheEntry, nil
}
return nil, trace.Wrap(err)
}
c.mu.Lock()
c.cache[k] = cacheEntry{
vnetConfig: vnetConfig,
expires: c.clock.Now().Add(5 * time.Minute),
}
c.cache[k] = clusterConfig
c.mu.Unlock()
return vnetConfig, nil
return clusterConfig, nil
})
if err != nil {
return nil, trace.Wrap(err)
}
return vnetConfig.(*vnet.VnetConfig), nil
return result.(*clusterConfig), nil
}
func clusterCacheKey(profileName, leafClusterName string) string {
if leafClusterName != "" {
return profileName + "/" + leafClusterName
func (c *clusterConfigCache) getClusterConfigUncached(ctx context.Context, clusterClient ClusterClient) (*clusterConfig, error) {
pingResp, err := clusterClient.CurrentCluster().Ping(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
return profileName
clusterName := pingResp.ClusterName
proxyPublicAddr := pingResp.ProxyPublicAddr
if strings.Contains(proxyPublicAddr, ":") {
proxyPublicAddr, _, err = net.SplitHostPort(pingResp.ProxyPublicAddr)
if err != nil {
return nil, trace.Wrap(err, "parsing proxy public addr")
}
}
dnsZones := []string{proxyPublicAddr}
ipv4CIDRRange := defaultIPv4CIDRRange
vnetConfig, err := clusterClient.CurrentCluster().GetVnetConfig(ctx)
if trace.IsNotFound(err) || trace.IsNotImplemented(err) {
// Use the defaults set above, nothing to do here.
} else if err != nil {
return nil, trace.Wrap(err)
} else {
for _, zone := range vnetConfig.GetSpec().GetCustomDnsZones() {
dnsZones = append(dnsZones, zone.GetSuffix())
}
ipv4CIDRRange = cmp.Or(vnetConfig.GetSpec().GetIpv4CidrRange(), defaultIPv4CIDRRange)
}
return &clusterConfig{
clusterName: clusterName,
proxyPublicAddr: proxyPublicAddr,
dnsZones: dnsZones,
ipv4CIDRRange: ipv4CIDRRange,
expires: c.clock.Now().Add(5 * time.Minute),
}, nil
}
+41 -31
View File
@@ -17,17 +17,14 @@
package vnet
import (
"cmp"
"context"
"log/slog"
"net"
"os"
"strings"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"github.com/gravitational/teleport/api/gen/proto/go/teleport/vnet/v1"
"github.com/gravitational/teleport/api/profile"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/utils"
@@ -71,7 +68,7 @@ func newOSConfigurator(tunName, ipv6Prefix, dnsAddr string) (*osConfigurator, er
homePath: homePath,
clientStore: client.NewFSClientStore(homePath),
}
configurator.clusterConfigCache = newClusterConfigCache(configurator.getVnetConfig, clockwork.NewRealClock())
configurator.clusterConfigCache = newClusterConfigCache(configurator.getClusterClient, clockwork.NewRealClock())
return configurator, nil
}
@@ -85,8 +82,14 @@ func (c *osConfigurator) updateOSConfiguration(ctx context.Context) error {
return trace.Wrap(err, "listing user profiles")
}
for _, profileName := range profileNames {
// TODO(nklaassen): support leaf clusters
vnetConfig, err := c.clusterConfigCache.getVnetConfig(ctx, profileName, "" /*leafClusterName*/)
rootClient, err := c.getClusterClient(ctx, profileName, "" /*leafClusterName*/)
if err != nil {
slog.WarnContext(ctx,
"Failed to create root cluster client, profile may be expired, not configuring VNet for this cluster",
"profile", profileName, "error", err)
continue
}
clusterConfig, err := c.clusterConfigCache.getClusterConfig(ctx, rootClient)
if err != nil {
slog.WarnContext(ctx,
"Failed to load VNet configuration, profile may be expired, not configuring VNet for this cluster",
@@ -94,18 +97,36 @@ func (c *osConfigurator) updateOSConfiguration(ctx context.Context) error {
continue
}
// profileName is the web proxy address, add the default DNS zone for it.
// TODO(nklaassen): add the custom DNS zones as well, after the rest of VNet supports it.
dnsZones = append(dnsZones, profileName)
for _, zone := range vnetConfig.GetSpec().GetCustomDnsZones() {
suffix := zone.GetSuffix()
// Trim any leading or trailing "." to match expected format.
zone := strings.TrimPrefix(strings.TrimSuffix(suffix, "."), ".")
dnsZones = append(dnsZones, zone)
}
dnsZones = append(dnsZones, clusterConfig.dnsZones...)
cidrRanges = append(cidrRanges, clusterConfig.ipv4CIDRRange)
cidrRange := cmp.Or(vnetConfig.GetSpec().GetIpv4CidrRange(), defaultIPv4CIDRRange)
cidrRanges = append(cidrRanges, cidrRange)
leafClusters, err := getLeafClusters(ctx, rootClient)
if err != nil {
slog.WarnContext(ctx,
"Failed to list leaf clusters, profile may be expired, not configuring VNet for this cluster",
"profile", profileName, "error", err)
continue
}
for _, leafClusterName := range leafClusters {
clusterClient, err := c.getClusterClient(ctx, profileName, leafClusterName)
if err != nil {
slog.WarnContext(ctx,
"Failed to create leaf cluster client, not configuring VNet for this cluster",
"profile", profileName, "leaf_cluster", leafClusterName, "error", err)
continue
}
clusterConfig, err := c.clusterConfigCache.getClusterConfig(ctx, clusterClient)
if err != nil {
slog.WarnContext(ctx,
"Failed to load VNet configuration, not configuring VNet for this cluster",
"profile", profileName, "leaf_cluster", leafClusterName, "error", err)
continue
}
dnsZones = append(dnsZones, clusterConfig.dnsZones...)
cidrRanges = append(cidrRanges, clusterConfig.ipv4CIDRRange)
}
}
dnsZones = utils.Deduplicate(dnsZones)
@@ -153,17 +174,7 @@ func (c *osConfigurator) setTunIPv4FromCIDR(cidrRange string) error {
return nil
}
func (c *osConfigurator) getVnetConfig(ctx context.Context, profileName, leafClusterName string) (*vnet.VnetConfig, error) {
clt, err := c.vnetConfigClient(ctx, profileName, leafClusterName)
if err != nil {
return nil, trace.Wrap(err, "getting vnet client for profile %s %s", profileName, leafClusterName)
}
vnetConfig, err := clt.GetVnetConfig(ctx, &vnet.GetVnetConfigRequest{})
return vnetConfig, trace.Wrap(err)
}
func (c *osConfigurator) vnetConfigClient(ctx context.Context, profileName, leafClusterName string) (vnet.VnetConfigServiceClient, error) {
func (c *osConfigurator) getClusterClient(ctx context.Context, profileName, leafClusterName string) (ClusterClient, error) {
// This runs in the root process, so obviously we don't have access to the client cache in the user
// process. This loads cluster profiles and credentials from TELEPORT_HOME.
clientConfig := &client.Config{
@@ -180,11 +191,10 @@ func (c *osConfigurator) vnetConfigClient(ctx context.Context, profileName, leaf
return nil, trace.Wrap(err, "creating new teleport client")
}
clusterClt, err := tc.ConnectToCluster(ctx)
clt, err := tc.ConnectToCluster(ctx)
if err != nil {
return nil, trace.Wrap(err, "connecting to cluster")
}
vnetConfigClt := clusterClt.CurrentCluster().VnetConfigServiceClient()
return vnetConfigClt, nil
return clt, nil
}
+59 -14
View File
@@ -291,9 +291,9 @@ func (p *echoAppProvider) GetCachedClient(ctx context.Context, profileName, leaf
}
if leafClusterName == "" {
return &fakeClusterClient{
clusterName: profileName,
authClient: &fakeAuthClient{
apps: rootCluster.apps,
clusterSpec: rootCluster,
clusterName: profileName,
},
}, nil
}
@@ -302,9 +302,9 @@ func (p *echoAppProvider) GetCachedClient(ctx context.Context, profileName, leaf
return nil, trace.NotFound("no cluster for %s.%s", profileName, leafClusterName)
}
return &fakeClusterClient{
clusterName: leafClusterName,
authClient: &fakeAuthClient{
apps: leafCluster.apps,
clusterSpec: leafCluster,
clusterName: leafClusterName,
},
}, nil
}
@@ -374,8 +374,7 @@ func (p *echoAppProvider) OnNewConnection(ctx context.Context, profileName, leaf
}
type fakeClusterClient struct {
authClient *fakeAuthClient
clusterName string
authClient *fakeAuthClient
}
func (c *fakeClusterClient) CurrentCluster() authclient.ClientI {
@@ -383,19 +382,20 @@ func (c *fakeClusterClient) CurrentCluster() authclient.ClientI {
}
func (c *fakeClusterClient) ClusterName() string {
return c.clusterName
return c.authClient.clusterName
}
// fakeAuthClient is a fake auth client that answers GetResources requests with a static list of apps and
// basic/faked predicate filtering.
type fakeAuthClient struct {
authclient.ClientI
apps []string
clusterSpec testClusterSpec
clusterName string
}
func (c *fakeAuthClient) GetResources(ctx context.Context, req *proto.ListResourcesRequest) (*proto.ListResourcesResponse, error) {
resp := &proto.ListResourcesResponse{}
for _, app := range c.apps {
for _, app := range c.clusterSpec.apps {
// Poor-man's predicate expression filter.
if !strings.Contains(req.PredicateExpression, app) {
continue
@@ -425,6 +425,39 @@ func (c *fakeAuthClient) GetResources(ctx context.Context, req *proto.ListResour
return resp, nil
}
func (c *fakeAuthClient) ListRemoteClusters(ctx context.Context, pageSize int, pageToken string) ([]types.RemoteCluster, string, error) {
remoteClusters := make([]types.RemoteCluster, 0, len(c.clusterSpec.leafClusters))
for leafClusterName := range c.clusterSpec.leafClusters {
rc, err := types.NewRemoteCluster(leafClusterName)
if err != nil {
return nil, "", trace.Wrap(err)
}
remoteClusters = append(remoteClusters, rc)
}
return remoteClusters, "", nil
}
func (c *fakeAuthClient) Ping(ctx context.Context) (proto.PingResponse, error) {
return proto.PingResponse{
ClusterName: c.clusterName,
ProxyPublicAddr: c.clusterName,
}, nil
}
func (c *fakeAuthClient) GetVnetConfig(ctx context.Context) (*vnet.VnetConfig, error) {
vnetConfig := &vnet.VnetConfig{
Spec: &vnet.VnetConfigSpec{
Ipv4CidrRange: c.clusterSpec.cidrRange,
},
}
for _, zone := range c.clusterSpec.customDNSZones {
vnetConfig.Spec.CustomDnsZones = append(vnetConfig.Spec.CustomDnsZones, &vnet.CustomDNSZone{
Suffix: zone,
})
}
return vnetConfig, nil
}
func TestDialFakeApp(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
@@ -460,13 +493,16 @@ func TestDialFakeApp(t *testing.T) {
"leaf1.example.com": {
apps: []string{"echo1.leaf1.example.com"},
},
"leaf2.example.com": {
apps: []string{"echo1.leaf2.example.com"},
},
},
},
"root2.example.com": {
apps: []string{"echo1.root2.example.com", "echo2.root2.example.com"},
leafClusters: map[string]testClusterSpec{
"leaf2.example.com": {
apps: []string{"echo1.leaf2.example.com"},
"leaf3.example.com": {
apps: []string{"echo1.leaf3.example.com"},
},
},
},
@@ -500,6 +536,14 @@ func TestDialFakeApp(t *testing.T) {
app: "echo.nested.myzone.example.com",
expectCIDR: "192.168.2.0/24",
},
{
app: "echo1.leaf1.example.com",
expectCIDR: defaultIPv4CIDRRange,
},
{
app: "echo1.leaf2.example.com",
expectCIDR: defaultIPv4CIDRRange,
},
{
app: "echo1.root2.example.com",
expectCIDR: defaultIPv4CIDRRange,
@@ -508,6 +552,10 @@ func TestDialFakeApp(t *testing.T) {
app: "echo2.root2.example.com",
expectCIDR: defaultIPv4CIDRRange,
},
{
app: "echo1.leaf3.example.com",
expectCIDR: defaultIPv4CIDRRange,
},
}
t.Run("valid", func(t *testing.T) {
@@ -555,9 +603,6 @@ func TestDialFakeApp(t *testing.T) {
"not.an.app.example.com.",
"not.in.a.custom.zone",
"in.an.invalid.zone",
// Leaf clusters not supported yet.
"echo1.leaf1.example.com.",
"echo2.leaf1.example.com.",
}
for _, fqdn := range invalidTestCases {
t.Run(fqdn, func(t *testing.T) {
-11
View File
@@ -27,7 +27,6 @@ import (
"github.com/gravitational/trace"
"github.com/gravitational/teleport/api/client/proto"
vnetproto "github.com/gravitational/teleport/api/gen/proto/go/teleport/vnet/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/client/clientcache"
@@ -116,16 +115,6 @@ func (p *vnetAppProvider) GetDialOptions(ctx context.Context, profileName string
return dialOpts, nil
}
func (p *vnetAppProvider) GetVnetConfig(ctx context.Context, profileName, leafClusterName string) (*vnetproto.VnetConfig, error) {
clusterClient, err := p.clientCache.Get(ctx, profileName, leafClusterName)
if err != nil {
return nil, trace.Wrap(err)
}
vnetConfigClient := clusterClient.AuthClient.VnetConfigServiceClient()
vnetConfig, err := vnetConfigClient.GetVnetConfig(ctx, &vnetproto.GetVnetConfigRequest{})
return vnetConfig, trace.Wrap(err)
}
// OnNewConnection gets called before each VNet connection. It's a noop as tsh doesn't need to do
// anything extra here.
func (p *vnetAppProvider) OnNewConnection(ctx context.Context, profileName, leafClusterName string, app types.Application) error {