diff --git a/lib/client/api.go b/lib/client/api.go index 3e44e66fff1ba..cf39748f830a5 100644 --- a/lib/client/api.go +++ b/lib/client/api.go @@ -1609,11 +1609,19 @@ func (tc *TeleportClient) GetTargetNode(ctx context.Context, clt authclient.Clie return nil, trace.Wrap(err) } - if len(resources) == 0 { + switch len(resources) { + case 0: return nil, trace.NotFound("no matching SSH hosts found for search terms or query expression") - } - - if len(resources) > 1 { + case 1: + node, ok := resources[0].ResourceWithLabels.(*types.ServerV2) + if !ok { + return nil, trace.BadParameter("expected node resource, got %T", resources[0].ResourceWithLabels) + } + return &TargetNode{ + Hostname: node.GetHostname(), + Addr: node.GetName() + ":0", + }, nil + default: // If routing does not allow choosing the most recent host, then abort with // an ambiguous host error. cnc, err := clt.GetClusterNetworkingConfig(ctx) @@ -1626,22 +1634,21 @@ func (tc *TeleportClient) GetTargetNode(ctx context.Context, clt authclient.Clie return a.Expiry().Compare(b.Expiry()) }) - } + // Sorting above is oldest expiry to newest expiry, so proceed + // with the last item server in the slice. + server, ok := resources[len(resources)-1].ResourceWithLabels.(types.Server) + if !ok { + return nil, trace.BadParameter("received unexpected resource type %T", resources[0].ResourceWithLabels) + } - // Sorting above is oldest expiry to newest expiry, so proceed - // with the last item server in the slice. - server, ok := resources[len(resources)-1].ResourceWithLabels.(types.Server) - if !ok { - return nil, trace.BadParameter("received unexpected resource type %T", resources[0].ResourceWithLabels) + // Dialing is happening by UUID but a port is still required by + // the Proxy dial request. Zero is an indicator to the Proxy that + // it may chose the appropriate port based on the target server. + return &TargetNode{ + Hostname: server.GetHostname(), + Addr: server.GetName() + ":0", + }, nil } - - // Dialing is happening by UUID but a port is still required by - // the Proxy dial request. Zero is an indicator to the Proxy that - // it may chose the appropriate port based on the target server. - return &TargetNode{ - Hostname: server.GetHostname(), - Addr: server.GetName() + ":0", - }, nil case err == nil: if resp.GetServer() == nil { return nil, trace.NotFound("no matching SSH hosts found") diff --git a/lib/tbot/service_ssh_multiplexer.go b/lib/tbot/service_ssh_multiplexer.go index e979905d32039..de07a21eea848 100644 --- a/lib/tbot/service_ssh_multiplexer.go +++ b/lib/tbot/service_ssh_multiplexer.go @@ -667,7 +667,7 @@ func (s *SSHMultiplexerService) handleConn( host = cleanTargetHost(host, proxyHost, clusterName) target = net.JoinHostPort(host, port) } else { - node, err := resolveTargetHostWithClient(ctx, authClient, expanded.Search, expanded.Query) + node, err := resolveTargetHostWithClient(ctx, authClient.APIClient, expanded.Search, expanded.Query) if err != nil { return trace.Wrap(err, "resolving target host") } diff --git a/lib/tbot/ssh_proxy.go b/lib/tbot/ssh_proxy.go index 6769fd83103ae..639c272882b4e 100644 --- a/lib/tbot/ssh_proxy.go +++ b/lib/tbot/ssh_proxy.go @@ -23,6 +23,7 @@ import ( "log/slog" "net" "path/filepath" + "slices" "strings" "github.com/gravitational/trace" @@ -220,38 +221,61 @@ func resolveTargetHost(ctx context.Context, cfg client.Config, search, query str // resolveTargetHostWithClient resolves the target host using the provided // client and search and query parameters. func resolveTargetHostWithClient( - ctx context.Context, clt client.ListUnifiedResourcesClient, search, query string, + ctx context.Context, clt *client.Client, search, query string, ) (types.Server, error) { - resources, _, err := client.GetUnifiedResourcePage(ctx, clt, &proto.ListUnifiedResourcesRequest{ - // We only want a single node, but, we set limit=2 so we can throw a - // helpful error when multiple match. In the happy path, where a single - // node matches, this does not degrade performance because even if - // limit=1 the UnifiedResource cache will still iterate to the end to - // determine if there is a NextKey to return. - Limit: 2, - Kinds: []string{types.KindNode}, + resp, err := clt.ResolveSSHTarget(ctx, &proto.ResolveSSHTargetRequest{ SearchKeywords: libclient.ParseSearchKeywords(search, ','), PredicateExpression: query, - SortBy: types.SortBy{Field: types.ResourceKind}, }) - if err != nil { - return nil, trace.Wrap(err) - } - if len(resources) == 0 { - return nil, trace.NotFound("no matching SSH hosts found for search terms or query expression") - } - if len(resources) > 1 { - names := make([]string, len(resources)) - for i, res := range resources { - names[i] = res.GetName() + switch { + //TODO(tross): DELETE IN v20.0.0 + case trace.IsNotImplemented(err): + resources, err := client.GetAllUnifiedResources(ctx, clt, &proto.ListUnifiedResourcesRequest{ + Kinds: []string{types.KindNode}, + SearchKeywords: libclient.ParseSearchKeywords(search, ','), + PredicateExpression: query, + SortBy: types.SortBy{Field: types.ResourceMetadataName}, + }) + if err != nil { + return nil, trace.Wrap(err) } - return nil, trace.BadParameter("found multiple matching SSH hosts %v", names) - } - node := resources[0].ResourceWithLabels.(*types.ServerV2) - if node == nil { - return nil, trace.BadParameter("expected node resource, got %T", resources[0].ResourceWithLabels) + + switch len(resources) { + case 0: + return nil, trace.NotFound("no matching SSH hosts found for search terms or query expression") + case 1: + node, ok := resources[0].ResourceWithLabels.(*types.ServerV2) + if !ok { + return nil, trace.BadParameter("expected node resource, got %T", resources[0].ResourceWithLabels) + } + return node, nil + default: + // If routing does not allow choosing the most recent host, then abort with + // an ambiguous host error. + cnc, err := clt.GetClusterNetworkingConfig(ctx) + if err != nil || cnc.GetRoutingStrategy() != types.RoutingStrategy_MOST_RECENT { + return nil, trace.BadParameter("found multiple matching SSH hosts %v", resources[:2]) + } + + // Sort the resource by expiry so we can identify the most "recent". + slices.SortFunc(resources, func(a, b *types.EnrichedResource) int { + return a.Expiry().Compare(b.Expiry()) + }) + + // Sorting above is oldest expiry to newest expiry, so proceed + // with the last item server in the slice. + server, ok := resources[len(resources)-1].ResourceWithLabels.(types.Server) + if !ok { + return nil, trace.BadParameter("received unexpected resource type %T", resources[0].ResourceWithLabels) + } + + return server, nil + } + case err == nil: + return resp.GetServer(), nil + default: + return nil, trace.Wrap(err) } - return node, nil } func parseIdentity(destPath, proxy, cluster string, insecure, fips bool) (*identity.Facade, agent.ExtendedAgent, error) {