mirror of
https://github.com/gravitational/teleport.git
synced 2026-11-02 14:59:53 +08:00
Historically, each component responsible for managing a particular resource has been responsible for deleting said resources if Teleport was shutting down for good. This is done to provide a better user experience by trying to eliminate the chance of a stale resource from being present for the full TTL of the backend item. However, the mechanism to do so requires the process responsible for the resources to delete each resource individually. For dynamic resources this could require several thousand Delete RPCs during shutdown. Since the shutdown process is time bound this could result in some deletions from being successful and lead to the same stale resource issues. This also poses a problem and races with any inbound heartbeats being sent via the inventory control stream. All resource deletion mechanism are unary RPCs that get processed outside of the inventory control stream, which without careful coordination could result in all the deletions occurring before any in flight heartbeats are processed by the inventory control stream. In an attempt to simplify the deletion process a new UpstreamInventoryGoodbye message has been added to the inventory control stream in order to allow deletion to occur in-band. Instead of sending individual delete RPCs when a process is terminating it can now send a single UpstreamInventoryGoodbye via the inventory control stream. Any control streams that receive a GoodBye prior to being terminated indicate to auth that it should remove all resources that were being represented by said stream. Additionally, the DownstreamInventoryHello was updated to include supported capabilities to better coordinate backward compatibility and supported features by the auth server. This allows the agent to get a better understanding of what the auth server supports without having to do a series of version checks.
604 lines
17 KiB
Go
604 lines
17 KiB
Go
/*
|
|
Copyright 2022 Gravitational, Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package client
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"log/slog"
|
|
"sync"
|
|
|
|
"github.com/gravitational/trace"
|
|
|
|
"github.com/gravitational/teleport/api/client/proto"
|
|
"github.com/gravitational/teleport/api/internalutils/stream"
|
|
"github.com/gravitational/teleport/api/types"
|
|
)
|
|
|
|
// DownstreamInventoryControlStream is the client/agent side of a bidirectional stream established
|
|
// between teleport instances and auth servers.
|
|
type DownstreamInventoryControlStream interface {
|
|
// Send attempts to send an upstream message. An error returned from this
|
|
// method either indicates that the stream itself has failed, or that the
|
|
// supplied context was canceled.
|
|
Send(ctx context.Context, msg proto.UpstreamInventoryMessage) error
|
|
// Recv accesses the incoming/downstream message channel.
|
|
Recv() <-chan proto.DownstreamInventoryMessage
|
|
// Close closes the underlying stream without error.
|
|
Close() error
|
|
// CloseWithError closes the underlying stream with an error that can later
|
|
// be retrieved with Error(). Subsequent calls to CloseWithError have no effect.
|
|
CloseWithError(err error) error
|
|
// Done signals that the stream has been closed.
|
|
Done() <-chan struct{}
|
|
// Error checks for any error associated with stream closure (returns `nil` if
|
|
// the stream is open, or io.EOF if the stream was closed without error).
|
|
Error() error
|
|
}
|
|
|
|
// UpstreamInventoryControlStream is the server/controller side of a bidirectional stream established
|
|
// between teleport instances and auth servers.
|
|
type UpstreamInventoryControlStream interface {
|
|
// Send attempts to send a downstream message. An error returned from this
|
|
// method either indicates that the stream itself has failed, or that the
|
|
// supplied context was canceled.
|
|
Send(ctx context.Context, msg proto.DownstreamInventoryMessage) error
|
|
// Recv access the incoming/upstream message channel.
|
|
Recv() <-chan proto.UpstreamInventoryMessage
|
|
// PeerAddr gets the underlying TCP peer address (may be empty in some cases).
|
|
PeerAddr() string
|
|
// Close closes the underlying stream without error.
|
|
Close() error
|
|
// CloseWithError closes the underlying stream with an error that can later
|
|
// be retrieved with Error(). Subsequent calls to CloseWithError have no effect.
|
|
CloseWithError(err error) error
|
|
// Done signals that the stream has been closed.
|
|
Done() <-chan struct{}
|
|
// Error checks for any error associated with stream closure (returns `nil` if
|
|
// the stream is open, or io.EOF if the stream closed without error).
|
|
Error() error
|
|
}
|
|
|
|
type ICSPipeOption func(*pipeOptions)
|
|
|
|
type pipeOptions struct {
|
|
peerAddrFn func() string
|
|
}
|
|
|
|
func ICSPipePeerAddr(peerAddr string) ICSPipeOption {
|
|
return ICSPipePeerAddrFn(func() string {
|
|
return peerAddr
|
|
})
|
|
}
|
|
|
|
func ICSPipePeerAddrFn(fn func() string) ICSPipeOption {
|
|
return func(opts *pipeOptions) {
|
|
opts.peerAddrFn = fn
|
|
}
|
|
}
|
|
|
|
// InventoryControlStreamPipe creates the two halves of an inventory control stream over an in-memory
|
|
// pipe.
|
|
func InventoryControlStreamPipe(opts ...ICSPipeOption) (UpstreamInventoryControlStream, DownstreamInventoryControlStream) {
|
|
var options pipeOptions
|
|
for _, opt := range opts {
|
|
opt(&options)
|
|
}
|
|
pipe := &pipeControlStream{
|
|
downC: make(chan proto.DownstreamInventoryMessage),
|
|
upC: make(chan proto.UpstreamInventoryMessage),
|
|
doneC: make(chan struct{}),
|
|
peerAddrFn: options.peerAddrFn,
|
|
}
|
|
return upstreamPipeControlStream{pipe}, downstreamPipeControlStream{pipe}
|
|
}
|
|
|
|
type pipeControlStream struct {
|
|
downC chan proto.DownstreamInventoryMessage
|
|
upC chan proto.UpstreamInventoryMessage
|
|
peerAddrFn func() string
|
|
mu sync.Mutex
|
|
err error
|
|
doneC chan struct{}
|
|
}
|
|
|
|
func (p *pipeControlStream) Close() error {
|
|
return p.CloseWithError(nil)
|
|
}
|
|
|
|
func (p *pipeControlStream) CloseWithError(err error) error {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
if p.err != nil {
|
|
// stream already closed
|
|
return nil
|
|
}
|
|
|
|
if err != nil {
|
|
p.err = err
|
|
} else {
|
|
// represent "closure without error" with EOF.
|
|
p.err = io.EOF
|
|
}
|
|
close(p.doneC)
|
|
return nil
|
|
}
|
|
|
|
func (p *pipeControlStream) Done() <-chan struct{} {
|
|
return p.doneC
|
|
}
|
|
|
|
func (p *pipeControlStream) Error() error {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
return p.err
|
|
}
|
|
|
|
type upstreamPipeControlStream struct {
|
|
*pipeControlStream
|
|
}
|
|
|
|
func (u upstreamPipeControlStream) Send(ctx context.Context, msg proto.DownstreamInventoryMessage) error {
|
|
select {
|
|
case u.downC <- msg:
|
|
return nil
|
|
case <-u.Done():
|
|
return trace.Errorf("failed to send downstream inventory message (pipe closed)")
|
|
case <-ctx.Done():
|
|
return trace.Errorf("failed to send downstream inventory message: %v", ctx.Err())
|
|
}
|
|
}
|
|
|
|
func (u upstreamPipeControlStream) Recv() <-chan proto.UpstreamInventoryMessage {
|
|
return u.upC
|
|
}
|
|
|
|
func (u upstreamPipeControlStream) PeerAddr() string {
|
|
if u.peerAddrFn != nil {
|
|
return u.peerAddrFn()
|
|
}
|
|
return ""
|
|
}
|
|
|
|
type downstreamPipeControlStream struct {
|
|
*pipeControlStream
|
|
}
|
|
|
|
func (d downstreamPipeControlStream) Send(ctx context.Context, msg proto.UpstreamInventoryMessage) error {
|
|
select {
|
|
case d.upC <- msg:
|
|
return nil
|
|
case <-d.Done():
|
|
return trace.Errorf("failed to send upstream inventory message (pipe closed)")
|
|
case <-ctx.Done():
|
|
return trace.Errorf("failed to send upstream inventory message: %v", ctx.Err())
|
|
}
|
|
}
|
|
|
|
func (d downstreamPipeControlStream) Recv() <-chan proto.DownstreamInventoryMessage {
|
|
return d.downC
|
|
}
|
|
|
|
// InventoryControlStream opens a new control stream. The first message sent must be an
|
|
// UpstreamInventoryHello, and the first message received must be a DownstreamInventoryHello.
|
|
func (c *Client) InventoryControlStream(ctx context.Context) (DownstreamInventoryControlStream, error) {
|
|
cancelCtx, cancel := context.WithCancel(ctx)
|
|
stream, err := c.grpc.InventoryControlStream(cancelCtx)
|
|
if err != nil {
|
|
cancel()
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
return newDownstreamInventoryControlStream(stream, cancel), nil
|
|
}
|
|
|
|
func (c *Client) GetInventoryStatus(ctx context.Context, req proto.InventoryStatusRequest) (proto.InventoryStatusSummary, error) {
|
|
rsp, err := c.grpc.GetInventoryStatus(ctx, &req)
|
|
if err != nil {
|
|
return proto.InventoryStatusSummary{}, trace.Wrap(err)
|
|
}
|
|
|
|
return *rsp, nil
|
|
}
|
|
|
|
func (c *Client) PingInventory(ctx context.Context, req proto.InventoryPingRequest) (proto.InventoryPingResponse, error) {
|
|
rsp, err := c.grpc.PingInventory(ctx, &req)
|
|
if err != nil {
|
|
return proto.InventoryPingResponse{}, trace.Wrap(err)
|
|
}
|
|
|
|
return *rsp, nil
|
|
}
|
|
|
|
func (c *Client) GetInstances(ctx context.Context, filter types.InstanceFilter) stream.Stream[types.Instance] {
|
|
// set up cancelable context so that Stream.Done can close the stream if the caller
|
|
// halts early.
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
|
|
instances, err := c.grpc.GetInstances(ctx, &filter)
|
|
if err != nil {
|
|
cancel()
|
|
return stream.Fail[types.Instance](trace.Wrap(err))
|
|
}
|
|
return stream.Func[types.Instance](func() (types.Instance, error) {
|
|
instance, err := instances.Recv()
|
|
if err != nil {
|
|
if errors.Is(err, io.EOF) {
|
|
// io.EOF signals that stream has completed successfully
|
|
return nil, io.EOF
|
|
}
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
return instance, nil
|
|
}, cancel)
|
|
}
|
|
|
|
func newDownstreamInventoryControlStream(stream proto.AuthService_InventoryControlStreamClient, cancel context.CancelFunc) DownstreamInventoryControlStream {
|
|
ics := &downstreamICS{
|
|
sendC: make(chan upstreamSend),
|
|
recvC: make(chan proto.DownstreamInventoryMessage),
|
|
cancel: cancel,
|
|
doneC: make(chan struct{}),
|
|
}
|
|
|
|
go ics.runRecvLoop(stream)
|
|
go ics.runSendLoop(stream)
|
|
|
|
return ics
|
|
}
|
|
|
|
// upstreamSend is a helper message used to help us inject per-send context cancellation
|
|
type upstreamSend struct {
|
|
msg proto.UpstreamInventoryMessage
|
|
errC chan error
|
|
}
|
|
|
|
// downstreamICS is a helper which manages a proto.AuthService_InventoryControlStreamClient
|
|
// stream and wraps its API to use friendlier types and support select/cancellation.
|
|
type downstreamICS struct {
|
|
sendC chan upstreamSend
|
|
recvC chan proto.DownstreamInventoryMessage
|
|
mu sync.Mutex
|
|
cancel context.CancelFunc
|
|
doneC chan struct{}
|
|
err error
|
|
}
|
|
|
|
// runRecvLoop waits for incoming messages, converts them to the friendlier DownstreamInventoryMessage
|
|
// type, and pushes them to the recvC channel.
|
|
func (i *downstreamICS) runRecvLoop(stream proto.AuthService_InventoryControlStreamClient) {
|
|
for {
|
|
oneOf, err := stream.Recv()
|
|
if err != nil {
|
|
// preserve EOF to help distinguish "ok" closure.
|
|
if !errors.Is(err, io.EOF) {
|
|
err = trace.Errorf("inventory control stream closed: %v", trace.Wrap(err))
|
|
}
|
|
i.CloseWithError(err)
|
|
return
|
|
}
|
|
|
|
var msg proto.DownstreamInventoryMessage
|
|
|
|
switch {
|
|
case oneOf.GetHello() != nil:
|
|
msg = *oneOf.GetHello()
|
|
case oneOf.GetPing() != nil:
|
|
msg = *oneOf.GetPing()
|
|
case oneOf.GetUpdateLabels() != nil:
|
|
msg = *oneOf.GetUpdateLabels()
|
|
default:
|
|
slog.WarnContext(stream.Context(), "received unknown downstream message", "message", oneOf)
|
|
continue
|
|
}
|
|
|
|
select {
|
|
case i.recvC <- msg:
|
|
case <-i.Done():
|
|
// stream closed by other goroutine
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// runSendLoop pulls messages off of the sendC channel, applies the appropriate protobuf wrapper types,
|
|
// and sends them over the stream.
|
|
func (i *downstreamICS) runSendLoop(stream proto.AuthService_InventoryControlStreamClient) {
|
|
for {
|
|
select {
|
|
case sendMsg := <-i.sendC:
|
|
var oneOf proto.UpstreamInventoryOneOf
|
|
switch msg := sendMsg.msg.(type) {
|
|
case proto.UpstreamInventoryHello:
|
|
oneOf.Msg = &proto.UpstreamInventoryOneOf_Hello{
|
|
Hello: &msg,
|
|
}
|
|
case proto.InventoryHeartbeat:
|
|
oneOf.Msg = &proto.UpstreamInventoryOneOf_Heartbeat{
|
|
Heartbeat: &msg,
|
|
}
|
|
case proto.UpstreamInventoryPong:
|
|
oneOf.Msg = &proto.UpstreamInventoryOneOf_Pong{
|
|
Pong: &msg,
|
|
}
|
|
case proto.UpstreamInventoryAgentMetadata:
|
|
oneOf.Msg = &proto.UpstreamInventoryOneOf_AgentMetadata{
|
|
AgentMetadata: &msg,
|
|
}
|
|
case proto.UpstreamInventoryGoodbye:
|
|
oneOf.Msg = &proto.UpstreamInventoryOneOf_Goodbye{
|
|
Goodbye: &msg,
|
|
}
|
|
default:
|
|
sendMsg.errC <- trace.BadParameter("cannot send unexpected upstream msg type: %T", msg)
|
|
continue
|
|
}
|
|
err := stream.Send(&oneOf)
|
|
sendMsg.errC <- err
|
|
if err != nil {
|
|
// preserve EOF errors
|
|
if !errors.Is(err, io.EOF) {
|
|
err = trace.Errorf("upstream send failed: %v", err)
|
|
}
|
|
i.CloseWithError(err)
|
|
return
|
|
}
|
|
case <-i.Done():
|
|
// stream closed by other goroutine
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (i *downstreamICS) Send(ctx context.Context, msg proto.UpstreamInventoryMessage) error {
|
|
if err := ctx.Err(); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
errC := make(chan error, 1)
|
|
select {
|
|
case i.sendC <- upstreamSend{msg: msg, errC: errC}:
|
|
select {
|
|
case err := <-errC:
|
|
return trace.Wrap(err)
|
|
case <-ctx.Done():
|
|
return trace.Errorf("inventory control msg send result skipped: %w", ctx.Err())
|
|
}
|
|
case <-ctx.Done():
|
|
return trace.Errorf("inventory control msg not sent: %w", ctx.Err())
|
|
case <-i.Done():
|
|
err := i.Error()
|
|
if err == nil {
|
|
return trace.Errorf("inventory control stream externally closed during send")
|
|
}
|
|
return trace.Errorf("inventory control msg not sent: %w", err)
|
|
}
|
|
}
|
|
|
|
func (i *downstreamICS) Recv() <-chan proto.DownstreamInventoryMessage {
|
|
return i.recvC
|
|
}
|
|
|
|
func (i *downstreamICS) Done() <-chan struct{} {
|
|
return i.doneC
|
|
}
|
|
|
|
func (i *downstreamICS) Close() error {
|
|
return i.CloseWithError(nil)
|
|
}
|
|
|
|
func (i *downstreamICS) CloseWithError(err error) error {
|
|
i.mu.Lock()
|
|
defer i.mu.Unlock()
|
|
if i.err != nil {
|
|
// already closed
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
i.err = err
|
|
} else {
|
|
i.err = io.EOF
|
|
}
|
|
i.cancel()
|
|
close(i.doneC)
|
|
return nil
|
|
}
|
|
|
|
func (i *downstreamICS) Error() error {
|
|
i.mu.Lock()
|
|
defer i.mu.Unlock()
|
|
return i.err
|
|
}
|
|
|
|
// NewUpstreamInventoryControlStream wraps the server-side control stream handle. For use as part of the internals
|
|
// of the auth server's gRPC API implementation.
|
|
func NewUpstreamInventoryControlStream(stream proto.AuthService_InventoryControlStreamServer, peerAddr string) UpstreamInventoryControlStream {
|
|
ics := &upstreamICS{
|
|
sendC: make(chan downstreamSend),
|
|
recvC: make(chan proto.UpstreamInventoryMessage),
|
|
doneC: make(chan struct{}),
|
|
peerAddr: peerAddr,
|
|
}
|
|
|
|
go ics.runRecvLoop(stream)
|
|
go ics.runSendLoop(stream)
|
|
|
|
return ics
|
|
}
|
|
|
|
// downstreamSend is a helper message used to help us inject per-send context cancellation
|
|
type downstreamSend struct {
|
|
msg proto.DownstreamInventoryMessage
|
|
errC chan error
|
|
}
|
|
|
|
// upstreamICS is a helper which manages a proto.AuthService_InventoryControlStreamServer
|
|
// stream and wraps its API to use friendlier types and support select/cancellation.
|
|
type upstreamICS struct {
|
|
sendC chan downstreamSend
|
|
recvC chan proto.UpstreamInventoryMessage
|
|
peerAddr string
|
|
mu sync.Mutex
|
|
doneC chan struct{}
|
|
err error
|
|
}
|
|
|
|
// runRecvLoop waits for incoming messages, converts them to the friendlier UpstreamInventoryMessage
|
|
// type, and pushes them to the recvC channel.
|
|
func (i *upstreamICS) runRecvLoop(stream proto.AuthService_InventoryControlStreamServer) {
|
|
for {
|
|
oneOf, err := stream.Recv()
|
|
if err != nil {
|
|
// preserve eof errors
|
|
if !errors.Is(err, io.EOF) {
|
|
err = trace.Errorf("inventory control stream recv failed: %v", trace.Wrap(err))
|
|
}
|
|
i.CloseWithError(err)
|
|
return
|
|
}
|
|
|
|
var msg proto.UpstreamInventoryMessage
|
|
|
|
switch {
|
|
case oneOf.GetHello() != nil:
|
|
msg = *oneOf.GetHello()
|
|
case oneOf.GetHeartbeat() != nil:
|
|
msg = *oneOf.GetHeartbeat()
|
|
case oneOf.GetPong() != nil:
|
|
msg = *oneOf.GetPong()
|
|
case oneOf.GetAgentMetadata() != nil:
|
|
msg = *oneOf.GetAgentMetadata()
|
|
case oneOf.GetGoodbye() != nil:
|
|
msg = *oneOf.GetGoodbye()
|
|
default:
|
|
slog.WarnContext(stream.Context(), "received unknown upstream message", "message", oneOf)
|
|
continue
|
|
}
|
|
|
|
select {
|
|
case i.recvC <- msg:
|
|
case <-i.Done():
|
|
// stream closed by other goroutine
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// runSendLoop pulls messages off of the sendC channel, applies the appropriate protobuf wrapper types,
|
|
// and sends them over the channel.
|
|
func (i *upstreamICS) runSendLoop(stream proto.AuthService_InventoryControlStreamServer) {
|
|
for {
|
|
select {
|
|
case sendMsg := <-i.sendC:
|
|
var oneOf proto.DownstreamInventoryOneOf
|
|
switch msg := sendMsg.msg.(type) {
|
|
case proto.DownstreamInventoryHello:
|
|
oneOf.Msg = &proto.DownstreamInventoryOneOf_Hello{
|
|
Hello: &msg,
|
|
}
|
|
case proto.DownstreamInventoryPing:
|
|
oneOf.Msg = &proto.DownstreamInventoryOneOf_Ping{
|
|
Ping: &msg,
|
|
}
|
|
case proto.DownstreamInventoryUpdateLabels:
|
|
oneOf.Msg = &proto.DownstreamInventoryOneOf_UpdateLabels{
|
|
UpdateLabels: &msg,
|
|
}
|
|
default:
|
|
sendMsg.errC <- trace.BadParameter("cannot send unexpected downstream msg type: %T", msg)
|
|
continue
|
|
}
|
|
err := stream.Send(&oneOf)
|
|
sendMsg.errC <- err
|
|
if err != nil {
|
|
// preserve eof errors
|
|
if !errors.Is(err, io.EOF) {
|
|
err = trace.Errorf("downstream send failed: %v", err)
|
|
}
|
|
i.CloseWithError(err)
|
|
return
|
|
}
|
|
case <-i.Done():
|
|
// stream closed by other goroutine
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (i *upstreamICS) Send(ctx context.Context, msg proto.DownstreamInventoryMessage) error {
|
|
if err := ctx.Err(); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
errC := make(chan error, 1)
|
|
select {
|
|
case i.sendC <- downstreamSend{msg: msg, errC: errC}:
|
|
select {
|
|
case err := <-errC:
|
|
return trace.Wrap(err)
|
|
case <-ctx.Done():
|
|
return trace.Errorf("inventory control msg send result skipped: %w", ctx.Err())
|
|
}
|
|
case <-ctx.Done():
|
|
return trace.Errorf("inventory control msg not sent: %w", ctx.Err())
|
|
case <-i.Done():
|
|
err := i.Error()
|
|
if err == nil {
|
|
return trace.Errorf("inventory control stream externally closed during send")
|
|
}
|
|
return trace.Errorf("inventory control msg not sent: %w", err)
|
|
}
|
|
}
|
|
|
|
func (i *upstreamICS) Recv() <-chan proto.UpstreamInventoryMessage {
|
|
return i.recvC
|
|
}
|
|
|
|
func (i *upstreamICS) PeerAddr() string {
|
|
return i.peerAddr
|
|
}
|
|
|
|
func (i *upstreamICS) Done() <-chan struct{} {
|
|
return i.doneC
|
|
}
|
|
|
|
func (i *upstreamICS) Close() error {
|
|
return i.CloseWithError(nil)
|
|
}
|
|
|
|
func (i *upstreamICS) CloseWithError(err error) error {
|
|
i.mu.Lock()
|
|
defer i.mu.Unlock()
|
|
if i.err != nil {
|
|
// already closed
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
i.err = err
|
|
} else {
|
|
i.err = io.EOF
|
|
}
|
|
close(i.doneC)
|
|
return nil
|
|
}
|
|
|
|
func (i *upstreamICS) Error() error {
|
|
i.mu.Lock()
|
|
defer i.mu.Unlock()
|
|
return i.err
|
|
}
|