github.com/kubeshop/testkube@v1.17.23/pkg/api/v1/client/proxy_client.go (about) 1 package client 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "net/http" 8 "os" 9 "path/filepath" 10 11 "github.com/pkg/errors" 12 "k8s.io/client-go/kubernetes" 13 _ "k8s.io/client-go/plugin/pkg/client/auth" 14 "k8s.io/client-go/rest" 15 "k8s.io/client-go/tools/clientcmd" 16 17 "github.com/kubeshop/testkube/pkg/api/v1/testkube" 18 "github.com/kubeshop/testkube/pkg/executor/output" 19 "github.com/kubeshop/testkube/pkg/logs/events" 20 "github.com/kubeshop/testkube/pkg/problem" 21 ) 22 23 // GetClientSet configures Kube client set, can override host with local proxy 24 func GetClientSet(overrideHost string, clientType ClientType) (clientset kubernetes.Interface, err error) { 25 var restcfg *rest.Config 26 27 switch clientType { 28 case ClientCluster: 29 restcfg, err = rest.InClusterConfig() 30 if err != nil { 31 return clientset, errors.Wrap(err, "failed to get in cluster config") 32 } 33 case ClientProxy: 34 clcfg, err := clientcmd.NewDefaultClientConfigLoadingRules().Load() 35 if err != nil { 36 return clientset, errors.Wrap(err, "failed to get clientset config") 37 } 38 39 restcfg, err = clientcmd.NewNonInteractiveClientConfig( 40 *clcfg, "", &clientcmd.ConfigOverrides{}, nil).ClientConfig() 41 if err != nil { 42 return clientset, errors.Wrap(err, "failed to get non-interactive client config") 43 } 44 45 // override host is needed to override kubeconfig kubernetes proxy host name 46 // to local proxy passed to API server run local proxy first by `make api-proxy` 47 if overrideHost != "" { 48 restcfg.Host = overrideHost 49 } 50 default: 51 return clientset, fmt.Errorf("unsupported client type %v", clientType) 52 } 53 54 return kubernetes.NewForConfig(restcfg) 55 } 56 57 // NewProxyClient returns new proxy client 58 func NewProxyClient[A All](client kubernetes.Interface, config APIConfig) ProxyClient[A] { 59 return ProxyClient[A]{ 60 client: client, 61 config: config, 62 } 63 } 64 65 // ProxyClient implements proxy client 66 type ProxyClient[A All] struct { 67 client kubernetes.Interface 68 config APIConfig 69 } 70 71 // baseExecute is base execute method 72 func (t ProxyClient[A]) baseExec(method, uri, resource string, body []byte, params map[string]string) (resp rest.Result, err error) { 73 req := t.getProxy(method). 74 Suffix(uri) 75 if body != nil { 76 req.Body(body) 77 } 78 79 for key, value := range params { 80 if value != "" { 81 req.Param(key, value) 82 } 83 } 84 85 resp = req.Do(context.Background()) 86 87 if err = t.responseError(resp); err != nil { 88 return resp, fmt.Errorf("api/%s-%s returned error: %w", method, resource, err) 89 } 90 91 return resp, nil 92 } 93 94 // Execute is a method to make an api call for a single object 95 func (t ProxyClient[A]) Execute(method, uri string, body []byte, params map[string]string) (result A, err error) { 96 resp, err := t.baseExec(method, uri, fmt.Sprintf("%T", result), body, params) 97 if err != nil { 98 return result, err 99 } 100 101 return t.getFromResponse(resp) 102 } 103 104 // ExecuteMultiple is a method to make an api call for multiple objects 105 func (t ProxyClient[A]) ExecuteMultiple(method, uri string, body []byte, params map[string]string) (result []A, err error) { 106 resp, err := t.baseExec(method, uri, fmt.Sprintf("%T", result), body, params) 107 if err != nil { 108 return result, err 109 } 110 111 return t.getFromResponses(resp) 112 } 113 114 // Delete is a method to make delete api call 115 func (t ProxyClient[A]) Delete(uri, selector string, isContentExpected bool) error { 116 return t.ExecuteMethod(http.MethodDelete, uri, selector, isContentExpected) 117 } 118 119 func (t ProxyClient[A]) ExecuteMethod(method, uri string, selector string, isContentExpected bool) error { 120 resp, err := t.baseExec(method, uri, uri, nil, map[string]string{"selector": selector}) 121 if err != nil { 122 return err 123 } 124 125 if isContentExpected { 126 var code int 127 resp.StatusCode(&code) 128 if code != http.StatusNoContent { 129 respBody, err := resp.Raw() 130 if err != nil { 131 return err 132 } 133 return fmt.Errorf("request returned error: %s", respBody) 134 } 135 } 136 137 return nil 138 } 139 140 // GetURI returns uri for api method 141 func (t ProxyClient[A]) GetURI(pathTemplate string, params ...interface{}) string { 142 path := fmt.Sprintf(pathTemplate, params...) 143 return fmt.Sprintf("%s%s", Version, path) 144 } 145 146 // GetLogs returns logs stream from job pods, based on job pods logs 147 func (t ProxyClient[A]) GetLogs(uri string, logs chan output.Output) error { 148 resp, err := t.getProxy(http.MethodGet). 149 Suffix(uri). 150 SetHeader("Accept", "text/event-stream"). 151 Stream(context.Background()) 152 if err != nil { 153 return err 154 } 155 156 go func() { 157 defer close(logs) 158 defer resp.Close() 159 160 StreamToLogsChannel(resp, logs) 161 }() 162 163 return nil 164 } 165 166 // GetLogsV2 returns logs version 2 stream from log server, based on job pods logs 167 func (t ProxyClient[A]) GetLogsV2(uri string, logs chan events.Log) error { 168 resp, err := t.getProxy(http.MethodGet). 169 Suffix(uri). 170 SetHeader("Accept", "text/event-stream"). 171 Stream(context.Background()) 172 if err != nil { 173 return err 174 } 175 176 go func() { 177 defer close(logs) 178 defer resp.Close() 179 180 StreamToLogsChannelV2(resp, logs) 181 }() 182 183 return nil 184 } 185 186 // GetTestWorkflowExecutionNotifications returns logs stream from job pods, based on job pods logs 187 func (t ProxyClient[A]) GetTestWorkflowExecutionNotifications(uri string, notifications chan testkube.TestWorkflowExecutionNotification) error { 188 resp, err := t.getProxy(http.MethodGet). 189 Suffix(uri). 190 SetHeader("Accept", "text/event-stream"). 191 Stream(context.Background()) 192 if err != nil { 193 return err 194 } 195 196 go func() { 197 defer close(notifications) 198 defer resp.Close() 199 200 StreamToTestWorkflowExecutionNotificationsChannel(resp, notifications) 201 }() 202 203 return nil 204 } 205 206 // GetFile returns file artifact 207 func (t ProxyClient[A]) GetFile(uri, fileName, destination string, params map[string][]string) (name string, err error) { 208 req := t.getProxy(http.MethodGet). 209 Suffix(uri). 210 SetHeader("Accept", "text/event-stream") 211 212 for key, values := range params { 213 for _, value := range values { 214 if value != "" { 215 req.Param(key, value) 216 } 217 } 218 } 219 220 stream, err := req.Stream(context.Background()) 221 if err != nil { 222 return name, err 223 } 224 defer stream.Close() 225 226 target := filepath.Join(destination, fileName) 227 dir := filepath.Dir(target) 228 if _, err := os.Stat(dir); errors.Is(err, os.ErrNotExist) { 229 if err = os.MkdirAll(dir, os.ModePerm); err != nil { 230 return name, err 231 } 232 } else if err != nil { 233 return name, err 234 } 235 236 f, err := os.Create(target) 237 if err != nil { 238 return name, err 239 } 240 241 if _, err = f.ReadFrom(stream); err != nil { 242 return name, err 243 } 244 245 defer f.Close() 246 return f.Name(), err 247 } 248 249 func (t ProxyClient[A]) getProxy(requestType string) *rest.Request { 250 return t.client.CoreV1().RESTClient().Verb(requestType). 251 Namespace(t.config.Namespace). 252 Resource("services"). 253 SetHeader("Content-Type", "application/json"). 254 Name(fmt.Sprintf("%s:%d", t.config.ServiceName, t.config.ServicePort)). 255 SubResource("proxy") 256 } 257 258 func (t ProxyClient[A]) getFromResponse(resp rest.Result) (result A, err error) { 259 bytes, err := resp.Raw() 260 if err != nil { 261 return result, err 262 } 263 264 err = json.Unmarshal(bytes, &result) 265 return result, err 266 } 267 268 func (t ProxyClient[A]) getFromResponses(resp rest.Result) (result []A, err error) { 269 bytes, err := resp.Raw() 270 if err != nil { 271 return result, err 272 } 273 274 err = json.Unmarshal(bytes, &result) 275 return result, err 276 } 277 278 func (t ProxyClient[A]) getProblemFromResponse(resp rest.Result) (problem.Problem, error) { 279 bytes, respErr := resp.Raw() 280 281 problemResponse := problem.Problem{} 282 err := json.Unmarshal(bytes, &problemResponse) 283 284 // add kubeAPI client error to details 285 if respErr != nil { 286 problemResponse.Detail += ";\nresp error:" + respErr.Error() 287 } 288 289 return problemResponse, err 290 } 291 292 // responseError tries to lookup if response is of Problem type 293 func (t ProxyClient[A]) responseError(resp rest.Result) error { 294 if resp.Error() != nil { 295 pr, err := t.getProblemFromResponse(resp) 296 297 // if can't process response return content from response 298 if err != nil { 299 content, _ := resp.Raw() 300 return fmt.Errorf("api server response: '%s'\nerror: %w", content, resp.Error()) 301 } 302 303 return fmt.Errorf("api server problem: %s", pr.Detail) 304 } 305 306 return nil 307 }