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 }