Added oparent to sdc main
[sdc.git] / openecomp-be / lib / openecomp-sdc-externaltesting-lib / openecomp-sdc-externaltesting-impl / src / main / java / org / openecomp / core / externaltesting / impl / ExternalTestingManagerImpl.java
1 /*
2  * Copyright © 2019 iconectiv
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package org.openecomp.core.externaltesting.impl;
18
19 import com.amdocs.zusammen.utils.fileutils.json.JsonUtil;
20 import com.fasterxml.jackson.databind.ObjectMapper;
21 import com.google.gson.GsonBuilder;
22 import com.google.gson.JsonObject;
23 import com.google.gson.JsonParseException;
24 import lombok.EqualsAndHashCode;
25 import org.apache.commons.io.IOUtils;
26 import org.apache.commons.lang3.ArrayUtils;
27 import org.apache.commons.lang3.StringUtils;
28 import org.apache.commons.lang3.tuple.Pair;
29 import org.onap.sdc.tosca.services.YamlUtil;
30 import org.openecomp.core.externaltesting.api.*;
31 import org.openecomp.core.externaltesting.errors.ExternalTestingException;
32 import org.openecomp.sdc.heat.datatypes.manifest.FileData;
33 import org.openecomp.sdc.heat.datatypes.manifest.ManifestContent;
34 import org.openecomp.sdc.vendorsoftwareproduct.OrchestrationTemplateCandidateManager;
35 import org.openecomp.sdc.vendorsoftwareproduct.OrchestrationTemplateCandidateManagerFactory;
36 import org.openecomp.sdc.vendorsoftwareproduct.VendorSoftwareProductManager;
37 import org.openecomp.sdc.vendorsoftwareproduct.VspManagerFactory;
38 import org.openecomp.sdc.versioning.VersioningManager;
39 import org.openecomp.sdc.versioning.VersioningManagerFactory;
40 import org.openecomp.sdc.versioning.dao.types.Version;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
43 import org.springframework.core.ParameterizedTypeReference;
44 import org.springframework.core.io.ByteArrayResource;
45 import org.springframework.http.*;
46 import org.springframework.http.client.SimpleClientHttpRequestFactory;
47 import org.springframework.util.LinkedMultiValueMap;
48 import org.springframework.util.MultiValueMap;
49 import org.springframework.web.client.HttpStatusCodeException;
50 import org.springframework.web.client.ResourceAccessException;
51 import org.springframework.web.client.RestTemplate;
52 import org.springframework.web.util.UriComponentsBuilder;
53 import org.yaml.snakeyaml.Yaml;
54
55 import javax.annotation.PostConstruct;
56 import java.io.*;
57 import java.util.*;
58 import java.util.stream.Collectors;
59 import java.util.stream.Stream;
60 import java.util.zip.ZipEntry;
61 import java.util.zip.ZipInputStream;
62 import java.util.zip.ZipOutputStream;
63
64 public class ExternalTestingManagerImpl implements ExternalTestingManager {
65
66   private Logger logger = LoggerFactory.getLogger(ExternalTestingManagerImpl.class);
67
68   private static final String FILE_URL_PREFIX = "file://";
69   private static final String MANIFEST_JSON = "MANIFEST.json";
70   private static final String HTTP_STATUS = "httpStatus";
71   private static final String CODE = "code";
72   private static final String ERROR = "error";
73   private static final String MESSAGE = "message";
74   private static final String DETAIL = "detail";
75   private static final String PATH = "path";
76
77   private static final String VTP_SCENARIOS_URI = "%s/v1/vtp/scenarios";
78   private static final String VTP_TESTSUITE_URI = "%s/v1/vtp/scenarios/%s/testsuites";
79   private static final String VTP_TESTCASES_URI = "%s/v1/vtp/scenarios/%s/testcases";
80   private static final String VTP_TESTCASE_URI = "%s/v1/vtp/scenarios/%s/testsuites/%s/testcases/%s";
81   private static final String VTP_EXECUTIONS_URI = "%s/v1/vtp/executions";
82   private static final String VTP_EXECUTION_URI = "%s/v1/vtp/executions/%s";
83
84   private static final String INVALIDATE_STATE_ERROR_CODE = "SDC-TEST-001";
85   private static final String NO_ACCESS_CONFIGURATION_DEFINED = "No access configuration defined";
86
87   private static final String NO_SUCH_ENDPOINT_ERROR_CODE = "SDC-TEST-002";
88   private static final String ENDPOINT_ERROR_CODE = "SDC-TEST-003";
89   private static final String TESTING_HTTP_ERROR_CODE = "SDC-TEST-004";
90   private static final String SDC_RESOLVER_ERR = "SDC-TEST-005";
91
92   private static final String TOSCA_META = "TOSCA-Metadata/TOSCA.meta";
93   private static final String MAIN_SERVICE_TEMPLATE_YAML_FILE_NAME = "MainServiceTemplate.yaml";
94   private static final String TOSCA_META_ENTRY_DEFINITIONS="Entry-Definitions";
95   static final String VSP_ID = "vspId";
96   static final String VSP_VERSION = "vspVersion";
97
98   private static final String SDC_CSAR = "sdc-csar";
99   private static final String SDC_HEAT = "sdc-heat";
100
101
102   private VersioningManager versioningManager;
103   private VendorSoftwareProductManager vendorSoftwareProductManager;
104   private OrchestrationTemplateCandidateManager candidateManager;
105
106   private TestingAccessConfig accessConfig;
107   private List<RemoteTestingEndpointDefinition> endpoints;
108
109   private RestTemplate restTemplate;
110
111   public ExternalTestingManagerImpl() {
112     restTemplate = new RestTemplate();
113   }
114
115   ExternalTestingManagerImpl(VersioningManager versioningManager,
116                              VendorSoftwareProductManager vendorSoftwareProductManager,
117                              OrchestrationTemplateCandidateManager candidateManager) {
118     this();
119     this.versioningManager = versioningManager;
120     this.vendorSoftwareProductManager = vendorSoftwareProductManager;
121     this.candidateManager = candidateManager;
122   }
123
124   /**
125    * Read the configuration from the yaml file for this bean.  If we get an exception during load,
126    * don't force an error starting SDC but log a warning.  Do no warm...
127    */
128   @PostConstruct
129   public void init() {
130
131     if (versioningManager == null) {
132       versioningManager = VersioningManagerFactory.getInstance().createInterface();
133     }
134     if (vendorSoftwareProductManager == null) {
135       vendorSoftwareProductManager =
136               VspManagerFactory.getInstance().createInterface();
137     }
138     if (candidateManager == null) {
139       candidateManager =
140               OrchestrationTemplateCandidateManagerFactory.getInstance().createInterface();
141     }
142
143     loadConfig();
144   }
145
146   private Stream<RemoteTestingEndpointDefinition> mapEndpointString(String ep) {
147     RemoteTestingEndpointDefinition rv = new RemoteTestingEndpointDefinition();
148     String[] cfg = ep.split(",");
149     if (cfg.length < 4) {
150       logger.error("invalid endpoint definition {}", ep);
151       return Stream.empty();
152     }
153     else {
154       rv.setId(cfg[0]);
155       rv.setTitle(cfg[1]);
156       rv.setEnabled("true".equals(cfg[2]));
157       rv.setUrl(cfg[3]);
158       if (cfg.length > 4) {
159         rv.setScenarioFilter(cfg[4]);
160       }
161       if (cfg.length > 5) {
162         rv.setApiKey(cfg[5]);
163       }
164       return Stream.of(rv);
165     }
166   }
167
168   /**
169    * Load the configuration for this component.  When the SDC onboarding backend
170    * runs, it gets a system property called config.location.  We can use that
171    * to locate the config-externaltesting.yaml file.
172    */
173   private void loadConfig() {
174     String loc = System.getProperty("config.location");
175     File file = new File(loc, "externaltesting-configuration.yaml");
176     try (InputStream fileInput = new FileInputStream(file)) {
177       YamlUtil yamlUtil = new YamlUtil();
178       accessConfig = yamlUtil.yamlToObject(fileInput, TestingAccessConfig.class);
179
180       if (logger.isInfoEnabled()) {
181         String s = new ObjectMapper().writeValueAsString(accessConfig);
182         logger.info("loaded external testing config {}", s);
183       }
184
185       endpoints = accessConfig.getEndpoints().stream()
186               .flatMap(this::mapEndpointString)
187               .collect(Collectors.toList());
188
189       if (logger.isInfoEnabled()) {
190         String s = new ObjectMapper().writeValueAsString(endpoints);
191         logger.info("processed external testing config {}", s);
192       }
193     }
194     catch (IOException ex) {
195       logger.error("failed to read external testing config.  Disabling the feature", ex);
196       accessConfig = new TestingAccessConfig();
197       accessConfig.setEndpoints(new ArrayList<>());
198       accessConfig.setClient(new ClientConfiguration());
199       accessConfig.getClient().setEnabled(false);
200       endpoints = new ArrayList<>();
201     }
202   }
203
204   /**
205    * Return the configuration of this feature that we want to
206    * expose to the client.  Treated as a JSON blob for flexibility.
207    */
208   @Override
209   public ClientConfiguration getConfig() {
210     ClientConfiguration cc = null;
211     if (accessConfig != null) {
212       cc = accessConfig.getClient();
213     }
214     if (cc == null) {
215       cc = new ClientConfiguration();
216       cc.setEnabled(false);
217     }
218     return cc;
219   }
220
221   /**
222    * To allow for functional testing, we let a caller invoke
223    * a setConfig request to enable/disable the client.  This
224    * new value is not persisted.
225    * @return new client configuration
226    */
227   @Override
228   public ClientConfiguration setConfig(ClientConfiguration cc) {
229     if (accessConfig == null) {
230       accessConfig = new TestingAccessConfig();
231     }
232     accessConfig.setClient(cc);
233     return getConfig();
234   }
235
236   /**
237    * To allow for functional testing, we let a caller invoke
238    * a setEndpoints request to configure where the BE makes request to.
239    * @return new endpoint definitions.
240    */
241   @Override
242   public List<RemoteTestingEndpointDefinition> setEndpoints(List<RemoteTestingEndpointDefinition> endpoints) {
243     this.endpoints = endpoints;
244     return this.getEndpoints();
245   }
246
247
248
249   @Override
250   public TestTreeNode getTestCasesAsTree() {
251     TestTreeNode root = new TestTreeNode("root", "root");
252
253     // quick out in case of non-configured SDC
254     if (endpoints == null) {
255       return root;
256     }
257
258     for (RemoteTestingEndpointDefinition ep : endpoints) {
259       if (ep.isEnabled()) {
260         buildTreeFromEndpoint(ep, root);
261       }
262     }
263     return root;
264   }
265
266   private void buildTreeFromEndpoint(RemoteTestingEndpointDefinition ep, TestTreeNode root) {
267     try {
268       logger.debug("process endpoint {}", ep.getId());
269       getScenarios(ep.getId()).stream().filter(s ->
270               ((ep.getScenarioFilter() == null) || ep.getScenarioFilterPattern().matcher(s.getName()).matches()))
271               .forEach(s -> {
272                 addScenarioToTree(root, s);
273                 getTestSuites(ep.getId(), s.getName()).forEach(suite -> addSuiteToTree(root, s, suite));
274                 getTestCases(ep.getId(), s.getName()).forEach(tc -> {
275                   try {
276                     VtpTestCase details = getTestCase(ep.getId(), s.getName(), tc.getTestSuiteName(), tc.getTestCaseName());
277                     addTestCaseToTree(root, ep.getId(), s.getName(), tc.getTestSuiteName(), details);
278                   }
279                   catch (@SuppressWarnings("squid:S1166") ExternalTestingException ex) {
280                     // Not logging stack trace on purpose.  VTP was throwing exceptions for certain test cases.
281                     logger.warn("failed to load test case {}", tc.getTestCaseName());
282                   }
283                 });
284               });
285     }
286     catch (ExternalTestingException ex) {
287       logger.error("unable to contact testing endpoint {}", ep.getId(), ex);
288     }
289   }
290
291   private Optional<TestTreeNode> findNamedChild(TestTreeNode root, String name) {
292     if (root.getChildren() == null) {
293       return Optional.empty();
294     }
295     return root.getChildren().stream().filter(n->n.getName().equals(name)).findFirst();
296   }
297
298   /**
299    * Find the place in the tree to add the test case.
300    * @param root root of the tree.
301    * @param endpointName name of the endpoint to assign to the test case.
302    * @param scenarioName scenario to add this case to
303    * @param testSuiteName suite in the scenario to add this case to
304    * @param tc test case to add.
305    */
306   private void addTestCaseToTree(TestTreeNode root, String endpointName, String scenarioName, String testSuiteName, VtpTestCase tc) {
307     // return quickly.
308     if (tc == null) {
309       return;
310     }
311     findNamedChild(root, scenarioName)
312             .ifPresent(scenarioNode -> findNamedChild(scenarioNode, testSuiteName)
313                     .ifPresent(suiteNode -> {
314                       massageTestCaseForUI(tc, endpointName, scenarioName);
315                       if (suiteNode.getTests() == null) {
316                         suiteNode.setTests(new ArrayList<>());
317                       }
318                       suiteNode.getTests().add(tc);
319                     }));
320   }
321
322   private void massageTestCaseForUI(VtpTestCase testcase, String endpoint, String scenario) {
323     testcase.setEndpoint(endpoint);
324     // VTP workaround.
325     if (testcase.getScenario() == null) {
326       testcase.setScenario(scenario);
327     }
328   }
329
330   /**
331    * Add the test suite to the tree at the appropriate place if it does not already exist in the tree.
332    * @param root root of the tree.
333    * @param scenario scenario under which this suite should be placed
334    * @param suite test suite to add.
335    */
336   private void addSuiteToTree(final TestTreeNode root, final VtpNameDescriptionPair scenario, final VtpNameDescriptionPair suite) {
337     findNamedChild(root, scenario.getName()).ifPresent(parent -> {
338       if (parent.getChildren() == null) {
339         parent.setChildren(new ArrayList<>());
340       }
341       if (parent.getChildren().stream().noneMatch(n -> StringUtils.equals(n.getName(), suite.getName()))) {
342         parent.getChildren().add(new TestTreeNode(suite.getName(), suite.getDescription()));
343       }
344     });
345   }
346
347   /**
348    * Add the scenario to the tree if it does not already exist.
349    * @param root root of the tree.
350    * @param s scenario to add.
351    */
352   private void addScenarioToTree(TestTreeNode root, VtpNameDescriptionPair s) {
353     logger.debug("addScenario {} to {} with {}", s.getName(), root.getName(), root.getChildren());
354     if (root.getChildren() == null) {
355       root.setChildren(new ArrayList<>());
356     }
357     if (root.getChildren().stream().noneMatch(n->StringUtils.equals(n.getName(),s.getName()))) {
358       logger.debug("createScenario {} in {}", s.getName(), root.getName());
359       root.getChildren().add(new TestTreeNode(s.getName(), s.getDescription()));
360     }
361   }
362
363   /**
364    * Get the list of endpoints defined to the testing manager.
365    * @return list of endpoints or empty list if the manager is not configured.
366    */
367   public List<RemoteTestingEndpointDefinition> getEndpoints() {
368     if (endpoints != null) {
369       return endpoints.stream()
370               .filter(RemoteTestingEndpointDefinition::isEnabled)
371               .collect(Collectors.toList());
372     }
373     else {
374       return new ArrayList<>();
375     }
376   }
377
378   /**
379    * Code shared by getScenarios and getTestSuites.
380    */
381   private List<VtpNameDescriptionPair> returnNameDescriptionPairFromUrl(String url) {
382     ParameterizedTypeReference<List<VtpNameDescriptionPair>> t = new ParameterizedTypeReference<List<VtpNameDescriptionPair>>() {};
383     List<VtpNameDescriptionPair> rv = proxyGetRequestToExternalTestingSite(url, t);
384     if (rv == null) {
385       rv = new ArrayList<>();
386     }
387     return rv;
388   }
389
390   /**
391    * Get the list of scenarios at a given endpoint.
392    */
393   public List<VtpNameDescriptionPair> getScenarios(final String endpoint) {
394     String url = buildEndpointUrl(VTP_SCENARIOS_URI, endpoint, ArrayUtils.EMPTY_STRING_ARRAY);
395     return returnNameDescriptionPairFromUrl(url);
396   }
397
398   /**
399    * Get the list of test suites for an endpoint for the given scenario.
400    */
401   public List<VtpNameDescriptionPair> getTestSuites(final String endpoint, final String scenario) {
402     String url = buildEndpointUrl(VTP_TESTSUITE_URI, endpoint, new String[] {scenario});
403     return returnNameDescriptionPairFromUrl(url);
404   }
405
406   /**
407    * Get the list of test cases under a scenario.  This is the VTP API.  It would
408    * seem better to get the list of cases under a test suite but that is not supported.
409    */
410   @Override
411   public List<VtpTestCase> getTestCases(String endpoint, String scenario) {
412     String url = buildEndpointUrl(VTP_TESTCASES_URI, endpoint, new String[] {scenario});
413     ParameterizedTypeReference<List<VtpTestCase>> t = new ParameterizedTypeReference<List<VtpTestCase>>() {};
414     List<VtpTestCase> rv = proxyGetRequestToExternalTestingSite(url, t);
415     if (rv == null) {
416       rv = new ArrayList<>();
417     }
418     return rv;
419   }
420
421   /**
422    * Get a test case definition.
423    */
424   @Override
425   public VtpTestCase getTestCase(String endpoint, String scenario, String testSuite, String testCaseName) {
426     String url = buildEndpointUrl(VTP_TESTCASE_URI, endpoint, new String[] {scenario, testSuite, testCaseName});
427     ParameterizedTypeReference<VtpTestCase> t = new ParameterizedTypeReference<VtpTestCase>() {};
428     return proxyGetRequestToExternalTestingSite(url, t);
429   }
430
431   /**
432    * Return the results of a previous test execution.
433    * @param endpoint endpoint to query
434    * @param executionId execution to query.
435    * @return execution response from testing endpoint.
436    */
437   @Override
438   public VtpTestExecutionResponse getExecution(String endpoint,String executionId) {
439     String url = buildEndpointUrl(VTP_EXECUTION_URI, endpoint, new String[] {executionId});
440     ParameterizedTypeReference<VtpTestExecutionResponse> t = new ParameterizedTypeReference<VtpTestExecutionResponse>() {};
441     return proxyGetRequestToExternalTestingSite(url, t);
442   }
443
444   /**
445    * Execute a set of tests at a given endpoint.
446    * @param endpointName name of the endpoint
447    * @param testsToRun set of tests to run
448    * @return list of execution responses.
449    */
450   private List<VtpTestExecutionResponse> execute(final String endpointName, final List<VtpTestExecutionRequest> testsToRun, String requestId) {
451     if (endpoints == null) {
452       throw new ExternalTestingException(INVALIDATE_STATE_ERROR_CODE, 500, NO_ACCESS_CONFIGURATION_DEFINED);
453     }
454
455     RemoteTestingEndpointDefinition endpoint = endpoints.stream()
456             .filter(e -> StringUtils.equals(endpointName, e.getId()))
457             .findFirst()
458             .orElseThrow(() -> new ExternalTestingException(NO_SUCH_ENDPOINT_ERROR_CODE, 400, "No endpoint named " + endpointName + " is defined"));
459
460     // if the endpoint requires an API key, specify it in the headers.
461     HttpHeaders headers = new HttpHeaders();
462     if (endpoint.getApiKey() != null) {
463       headers.add("X-API-Key", endpoint.getApiKey());
464     }
465     headers.setContentType(MediaType.MULTIPART_FORM_DATA);
466
467     // build the body.
468     MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
469
470     for(VtpTestExecutionRequest test: testsToRun) {
471       if ((test.getParameters() != null) &&
472               (test.getParameters().containsKey(SDC_CSAR) || test.getParameters().containsKey(SDC_HEAT))) {
473         attachArchiveContent(test, body);
474       }
475     }
476
477     try {
478       // remove the endpoint from the test request since that is a FE/BE attribute
479       testsToRun.forEach(t -> t.setEndpoint(null));
480
481       body.add("executions", new ObjectMapper().writeValueAsString(testsToRun));
482     }
483     catch (IOException ex) {
484       logger.error("exception converting tests to string", ex);
485       VtpTestExecutionResponse err = new VtpTestExecutionResponse();
486       err.setHttpStatus(500);
487       err.setCode(TESTING_HTTP_ERROR_CODE);
488       err.setMessage("Execution failed due to " + ex.getMessage());
489       return Collections.singletonList(err);
490     }
491
492     // form and send request.
493     HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
494     String url = buildEndpointUrl(VTP_EXECUTIONS_URI, endpointName, ArrayUtils.EMPTY_STRING_ARRAY);
495     UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url);
496     if (requestId != null) {
497       builder = builder.queryParam("requestId", requestId);
498     }
499     ParameterizedTypeReference<List<VtpTestExecutionResponse>> t = new ParameterizedTypeReference<List<VtpTestExecutionResponse>>() {};
500     try {
501       return proxyRequestToExternalTestingSite(builder.toUriString(), requestEntity, t);
502     }
503     catch (ExternalTestingException ex) {
504       logger.error("exception caught invoking endpoint {}", endpointName, ex);
505       VtpTestExecutionResponse err = new VtpTestExecutionResponse();
506       err.setHttpStatus(ex.getHttpStatus());
507       err.setCode(TESTING_HTTP_ERROR_CODE);
508       err.setMessage(ex.getMessageCode() + ": " + ex.getDetail());
509       return Collections.singletonList(err);
510     }
511   }
512
513
514   /**
515    * Execute tests splitting them across endpoints and collecting the results.
516    * @param testsToRun list of tests to be executed.
517    * @return collection of result objects.
518    */
519   @Override
520   public List<VtpTestExecutionResponse> execute(final List<VtpTestExecutionRequest> testsToRun, String requestId) {
521     if (endpoints == null) {
522       throw new ExternalTestingException(INVALIDATE_STATE_ERROR_CODE, 500, NO_ACCESS_CONFIGURATION_DEFINED);
523     }
524
525     // partition the requests by endpoint.
526     Map<String, List<VtpTestExecutionRequest>> partitions =
527             testsToRun.stream().collect(Collectors.groupingBy(VtpTestExecutionRequest::getEndpoint));
528
529     // process each group and collect the results.
530     return partitions.entrySet().stream()
531             .flatMap(e -> execute(e.getKey(), e.getValue(), requestId).stream())
532             .collect(Collectors.toList());
533   }
534
535   /**
536    * Return URL with endpoint url as prefix.
537    * @param format format string.
538    * @param endpointName endpoint to address
539    * @param args args for format.
540    * @return qualified url.
541    */
542   private String buildEndpointUrl(String format, String endpointName, String[] args) {
543     if (endpoints != null) {
544       RemoteTestingEndpointDefinition ep = endpoints.stream()
545               .filter(e -> e.isEnabled() && e.getId().equals(endpointName))
546               .findFirst()
547               .orElseThrow(() -> new ExternalTestingException(NO_SUCH_ENDPOINT_ERROR_CODE, 500, "No endpoint named " + endpointName + " is defined")
548               );
549
550       Object[] newArgs = ArrayUtils.add(args, 0, ep.getUrl());
551       return String.format(format, newArgs);
552     }
553     throw new ExternalTestingException(INVALIDATE_STATE_ERROR_CODE, 500, NO_ACCESS_CONFIGURATION_DEFINED);
554   }
555
556   /**
557    * Proxy a get request to a testing endpoint.
558    * @param url URL to invoke.
559    * @param responseType type of response expected.
560    * @param <T> type of response expected
561    * @return instance of <T> parsed from the JSON response from endpoint.
562    */
563   private <T> T proxyGetRequestToExternalTestingSite(String url, ParameterizedTypeReference<T> responseType) {
564     return proxyRequestToExternalTestingSite(url, null, responseType);
565   }
566
567   /**
568    * Make the actual HTTP post (using Spring RestTemplate) to an endpoint.
569    * @param url URL to the endpoint
570    * @param request optional request body to send
571    * @param responseType expected type
572    * @param <T> extended type
573    * @return instance of expected type
574    */
575   private <R,T> T proxyRequestToExternalTestingSite(String url, HttpEntity<R> request, ParameterizedTypeReference<T> responseType) {
576     if (request != null) {
577       logger.debug("POST request to {} with {} for {}", url, request, responseType.getType().getTypeName());
578     }
579     else {
580       logger.debug("GET request to {} for {}", url, responseType.getType().getTypeName());
581     }
582     SimpleClientHttpRequestFactory rf =
583             (SimpleClientHttpRequestFactory) restTemplate.getRequestFactory();
584     if (rf != null) {
585       rf.setReadTimeout(10000);
586       rf.setConnectTimeout(10000);
587     }
588     ResponseEntity<T> re;
589     try {
590       if (request != null) {
591         re = restTemplate.exchange(url, HttpMethod.POST, request, responseType);
592       } else {
593         re = restTemplate.exchange(url, HttpMethod.GET, null, responseType);
594       }
595     }
596     catch (HttpStatusCodeException ex) {
597       // make my own exception out of this.
598       logger.warn("Unexpected HTTP Status from endpoint {}", ex.getRawStatusCode());
599       if ((ex.getResponseHeaders().getContentType() != null) &&
600               ((ex.getResponseHeaders().getContentType().isCompatibleWith(MediaType.APPLICATION_JSON)) ||
601                       (ex.getResponseHeaders().getContentType().isCompatibleWith(MediaType.parseMediaType("application/problem+json"))))) {
602         String s = ex.getResponseBodyAsString();
603         logger.warn("endpoint body content is {}", s);
604         try {
605           JsonObject o = new GsonBuilder().create().fromJson(s, JsonObject.class);
606           throw buildTestingException(ex.getRawStatusCode(), o);
607         }
608         catch (JsonParseException e) {
609           logger.warn("unexpected JSON response", e);
610           throw new ExternalTestingException(ENDPOINT_ERROR_CODE, ex.getStatusCode().value(), ex.getResponseBodyAsString(), ex);
611         }
612       }
613       else {
614         throw new ExternalTestingException(ENDPOINT_ERROR_CODE, ex.getStatusCode().value(), ex.getResponseBodyAsString(), ex);
615       }
616     }
617     catch (ResourceAccessException ex) {
618       throw new ExternalTestingException(ENDPOINT_ERROR_CODE, 500, ex.getMessage(), ex);
619     }
620     catch (Exception ex) {
621       throw new ExternalTestingException(ENDPOINT_ERROR_CODE, 500, "Generic Exception " + ex.getMessage(), ex);
622     }
623     if (re != null) {
624       logger.debug("http status of {} from external testing entity {}", re.getStatusCodeValue(), url);
625       return re.getBody();
626     }
627     else {
628       logger.error("null response from endpoint");
629       return null;
630     }
631   }
632
633   /**
634    * Errors from the endpoint could conform to the expected ETSI body or not.
635    * Here we try to handle various response body elements.
636    * @param statusCode http status code in response.
637    * @param o JSON object parsed from the http response body
638    * @return Testing error body that should be returned to the caller
639    */
640   private ExternalTestingException buildTestingException(int statusCode, JsonObject o) {
641     String code = null;
642     String message = null;
643
644     if (o.has(CODE)) {
645       code = o.get(CODE).getAsString();
646     }
647     else if (o.has(ERROR)) {
648       code = o.get(ERROR).getAsString();
649     }
650     else {
651       if (o.has(HTTP_STATUS)) {
652         code = o.get(HTTP_STATUS).getAsJsonPrimitive().getAsString();
653       }
654     }
655     if (o.has(MESSAGE)) {
656       if (!o.get(MESSAGE).isJsonNull()) {
657         message = o.get(MESSAGE).getAsString();
658       }
659     }
660     else if (o.has(DETAIL)) {
661       message = o.get(DETAIL).getAsString();
662     }
663     if (o.has(PATH)) {
664       if (message == null) {
665         message = o.get(PATH).getAsString();
666       }
667       else {
668         message = message + " " + o.get(PATH).getAsString();
669       }
670     }
671     return new ExternalTestingException(code, statusCode, message);
672   }
673
674   void attachArchiveContent(VtpTestExecutionRequest test, MultiValueMap<String, Object> body) {
675     Map<String, String> params = test.getParameters();
676     String vspId = params.get(VSP_ID);
677     String version = params.get(VSP_VERSION);
678
679     try {
680       extractMetadata(test, body, vspId, version);
681     } catch (IOException ex) {
682       logger.error("metadata extraction failed", ex);
683     }
684   }
685
686   /**
687    * Extract the metadata from the VSP CSAR file.
688    *
689    * @param requestItem item to add metadata to for processing
690    * @param vspId       VSP identifier
691    * @param version     VSP version
692    */
693   private void extractMetadata(VtpTestExecutionRequest requestItem, MultiValueMap<String, Object> body, String vspId, String version) throws IOException {
694
695     Version ver = new Version(version);
696     logger.debug("attempt to retrieve archive for VSP {} version {}", vspId, ver.getId());
697
698     Optional<Pair<String, byte[]>> ozip = candidateManager.get(vspId, ver);
699     if (!ozip.isPresent()) {
700       ozip = vendorSoftwareProductManager.get(vspId, ver);
701     }
702
703     if (!ozip.isPresent()) {
704       List<Version> versions = versioningManager.list(vspId);
705       String knownVersions = versions
706               .stream()
707               .map(v -> String.format("%d.%d: %s (%s)", v.getMajor(), v.getMinor(), v.getStatus(), v.getId()))
708               .collect(Collectors.joining("\n"));
709
710       String detail = String.format("Archive processing failed.  Unable to find archive for VSP ID %s and Version %s.  Known versions are:\n%s",
711               vspId, version, knownVersions);
712
713       throw new ExternalTestingException(SDC_RESOLVER_ERR, 500, detail);
714     }
715
716     // safe here to do get.
717     Pair<String, byte[]> zip = ozip.get();
718     processArchive(requestItem, body, zip.getRight());
719   }
720
721   private void processArchive(final VtpTestExecutionRequest test, final MultiValueMap<String, Object> body, final byte[] zip) {
722
723     // We need to make one pass through the zip input stream.  Pull out files that match our expectations into a temporary
724     // map that we can process over.   These are not huge files so we shouldn't need to worry about memory.
725
726     List<String> extensions = Arrays.asList(".yaml", ".meta", ".yml", ".json", ".env");
727     final Map<String, byte[]> contentWeCareAbout = extractRelevantContent(zip, extensions);
728
729     // VTP does not support concurrent executions of the same test with the same associated file name.
730     // It writes files to /tmp and if we were to send two requests with the same file, the results are unpredictable.
731     String key = UUID.randomUUID().toString();
732     key = key.substring(0, key.indexOf('-'));
733
734     // if there's a MANIFEST.json file, we're dealing with a heat archive.
735     // otherwise, we will treat it as a CSAR.
736     if (contentWeCareAbout.containsKey(MANIFEST_JSON)) {
737       byte[] data = processHeatArchive(contentWeCareAbout);
738       if (data != null) {
739         body.add("file", new NamedByteArrayResource(data, key + ".heat.zip"));
740         test.getParameters().put(SDC_HEAT, FILE_URL_PREFIX + key + ".heat.zip");
741       }
742     }
743     else {
744       byte[] data = processCsarArchive(contentWeCareAbout);
745       if ((data != null) && (data.length != 0)) {
746         body.add("file", new NamedByteArrayResource(data, key + ".csar.zip"));
747         test.getParameters().put(SDC_CSAR, FILE_URL_PREFIX + key + ".csar.zip");
748       }
749     }
750   }
751
752   /**
753    * Process the archive as a heat archive.  Load the MANIFEST.json file and pull out the referenced
754    * heat and environment files.
755    * @param content relevant content from the heat archive.
756    * @return byte array of client to send to endpoint.
757    */
758   private byte[] processHeatArchive(Map<String,byte[]> content) {
759     byte[] manifestBytes = content.get(MANIFEST_JSON);
760     ManifestContent manifest = JsonUtil.json2Object(new String(manifestBytes), ManifestContent.class);
761
762     try(ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
763       try(ZipOutputStream zipOutput = new ZipOutputStream(baos)) {
764         for (FileData item : manifest.getData()) {
765           processManifestItem(item, zipOutput, content);
766         }
767
768         return baos.toByteArray();
769       }
770     } catch (IOException ex) {
771       logger.error("IO Exception parsing zip", ex);
772       throw new ExternalTestingException(SDC_RESOLVER_ERR, 500, ex.getMessage());
773     }
774   }
775
776   private void processManifestItem(FileData item, ZipOutputStream zipOutput, Map<String,byte[]> contentMap) throws IOException {
777     if ((item.getType() == FileData.Type.HEAT) || (item.getType() == FileData.Type.HEAT_ENV)) {
778       byte[] content = contentMap.get(item.getFile());
779       if (content == null) {
780         logger.warn("manifest included {} but not in content extracted", item.getFile());
781       }
782       else {
783         ZipEntry zi = new ZipEntry(item.getFile());
784         zipOutput.putNextEntry(zi);
785         zipOutput.write(content);
786         zipOutput.closeEntry();
787       }
788
789       // recurse
790       if (item.getData() != null) {
791         for(FileData subitem: item.getData()) {
792           processManifestItem(subitem, zipOutput, contentMap);
793         }
794       }
795     }
796   }
797
798   /**
799    * Process the archive as a CSAR file.
800    * @param content relevant extracted content.
801    * @return byte array of client to send to endpoint.
802    */
803   private byte[] processCsarArchive(Map<String,byte[]> content) {
804     // look for the entry point file.
805     String fileToGet = null;
806     if (content.containsKey(TOSCA_META)) {
807       fileToGet = getEntryDefinitionPointer(content.get(TOSCA_META)).orElse(null);
808     }
809     if (fileToGet == null) {
810       // fall back to the SDC standard location.  not required to be here though...
811       fileToGet = MAIN_SERVICE_TEMPLATE_YAML_FILE_NAME;
812     }
813
814     if (!content.containsKey(fileToGet)) {
815       // user story says to let the call to the VTP go through without the attachment.
816       return ArrayUtils.EMPTY_BYTE_ARRAY;
817     }
818
819     try(ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
820       try(ZipOutputStream zipOutput = new ZipOutputStream(baos)) {
821         processCsarArchiveEntry(fileToGet, zipOutput, content);
822         return baos.toByteArray();
823       }
824     } catch (IOException ex) {
825       logger.error("IO Exception parsing zip", ex);
826       throw new ExternalTestingException(SDC_RESOLVER_ERR, 500, ex.getMessage());
827     }
828   }
829
830   /**
831    * Add the named file (if it exists) from the contentMap into the output zip to send to VTP.
832    * If the file is a yaml file, peek inside and also add any imported files.
833    * @param filename name to apply to the zip entry being created
834    * @param zipOutput zip output stream to append new entry to
835    * @param contentMap map of content we are processing
836    * @throws IOException thrown in the event of processing errors.
837    */
838   private void processCsarArchiveEntry(String filename, ZipOutputStream zipOutput, Map<String, byte[]> contentMap) throws IOException {
839     byte[] content = contentMap.get(filename);
840     if (content == null) {
841       // no such content, just return.
842       return;
843     }
844
845     ZipEntry zi = new ZipEntry(filename);
846     zipOutput.putNextEntry(zi);
847     zipOutput.write(content);
848     zipOutput.closeEntry();
849
850     // if this is a yaml file, we should peek inside for includes.
851     if (filename.endsWith(".yaml") || filename.endsWith(".yml")) {
852       Yaml yaml = new Yaml();
853       @SuppressWarnings("unchecked")
854       Map<String, Object> yamlContent = (Map<String, Object>) yaml.load(new ByteArrayInputStream(content));
855       if (!yamlContent.containsKey("imports")) {
856         return;
857       }
858
859       Object imports = yamlContent.get("imports");
860       if (imports instanceof ArrayList) {
861         @SuppressWarnings("unchecked") ArrayList<String> lst = (ArrayList<String>) imports;
862         for (String imp : lst) {
863           File f = new File(filename);
864           File impFile = new File(f.getParent(), imp);
865           logger.debug("look for import {} with {}", imp, impFile.getPath());
866           processCsarArchiveEntry(impFile.getPath(), zipOutput, contentMap);
867         }
868       }
869       else {
870         logger.warn("archive {} contains imports but it is not an array.  Unexpected, this is.", filename);
871       }
872     }
873   }
874
875   private Optional<String> getEntryDefinitionPointer(byte[] toscaMetadataFile) {
876     try {
877       Properties p = new Properties();
878       p.load(new ByteArrayInputStream(toscaMetadataFile));
879       return Optional.ofNullable(p.getProperty(TOSCA_META_ENTRY_DEFINITIONS));
880     }
881     catch (IOException ex) {
882       logger.error("failed to process tosca metadata file {}", TOSCA_META, ex);
883     }
884
885     return Optional.empty();
886   }
887
888   /**
889    * We don't want to send the entire CSAR file to VTP.  Here we take a pass through the
890    * archive (heat/csar) file and pull out the files we care about.
891    * @param zip csar/heat zip to iterate over
892    * @return relevant content from the archive file as a map.
893    */
894   private Map<String, byte[]> extractRelevantContent(final byte[] zip, final List<String> extensions) {
895     final Map<String, byte[]> rv = new HashMap<>();  // FYI, rv = return value.
896     try (ByteArrayInputStream is = new ByteArrayInputStream(zip)) {
897       try (ZipInputStream zipStream = new ZipInputStream(is)) {
898         ZipEntry entry;
899         while ((entry = zipStream.getNextEntry()) != null) {
900           final String entryName = entry.getName();
901
902           // NOTE: leaving this debugging in for dublin...
903           logger.debug("archive contains entry {}", entryName);
904
905           extractIfMatching(extensions, rv, zipStream, entryName);
906         }
907       }
908     }
909     catch (IOException ex) {
910       logger.error("error encountered processing archive", ex);
911       throw new ExternalTestingException(SDC_RESOLVER_ERR, 500, ex.getMessage());
912     }
913     return rv;
914   }
915
916   private void extractIfMatching(List<String> extensions, Map<String, byte[]> rv, ZipInputStream zipStream, String entryName) throws IOException {
917     int idx = entryName.lastIndexOf('.');
918     if ((idx >= 0) && (extensions.contains(entryName.substring(idx)))) {
919       byte[] content = IOUtils.toByteArray(zipStream);
920       rv.put(entryName, content);
921     }
922   }
923
924   /**
925    * We need to name the byte array we add to the multipart request sent to the VTP.
926    */
927   @EqualsAndHashCode(callSuper = false)
928   protected class NamedByteArrayResource extends ByteArrayResource {
929     private String filename;
930     NamedByteArrayResource(byte[] bytes, String filename) {
931       super(bytes, filename);
932       this.filename = filename;
933     }
934     @Override
935     public String getFilename() {
936       return this.filename;
937     }
938   }
939
940
941 }