github.com/kyma-project/kyma/components/asset-store-controller-manager@v0.0.0-20191203152857-3792b5df17c5/internal/assethook/metadata_engine.go (about)

     1  package assethook
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"mime/multipart"
    11  	"net/http"
    12  	"os"
    13  	"path/filepath"
    14  	"time"
    15  
    16  	"github.com/kyma-project/kyma/components/asset-store-controller-manager/internal/assethook/api/v1alpha1"
    17  	pkgPath "github.com/kyma-project/kyma/components/asset-store-controller-manager/internal/path"
    18  	"github.com/kyma-project/kyma/components/asset-store-controller-manager/pkg/apis/assetstore/v1alpha2"
    19  	"github.com/pkg/errors"
    20  )
    21  
    22  //go:generate mockery -name=MetadataExtractor -output=automock -outpkg=automock -case=underscore
    23  type MetadataExtractor interface {
    24  	Extract(ctx context.Context, basePath string, files []string, services []v1alpha2.WebhookService) ([]File, error)
    25  }
    26  
    27  type File struct {
    28  	Name     string
    29  	Metadata *json.RawMessage
    30  }
    31  
    32  type metadataEngine struct {
    33  	timeout    time.Duration
    34  	fileReader func(filename string) ([]byte, error)
    35  	httpClient HttpClient
    36  }
    37  
    38  func NewMetadataExtractor(httpClient HttpClient, timeout time.Duration) MetadataExtractor {
    39  	return &metadataEngine{
    40  		httpClient: httpClient,
    41  		timeout:    timeout,
    42  		fileReader: ioutil.ReadFile,
    43  	}
    44  }
    45  
    46  func (e *metadataEngine) Extract(ctx context.Context, basePath string, files []string, services []v1alpha2.WebhookService) ([]File, error) {
    47  	results := make(map[string]*json.RawMessage)
    48  	for _, service := range services {
    49  		filtered, err := pkgPath.Filter(files, service.Filter)
    50  		if err != nil {
    51  			return nil, errors.Wrapf(err, "while filtering files with regex %s", service.Filter)
    52  		}
    53  
    54  		body, contentType, err := e.buildQuery(basePath, filtered)
    55  		if err != nil {
    56  			return nil, errors.Wrap(err, "while building multipart query")
    57  		}
    58  
    59  		response := &v1alpha1.MetadataResponse{}
    60  		err = e.do(ctx, contentType, service, body, response)
    61  		if err != nil {
    62  			return nil, errors.Wrap(err, "while sending request to metadata webhook")
    63  		}
    64  
    65  		results = e.replaceMetadata(results, response.Data)
    66  	}
    67  
    68  	return e.toFiles(results), nil
    69  }
    70  
    71  func (*metadataEngine) replaceMetadata(current map[string]*json.RawMessage, results []v1alpha1.MetadataResultSuccess) map[string]*json.RawMessage {
    72  	for _, result := range results {
    73  		current[result.FilePath] = result.Metadata
    74  	}
    75  
    76  	return current
    77  }
    78  
    79  func (*metadataEngine) toFiles(results map[string]*json.RawMessage) []File {
    80  	files := make([]File, 0, len(results))
    81  	for k, v := range results {
    82  		files = append(files, File{Name: k, Metadata: v})
    83  	}
    84  
    85  	return files
    86  }
    87  
    88  func (e *metadataEngine) buildQuery(basePath string, files []string) (io.Reader, string, error) {
    89  	b := &bytes.Buffer{}
    90  	formWriter := multipart.NewWriter(b)
    91  	defer formWriter.Close()
    92  
    93  	for _, file := range files {
    94  		path := filepath.Join(basePath, file)
    95  		if err := e.buildQueryField(formWriter, file, path); err != nil {
    96  			return nil, "", errors.Wrapf(err, "while building query part")
    97  		}
    98  	}
    99  
   100  	return b, formWriter.FormDataContentType(), nil
   101  }
   102  
   103  func (e *metadataEngine) buildQueryField(writer *multipart.Writer, filename, path string) error {
   104  	file, err := os.Open(path)
   105  	if err != nil {
   106  		return errors.Wrapf(err, "while opening file %s", filename)
   107  	}
   108  	defer file.Close()
   109  
   110  	part, err := writer.CreateFormFile(filename, filepath.Base(file.Name()))
   111  	if err != nil {
   112  		return errors.Wrapf(err, "while creating part for file %s", filename)
   113  	}
   114  
   115  	_, err = io.Copy(part, file)
   116  	if err != nil {
   117  		return errors.Wrapf(err, "while copying file %s to part", filename)
   118  	}
   119  
   120  	return nil
   121  }
   122  
   123  func (e *metadataEngine) do(ctx context.Context, contentType string, webhook v1alpha2.WebhookService, body io.Reader, response interface{}) error {
   124  	ctx, cancel := context.WithTimeout(ctx, e.timeout)
   125  	defer cancel()
   126  
   127  	req, err := http.NewRequest("POST", e.getWebhookUrl(webhook), body)
   128  	if err != nil {
   129  		return err
   130  	}
   131  
   132  	req.Header.Set("Content-Type", contentType)
   133  	req.WithContext(ctx)
   134  
   135  	rsp, err := e.httpClient.Do(req)
   136  	if err != nil {
   137  		return errors.Wrapf(err, "while sending request to webhook")
   138  	}
   139  	defer rsp.Body.Close()
   140  
   141  	if rsp.StatusCode != http.StatusOK {
   142  		return fmt.Errorf("invalid response from %s, code: %d", req.URL, rsp.StatusCode)
   143  	}
   144  
   145  	responseBytes, err := ioutil.ReadAll(rsp.Body)
   146  	if err != nil {
   147  		return errors.Wrapf(err, "while reading response body")
   148  	}
   149  
   150  	err = json.Unmarshal(responseBytes, response)
   151  	if err != nil {
   152  		return errors.Wrapf(err, "while parsing response body")
   153  	}
   154  
   155  	return nil
   156  }
   157  
   158  func (*metadataEngine) getWebhookUrl(service v1alpha2.WebhookService) string {
   159  	return fmt.Sprintf("http://%s.%s.svc.cluster.local%s", service.Name, service.Namespace, service.Endpoint)
   160  }