github.com/kubeshop/testkube@v1.17.23/pkg/api/v1/client/direct_client.go (about) 1 package client 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io" 8 "net/http" 9 "os" 10 "path/filepath" 11 12 "github.com/pkg/errors" 13 "golang.org/x/oauth2" 14 15 "github.com/kubeshop/testkube/pkg/api/v1/testkube" 16 "github.com/kubeshop/testkube/pkg/executor/output" 17 "github.com/kubeshop/testkube/pkg/logs/events" 18 "github.com/kubeshop/testkube/pkg/oauth" 19 "github.com/kubeshop/testkube/pkg/problem" 20 ) 21 22 type transport struct { 23 headers map[string]string 24 base http.RoundTripper 25 } 26 27 // RoundTrip is a method to adjust http request 28 func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) { 29 for k, v := range t.headers { 30 req.Header.Add(k, v) 31 } 32 33 base := t.base 34 if base == nil { 35 base = http.DefaultTransport 36 } 37 38 return base.RoundTrip(req) 39 } 40 41 func ConfigureClient(client *http.Client, token *oauth2.Token, cloudApiKey string) { 42 if token != nil { 43 client.Transport = &transport{headers: map[string]string{ 44 "Authorization": oauth.AuthorizationPrefix + " " + token.AccessToken}} 45 } 46 if cloudApiKey != "" { 47 client.Transport = &transport{headers: map[string]string{ 48 "Authorization": "Bearer " + cloudApiKey}} 49 } 50 } 51 52 // NewDirectClient returns new direct client 53 func NewDirectClient[A All](httpClient *http.Client, apiURI, apiPathPrefix string) DirectClient[A] { 54 if apiPathPrefix == "" { 55 apiPathPrefix = "/" + Version 56 } 57 58 return DirectClient[A]{ 59 client: httpClient, 60 sseClient: httpClient, 61 apiURI: apiURI, 62 apiPathPrefix: apiPathPrefix, 63 } 64 } 65 66 // DirectClient implements direct client 67 type DirectClient[A All] struct { 68 client *http.Client 69 sseClient *http.Client 70 apiURI string 71 apiPathPrefix string 72 } 73 74 // baseExecute is base execute method 75 func (t DirectClient[A]) baseExec(method, uri, resource string, body []byte, params map[string]string) (resp *http.Response, err error) { 76 var buffer io.Reader 77 if body != nil { 78 buffer = bytes.NewBuffer(body) 79 } 80 81 req, err := http.NewRequest(method, uri, buffer) 82 if err != nil { 83 return resp, err 84 } 85 86 req.Header.Set("Content-Type", "application/json") 87 q := req.URL.Query() 88 for key, value := range params { 89 if value != "" { 90 q.Add(key, value) 91 } 92 } 93 req.URL.RawQuery = q.Encode() 94 95 resp, err = t.client.Do(req) 96 if err != nil { 97 return resp, err 98 } 99 100 if err = t.responseError(resp); err != nil { 101 return resp, fmt.Errorf("api/%s-%s returned error: %w", method, resource, err) 102 } 103 104 return resp, nil 105 } 106 107 func (t DirectClient[A]) WithSSEClient(client *http.Client) DirectClient[A] { 108 t.sseClient = client 109 return t 110 } 111 112 // Execute is a method to make an api call for a single object 113 func (t DirectClient[A]) Execute(method, uri string, body []byte, params map[string]string) (result A, err error) { 114 resp, err := t.baseExec(method, uri, fmt.Sprintf("%T", result), body, params) 115 if err != nil { 116 return result, err 117 } 118 defer resp.Body.Close() 119 120 return t.getFromResponse(resp) 121 } 122 123 // ExecuteMultiple is a method to make an api call for multiple objects 124 func (t DirectClient[A]) ExecuteMultiple(method, uri string, body []byte, params map[string]string) (result []A, err error) { 125 resp, err := t.baseExec(method, uri, fmt.Sprintf("%T", result), body, params) 126 if err != nil { 127 return result, err 128 } 129 defer resp.Body.Close() 130 131 return t.getFromResponses(resp) 132 } 133 134 // Delete is a method to make delete api call 135 func (t DirectClient[A]) Delete(uri, selector string, isContentExpected bool) error { 136 return t.ExecuteMethod(http.MethodDelete, uri, selector, isContentExpected) 137 } 138 139 func (t DirectClient[A]) ExecuteMethod(method, uri string, selector string, isContentExpected bool) error { 140 resp, err := t.baseExec(method, uri, uri, nil, map[string]string{"selector": selector}) 141 if err != nil { 142 return err 143 } 144 defer resp.Body.Close() 145 146 if isContentExpected && resp.StatusCode != http.StatusNoContent { 147 respBody, err := io.ReadAll(resp.Body) 148 if err != nil { 149 return err 150 } 151 152 return fmt.Errorf("request returned error: %s", respBody) 153 } 154 155 return nil 156 } 157 158 // GetURI returns uri for api method 159 func (t DirectClient[A]) GetURI(pathTemplate string, params ...interface{}) string { 160 path := fmt.Sprintf(pathTemplate, params...) 161 return fmt.Sprintf("%s%s%s", t.apiURI, t.apiPathPrefix, path) 162 } 163 164 // GetLogs returns logs stream from job pods, based on job pods logs 165 func (t DirectClient[A]) GetLogs(uri string, logs chan output.Output) error { 166 req, err := http.NewRequest(http.MethodGet, uri, nil) 167 if err != nil { 168 return err 169 } 170 171 req.Header.Set("Accept", "text/event-stream") 172 resp, err := t.sseClient.Do(req) 173 if err != nil { 174 return err 175 } 176 177 go func() { 178 defer close(logs) 179 defer resp.Body.Close() 180 181 StreamToLogsChannel(resp.Body, logs) 182 }() 183 184 return nil 185 } 186 187 // GetLogsV2 returns logs stream version 2 from log server, based on job pods logs 188 func (t DirectClient[A]) GetLogsV2(uri string, logs chan events.Log) error { 189 req, err := http.NewRequest(http.MethodGet, uri, nil) 190 if err != nil { 191 return err 192 } 193 194 req.Header.Set("Accept", "text/event-stream") 195 resp, err := t.sseClient.Do(req) 196 if err != nil { 197 return err 198 } 199 200 if resp.StatusCode != http.StatusOK { 201 return errors.New("error getting logs, invalid status code: " + resp.Status) 202 } 203 204 go func() { 205 defer close(logs) 206 defer resp.Body.Close() 207 208 StreamToLogsChannelV2(resp.Body, logs) 209 }() 210 211 return nil 212 } 213 214 // GetTestWorkflowExecutionNotifications returns logs stream from job pods, based on job pods logs 215 func (t DirectClient[A]) GetTestWorkflowExecutionNotifications(uri string, notifications chan testkube.TestWorkflowExecutionNotification) error { 216 req, err := http.NewRequest(http.MethodGet, uri, nil) 217 if err != nil { 218 return err 219 } 220 221 req.Header.Set("Accept", "text/event-stream") 222 resp, err := t.sseClient.Do(req) 223 if err != nil { 224 return err 225 } 226 227 go func() { 228 defer close(notifications) 229 defer resp.Body.Close() 230 231 StreamToTestWorkflowExecutionNotificationsChannel(resp.Body, notifications) 232 }() 233 234 return nil 235 } 236 237 // GetFile returns file artifact 238 func (t DirectClient[A]) GetFile(uri, fileName, destination string, params map[string][]string) (name string, err error) { 239 req, err := http.NewRequest(http.MethodGet, uri, nil) 240 if err != nil { 241 return "", err 242 } 243 244 q := req.URL.Query() 245 for key, values := range params { 246 for _, value := range values { 247 if value != "" { 248 q.Add(key, value) 249 } 250 } 251 } 252 req.URL.RawQuery = q.Encode() 253 254 resp, err := t.client.Do(req) 255 if err != nil { 256 return name, err 257 } 258 defer resp.Body.Close() 259 260 if resp.StatusCode > 299 { 261 return name, fmt.Errorf("error: %d", resp.StatusCode) 262 } 263 264 target := filepath.Join(destination, fileName) 265 dir := filepath.Dir(target) 266 if _, err := os.Stat(dir); errors.Is(err, os.ErrNotExist) { 267 if err = os.MkdirAll(dir, os.ModePerm); err != nil { 268 return name, err 269 } 270 } else if err != nil { 271 return name, err 272 } 273 274 f, err := os.Create(target) 275 if err != nil { 276 return name, err 277 } 278 279 if _, err = io.Copy(f, resp.Body); err != nil { 280 return name, err 281 } 282 283 if err = t.responseError(resp); err != nil { 284 return name, fmt.Errorf("api/download-file returned error: %w", err) 285 } 286 287 return f.Name(), nil 288 } 289 290 func (t DirectClient[A]) getFromResponse(resp *http.Response) (result A, err error) { 291 err = json.NewDecoder(resp.Body).Decode(&result) 292 return 293 } 294 295 func (t DirectClient[A]) getFromResponses(resp *http.Response) (result []A, err error) { 296 err = json.NewDecoder(resp.Body).Decode(&result) 297 return 298 } 299 300 // responseError tries to lookup if response is of Problem type 301 func (t DirectClient[A]) responseError(resp *http.Response) error { 302 if resp.StatusCode >= 400 { 303 var pr problem.Problem 304 305 bytes, err := io.ReadAll(resp.Body) 306 if err != nil { 307 return fmt.Errorf("can't get problem from api response: can't read response body %w", err) 308 } 309 310 err = json.Unmarshal(bytes, &pr) 311 if err != nil { 312 return fmt.Errorf("can't get problem from api response: %w, output: %s", err, string(bytes)) 313 } 314 315 return fmt.Errorf("problem: %+v", pr.Detail) 316 } 317 318 return nil 319 }