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