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 }