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  }