github.com/pachyderm/pachyderm@v1.13.4/src/client/client.go (about) 1 package client 2 3 import ( 4 "crypto/x509" 5 "encoding/base64" 6 "fmt" 7 "io/ioutil" 8 "os" 9 "path" 10 "path/filepath" 11 "strconv" 12 "strings" 13 "time" 14 15 "golang.org/x/net/context" 16 17 "google.golang.org/grpc" 18 "google.golang.org/grpc/credentials" 19 20 // Import registers the grpc GZIP encoder 21 _ "google.golang.org/grpc/encoding/gzip" 22 "google.golang.org/grpc/keepalive" 23 "google.golang.org/grpc/metadata" 24 25 types "github.com/gogo/protobuf/types" 26 log "github.com/sirupsen/logrus" 27 28 "github.com/pachyderm/pachyderm/src/client/admin" 29 "github.com/pachyderm/pachyderm/src/client/auth" 30 "github.com/pachyderm/pachyderm/src/client/debug" 31 "github.com/pachyderm/pachyderm/src/client/enterprise" 32 "github.com/pachyderm/pachyderm/src/client/health" 33 "github.com/pachyderm/pachyderm/src/client/limit" 34 "github.com/pachyderm/pachyderm/src/client/pfs" 35 "github.com/pachyderm/pachyderm/src/client/pkg/config" 36 "github.com/pachyderm/pachyderm/src/client/pkg/errors" 37 "github.com/pachyderm/pachyderm/src/client/pkg/grpcutil" 38 "github.com/pachyderm/pachyderm/src/client/pkg/tls" 39 "github.com/pachyderm/pachyderm/src/client/pkg/tracing" 40 "github.com/pachyderm/pachyderm/src/client/pps" 41 "github.com/pachyderm/pachyderm/src/client/transaction" 42 "github.com/pachyderm/pachyderm/src/client/version/versionpb" 43 ) 44 45 const ( 46 // MaxListItemsLog specifies the maximum number of items we log in response to a List* API 47 MaxListItemsLog = 10 48 // StorageSecretName is the name of the Kubernetes secret in which 49 // storage credentials are stored. 50 // TODO: The value "pachyderm-storage-secret" is hardcoded in the obj package to avoid a 51 // obj -> client dependency, so any changes to this variable need to be applied there. 52 // The obj package should eventually get refactored so that it does not have this dependency. 53 StorageSecretName = "pachyderm-storage-secret" 54 // PachctlSecretName is the name of the Kubernetes secret in which 55 // pachctl credentials are stored. 56 PachctlSecretName = "pachyderm-pachctl-secret" 57 ) 58 59 // PfsAPIClient is an alias for pfs.APIClient. 60 type PfsAPIClient pfs.APIClient 61 62 // PpsAPIClient is an alias for pps.APIClient. 63 type PpsAPIClient pps.APIClient 64 65 // ObjectAPIClient is an alias for pfs.ObjectAPIClient 66 type ObjectAPIClient pfs.ObjectAPIClient 67 68 // AuthAPIClient is an alias of auth.APIClient 69 type AuthAPIClient auth.APIClient 70 71 // VersionAPIClient is an alias of versionpb.APIClient 72 type VersionAPIClient versionpb.APIClient 73 74 // AdminAPIClient is an alias of admin.APIClient 75 type AdminAPIClient admin.APIClient 76 77 // TransactionAPIClient is an alias of transaction.APIClient 78 type TransactionAPIClient transaction.APIClient 79 80 // DebugClient is an alias of debug.DebugClient 81 type DebugClient debug.DebugClient 82 83 // An APIClient is a wrapper around pfs, pps and block APIClients. 84 type APIClient struct { 85 PfsAPIClient 86 PpsAPIClient 87 ObjectAPIClient 88 AuthAPIClient 89 VersionAPIClient 90 AdminAPIClient 91 TransactionAPIClient 92 DebugClient 93 Enterprise enterprise.APIClient // not embedded--method name conflicts with AuthAPIClient 94 95 // addr is a "host:port" string pointing at a pachd endpoint 96 addr string 97 98 // The trusted CAs, for authenticating a pachd server over TLS 99 caCerts *x509.CertPool 100 101 // gzipCompress configures whether to enable compression by default for all calls 102 gzipCompress bool 103 104 // clientConn is a cached grpc connection to 'addr' 105 clientConn *grpc.ClientConn 106 107 // healthClient is a cached healthcheck client connected to 'addr' 108 healthClient health.HealthClient 109 110 // streamSemaphore limits the number of concurrent message streams between 111 // this client and pachd 112 limiter limit.ConcurrencyLimiter 113 114 // metricsUserID is an identifier that is included in usage metrics sent to 115 // Pachyderm Inc. and is used to count the number of unique Pachyderm users. 116 // If unset, no usage metrics are sent back to Pachyderm Inc. 117 metricsUserID string 118 119 // metricsPrefix is used to send information from this client to Pachyderm Inc 120 // for usage metrics 121 metricsPrefix string 122 123 // authenticationToken is an identifier that authenticates the caller in case 124 // they want to access privileged data 125 authenticationToken string 126 127 // The context used in requests, can be set with WithCtx 128 ctx context.Context 129 130 portForwarder *PortForwarder 131 132 storageV2 bool 133 } 134 135 // GetAddress returns the pachd host:port with which 'c' is communicating. If 136 // 'c' was created using NewInCluster or NewOnUserMachine then this is how the 137 // address may be retrieved from the environment. 138 func (c *APIClient) GetAddress() string { 139 return c.addr 140 } 141 142 // DefaultMaxConcurrentStreams defines the max number of Putfiles or Getfiles happening simultaneously 143 const DefaultMaxConcurrentStreams = 100 144 145 // DefaultDialTimeout is the max amount of time APIClient.connect() will wait 146 // for a connection to be established unless overridden by WithDialTimeout() 147 const DefaultDialTimeout = 30 * time.Second 148 149 type clientSettings struct { 150 maxConcurrentStreams int 151 gzipCompress bool 152 dialTimeout time.Duration 153 caCerts *x509.CertPool 154 storageV2 bool 155 unaryInterceptors []grpc.UnaryClientInterceptor 156 streamInterceptors []grpc.StreamClientInterceptor 157 } 158 159 // NewFromAddress constructs a new APIClient for the server at addr. 160 func NewFromAddress(addr string, options ...Option) (*APIClient, error) { 161 // Validate address 162 if strings.Contains(addr, "://") { 163 return nil, errors.Errorf("address shouldn't contain protocol (\"://\"), but is: %q", addr) 164 } 165 // Apply creation options 166 settings := clientSettings{ 167 maxConcurrentStreams: DefaultMaxConcurrentStreams, 168 dialTimeout: DefaultDialTimeout, 169 } 170 storageV2Env, ok := os.LookupEnv("STORAGE_V2") 171 if ok { 172 storageV2, err := strconv.ParseBool(storageV2Env) 173 if err != nil { 174 return nil, err 175 } 176 if storageV2 { 177 settings.storageV2 = storageV2 178 } 179 } 180 for _, option := range options { 181 if err := option(&settings); err != nil { 182 return nil, err 183 } 184 } 185 if tracing.IsActive() { 186 settings.unaryInterceptors = append(settings.unaryInterceptors, tracing.UnaryClientInterceptor()) 187 settings.streamInterceptors = append(settings.streamInterceptors, tracing.StreamClientInterceptor()) 188 } 189 c := &APIClient{ 190 addr: addr, 191 caCerts: settings.caCerts, 192 limiter: limit.New(settings.maxConcurrentStreams), 193 gzipCompress: settings.gzipCompress, 194 storageV2: settings.storageV2, 195 } 196 if err := c.connect(settings.dialTimeout, settings.unaryInterceptors, settings.streamInterceptors); err != nil { 197 return nil, err 198 } 199 return c, nil 200 } 201 202 // Option is a client creation option that may be passed to NewOnUserMachine(), or NewInCluster() 203 type Option func(*clientSettings) error 204 205 // WithMaxConcurrentStreams instructs the New* functions to create client that 206 // can have at most 'streams' concurrent streams open with pachd at a time 207 func WithMaxConcurrentStreams(streams int) Option { 208 return func(settings *clientSettings) error { 209 settings.maxConcurrentStreams = streams 210 return nil 211 } 212 } 213 214 func addCertFromFile(pool *x509.CertPool, path string) error { 215 bytes, err := ioutil.ReadFile(path) 216 if err != nil { 217 return errors.Wrapf(err, "could not read x509 cert from \"%s\"", path) 218 } 219 if ok := pool.AppendCertsFromPEM(bytes); !ok { 220 return errors.Errorf("could not add %s to cert pool as PEM", path) 221 } 222 return nil 223 } 224 225 // WithRootCAs instructs the New* functions to create client that uses the 226 // given signed x509 certificates as the trusted root certificates (instead of 227 // the system certs). Introduced to pass certs provided via command-line flags 228 func WithRootCAs(path string) Option { 229 return func(settings *clientSettings) error { 230 settings.caCerts = x509.NewCertPool() 231 return addCertFromFile(settings.caCerts, path) 232 } 233 } 234 235 // WithAdditionalRootCAs instructs the New* functions to additionally trust the 236 // given base64-encoded, signed x509 certificates as root certificates. 237 // Introduced to pass certs in the Pachyderm config 238 func WithAdditionalRootCAs(pemBytes []byte) Option { 239 return func(settings *clientSettings) error { 240 // append certs from config 241 if settings.caCerts == nil { 242 settings.caCerts = x509.NewCertPool() 243 } 244 if ok := settings.caCerts.AppendCertsFromPEM(pemBytes); !ok { 245 return errors.Errorf("server CA certs are present in Pachyderm config, but could not be added to client") 246 } 247 return nil 248 } 249 } 250 251 // WithSystemCAs uses the system certs for client creatin. 252 func WithSystemCAs(settings *clientSettings) error { 253 certs, err := x509.SystemCertPool() 254 if err != nil { 255 return errors.Wrap(err, "could not retrieve system cert pool") 256 } 257 settings.caCerts = certs 258 return nil 259 } 260 261 // WithDialTimeout instructs the New* functions to use 't' as the deadline to 262 // connect to pachd 263 func WithDialTimeout(t time.Duration) Option { 264 return func(settings *clientSettings) error { 265 settings.dialTimeout = t 266 return nil 267 } 268 } 269 270 // WithGZIPCompression enabled GZIP compression for data on the wire 271 func WithGZIPCompression() Option { 272 return func(settings *clientSettings) error { 273 settings.gzipCompress = true 274 return nil 275 } 276 } 277 278 // WithAdditionalPachdCert instructs the New* functions to additionally trust 279 // the signed cert mounted in Pachd's cert volume. This is used by Pachd 280 // when connecting to itself (if no cert is present, the clients cert pool 281 // will not be modified, so that if no other options have been passed, pachd 282 // will connect to itself over an insecure connection) 283 func WithAdditionalPachdCert() Option { 284 return func(settings *clientSettings) error { 285 if _, err := os.Stat(tls.VolumePath); err == nil { 286 if settings.caCerts == nil { 287 settings.caCerts = x509.NewCertPool() 288 } 289 return addCertFromFile(settings.caCerts, path.Join(tls.VolumePath, tls.CertFile)) 290 } 291 return nil 292 } 293 } 294 295 // WithAdditionalUnaryClientInterceptors instructs the New* functions to add the provided 296 // UnaryClientInterceptors to the gRPC dial options when opening a client connection. Internally, 297 // all of the provided options are coalesced into one chain, so it is safe to provide this option 298 // more than once. 299 // 300 // This client creates both Unary and Stream client connections, so you will probably want to supply 301 // a corresponding WithAdditionalStreamClientInterceptors call. 302 func WithAdditionalUnaryClientInterceptors(interceptors ...grpc.UnaryClientInterceptor) Option { 303 return func(settings *clientSettings) error { 304 settings.unaryInterceptors = append(settings.unaryInterceptors, interceptors...) 305 return nil 306 } 307 } 308 309 // WithAdditionalStreamClientInterceptors instructs the New* functions to add the provided 310 // StreamClientInterceptors to the gRPC dial options when opening a client connection. Internally, 311 // all of the provided options are coalesced into one chain, so it is safe to provide this option 312 // more than once. 313 // 314 // This client creates both Unary and Stream client connections, so you will probably want to supply 315 // a corresponding WithAdditionalUnaryClientInterceptors option. 316 func WithAdditionalStreamClientInterceptors(interceptors ...grpc.StreamClientInterceptor) Option { 317 return func(settings *clientSettings) error { 318 settings.streamInterceptors = append(settings.streamInterceptors, interceptors...) 319 return nil 320 } 321 } 322 323 func getCertOptionsFromEnv() ([]Option, error) { 324 var options []Option 325 if certPaths, ok := os.LookupEnv("PACH_CA_CERTS"); ok { 326 fmt.Fprintln(os.Stderr, "WARNING: 'PACH_CA_CERTS' is deprecated and will be removed in a future release, use Pachyderm contexts instead.") 327 328 pachdAddressStr, ok := os.LookupEnv("PACHD_ADDRESS") 329 if !ok { 330 return nil, errors.New("cannot set 'PACH_CA_CERTS' without setting 'PACHD_ADDRESS'") 331 } 332 333 pachdAddress, err := grpcutil.ParsePachdAddress(pachdAddressStr) 334 if err != nil { 335 return nil, errors.Wrapf(err, "could not parse 'PACHD_ADDRESS'") 336 } 337 338 if !pachdAddress.Secured { 339 return nil, errors.Errorf("cannot set 'PACH_CA_CERTS' if 'PACHD_ADDRESS' is not using grpcs") 340 } 341 342 paths := strings.Split(certPaths, ",") 343 for _, p := range paths { 344 // Try to read all certs under 'p'--skip any that we can't read/stat 345 if err := filepath.Walk(p, func(p string, info os.FileInfo, err error) error { 346 if err != nil { 347 log.Warnf("skipping \"%s\", could not stat path: %v", p, err) 348 return nil // Don't try and fix any errors encountered by Walk() itself 349 } 350 if info.IsDir() { 351 return nil // We'll just read the children of any directories when we traverse them 352 } 353 pemBytes, err := ioutil.ReadFile(p) 354 if err != nil { 355 log.Warnf("could not read server CA certs at %s: %v", p, err) 356 return nil 357 } 358 options = append(options, WithAdditionalRootCAs(pemBytes)) 359 return nil 360 }); err != nil { 361 return nil, err 362 } 363 } 364 } 365 return options, nil 366 } 367 368 // getUserMachineAddrAndOpts is a helper for NewOnUserMachine that uses 369 // environment variables, config files, etc to figure out which address a user 370 // running a command should connect to. 371 func getUserMachineAddrAndOpts(context *config.Context) (*grpcutil.PachdAddress, []Option, error) { 372 var options []Option 373 374 // 1) PACHD_ADDRESS environment variable (shell-local) overrides global config 375 if envAddrStr, ok := os.LookupEnv("PACHD_ADDRESS"); ok { 376 fmt.Fprintln(os.Stderr, "WARNING: 'PACHD_ADDRESS' is deprecated and will be removed in a future release, use Pachyderm contexts instead.") 377 378 envAddr, err := grpcutil.ParsePachdAddress(envAddrStr) 379 if err != nil { 380 return nil, nil, errors.Wrapf(err, "could not parse 'PACHD_ADDRESS'") 381 } 382 options, err := getCertOptionsFromEnv() 383 if err != nil { 384 return nil, nil, err 385 } 386 387 return envAddr, options, nil 388 } 389 390 // 2) Get target address from global config if possible 391 if context != nil && (context.ServerCAs != "" || context.PachdAddress != "") { 392 pachdAddress, err := grpcutil.ParsePachdAddress(context.PachdAddress) 393 if err != nil { 394 return nil, nil, errors.Wrap(err, "could not parse the active context's pachd address") 395 } 396 397 // Proactively return an error in this case, instead of falling back to the default address below 398 if context.ServerCAs != "" && !pachdAddress.Secured { 399 return nil, nil, errors.New("must set pachd_address to grpcs://... if server_cas is set") 400 } 401 402 if pachdAddress.Secured { 403 options = append(options, WithSystemCAs) 404 } 405 // Also get cert info from config (if set) 406 if context.ServerCAs != "" { 407 pemBytes, err := base64.StdEncoding.DecodeString(context.ServerCAs) 408 if err != nil { 409 return nil, nil, errors.Wrap(err, "could not decode server CA certs in config") 410 } 411 return pachdAddress, []Option{WithAdditionalRootCAs(pemBytes)}, nil 412 } 413 return pachdAddress, options, nil 414 } 415 416 // 3) Use default address (broadcast) if nothing else works 417 options, err := getCertOptionsFromEnv() // error if PACH_CA_CERTS is set 418 if err != nil { 419 return nil, nil, err 420 } 421 return nil, options, nil 422 } 423 424 func portForwarder(context *config.Context) (*PortForwarder, uint16, error) { 425 fw, err := NewPortForwarder(context, "") 426 if err != nil { 427 return nil, 0, errors.Wrap(err, "failed to initialize port forwarder") 428 } 429 430 port, err := fw.RunForDaemon(0, 650) 431 if err != nil { 432 return nil, 0, err 433 } 434 435 log.Debugf("Implicit port forwarder listening on port %d", port) 436 437 return fw, port, nil 438 } 439 440 // NewForTest constructs a new APIClient for tests. 441 func NewForTest() (*APIClient, error) { 442 cfg, err := config.Read(false, false) 443 if err != nil { 444 return nil, errors.Wrap(err, "could not read config") 445 } 446 _, context, err := cfg.ActiveContext(true) 447 if err != nil { 448 return nil, errors.Wrap(err, "could not get active context") 449 } 450 451 // create new pachctl client 452 pachdAddress, cfgOptions, err := getUserMachineAddrAndOpts(context) 453 if err != nil { 454 return nil, err 455 } 456 457 if pachdAddress == nil { 458 pachdAddress = &grpcutil.DefaultPachdAddress 459 } 460 461 client, err := NewFromAddress(pachdAddress.Hostname(), cfgOptions...) 462 if err != nil { 463 return nil, errors.Wrapf(err, "could not connect to pachd at %s", pachdAddress.Qualified()) 464 } 465 return client, nil 466 } 467 468 // NewOnUserMachine constructs a new APIClient using $HOME/.pachyderm/config 469 // if it exists. This is intended to be used in the pachctl binary. 470 // 471 // TODO(msteffen) this logic is fairly linux/unix specific, and makes the 472 // pachyderm client library incompatible with Windows. We may want to move this 473 // (and similar) logic into src/server and have it call a NewFromOptions() 474 // constructor. 475 func NewOnUserMachine(prefix string, options ...Option) (*APIClient, error) { 476 cfg, err := config.Read(false, false) 477 if err != nil { 478 return nil, errors.Wrap(err, "could not read config") 479 } 480 _, context, err := cfg.ActiveContext(true) 481 if err != nil { 482 return nil, errors.Wrap(err, "could not get active context") 483 } 484 485 // create new pachctl client 486 pachdAddress, cfgOptions, err := getUserMachineAddrAndOpts(context) 487 if err != nil { 488 return nil, err 489 } 490 491 var fw *PortForwarder 492 if pachdAddress == nil && context.PortForwarders != nil { 493 pachdLocalPort, ok := context.PortForwarders["pachd"] 494 if ok { 495 log.Debugf("Connecting to explicitly port forwarded pachd instance on port %d", pachdLocalPort) 496 pachdAddress = &grpcutil.PachdAddress{ 497 Secured: false, 498 Host: "localhost", 499 Port: uint16(pachdLocalPort), 500 } 501 } 502 } 503 if pachdAddress == nil { 504 var pachdLocalPort uint16 505 fw, pachdLocalPort, err = portForwarder(context) 506 if err != nil { 507 return nil, err 508 } 509 pachdAddress = &grpcutil.PachdAddress{ 510 Secured: false, 511 Host: "localhost", 512 Port: pachdLocalPort, 513 } 514 } 515 516 client, err := NewFromAddress(pachdAddress.Hostname(), append(options, cfgOptions...)...) 517 if err != nil { 518 return nil, errors.Wrapf(err, "could not connect to pachd at %q", pachdAddress.Qualified()) 519 } 520 521 // Add metrics info & authentication token 522 client.metricsPrefix = prefix 523 if cfg.UserID != "" && cfg.V2.Metrics { 524 client.metricsUserID = cfg.UserID 525 } 526 if context.SessionToken != "" { 527 client.authenticationToken = context.SessionToken 528 } 529 530 // Verify cluster deployment ID 531 clusterInfo, err := client.InspectCluster() 532 if err != nil { 533 return nil, errors.Wrap(err, "could not get cluster ID") 534 } 535 if context.ClusterDeploymentID != clusterInfo.DeploymentID { 536 if context.ClusterDeploymentID == "" { 537 context.ClusterDeploymentID = clusterInfo.DeploymentID 538 if err = cfg.Write(); err != nil { 539 return nil, errors.Wrap(err, "could not write config to save cluster deployment ID") 540 } 541 } else { 542 return nil, errors.Errorf("connected to the wrong cluster (context cluster deployment ID = %q vs reported cluster deployment ID = %q)", context.ClusterDeploymentID, clusterInfo.DeploymentID) 543 } 544 } 545 546 // Add port forwarding. This will set it to nil if port forwarding is 547 // disabled, or an address is explicitly set. 548 client.portForwarder = fw 549 550 return client, nil 551 } 552 553 // NewInCluster constructs a new APIClient using env vars that Kubernetes creates. 554 // This should be used to access Pachyderm from within a Kubernetes cluster 555 // with Pachyderm running on it. 556 func NewInCluster(options ...Option) (*APIClient, error) { 557 // first try the pachd peer service (only supported on pachyderm >= 1.10), 558 // which will work when TLS is enabled 559 internalHost := os.Getenv("PACHD_PEER_SERVICE_HOST") 560 internalPort := os.Getenv("PACHD_PEER_SERVICE_PORT") 561 if internalHost != "" && internalPort != "" { 562 return NewFromAddress(fmt.Sprintf("%s:%s", internalHost, internalPort), options...) 563 } 564 565 host, ok := os.LookupEnv("PACHD_SERVICE_HOST") 566 if !ok { 567 return nil, errors.Errorf("PACHD_SERVICE_HOST not set") 568 } 569 port, ok := os.LookupEnv("PACHD_SERVICE_PORT") 570 if !ok { 571 return nil, errors.Errorf("PACHD_SERVICE_PORT not set") 572 } 573 // create new pachctl client 574 return NewFromAddress(fmt.Sprintf("%s:%s", host, port), options...) 575 } 576 577 // NewInWorker constructs a new APIClient intended to be used from a worker 578 // to talk to the sidecar pachd container 579 func NewInWorker(options ...Option) (*APIClient, error) { 580 cfg, err := config.Read(false, true) 581 if err != nil { 582 return nil, errors.Wrap(err, "could not read config") 583 } 584 _, context, err := cfg.ActiveContext(true) 585 if err != nil { 586 return nil, errors.Wrap(err, "could not get active context") 587 } 588 589 if localPort, ok := os.LookupEnv("PEER_PORT"); ok { 590 client, err := NewFromAddress(fmt.Sprintf("127.0.0.1:%s", localPort), options...) 591 if err != nil { 592 return nil, errors.Wrap(err, "could not create client") 593 } 594 if context.SessionToken != "" { 595 client.authenticationToken = context.SessionToken 596 } 597 return client, nil 598 } 599 return nil, errors.New("PEER_PORT not set") 600 } 601 602 // Close the connection to gRPC 603 func (c *APIClient) Close() error { 604 if err := c.clientConn.Close(); err != nil { 605 return err 606 } 607 608 if c.portForwarder != nil { 609 c.portForwarder.Close() 610 } 611 612 return nil 613 } 614 615 // DeleteAll deletes everything in the cluster. 616 // Use with caution, there is no undo. 617 // TODO: rewrite this to use transactions 618 func (c APIClient) DeleteAll() error { 619 if _, err := c.AuthAPIClient.Deactivate( 620 c.Ctx(), 621 &auth.DeactivateRequest{}, 622 ); err != nil && !auth.IsErrNotActivated(err) { 623 return grpcutil.ScrubGRPC(err) 624 } 625 if _, err := c.PpsAPIClient.DeleteAll( 626 c.Ctx(), 627 &types.Empty{}, 628 ); err != nil { 629 return grpcutil.ScrubGRPC(err) 630 } 631 if _, err := c.PfsAPIClient.DeleteAll( 632 c.Ctx(), 633 &types.Empty{}, 634 ); err != nil { 635 return grpcutil.ScrubGRPC(err) 636 } 637 if _, err := c.TransactionAPIClient.DeleteAll( 638 c.Ctx(), 639 &transaction.DeleteAllRequest{}, 640 ); err != nil { 641 return grpcutil.ScrubGRPC(err) 642 } 643 return nil 644 } 645 646 // SetMaxConcurrentStreams Sets the maximum number of concurrent streams the 647 // client can have. It is not safe to call this operations while operations are 648 // outstanding. 649 func (c *APIClient) SetMaxConcurrentStreams(n int) { 650 c.limiter = limit.New(n) 651 } 652 653 // DefaultDialOptions is a helper returning a slice of grpc.Dial options 654 // such that grpc.Dial() is synchronous: the call doesn't return until 655 // the connection has been established and it's safe to send RPCs 656 func DefaultDialOptions() []grpc.DialOption { 657 return []grpc.DialOption{ 658 // Don't return from Dial() until the connection has been established. 659 grpc.WithBlock(), 660 grpc.WithKeepaliveParams(keepalive.ClientParameters{ 661 Time: 20 * time.Second, 662 Timeout: 20 * time.Second, 663 PermitWithoutStream: true, 664 }), 665 grpc.WithDefaultCallOptions( 666 grpc.MaxCallRecvMsgSize(grpcutil.MaxMsgSize), 667 grpc.MaxCallSendMsgSize(grpcutil.MaxMsgSize), 668 ), 669 } 670 } 671 672 func (c *APIClient) connect(timeout time.Duration, unaryInterceptors []grpc.UnaryClientInterceptor, streamInterceptors []grpc.StreamClientInterceptor) error { 673 dialOptions := DefaultDialOptions() 674 if c.caCerts == nil { 675 dialOptions = append(dialOptions, grpc.WithInsecure()) 676 } else { 677 tlsCreds := credentials.NewClientTLSFromCert(c.caCerts, "") 678 dialOptions = append(dialOptions, grpc.WithTransportCredentials(tlsCreds)) 679 } 680 if c.gzipCompress { 681 dialOptions = append(dialOptions, grpc.WithDefaultCallOptions(grpc.UseCompressor("gzip"))) 682 } 683 if len(unaryInterceptors) > 0 { 684 dialOptions = append(dialOptions, grpc.WithChainUnaryInterceptor(unaryInterceptors...)) 685 } 686 if len(streamInterceptors) > 0 { 687 dialOptions = append(dialOptions, grpc.WithChainStreamInterceptor(streamInterceptors...)) 688 } 689 ctx, cancel := context.WithTimeout(context.Background(), timeout) 690 defer cancel() 691 addr := c.addr 692 if !strings.HasPrefix(addr, "dns:///") { 693 addr = "dns:///" + c.addr 694 } 695 696 // TODO: the 'dns:///' prefix above causes connecting to hang on windows 697 // unless we also prevent the resolver from fetching a service config (which 698 // we don't use anyway). Don't ask me why. 699 dialOptions = append(dialOptions, grpc.WithDisableServiceConfig()) 700 701 clientConn, err := grpc.DialContext(ctx, addr, dialOptions...) 702 if err != nil { 703 return err 704 } 705 c.PfsAPIClient = pfs.NewAPIClient(clientConn) 706 c.PpsAPIClient = pps.NewAPIClient(clientConn) 707 c.ObjectAPIClient = pfs.NewObjectAPIClient(clientConn) 708 c.AuthAPIClient = auth.NewAPIClient(clientConn) 709 c.Enterprise = enterprise.NewAPIClient(clientConn) 710 c.VersionAPIClient = versionpb.NewAPIClient(clientConn) 711 c.AdminAPIClient = admin.NewAPIClient(clientConn) 712 c.TransactionAPIClient = transaction.NewAPIClient(clientConn) 713 c.DebugClient = debug.NewDebugClient(clientConn) 714 c.clientConn = clientConn 715 c.healthClient = health.NewHealthClient(clientConn) 716 return nil 717 } 718 719 // AddMetadata adds necessary metadata (including authentication credentials) 720 // to the context 'ctx', preserving any metadata that is present in either the 721 // incoming or outgoing metadata of 'ctx'. 722 func (c *APIClient) AddMetadata(ctx context.Context) context.Context { 723 // TODO(msteffen): There are several places in this client where it's possible 724 // to set per-request metadata (specifically auth tokens): client.WithCtx(), 725 // client.SetAuthToken(), etc. These should be consolidated, as this API 726 // doesn't make it obvious how these settings are resolved when they conflict. 727 clientData := make(map[string]string) 728 if c.authenticationToken != "" { 729 clientData[auth.ContextTokenKey] = c.authenticationToken 730 } 731 // metadata API downcases all the key names 732 if c.metricsUserID != "" { 733 clientData["userid"] = c.metricsUserID 734 clientData["prefix"] = c.metricsPrefix 735 } 736 737 // Rescue any metadata pairs already in 'ctx' (otherwise 738 // metadata.NewOutgoingContext() would drop them). Note that this is similar 739 // to metadata.Join(), but distinct because it discards conflicting k/v pairs 740 // instead of merging them) 741 incomingMD, _ := metadata.FromIncomingContext(ctx) 742 outgoingMD, _ := metadata.FromOutgoingContext(ctx) 743 clientMD := metadata.New(clientData) 744 finalMD := make(metadata.MD) // Collect k/v pairs 745 for _, md := range []metadata.MD{incomingMD, outgoingMD, clientMD} { 746 for k, v := range md { 747 finalMD[k] = v 748 } 749 } 750 return metadata.NewOutgoingContext(ctx, finalMD) 751 } 752 753 // Ctx is a convenience function that returns adds Pachyderm authn metadata 754 // to context.Background(). 755 func (c *APIClient) Ctx() context.Context { 756 if c.ctx == nil { 757 return c.AddMetadata(context.Background()) 758 } 759 return c.AddMetadata(c.ctx) 760 } 761 762 // WithCtx returns a new APIClient that uses ctx for requests it sends. Note 763 // that the new APIClient will still use the authentication token and metrics 764 // metadata of this client, so this is only useful for propagating other 765 // context-associated metadata. 766 func (c *APIClient) WithCtx(ctx context.Context) *APIClient { 767 result := *c // copy c 768 result.ctx = ctx 769 return &result 770 } 771 772 // SetAuthToken sets the authentication token that will be used for all 773 // API calls for this client. 774 func (c *APIClient) SetAuthToken(token string) { 775 c.authenticationToken = token 776 }