github.com/kubeshop/testkube@v1.17.23/pkg/api/v1/client/uploads.go (about)

     1  package client
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"mime/multipart"
    10  	"net/http"
    11  	"path/filepath"
    12  	"strings"
    13  	"time"
    14  
    15  	"k8s.io/client-go/kubernetes"
    16  	"k8s.io/client-go/rest"
    17  
    18  	"github.com/kubeshop/testkube/pkg/problem"
    19  )
    20  
    21  const uri string = "/uploads"
    22  
    23  type CopyFileClient interface {
    24  	UploadFile(parentName string, parentType TestingType, filePath string, fileContent []byte, timeout time.Duration) error
    25  }
    26  
    27  type CopyFileDirectClient struct {
    28  	client        *http.Client
    29  	apiURI        string
    30  	apiPathPrefix string
    31  }
    32  
    33  func NewCopyFileDirectClient(httpClient *http.Client, apiURI, apiPathPrefix string) *CopyFileDirectClient {
    34  	return &CopyFileDirectClient{
    35  		client:        httpClient,
    36  		apiURI:        apiURI,
    37  		apiPathPrefix: apiPathPrefix,
    38  	}
    39  }
    40  
    41  type CopyFileProxyClient struct {
    42  	client kubernetes.Interface
    43  	config APIConfig
    44  }
    45  
    46  func NewCopyFileProxyClient(client kubernetes.Interface, config APIConfig) *CopyFileProxyClient {
    47  	return &CopyFileProxyClient{
    48  		client: client,
    49  		config: config,
    50  	}
    51  }
    52  
    53  // UploadFile uploads a copy file to the API server
    54  func (c CopyFileDirectClient) UploadFile(parentName string, parentType TestingType, filePath string, fileContent []byte, timeout time.Duration) error {
    55  	body, writer, err := createUploadFileBody(filePath, fileContent, parentName, parentType)
    56  	if err != nil {
    57  		return err
    58  	}
    59  
    60  	req, err := http.NewRequest(http.MethodPost, c.getUri(), body)
    61  	if err != nil {
    62  		return err
    63  	}
    64  
    65  	req.Header.Set("Content-Type", writer.FormDataContentType())
    66  
    67  	clientTimeout := c.client.Timeout
    68  	if timeout != clientTimeout {
    69  		c.client.Timeout = timeout
    70  	}
    71  	resp, err := c.client.Do(req)
    72  	c.client.Timeout = clientTimeout
    73  	if err != nil {
    74  		return err
    75  	}
    76  
    77  	if err = httpResponseError(resp); err != nil {
    78  		return fmt.Errorf("api %s returned error: %w", uri, err)
    79  	}
    80  
    81  	return nil
    82  }
    83  
    84  func (c CopyFileDirectClient) getUri() string {
    85  	return strings.Join([]string{c.apiPathPrefix, c.apiURI, "/", Version, uri}, "")
    86  }
    87  
    88  // UploadFile uploads a copy file to the API server
    89  func (c CopyFileProxyClient) UploadFile(parentName string, parentType TestingType, filePath string, fileContent []byte, timeout time.Duration) error {
    90  	body, writer, err := createUploadFileBody(filePath, fileContent, parentName, parentType)
    91  	if err != nil {
    92  		return err
    93  	}
    94  
    95  	// by default the timeout is 0 for the K8s client, which means no timeout
    96  	clientTimeout := time.Duration(0)
    97  	if timeout != clientTimeout {
    98  		clientTimeout = timeout
    99  	}
   100  	req := c.client.CoreV1().RESTClient().Verb(http.MethodPost).
   101  		Namespace(c.config.Namespace).
   102  		Resource("services").
   103  		SetHeader("Content-Type", writer.FormDataContentType()).
   104  		Name(fmt.Sprintf("%s:%d", c.config.ServiceName, c.config.ServicePort)).
   105  		SubResource("proxy").
   106  		Timeout(clientTimeout).
   107  		Suffix(Version + uri).
   108  		Body(body)
   109  
   110  	resp := req.Do(context.Background())
   111  
   112  	if err := k8sResponseError(resp); err != nil {
   113  		return fmt.Errorf("api %s returned error: %w", uri, err)
   114  	}
   115  
   116  	return nil
   117  }
   118  
   119  func createUploadFileBody(filePath string, fileContent []byte, parentName string, parentType TestingType) (*bytes.Buffer, *multipart.Writer, error) {
   120  	body := &bytes.Buffer{}
   121  	writer := multipart.NewWriter(body)
   122  	part, err := writer.CreateFormFile("attachment", filepath.Base(filePath))
   123  	if err != nil {
   124  		return body, writer, fmt.Errorf("could not send file: %w", err)
   125  	}
   126  
   127  	if _, err := io.Copy(part, bytes.NewBuffer(fileContent)); err != nil {
   128  		return body, writer, fmt.Errorf("could not write file: %w", err)
   129  	}
   130  	err = writer.WriteField("parentName", parentName)
   131  	if err != nil {
   132  		return body, writer, fmt.Errorf("could not add parentName: %w", err)
   133  	}
   134  	err = writer.WriteField("parentType", string(parentType))
   135  	if err != nil {
   136  		return body, writer, fmt.Errorf("could not add parentType: %w", err)
   137  	}
   138  	err = writer.WriteField("filePath", filePath)
   139  	if err != nil {
   140  		return body, writer, fmt.Errorf("could not add filePath: %w", err)
   141  	}
   142  	err = writer.Close()
   143  	if err != nil {
   144  		return body, writer, fmt.Errorf("could not close copyfile writer: %w", err)
   145  	}
   146  	return body, writer, nil
   147  }
   148  
   149  // httpResponseError tries to lookup if response is of Problem type
   150  func httpResponseError(resp *http.Response) error {
   151  	if resp.StatusCode >= 400 {
   152  		var pr problem.Problem
   153  
   154  		bytes, err := io.ReadAll(resp.Body)
   155  		if err != nil {
   156  			return fmt.Errorf("can't get problem from api response: can't read response body %w", err)
   157  		}
   158  
   159  		err = json.Unmarshal(bytes, &pr)
   160  		if err != nil {
   161  			return fmt.Errorf("can't get problem from api response: %w, output: %s", err, string(bytes))
   162  		}
   163  
   164  		return fmt.Errorf("problem: %+v", pr.Detail)
   165  	}
   166  
   167  	return nil
   168  }
   169  
   170  // k8sResponseError tries to lookup if response is of Problem type
   171  func k8sResponseError(resp rest.Result) error {
   172  	if resp.Error() != nil {
   173  		pr, err := getProblemFromK8sResponse(resp)
   174  
   175  		// if can't process response return content from response
   176  		if err != nil {
   177  			content, _ := resp.Raw()
   178  			return fmt.Errorf("api server response: '%s'\nerror: %w", content, resp.Error())
   179  		}
   180  
   181  		return fmt.Errorf("api server problem: %s", pr.Detail)
   182  	}
   183  
   184  	return nil
   185  }
   186  
   187  // getProblemFromK8sResponse gets the error message from the K8s response
   188  func getProblemFromK8sResponse(resp rest.Result) (problem.Problem, error) {
   189  	bytes, respErr := resp.Raw()
   190  
   191  	problemResponse := problem.Problem{}
   192  	err := json.Unmarshal(bytes, &problemResponse)
   193  
   194  	// add kubeAPI client error to details
   195  	if respErr != nil {
   196  		problemResponse.Detail += ";\nresp error:" + respErr.Error()
   197  	}
   198  
   199  	return problemResponse, err
   200  }