go.ligato.io/vpp-agent/v3@v3.5.0/cmd/agentctl/client/client.go (about)

     1  //  Copyright (c) 2019 Cisco and/or its affiliates.
     2  //
     3  //  Licensed under the Apache License, Version 2.0 (the "License");
     4  //  you may not use this file except in compliance with the License.
     5  //  You may obtain a copy of the License at:
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  //  Unless required by applicable law or agreed to in writing, software
    10  //  distributed under the License is distributed on an "AS IS" BASIS,
    11  //  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  //  See the License for the specific language governing permissions and
    13  //  limitations under the License.
    14  
    15  package client
    16  
    17  import (
    18  	"context"
    19  	"crypto/tls"
    20  	"fmt"
    21  	"net"
    22  	"net/http"
    23  	"net/url"
    24  	"path"
    25  	"strconv"
    26  	"strings"
    27  	"time"
    28  
    29  	"github.com/docker/docker/api/types/versions"
    30  	clientv3 "go.etcd.io/etcd/client/v3"
    31  	"go.fd.io/govpp/proxy"
    32  	"go.ligato.io/cn-infra/v2/db/keyval"
    33  	"go.ligato.io/cn-infra/v2/db/keyval/etcd"
    34  	"go.ligato.io/cn-infra/v2/logging"
    35  	"go.ligato.io/cn-infra/v2/logging/logrus"
    36  	"google.golang.org/grpc"
    37  	"google.golang.org/grpc/credentials"
    38  	"google.golang.org/grpc/credentials/insecure"
    39  
    40  	"go.ligato.io/vpp-agent/v3/client"
    41  	"go.ligato.io/vpp-agent/v3/client/remoteclient"
    42  	"go.ligato.io/vpp-agent/v3/cmd/agentctl/api"
    43  	"go.ligato.io/vpp-agent/v3/cmd/agentctl/api/types"
    44  	"go.ligato.io/vpp-agent/v3/pkg/debug"
    45  	"go.ligato.io/vpp-agent/v3/proto/ligato/configurator"
    46  	"go.ligato.io/vpp-agent/v3/proto/ligato/generic"
    47  )
    48  
    49  const (
    50  	// DefaultAgentHost defines default host address for agent.
    51  	DefaultAgentHost = "127.0.0.1"
    52  	// DefaultPortGRPC defines default port for GRPC connection.
    53  	DefaultPortGRPC = 9111
    54  	// DefaultPortHTTP defines default port for HTTP connection.
    55  	DefaultPortHTTP = 9191
    56  )
    57  
    58  // Constants for etcd connection.
    59  const (
    60  	// DefaultTimeout defines default timeout for client HTTP requests.
    61  	DefaultTimeout = time.Second * 120
    62  	// DefaultEtcdDialTimeout defines default timeout for dialing Etcd.
    63  	DefaultEtcdDialTimeout = time.Second * 3
    64  	// DefaultEtcdOpTimeout defines default timeout for a pending Etcd operation.
    65  	DefaultEtcdOpTimeout = time.Second * 10
    66  )
    67  
    68  var _ APIClient = (*Client)(nil)
    69  
    70  // Client is the API client that performs all operations
    71  // against a Ligato agent.
    72  type Client struct {
    73  	scheme   string
    74  	host     string
    75  	proto    string
    76  	addr     string
    77  	basePath string
    78  
    79  	grpcPort        int
    80  	grpcAddr        string
    81  	grpcTLS         *tls.Config
    82  	httpPort        int
    83  	httpAddr        string
    84  	httpTLS         *tls.Config
    85  	kvdbEndpoints   []string
    86  	kvdbDialTimeout time.Duration
    87  	kvdbTLS         *tls.Config
    88  	serviceLabel    string
    89  
    90  	grpcClient       *grpc.ClientConn
    91  	httpClient       *http.Client
    92  	govppProxyClient *proxy.Client
    93  
    94  	customHTTPHeaders map[string]string
    95  	version           string
    96  	manualOverride    bool
    97  	negotiateVersion  bool
    98  	negotiated        bool
    99  }
   100  
   101  // NewClient returns client with host option.
   102  func NewClient(host string) (*Client, error) {
   103  	return NewClientWithOpts(WithHost(host))
   104  }
   105  
   106  // NewClientWithOpts returns client with ops applied.
   107  func NewClientWithOpts(ops ...Opt) (*Client, error) {
   108  	c := &Client{
   109  		host:     DefaultAgentHost,
   110  		version:  api.DefaultVersion,
   111  		proto:    "tcp",
   112  		scheme:   "http",
   113  		grpcPort: DefaultPortGRPC,
   114  		httpPort: DefaultPortHTTP,
   115  	}
   116  	for _, op := range ops {
   117  		if err := op(c); err != nil {
   118  			return nil, err
   119  		}
   120  	}
   121  	c.grpcAddr = net.JoinHostPort(c.host, strconv.Itoa(c.grpcPort))
   122  	c.httpAddr = net.JoinHostPort(c.host, strconv.Itoa(c.httpPort))
   123  	return c, nil
   124  }
   125  
   126  func (c *Client) AgentHost() string {
   127  	return c.host
   128  }
   129  
   130  func (c *Client) Version() string {
   131  	return c.version
   132  }
   133  
   134  // Close the transport used by the client
   135  func (c *Client) Close() error {
   136  	if c.httpClient != nil {
   137  		if t, ok := c.httpClient.Transport.(*http.Transport); ok {
   138  			t.CloseIdleConnections()
   139  		}
   140  	}
   141  	if c.grpcClient != nil {
   142  		if err := c.grpcClient.Close(); err != nil {
   143  			return err
   144  		}
   145  	}
   146  	return nil
   147  }
   148  
   149  // GRPCConn returns configured gRPC client.
   150  func (c *Client) GRPCConn() (*grpc.ClientConn, error) {
   151  	if c.grpcClient == nil {
   152  		conn, err := connectGrpc(c.grpcAddr, c.grpcTLS)
   153  		if err != nil {
   154  			return nil, err
   155  		}
   156  		c.grpcClient = conn
   157  	}
   158  	return c.grpcClient, nil
   159  }
   160  
   161  func (c *Client) GenericClient() (client.GenericClient, error) {
   162  	conn, err := c.GRPCConn()
   163  	if err != nil {
   164  		return nil, err
   165  	}
   166  	return remoteclient.NewClientGRPC(conn, remoteclient.UseRemoteRegistry("config"))
   167  }
   168  
   169  // ConfiguratorClient returns "config" with gRPC connection.
   170  func (c *Client) ConfiguratorClient() (configurator.ConfiguratorServiceClient, error) {
   171  	conn, err := c.GRPCConn()
   172  	if err != nil {
   173  		return nil, err
   174  	}
   175  	return configurator.NewConfiguratorServiceClient(conn), nil
   176  }
   177  
   178  // MetaServiceClient creates new client for using meta service
   179  func (c *Client) MetaServiceClient() (generic.MetaServiceClient, error) {
   180  	conn, err := c.GRPCConn()
   181  	if err != nil {
   182  		return nil, err
   183  	}
   184  	return generic.NewMetaServiceClient(conn), nil
   185  }
   186  
   187  // HTTPClient returns configured HTTP client.
   188  func (c *Client) HTTPClient() *http.Client {
   189  	if c.httpClient == nil {
   190  		tr := http.DefaultTransport.(*http.Transport).Clone()
   191  		tr.TLSClientConfig = c.httpTLS
   192  		c.httpClient = &http.Client{
   193  			Transport: tr,
   194  			Timeout:   DefaultTimeout,
   195  		}
   196  	}
   197  	return c.httpClient
   198  }
   199  
   200  // GoVPPProxyClient returns configured GoVPP proxy client that is already connected to the exposed
   201  // GoVPP proxy from vpp-agent
   202  func (c *Client) GoVPPProxyClient() (*proxy.Client, error) {
   203  	if c.govppProxyClient == nil {
   204  		cc, err := proxy.Connect(c.httpAddr)
   205  		if err != nil {
   206  			return nil, fmt.Errorf("connecting to proxy failed due to: %v", err)
   207  		}
   208  		c.govppProxyClient = cc
   209  	}
   210  	return c.govppProxyClient, nil
   211  }
   212  
   213  // KVDBClient returns configured KVDB client.
   214  func (c *Client) KVDBClient() (KVDBAPIClient, error) {
   215  	kvdb, err := connectEtcd(c.kvdbEndpoints, c.kvdbDialTimeout, c.kvdbTLS)
   216  	if err != nil {
   217  		return nil, fmt.Errorf("connecting to Etcd failed: %v", err)
   218  	}
   219  	return NewKVDBClient(kvdb, c.serviceLabel), nil
   220  }
   221  
   222  // ParseHostURL parses a url string, validates the string is a host url, and
   223  // returns the parsed URL
   224  func ParseHostURL(host string) (*url.URL, error) {
   225  	if !strings.Contains(host, "://") {
   226  		host = "tcp://" + host
   227  	}
   228  	protoAddrParts := strings.SplitN(host, "://", 2)
   229  	if len(protoAddrParts) == 1 {
   230  		return nil, fmt.Errorf("unable to parse agent host `%s`", host)
   231  	}
   232  	var basePath string
   233  	proto, addr := protoAddrParts[0], protoAddrParts[1]
   234  	if proto == "tcp" {
   235  		parsed, err := url.Parse("tcp://" + addr)
   236  		if err != nil {
   237  			return nil, err
   238  		}
   239  		addr = parsed.Host
   240  		basePath = parsed.Path
   241  	}
   242  	return &url.URL{
   243  		Scheme: proto,
   244  		Host:   addr,
   245  		Path:   basePath,
   246  	}, nil
   247  }
   248  
   249  // getAPIPath returns the versioned request path to call the api.
   250  // It appends the query parameters to the path if they are not empty.
   251  func (c *Client) getAPIPath(ctx context.Context, p string, query url.Values) string {
   252  	var apiPath string
   253  	if c.negotiateVersion && !c.negotiated {
   254  		c.NegotiateAPIVersion(ctx)
   255  	}
   256  	if c.version != "" {
   257  		v := strings.TrimPrefix(c.version, "v")
   258  		apiPath = path.Join(c.basePath, "/v"+v, p)
   259  	} else {
   260  		apiPath = path.Join(c.basePath, p)
   261  	}
   262  	return (&url.URL{Path: apiPath, RawQuery: query.Encode()}).String()
   263  }
   264  
   265  func (c *Client) NegotiateAPIVersion(ctx context.Context) {
   266  	if !c.manualOverride {
   267  		version, _ := c.AgentVersion(ctx)
   268  		c.negotiateAPIVersionPing(version)
   269  	}
   270  }
   271  
   272  func (c *Client) NegotiateAPIVersionPing(p *types.Version) {
   273  	if !c.manualOverride {
   274  		c.negotiateAPIVersionPing(p)
   275  	}
   276  }
   277  
   278  // negotiateAPIVersionPing queries the API and updates the version to match the
   279  // API version. Any errors are silently ignored.
   280  func (c *Client) negotiateAPIVersionPing(p *types.Version) {
   281  	// try the latest version before versioning headers existed
   282  	if p.APIVersion == "" {
   283  		p.APIVersion = "0.1"
   284  	}
   285  	// if the client is not initialized with a version, start with the latest supported version
   286  	if c.version == "" {
   287  		c.version = api.DefaultVersion
   288  	}
   289  	// if server version is lower than the client version, downgrade
   290  	if versions.LessThan(p.APIVersion, c.version) {
   291  		c.version = p.APIVersion
   292  	}
   293  	// Store the results, so that automatic API version negotiation (if enabled)
   294  	// won't be performed on the next request.
   295  	if c.negotiateVersion {
   296  		c.negotiated = true
   297  	}
   298  }
   299  
   300  func connectGrpc(addr string, tc *tls.Config) (*grpc.ClientConn, error) {
   301  	dialOpt := grpc.WithTransportCredentials(insecure.NewCredentials())
   302  	if tc != nil {
   303  		dialOpt = grpc.WithTransportCredentials(credentials.NewTLS(tc))
   304  	}
   305  	logging.Debugf("dialing grpc address: %v", addr)
   306  	return grpc.Dial(addr, dialOpt)
   307  }
   308  
   309  func connectEtcd(endpoints []string, dialTimeout time.Duration, tc *tls.Config) (keyval.CoreBrokerWatcher, error) {
   310  	log := logrus.NewLogger("etcd-client")
   311  	if debug.IsEnabledFor("kvdb") {
   312  		log.SetLevel(logging.DebugLevel)
   313  	} else {
   314  		log.SetLevel(logging.WarnLevel)
   315  	}
   316  	dt := DefaultEtcdDialTimeout
   317  	if dialTimeout > 0 {
   318  		dt = dialTimeout
   319  	}
   320  	ctx, cancel := context.WithTimeout(context.Background(), dt)
   321  	defer cancel()
   322  	cfg := etcd.ClientConfig{
   323  		Config: &clientv3.Config{
   324  			Endpoints:   endpoints,
   325  			DialTimeout: dt,
   326  			TLS:         tc,
   327  			Context:     ctx,
   328  		},
   329  		OpTimeout: DefaultEtcdOpTimeout,
   330  	}
   331  	kvdb, err := etcd.NewEtcdConnectionWithBytes(cfg, log)
   332  	if err != nil {
   333  		return nil, err
   334  	}
   335  	return kvdb, nil
   336  }