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

     1  package assethook
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"mime/multipart"
     9  	"net/http"
    10  	"os"
    11  	"path/filepath"
    12  	"sync"
    13  	"time"
    14  
    15  	pkgPath "github.com/kyma-project/kyma/components/asset-store-controller-manager/internal/path"
    16  	"github.com/kyma-project/kyma/components/asset-store-controller-manager/pkg/apis/assetstore/v1alpha2"
    17  	"github.com/pkg/errors"
    18  	"k8s.io/apimachinery/pkg/runtime"
    19  )
    20  
    21  type Result struct {
    22  	Success  bool
    23  	Messages map[string][]Message
    24  }
    25  
    26  type Message struct {
    27  	Filename string
    28  	Message  string
    29  }
    30  
    31  type processor struct {
    32  	onSuccess      func(ctx context.Context, basePath, filePath string, responseBody io.Reader, messagesChan chan Message, errChan chan error)
    33  	onFail         func(ctx context.Context, basePath, filePath string, responseBody io.Reader, messagesChan chan Message, errChan chan error)
    34  	workers        int
    35  	continueOnFail bool
    36  	timeout        time.Duration
    37  	httpClient     HttpClient
    38  }
    39  
    40  //go:generate mockery -name=HttpClient -output=automock -outpkg=automock -case=underscore
    41  type HttpClient interface {
    42  	Do(req *http.Request) (*http.Response, error)
    43  }
    44  
    45  //go:generate mockery -name=httpProcessor -output=automock -outpkg=automock -case=underscore
    46  type httpProcessor interface {
    47  	Do(ctx context.Context, basePath string, files []string, services []v1alpha2.AssetWebhookService) (map[string][]Message, error)
    48  }
    49  
    50  func (*processor) parseParameters(metadata *runtime.RawExtension) string {
    51  	if nil == metadata {
    52  		return ""
    53  	}
    54  
    55  	return string(metadata.Raw)
    56  }
    57  
    58  func (p *processor) iterateFiles(files []string, filter string) (chan string, error) {
    59  	filtered, err := pkgPath.Filter(files, filter)
    60  	if err != nil {
    61  		return nil, errors.Wrapf(err, "while filtering files with regex %s", filter)
    62  	}
    63  
    64  	fileNameChan := make(chan string, len(filtered))
    65  	defer close(fileNameChan)
    66  	for _, fileName := range filtered {
    67  		fileNameChan <- fileName
    68  	}
    69  
    70  	return fileNameChan, nil
    71  }
    72  
    73  func (p *processor) Do(ctx context.Context, basePath string, files []string, services []v1alpha2.AssetWebhookService) (map[string][]Message, error) {
    74  	ctx, cancel := context.WithCancel(ctx)
    75  	defer cancel()
    76  	results := make(map[string][]Message)
    77  	for _, service := range services {
    78  		success, messages, err := p.doService(ctx, cancel, basePath, files, service)
    79  		if err != nil {
    80  			return nil, err
    81  		}
    82  		if !success {
    83  			name := fmt.Sprintf("%s/%s%s", service.Namespace, service.Name, service.Endpoint)
    84  			results[name] = messages
    85  		}
    86  	}
    87  
    88  	return results, nil
    89  }
    90  
    91  func (p *processor) doService(ctx context.Context, cancel context.CancelFunc, basePath string, files []string, service v1alpha2.AssetWebhookService) (bool, []Message, error) {
    92  	fileChan, err := p.iterateFiles(files, service.Filter)
    93  	if err != nil {
    94  		return false, nil, errors.Wrap(err, "while creating files channel")
    95  	}
    96  	messagesChan := make(chan Message)
    97  	errChan := make(chan error)
    98  	go func() {
    99  		defer close(messagesChan)
   100  		defer close(errChan)
   101  
   102  		var waitGroup sync.WaitGroup
   103  		for i := 0; i < p.workers; i++ {
   104  			waitGroup.Add(1)
   105  			go func() {
   106  				defer waitGroup.Done()
   107  				p.doFiles(ctx, cancel, basePath, service, fileChan, messagesChan, errChan)
   108  			}()
   109  		}
   110  		waitGroup.Wait()
   111  	}()
   112  
   113  	var waitGroup sync.WaitGroup
   114  	var errs []error
   115  	waitGroup.Add(1)
   116  	go func() {
   117  		defer waitGroup.Done()
   118  		for e := range errChan {
   119  			errs = append(errs, e)
   120  		}
   121  	}()
   122  
   123  	var messages []Message
   124  	waitGroup.Add(1)
   125  	go func() {
   126  		defer waitGroup.Done()
   127  		for msg := range messagesChan {
   128  			messages = append(messages, msg)
   129  		}
   130  	}()
   131  	waitGroup.Wait()
   132  
   133  	if len(errs) > 0 {
   134  		msg := errs[0].Error()
   135  		for _, e := range errs[1:] {
   136  			msg = fmt.Sprintf("%s, %s", msg, e.Error())
   137  		}
   138  		return false, nil, errors.New(msg)
   139  	}
   140  
   141  	if len(messages) == 0 {
   142  		return true, nil, nil
   143  	}
   144  	return false, messages, nil
   145  }
   146  
   147  func (p *processor) doFiles(ctx context.Context, cancel context.CancelFunc, basePath string, service v1alpha2.AssetWebhookService, pathChan chan string, messagesChan chan Message, errChan chan error) {
   148  	for {
   149  		select {
   150  		case <-ctx.Done():
   151  			return
   152  		case <-errChan:
   153  			return
   154  		case path, ok := <-pathChan:
   155  			if !ok {
   156  				return
   157  			}
   158  
   159  			p.doFile(ctx, cancel, basePath, path, service, messagesChan, errChan)
   160  		}
   161  	}
   162  }
   163  
   164  func (p *processor) doFile(ctx context.Context, cancel context.CancelFunc, basePath string, path string, service v1alpha2.AssetWebhookService, messagesChan chan Message, errChan chan error) {
   165  	body, contentType, err := p.buildQuery(basePath, path, p.parseParameters(service.Parameters))
   166  	if err != nil {
   167  		errChan <- errors.Wrap(err, "while building multipart query")
   168  		return
   169  	}
   170  
   171  	success, modified, rspBody, err := p.call(ctx, contentType, service.WebhookService, body)
   172  	if err != nil {
   173  		errChan <- errors.Wrap(err, "while sending request to webhook")
   174  		return
   175  	}
   176  	defer rspBody.Close()
   177  
   178  	if success && modified && p.onSuccess != nil {
   179  		p.onSuccess(ctx, basePath, path, rspBody, messagesChan, errChan)
   180  	} else if !success && p.onFail != nil {
   181  		p.onFail(ctx, basePath, path, rspBody, messagesChan, errChan)
   182  	}
   183  
   184  	if !success && !p.continueOnFail {
   185  		cancel()
   186  	}
   187  }
   188  
   189  func (p *processor) buildQuery(basePath, filePath, parameters string) (io.Reader, string, error) {
   190  	buffer := &bytes.Buffer{}
   191  	formWriter := multipart.NewWriter(buffer)
   192  	defer formWriter.Close()
   193  
   194  	path := filepath.Join(basePath, filePath)
   195  	file, err := os.Open(path)
   196  	if err != nil {
   197  		return nil, "", errors.Wrapf(err, "while opening file %s", filePath)
   198  	}
   199  	defer file.Close()
   200  
   201  	contentWriter, err := formWriter.CreateFormFile("content", filepath.Base(file.Name()))
   202  	if err != nil {
   203  		return nil, "", errors.Wrapf(err, "while creating content field for file %s", filePath)
   204  	}
   205  
   206  	_, err = io.Copy(contentWriter, file)
   207  	if err != nil {
   208  		return nil, "", errors.Wrapf(err, "while copying file %s to content field", filePath)
   209  	}
   210  
   211  	err = formWriter.WriteField("parameters", parameters)
   212  	if err != nil {
   213  		return nil, "", errors.Wrapf(err, "while creating parameters field for parameters %s", parameters)
   214  	}
   215  
   216  	return buffer, formWriter.FormDataContentType(), nil
   217  }
   218  
   219  func (p *processor) call(ctx context.Context, contentType string, webhook v1alpha2.WebhookService, body io.Reader) (bool, bool, io.ReadCloser, error) {
   220  	context, cancel := context.WithTimeout(ctx, p.timeout)
   221  	defer cancel()
   222  
   223  	req, err := http.NewRequest("POST", p.getWebhookUrl(webhook), body)
   224  	if err != nil {
   225  		return false, false, nil, errors.Wrap(err, "while creating request")
   226  	}
   227  
   228  	req.Header.Set("Content-Type", contentType)
   229  	req.WithContext(context)
   230  
   231  	rsp, err := p.httpClient.Do(req)
   232  	if err != nil {
   233  		return false, false, nil, errors.Wrapf(err, "while sending request to webhook")
   234  	}
   235  
   236  	switch rsp.StatusCode {
   237  	case http.StatusOK, http.StatusUnprocessableEntity:
   238  		success := rsp.StatusCode == http.StatusOK
   239  		return success, success, rsp.Body, nil
   240  	case http.StatusNotModified:
   241  		return true, false, rsp.Body, nil
   242  	default:
   243  		return false, false, rsp.Body, fmt.Errorf("invalid response from %s, code: %d", req.URL, rsp.StatusCode)
   244  	}
   245  }
   246  
   247  func (*processor) getWebhookUrl(service v1alpha2.WebhookService) string {
   248  	return fmt.Sprintf("http://%s.%s.svc.cluster.local%s", service.Name, service.Namespace, service.Endpoint)
   249  }