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 }