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  }