github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/lorry/client/httpclient.go (about)

     1  /*
     2  Copyright (C) 2022-2023 ApeCloud Co., Ltd
     3  
     4  This file is part of KubeBlocks project
     5  
     6  This program is free software: you can redistribute it and/or modify
     7  it under the terms of the GNU Affero General Public License as published by
     8  the Free Software Foundation, either version 3 of the License, or
     9  (at your option) any later version.
    10  
    11  This program is distributed in the hope that it will be useful
    12  but WITHOUT ANY WARRANTY; without even the implied warranty of
    13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    14  GNU Affero General Public License for more details.
    15  
    16  You should have received a copy of the GNU Affero General Public License
    17  along with this program.  If not, see <http://www.gnu.org/licenses/>.
    18  */
    19  
    20  package client
    21  
    22  import (
    23  	"bytes"
    24  	"context"
    25  	"encoding/json"
    26  	"fmt"
    27  	"io"
    28  	"net"
    29  	"net/http"
    30  	"sort"
    31  	"strings"
    32  	"time"
    33  
    34  	"github.com/go-logr/logr"
    35  	"github.com/pkg/errors"
    36  	corev1 "k8s.io/api/core/v1"
    37  	ctrl "sigs.k8s.io/controller-runtime"
    38  
    39  	intctrlutil "github.com/1aal/kubeblocks/pkg/controllerutil"
    40  )
    41  
    42  const (
    43  	urlTemplate = "http://%s:%d/v1.0/"
    44  )
    45  
    46  type HTTPClient struct {
    47  	lorryClient
    48  	Client           *http.Client
    49  	URL              string
    50  	cache            map[string]*OperationResult
    51  	CacheTTL         time.Duration
    52  	ReconcileTimeout time.Duration
    53  	RequestTimeout   time.Duration
    54  	logger           logr.Logger
    55  }
    56  
    57  var _ Client = &HTTPClient{}
    58  
    59  type OperationResult struct {
    60  	response *http.Response
    61  	err      error
    62  	respTime time.Time
    63  }
    64  
    65  func NewHTTPClientWithPod(pod *corev1.Pod) (*HTTPClient, error) {
    66  	logger := ctrl.Log.WithName("Lorry HTTP client")
    67  	ip := pod.Status.PodIP
    68  	if ip == "" {
    69  		return nil, fmt.Errorf("pod %v has no ip", pod.Name)
    70  	}
    71  
    72  	port, err := intctrlutil.GetLorryHTTPPort(pod)
    73  	if err != nil {
    74  		logger.Info("not lorry in the pod, just return nil without error")
    75  		return nil, nil
    76  	}
    77  
    78  	// don't use default http-client
    79  	dialer := &net.Dialer{
    80  		Timeout: 5 * time.Second,
    81  	}
    82  	netTransport := &http.Transport{
    83  		Dial:                dialer.Dial,
    84  		TLSHandshakeTimeout: 5 * time.Second,
    85  	}
    86  	client := &http.Client{
    87  		Timeout:   time.Second * 30,
    88  		Transport: netTransport,
    89  	}
    90  
    91  	operationClient := &HTTPClient{
    92  		Client:           client,
    93  		URL:              fmt.Sprintf(urlTemplate, ip, port),
    94  		CacheTTL:         60 * time.Second,
    95  		RequestTimeout:   30 * time.Second,
    96  		ReconcileTimeout: 500 * time.Millisecond,
    97  		cache:            make(map[string]*OperationResult),
    98  		logger:           ctrl.Log.WithName("Lorry HTTP client"),
    99  	}
   100  	operationClient.lorryClient = lorryClient{requester: operationClient}
   101  	return operationClient, nil
   102  }
   103  
   104  func NewHTTPClientWithURL(url string) (*HTTPClient, error) {
   105  	if url == "" {
   106  		return nil, fmt.Errorf("no url")
   107  	}
   108  
   109  	// don't use default http-client
   110  	dialer := &net.Dialer{
   111  		Timeout: 5 * time.Second,
   112  	}
   113  	netTransport := &http.Transport{
   114  		Dial:                dialer.Dial,
   115  		TLSHandshakeTimeout: 5 * time.Second,
   116  	}
   117  	client := &http.Client{
   118  		Timeout:   time.Second * 30,
   119  		Transport: netTransport,
   120  	}
   121  
   122  	operationClient := &HTTPClient{
   123  		Client:           client,
   124  		URL:              url,
   125  		CacheTTL:         60 * time.Second,
   126  		RequestTimeout:   30 * time.Second,
   127  		ReconcileTimeout: 500 * time.Millisecond,
   128  		cache:            make(map[string]*OperationResult),
   129  	}
   130  	operationClient.lorryClient = lorryClient{requester: operationClient}
   131  	return operationClient, nil
   132  }
   133  
   134  func (cli *HTTPClient) Request(ctx context.Context, operation, method string, req map[string]any) (map[string]any, error) {
   135  	ctxWithReconcileTimeout, cancel := context.WithTimeout(ctx, cli.ReconcileTimeout)
   136  	defer cancel()
   137  
   138  	// Request sql channel via http request
   139  	url := fmt.Sprintf("%s%s", cli.URL, strings.ToLower(operation))
   140  
   141  	var reader io.Reader = nil
   142  	if req != nil {
   143  		body, err := json.Marshal(req)
   144  		if err != nil {
   145  			return nil, errors.Wrap(err, "request encode failed")
   146  		}
   147  		reader = bytes.NewReader(body)
   148  	}
   149  
   150  	resp, err := cli.InvokeComponentInRoutine(ctxWithReconcileTimeout, url, method, reader)
   151  	if err != nil {
   152  		return nil, err
   153  	}
   154  
   155  	switch resp.StatusCode {
   156  	case http.StatusOK, http.StatusUnavailableForLegalReasons:
   157  		return parseBody(resp.Body)
   158  	case http.StatusNoContent:
   159  		return nil, nil
   160  	case http.StatusNotImplemented, http.StatusInternalServerError:
   161  		fallthrough
   162  	default:
   163  		msg, err := io.ReadAll(resp.Body)
   164  		if err != nil {
   165  			return nil, err
   166  		}
   167  		return nil, fmt.Errorf(string(msg))
   168  	}
   169  }
   170  
   171  func (cli *HTTPClient) InvokeComponentInRoutine(ctxWithReconcileTimeout context.Context, url, method string, body io.Reader) (*http.Response, error) {
   172  	ch := make(chan *OperationResult, 1)
   173  	go cli.InvokeComponent(ctxWithReconcileTimeout, url, method, body, ch)
   174  	var resp *http.Response
   175  	var err error
   176  	select {
   177  	case <-ctxWithReconcileTimeout.Done():
   178  		err = fmt.Errorf("invoke error : %v", ctxWithReconcileTimeout.Err())
   179  	case result := <-ch:
   180  		resp = result.response
   181  		err = result.err
   182  	}
   183  	return resp, err
   184  }
   185  
   186  func (cli *HTTPClient) InvokeComponent(ctxWithReconcileTimeout context.Context, url, method string, body io.Reader, ch chan *OperationResult) {
   187  	ctxWithRequestTimeout, cancel := context.WithTimeout(context.Background(), cli.RequestTimeout)
   188  	defer cancel()
   189  	req, err := http.NewRequestWithContext(ctxWithRequestTimeout, method, url, body)
   190  	if err != nil {
   191  		operationRes := &OperationResult{
   192  			response: nil,
   193  			err:      err,
   194  			respTime: time.Now(),
   195  		}
   196  		ch <- operationRes
   197  		return
   198  	}
   199  
   200  	mapKey := GetMapKeyFromRequest(req)
   201  	operationRes, ok := cli.cache[mapKey]
   202  	if ok {
   203  		delete(cli.cache, mapKey)
   204  		if time.Since(operationRes.respTime) <= cli.CacheTTL {
   205  			ch <- operationRes
   206  			return
   207  		}
   208  	}
   209  
   210  	resp, err := cli.Client.Do(req)
   211  	operationRes = &OperationResult{
   212  		response: resp,
   213  		err:      err,
   214  		respTime: time.Now(),
   215  	}
   216  	select {
   217  	case <-ctxWithReconcileTimeout.Done():
   218  		cli.cache[mapKey] = operationRes
   219  	default:
   220  		ch <- operationRes
   221  	}
   222  }
   223  
   224  func GetMapKeyFromRequest(req *http.Request) string {
   225  	var buf bytes.Buffer
   226  	buf.WriteString(req.URL.String())
   227  
   228  	if req.Body != nil {
   229  		all, err := io.ReadAll(req.Body)
   230  		if err != nil {
   231  			return ""
   232  		}
   233  		req.Body = io.NopCloser(bytes.NewReader(all))
   234  		buf.Write(all)
   235  	}
   236  	keys := make([]string, 0, len(req.Header))
   237  	for k := range req.Header {
   238  		keys = append(keys, k)
   239  	}
   240  	sort.Strings(keys)
   241  	for _, k := range keys {
   242  		buf.WriteString(fmt.Sprintf("%s:%s", k, req.Header[k]))
   243  	}
   244  
   245  	return buf.String()
   246  }
   247  
   248  func parseBody(body io.Reader) (map[string]any, error) {
   249  	result := map[string]any{}
   250  	data, err := io.ReadAll(body)
   251  	if err != nil {
   252  		return nil, errors.Wrap(err, "read response body failed")
   253  	}
   254  	err = json.Unmarshal(data, &result)
   255  	if err != nil {
   256  		return nil, errors.Wrap(err, "decode body failed")
   257  	}
   258  
   259  	return result, nil
   260  }
   261  
   262  func convertToArrayOfMap(value any) ([]map[string]any, error) {
   263  	array, ok := value.([]any)
   264  	if !ok {
   265  		return nil, fmt.Errorf("resp errors: %v", value)
   266  	}
   267  
   268  	result := make([]map[string]any, 0, len(array))
   269  	for _, v := range array {
   270  		m, ok := v.(map[string]any)
   271  		if !ok {
   272  			return nil, fmt.Errorf("resp errors: %v", value)
   273  		}
   274  		result = append(result, m)
   275  	}
   276  	return result, nil
   277  }