github.com/gravitational/teleport/api@v0.0.0-20240507183017-3110591cbafc/client/inventory.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 client
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"io"
    23  	"sync"
    24  
    25  	"github.com/gravitational/trace"
    26  
    27  	"github.com/gravitational/teleport/api/client/proto"
    28  	"github.com/gravitational/teleport/api/internalutils/stream"
    29  	"github.com/gravitational/teleport/api/types"
    30  )
    31  
    32  // DownstreamInventoryControlStream is the client/agent side of a bidirectional stream established
    33  // between teleport instances and auth servers.
    34  type DownstreamInventoryControlStream interface {
    35  	// Send attempts to send an upstream message. An error returned from this
    36  	// method either indicates that the stream itself has failed, or that the
    37  	// supplied context was canceled.
    38  	Send(ctx context.Context, msg proto.UpstreamInventoryMessage) error
    39  	// Recv accesses the incoming/downstream message channel.
    40  	Recv() <-chan proto.DownstreamInventoryMessage
    41  	// Close closes the underlying stream without error.
    42  	Close() error
    43  	// CloseWithError closes the underlying stream with an error that can later
    44  	// be retrieved with Error(). Subsequent calls to CloseWithError have no effect.
    45  	CloseWithError(err error) error
    46  	// Done signals that the stream has been closed.
    47  	Done() <-chan struct{}
    48  	// Error checks for any error associated with stream closure (returns `nil` if
    49  	// the stream is open, or io.EOF if the stream was closed without error).
    50  	Error() error
    51  }
    52  
    53  // UpstreamInventoryControlStream is the server/controller side of a bidirectional stream established
    54  // between teleport instances and auth servers.
    55  type UpstreamInventoryControlStream interface {
    56  	// Send attempts to send a downstream message.  An error returned from this
    57  	// method either indicates that the stream itself has failed, or that the
    58  	// supplied context was canceled.
    59  	Send(ctx context.Context, msg proto.DownstreamInventoryMessage) error
    60  	// Recv access the incoming/upstream message channel.
    61  	Recv() <-chan proto.UpstreamInventoryMessage
    62  	// PeerAddr gets the underlying TCP peer address (may be empty in some cases).
    63  	PeerAddr() string
    64  	// Close closes the underlying stream without error.
    65  	Close() error
    66  	// CloseWithError closes the underlying stream with an error that can later
    67  	// be retrieved with Error(). Subsequent calls to CloseWithError have no effect.
    68  	CloseWithError(err error) error
    69  	// Done signals that the stream has been closed.
    70  	Done() <-chan struct{}
    71  	// Error checks for any error associated with stream closure (returns `nil` if
    72  	// the stream is open, or io.EOF if the stream closed without error).
    73  	Error() error
    74  }
    75  
    76  type ICSPipeOption func(*pipeOptions)
    77  
    78  type pipeOptions struct {
    79  	peerAddrFn func() string
    80  }
    81  
    82  func ICSPipePeerAddr(peerAddr string) ICSPipeOption {
    83  	return ICSPipePeerAddrFn(func() string {
    84  		return peerAddr
    85  	})
    86  }
    87  
    88  func ICSPipePeerAddrFn(fn func() string) ICSPipeOption {
    89  	return func(opts *pipeOptions) {
    90  		opts.peerAddrFn = fn
    91  	}
    92  }
    93  
    94  // InventoryControlStreamPipe creates the two halves of an inventory control stream over an in-memory
    95  // pipe.
    96  func InventoryControlStreamPipe(opts ...ICSPipeOption) (UpstreamInventoryControlStream, DownstreamInventoryControlStream) {
    97  	var options pipeOptions
    98  	for _, opt := range opts {
    99  		opt(&options)
   100  	}
   101  	pipe := &pipeControlStream{
   102  		downC:      make(chan proto.DownstreamInventoryMessage),
   103  		upC:        make(chan proto.UpstreamInventoryMessage),
   104  		doneC:      make(chan struct{}),
   105  		peerAddrFn: options.peerAddrFn,
   106  	}
   107  	return upstreamPipeControlStream{pipe}, downstreamPipeControlStream{pipe}
   108  }
   109  
   110  type pipeControlStream struct {
   111  	downC      chan proto.DownstreamInventoryMessage
   112  	upC        chan proto.UpstreamInventoryMessage
   113  	peerAddrFn func() string
   114  	mu         sync.Mutex
   115  	err        error
   116  	doneC      chan struct{}
   117  }
   118  
   119  func (p *pipeControlStream) Close() error {
   120  	return p.CloseWithError(nil)
   121  }
   122  
   123  func (p *pipeControlStream) CloseWithError(err error) error {
   124  	p.mu.Lock()
   125  	defer p.mu.Unlock()
   126  	if p.err != nil {
   127  		// stream already closed
   128  		return nil
   129  	}
   130  
   131  	if err != nil {
   132  		p.err = err
   133  	} else {
   134  		// represent "closure without error" with EOF.
   135  		p.err = io.EOF
   136  	}
   137  	close(p.doneC)
   138  	return nil
   139  }
   140  
   141  func (p *pipeControlStream) Done() <-chan struct{} {
   142  	return p.doneC
   143  }
   144  
   145  func (p *pipeControlStream) Error() error {
   146  	p.mu.Lock()
   147  	defer p.mu.Unlock()
   148  	return p.err
   149  }
   150  
   151  type upstreamPipeControlStream struct {
   152  	*pipeControlStream
   153  }
   154  
   155  func (u upstreamPipeControlStream) Send(ctx context.Context, msg proto.DownstreamInventoryMessage) error {
   156  	select {
   157  	case u.downC <- msg:
   158  		return nil
   159  	case <-u.Done():
   160  		return trace.Errorf("failed to send downstream inventory message (pipe closed)")
   161  	case <-ctx.Done():
   162  		return trace.Errorf("failed to send downstream inventory message: %v", ctx.Err())
   163  	}
   164  }
   165  
   166  func (u upstreamPipeControlStream) Recv() <-chan proto.UpstreamInventoryMessage {
   167  	return u.upC
   168  }
   169  
   170  func (u upstreamPipeControlStream) PeerAddr() string {
   171  	if u.peerAddrFn != nil {
   172  		return u.peerAddrFn()
   173  	}
   174  	return ""
   175  }
   176  
   177  type downstreamPipeControlStream struct {
   178  	*pipeControlStream
   179  }
   180  
   181  func (d downstreamPipeControlStream) Send(ctx context.Context, msg proto.UpstreamInventoryMessage) error {
   182  	select {
   183  	case d.upC <- msg:
   184  		return nil
   185  	case <-d.Done():
   186  		return trace.Errorf("failed to send upstream inventory message (pipe closed)")
   187  	case <-ctx.Done():
   188  		return trace.Errorf("failed to send upstream inventory message: %v", ctx.Err())
   189  	}
   190  }
   191  
   192  func (d downstreamPipeControlStream) Recv() <-chan proto.DownstreamInventoryMessage {
   193  	return d.downC
   194  }
   195  
   196  // InventoryControlStream opens a new control stream.  The first message sent must be an
   197  // UpstreamInventoryHello, and the first message received must be a DownstreamInventoryHello.
   198  func (c *Client) InventoryControlStream(ctx context.Context) (DownstreamInventoryControlStream, error) {
   199  	cancelCtx, cancel := context.WithCancel(ctx)
   200  	stream, err := c.grpc.InventoryControlStream(cancelCtx)
   201  	if err != nil {
   202  		cancel()
   203  		return nil, trace.Wrap(err)
   204  	}
   205  	return newDownstreamInventoryControlStream(stream, cancel), nil
   206  }
   207  
   208  func (c *Client) GetInventoryStatus(ctx context.Context, req proto.InventoryStatusRequest) (proto.InventoryStatusSummary, error) {
   209  	rsp, err := c.grpc.GetInventoryStatus(ctx, &req)
   210  	if err != nil {
   211  		return proto.InventoryStatusSummary{}, trace.Wrap(err)
   212  	}
   213  
   214  	return *rsp, nil
   215  }
   216  
   217  func (c *Client) PingInventory(ctx context.Context, req proto.InventoryPingRequest) (proto.InventoryPingResponse, error) {
   218  	rsp, err := c.grpc.PingInventory(ctx, &req)
   219  	if err != nil {
   220  		return proto.InventoryPingResponse{}, trace.Wrap(err)
   221  	}
   222  
   223  	return *rsp, nil
   224  }
   225  
   226  func (c *Client) GetInstances(ctx context.Context, filter types.InstanceFilter) stream.Stream[types.Instance] {
   227  	// set up cancelable context so that Stream.Done can close the stream if the caller
   228  	// halts early.
   229  	ctx, cancel := context.WithCancel(ctx)
   230  
   231  	instances, err := c.grpc.GetInstances(ctx, &filter)
   232  	if err != nil {
   233  		cancel()
   234  		return stream.Fail[types.Instance](trace.Wrap(err))
   235  	}
   236  	return stream.Func[types.Instance](func() (types.Instance, error) {
   237  		instance, err := instances.Recv()
   238  		if err != nil {
   239  			if errors.Is(err, io.EOF) {
   240  				// io.EOF signals that stream has completed successfully
   241  				return nil, io.EOF
   242  			}
   243  			return nil, trace.Wrap(err)
   244  		}
   245  		return instance, nil
   246  	}, cancel)
   247  }
   248  
   249  func newDownstreamInventoryControlStream(stream proto.AuthService_InventoryControlStreamClient, cancel context.CancelFunc) DownstreamInventoryControlStream {
   250  	ics := &downstreamICS{
   251  		sendC:  make(chan upstreamSend),
   252  		recvC:  make(chan proto.DownstreamInventoryMessage),
   253  		cancel: cancel,
   254  		doneC:  make(chan struct{}),
   255  	}
   256  
   257  	go ics.runRecvLoop(stream)
   258  	go ics.runSendLoop(stream)
   259  
   260  	return ics
   261  }
   262  
   263  // upstreamSend is a helper message used to help us inject per-send context cancellation
   264  type upstreamSend struct {
   265  	msg  proto.UpstreamInventoryMessage
   266  	errC chan error
   267  }
   268  
   269  // downstreamICS is a helper which manages a proto.AuthService_InventoryControlStreamClient
   270  // stream and wraps its API to use friendlier types and support select/cancellation.
   271  type downstreamICS struct {
   272  	sendC  chan upstreamSend
   273  	recvC  chan proto.DownstreamInventoryMessage
   274  	mu     sync.Mutex
   275  	cancel context.CancelFunc
   276  	doneC  chan struct{}
   277  	err    error
   278  }
   279  
   280  // runRecvLoop waits for incoming messages, converts them to the friendlier DownstreamInventoryMessage
   281  // type, and pushes them to the recvC channel.
   282  func (i *downstreamICS) runRecvLoop(stream proto.AuthService_InventoryControlStreamClient) {
   283  	for {
   284  		oneOf, err := stream.Recv()
   285  		if err != nil {
   286  			// preserve EOF to help distinguish "ok" closure.
   287  			if !errors.Is(err, io.EOF) {
   288  				err = trace.Errorf("inventory control stream closed: %v", trace.Wrap(err))
   289  			}
   290  			i.CloseWithError(err)
   291  			return
   292  		}
   293  
   294  		var msg proto.DownstreamInventoryMessage
   295  
   296  		switch {
   297  		case oneOf.GetHello() != nil:
   298  			msg = *oneOf.GetHello()
   299  		case oneOf.GetPing() != nil:
   300  			msg = *oneOf.GetPing()
   301  		case oneOf.GetUpdateLabels() != nil:
   302  			msg = *oneOf.GetUpdateLabels()
   303  		default:
   304  			// TODO: log unknown message variants once we have a better story around
   305  			// logging in api/* packages.
   306  			continue
   307  		}
   308  
   309  		select {
   310  		case i.recvC <- msg:
   311  		case <-i.Done():
   312  			// stream closed by other goroutine
   313  			return
   314  		}
   315  	}
   316  }
   317  
   318  // runSendLoop pulls messages off of the sendC channel, applies the appropriate protobuf wrapper types,
   319  // and sends them over the stream.
   320  func (i *downstreamICS) runSendLoop(stream proto.AuthService_InventoryControlStreamClient) {
   321  	for {
   322  		select {
   323  		case sendMsg := <-i.sendC:
   324  			var oneOf proto.UpstreamInventoryOneOf
   325  			switch msg := sendMsg.msg.(type) {
   326  			case proto.UpstreamInventoryHello:
   327  				oneOf.Msg = &proto.UpstreamInventoryOneOf_Hello{
   328  					Hello: &msg,
   329  				}
   330  			case proto.InventoryHeartbeat:
   331  				oneOf.Msg = &proto.UpstreamInventoryOneOf_Heartbeat{
   332  					Heartbeat: &msg,
   333  				}
   334  			case proto.UpstreamInventoryPong:
   335  				oneOf.Msg = &proto.UpstreamInventoryOneOf_Pong{
   336  					Pong: &msg,
   337  				}
   338  			case proto.UpstreamInventoryAgentMetadata:
   339  				oneOf.Msg = &proto.UpstreamInventoryOneOf_AgentMetadata{
   340  					AgentMetadata: &msg,
   341  				}
   342  			default:
   343  				sendMsg.errC <- trace.BadParameter("cannot send unexpected upstream msg type: %T", msg)
   344  				continue
   345  			}
   346  			err := stream.Send(&oneOf)
   347  			sendMsg.errC <- err
   348  			if err != nil {
   349  				// preserve EOF errors
   350  				if !errors.Is(err, io.EOF) {
   351  					err = trace.Errorf("upstream send failed: %v", err)
   352  				}
   353  				i.CloseWithError(err)
   354  				return
   355  			}
   356  		case <-i.Done():
   357  			// stream closed by other goroutine
   358  			return
   359  		}
   360  	}
   361  }
   362  
   363  func (i *downstreamICS) Send(ctx context.Context, msg proto.UpstreamInventoryMessage) error {
   364  	errC := make(chan error, 1)
   365  	select {
   366  	case i.sendC <- upstreamSend{msg: msg, errC: errC}:
   367  		select {
   368  		case err := <-errC:
   369  			return trace.Wrap(err)
   370  		case <-ctx.Done():
   371  			return trace.Errorf("inventory control msg send result skipped: %v", ctx.Err())
   372  		}
   373  	case <-ctx.Done():
   374  		return trace.Errorf("inventory control msg not sent: %v", ctx.Err())
   375  	case <-i.Done():
   376  		err := i.Error()
   377  		if err == nil {
   378  			return trace.Errorf("inventory control stream externally closed during send")
   379  		}
   380  		return trace.Errorf("inventory control msg not sent: %v", err)
   381  	}
   382  }
   383  
   384  func (i *downstreamICS) Recv() <-chan proto.DownstreamInventoryMessage {
   385  	return i.recvC
   386  }
   387  
   388  func (i *downstreamICS) Done() <-chan struct{} {
   389  	return i.doneC
   390  }
   391  
   392  func (i *downstreamICS) Close() error {
   393  	return i.CloseWithError(nil)
   394  }
   395  
   396  func (i *downstreamICS) CloseWithError(err error) error {
   397  	i.mu.Lock()
   398  	defer i.mu.Unlock()
   399  	if i.err != nil {
   400  		// already closed
   401  		return nil
   402  	}
   403  	if err != nil {
   404  		i.err = err
   405  	} else {
   406  		i.err = io.EOF
   407  	}
   408  	i.cancel()
   409  	close(i.doneC)
   410  	return nil
   411  }
   412  
   413  func (i *downstreamICS) Error() error {
   414  	i.mu.Lock()
   415  	defer i.mu.Unlock()
   416  	return i.err
   417  }
   418  
   419  // NewUpstreamInventoryControlStream wraps the server-side control stream handle. For use as part of the internals
   420  // of the auth server's gRPC API implementation.
   421  func NewUpstreamInventoryControlStream(stream proto.AuthService_InventoryControlStreamServer, peerAddr string) UpstreamInventoryControlStream {
   422  	ics := &upstreamICS{
   423  		sendC:    make(chan downstreamSend),
   424  		recvC:    make(chan proto.UpstreamInventoryMessage),
   425  		doneC:    make(chan struct{}),
   426  		peerAddr: peerAddr,
   427  	}
   428  
   429  	go ics.runRecvLoop(stream)
   430  	go ics.runSendLoop(stream)
   431  
   432  	return ics
   433  }
   434  
   435  // downstreamSend is a helper message used to help us inject per-send context cancellation
   436  type downstreamSend struct {
   437  	msg  proto.DownstreamInventoryMessage
   438  	errC chan error
   439  }
   440  
   441  // upstreamICS is a helper which manages a proto.AuthService_InventoryControlStreamServer
   442  // stream and wraps its API to use friendlier types and support select/cancellation.
   443  type upstreamICS struct {
   444  	sendC    chan downstreamSend
   445  	recvC    chan proto.UpstreamInventoryMessage
   446  	peerAddr string
   447  	mu       sync.Mutex
   448  	doneC    chan struct{}
   449  	err      error
   450  }
   451  
   452  // runRecvLoop waits for incoming messages, converts them to the friendlier UpstreamInventoryMessage
   453  // type, and pushes them to the recvC channel.
   454  func (i *upstreamICS) runRecvLoop(stream proto.AuthService_InventoryControlStreamServer) {
   455  	for {
   456  		oneOf, err := stream.Recv()
   457  		if err != nil {
   458  			// preserve eof errors
   459  			if !errors.Is(err, io.EOF) {
   460  				err = trace.Errorf("inventory control stream recv failed: %v", trace.Wrap(err))
   461  			}
   462  			i.CloseWithError(err)
   463  			return
   464  		}
   465  
   466  		var msg proto.UpstreamInventoryMessage
   467  
   468  		switch {
   469  		case oneOf.GetHello() != nil:
   470  			msg = *oneOf.GetHello()
   471  		case oneOf.GetHeartbeat() != nil:
   472  			msg = *oneOf.GetHeartbeat()
   473  		case oneOf.GetPong() != nil:
   474  			msg = *oneOf.GetPong()
   475  		case oneOf.GetAgentMetadata() != nil:
   476  			msg = *oneOf.GetAgentMetadata()
   477  		default:
   478  			// TODO: log unknown message variants once we have a better story around
   479  			// logging in api/* packages.
   480  			continue
   481  		}
   482  
   483  		select {
   484  		case i.recvC <- msg:
   485  		case <-i.Done():
   486  			// stream closed by other goroutine
   487  			return
   488  		}
   489  	}
   490  }
   491  
   492  // runSendLoop pulls messages off of the sendC channel, applies the appropriate protobuf wrapper types,
   493  // and sends them over the channel.
   494  func (i *upstreamICS) runSendLoop(stream proto.AuthService_InventoryControlStreamServer) {
   495  	for {
   496  		select {
   497  		case sendMsg := <-i.sendC:
   498  			var oneOf proto.DownstreamInventoryOneOf
   499  			switch msg := sendMsg.msg.(type) {
   500  			case proto.DownstreamInventoryHello:
   501  				oneOf.Msg = &proto.DownstreamInventoryOneOf_Hello{
   502  					Hello: &msg,
   503  				}
   504  			case proto.DownstreamInventoryPing:
   505  				oneOf.Msg = &proto.DownstreamInventoryOneOf_Ping{
   506  					Ping: &msg,
   507  				}
   508  			case proto.DownstreamInventoryUpdateLabels:
   509  				oneOf.Msg = &proto.DownstreamInventoryOneOf_UpdateLabels{
   510  					UpdateLabels: &msg,
   511  				}
   512  			default:
   513  				sendMsg.errC <- trace.BadParameter("cannot send unexpected upstream msg type: %T", msg)
   514  				continue
   515  			}
   516  			err := stream.Send(&oneOf)
   517  			sendMsg.errC <- err
   518  			if err != nil {
   519  				// preserve eof errors
   520  				if !errors.Is(err, io.EOF) {
   521  					err = trace.Errorf("downstream send failed: %v", err)
   522  				}
   523  				i.CloseWithError(err)
   524  				return
   525  			}
   526  		case <-i.Done():
   527  			// stream closed by other goroutine
   528  			return
   529  		}
   530  	}
   531  }
   532  
   533  func (i *upstreamICS) Send(ctx context.Context, msg proto.DownstreamInventoryMessage) error {
   534  	errC := make(chan error, 1)
   535  	select {
   536  	case i.sendC <- downstreamSend{msg: msg, errC: errC}:
   537  		select {
   538  		case err := <-errC:
   539  			return trace.Wrap(err)
   540  		case <-ctx.Done():
   541  			return trace.Errorf("inventory control msg send result skipped: %v", ctx.Err())
   542  		}
   543  	case <-ctx.Done():
   544  		return trace.Errorf("inventory control msg not sent: %v", ctx.Err())
   545  	case <-i.Done():
   546  		err := i.Error()
   547  		if err == nil {
   548  			return trace.Errorf("inventory control stream externally closed during send")
   549  		}
   550  		return trace.Errorf("inventory control msg not sent: %v", err)
   551  	}
   552  }
   553  
   554  func (i *upstreamICS) Recv() <-chan proto.UpstreamInventoryMessage {
   555  	return i.recvC
   556  }
   557  
   558  func (i *upstreamICS) PeerAddr() string {
   559  	return i.peerAddr
   560  }
   561  
   562  func (i *upstreamICS) Done() <-chan struct{} {
   563  	return i.doneC
   564  }
   565  
   566  func (i *upstreamICS) Close() error {
   567  	return i.CloseWithError(nil)
   568  }
   569  
   570  func (i *upstreamICS) CloseWithError(err error) error {
   571  	i.mu.Lock()
   572  	defer i.mu.Unlock()
   573  	if i.err != nil {
   574  		// already closed
   575  		return nil
   576  	}
   577  	if err != nil {
   578  		i.err = err
   579  	} else {
   580  		i.err = io.EOF
   581  	}
   582  	close(i.doneC)
   583  	return nil
   584  }
   585  
   586  func (i *upstreamICS) Error() error {
   587  	i.mu.Lock()
   588  	defer i.mu.Unlock()
   589  	return i.err
   590  }