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 }