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  }