2 * Copyright © 2019 iconectiv
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package org.openecomp.core.externaltesting.impl;
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;
59 import javax.annotation.PostConstruct;
62 import java.util.stream.Collectors;
63 import java.util.stream.Stream;
64 import java.util.zip.ZipEntry;
65 import java.util.zip.ZipOutputStream;
67 public class ExternalTestingManagerImpl implements ExternalTestingManager {
69 private Logger logger = LoggerFactory.getLogger(ExternalTestingManagerImpl.class);
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";
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";
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";
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";
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";
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");
107 private VersioningManager versioningManager;
108 private VendorSoftwareProductManager vendorSoftwareProductManager;
109 private OrchestrationTemplateCandidateManager candidateManager;
111 private TestingAccessConfig accessConfig;
112 private List<RemoteTestingEndpointDefinition> endpoints;
114 private RestTemplate restTemplate;
116 public ExternalTestingManagerImpl() {
117 restTemplate = new RestTemplate();
120 ExternalTestingManagerImpl(VersioningManager versioningManager,
121 VendorSoftwareProductManager vendorSoftwareProductManager,
122 OrchestrationTemplateCandidateManager candidateManager) {
124 this.versioningManager = versioningManager;
125 this.vendorSoftwareProductManager = vendorSoftwareProductManager;
126 this.candidateManager = candidateManager;
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...
136 if (versioningManager == null) {
137 versioningManager = VersioningManagerFactory.getInstance().createInterface();
139 if (vendorSoftwareProductManager == null) {
140 vendorSoftwareProductManager =
141 VspManagerFactory.getInstance().createInterface();
143 if (candidateManager == null) {
145 OrchestrationTemplateCandidateManagerFactory.getInstance().createInterface();
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();
161 rv.setEnabled("true".equals(cfg[2]));
163 if (cfg.length > 4) {
164 rv.setScenarioFilter(cfg[4]);
166 if (cfg.length > 5) {
167 rv.setApiKey(cfg[5]);
169 return Stream.of(rv);
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.
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);
185 if (logger.isInfoEnabled()) {
186 String s = new ObjectMapper().writeValueAsString(accessConfig);
187 logger.info("loaded external testing config {}", s);
190 endpoints = accessConfig.getEndpoints().stream()
191 .flatMap(this::mapEndpointString)
192 .collect(Collectors.toList());
194 if (logger.isInfoEnabled()) {
195 String s = new ObjectMapper().writeValueAsString(endpoints);
196 logger.info("processed external testing config {}", s);
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<>();
210 * Return the configuration of this feature that we want to
211 * expose to the client. Treated as a JSON blob for flexibility.
214 public ClientConfiguration getConfig() {
215 ClientConfiguration cc = null;
216 if (accessConfig != null) {
217 cc = accessConfig.getClient();
220 cc = new ClientConfiguration();
221 cc.setEnabled(false);
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
233 public ClientConfiguration setConfig(ClientConfiguration cc) {
234 if (accessConfig == null) {
235 accessConfig = new TestingAccessConfig();
237 accessConfig.setClient(cc);
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.
247 public List<RemoteTestingEndpointDefinition> setEndpoints(List<RemoteTestingEndpointDefinition> endpoints) {
248 this.endpoints = endpoints;
249 return this.getEndpoints();
255 public TestTreeNode getTestCasesAsTree() {
256 TestTreeNode root = new TestTreeNode("root", "root");
258 // quick out in case of non-configured SDC
259 if (endpoints == null) {
263 for (RemoteTestingEndpointDefinition ep : endpoints) {
264 if (ep.isEnabled()) {
265 buildTreeFromEndpoint(ep, root);
271 private void buildTreeFromEndpoint(RemoteTestingEndpointDefinition ep, TestTreeNode root) {
273 logger.debug("process endpoint {}", ep.getId());
274 getScenarios(ep.getId()).stream().filter(s ->
275 ((ep.getScenarioFilter() == null) || ep.getScenarioFilterPattern().matcher(s.getName()).matches()))
277 addScenarioToTree(root, s);
278 getTestSuites(ep.getId(), s.getName()).forEach(suite -> addSuiteToTree(root, s, suite));
279 getTestCases(ep.getId(), s.getName()).forEach(tc -> {
281 VtpTestCase details = getTestCase(ep.getId(), s.getName(), tc.getTestSuiteName(), tc.getTestCaseName());
282 addTestCaseToTree(root, ep.getId(), s.getName(), tc.getTestSuiteName(), details);
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());
291 catch (ExternalTestingException ex) {
292 logger.error("unable to contact testing endpoint {}", ep.getId(), ex);
296 private Optional<TestTreeNode> findNamedChild(TestTreeNode root, String name) {
297 if (root.getChildren() == null) {
298 return Optional.empty();
300 return root.getChildren().stream().filter(n->n.getName().equals(name)).findFirst();
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.
311 private void addTestCaseToTree(TestTreeNode root, String endpointName, String scenarioName, String testSuiteName, VtpTestCase tc) {
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<>());
323 suiteNode.getTests().add(tc);
327 private void massageTestCaseForUI(VtpTestCase testcase, String endpoint, String scenario) {
328 testcase.setEndpoint(endpoint);
330 if (testcase.getScenario() == null) {
331 testcase.setScenario(scenario);
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.
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<>());
346 if (parent.getChildren().stream().noneMatch(n -> StringUtils.equals(n.getName(), suite.getName()))) {
347 parent.getChildren().add(new TestTreeNode(suite.getName(), suite.getDescription()));
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.
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<>());
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()));
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.
372 public List<RemoteTestingEndpointDefinition> getEndpoints() {
373 if (endpoints != null) {
374 return endpoints.stream()
375 .filter(RemoteTestingEndpointDefinition::isEnabled)
376 .collect(Collectors.toList());
379 return new ArrayList<>();
384 * Code shared by getScenarios and getTestSuites.
386 private List<VtpNameDescriptionPair> returnNameDescriptionPairFromUrl(String url) {
387 ParameterizedTypeReference<List<VtpNameDescriptionPair>> t = new ParameterizedTypeReference<List<VtpNameDescriptionPair>>() {};
388 List<VtpNameDescriptionPair> rv = proxyGetRequestToExternalTestingSite(url, t);
390 rv = new ArrayList<>();
396 * Get the list of scenarios at a given endpoint.
398 public List<VtpNameDescriptionPair> getScenarios(final String endpoint) {
399 String url = buildEndpointUrl(VTP_SCENARIOS_URI, endpoint, ArrayUtils.EMPTY_STRING_ARRAY);
400 return returnNameDescriptionPairFromUrl(url);
404 * Get the list of test suites for an endpoint for the given scenario.
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);
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.
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);
421 rv = new ArrayList<>();
427 * Get a test case definition.
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);
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.
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);
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.
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);
460 RemoteTestingEndpointDefinition endpoint = endpoints.stream()
461 .filter(e -> StringUtils.equals(endpointName, e.getId()))
463 .orElseThrow(() -> new ExternalTestingException(NO_SUCH_ENDPOINT_ERROR_CODE, 400, "No endpoint named " + endpointName + " is defined"));
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());
470 headers.setContentType(MediaType.MULTIPART_FORM_DATA);
473 MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
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);
483 // remove the endpoint from the test request since that is a FE/BE attribute
484 testsToRun.forEach(t -> t.setEndpoint(null));
486 body.add("executions", new ObjectMapper().writeValueAsString(testsToRun));
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);
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);
504 ParameterizedTypeReference<List<VtpTestExecutionResponse>> t = new ParameterizedTypeReference<List<VtpTestExecutionResponse>>() {};
506 return proxyRequestToExternalTestingSite(builder.toUriString(), requestEntity, t);
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);
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.
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);
530 // partition the requests by endpoint.
531 Map<String, List<VtpTestExecutionRequest>> partitions =
532 testsToRun.stream().collect(Collectors.groupingBy(VtpTestExecutionRequest::getEndpoint));
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());
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.
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))
552 .orElseThrow(() -> new ExternalTestingException(NO_SUCH_ENDPOINT_ERROR_CODE, 500, "No endpoint named " + endpointName + " is defined")
555 Object[] newArgs = ArrayUtils.add(args, 0, ep.getUrl());
556 return String.format(format, newArgs);
558 throw new ExternalTestingException(INVALIDATE_STATE_ERROR_CODE, 500, NO_ACCESS_CONFIGURATION_DEFINED);
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.
568 private <T> T proxyGetRequestToExternalTestingSite(String url, ParameterizedTypeReference<T> responseType) {
569 return proxyRequestToExternalTestingSite(url, null, responseType);
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
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());
585 logger.debug("GET request to {} for {}", url, responseType.getType().getTypeName());
587 SimpleClientHttpRequestFactory rf =
588 (SimpleClientHttpRequestFactory) restTemplate.getRequestFactory();
590 rf.setReadTimeout(10000);
591 rf.setConnectTimeout(10000);
593 ResponseEntity<T> re;
595 if (request != null) {
596 re = restTemplate.exchange(url, HttpMethod.POST, request, responseType);
598 re = restTemplate.exchange(url, HttpMethod.GET, null, responseType);
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);
610 JsonObject o = new GsonBuilder().create().fromJson(s, JsonObject.class);
611 throw buildTestingException(ex.getRawStatusCode(), o);
613 catch (JsonParseException e) {
614 logger.warn("unexpected JSON response", e);
615 throw new ExternalTestingException(ENDPOINT_ERROR_CODE, ex.getStatusCode().value(), ex.getResponseBodyAsString(), ex);
619 throw new ExternalTestingException(ENDPOINT_ERROR_CODE, ex.getStatusCode().value(), ex.getResponseBodyAsString(), ex);
622 catch (ResourceAccessException ex) {
623 throw new ExternalTestingException(ENDPOINT_ERROR_CODE, 500, ex.getMessage(), ex);
625 catch (Exception ex) {
626 throw new ExternalTestingException(ENDPOINT_ERROR_CODE, 500, "Generic Exception " + ex.getMessage(), ex);
629 logger.debug("http status of {} from external testing entity {}", re.getStatusCodeValue(), url);
633 logger.error("null response from endpoint");
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
645 private ExternalTestingException buildTestingException(int statusCode, JsonObject o) {
647 String message = null;
650 code = o.get(CODE).getAsString();
652 else if (o.has(ERROR)) {
653 code = o.get(ERROR).getAsString();
656 if (o.has(HTTP_STATUS)) {
657 code = o.get(HTTP_STATUS).getAsJsonPrimitive().getAsString();
660 if (o.has(MESSAGE)) {
661 if (!o.get(MESSAGE).isJsonNull()) {
662 message = o.get(MESSAGE).getAsString();
665 else if (o.has(DETAIL)) {
666 message = o.get(DETAIL).getAsString();
669 if (message == null) {
670 message = o.get(PATH).getAsString();
673 message = message + " " + o.get(PATH).getAsString();
676 return new ExternalTestingException(code, statusCode, message);
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);
685 extractMetadata(test, body, vspId, version);
686 } catch (IOException ex) {
687 logger.error("metadata extraction failed", ex);
692 * Extract the metadata from the VSP CSAR file.
694 * @param requestItem item to add metadata to for processing
695 * @param vspId VSP identifier
696 * @param version VSP version
698 private void extractMetadata(VtpTestExecutionRequest requestItem, MultiValueMap<String, Object> body, String vspId, String version) throws IOException {
700 Version ver = new Version(version);
701 logger.debug("attempt to retrieve archive for VSP {} version {}", vspId, ver.getId());
703 Optional<Pair<String, byte[]>> ozip = candidateManager.get(vspId, ver);
704 if (!ozip.isPresent()) {
705 ozip = vendorSoftwareProductManager.get(vspId, ver);
708 if (!ozip.isPresent()) {
709 List<Version> versions = versioningManager.list(vspId);
710 String knownVersions = versions
712 .map(v -> String.format("%d.%d: %s (%s)", v.getMajor(), v.getMinor(), v.getStatus(), v.getId()))
713 .collect(Collectors.joining("\n"));
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);
718 throw new ExternalTestingException(SDC_RESOLVER_ERR, 500, detail);
721 // safe here to do get.
722 Pair<String, byte[]> zip = ozip.get();
723 processArchive(requestItem, body, zip.getRight());
726 private void processArchive(final VtpTestExecutionRequest test, final MultiValueMap<String, Object> body, final byte[] zip) {
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);
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('-'));
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);
742 body.add("file", new NamedByteArrayResource(data, key + ".heat.zip"));
743 test.getParameters().put(SDC_HEAT, FILE_URL_PREFIX + key + ".heat.zip");
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");
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.
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);
765 try(ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
766 try(ZipOutputStream zipOutput = new ZipOutputStream(baos)) {
767 for (FileData item : manifest.getData()) {
768 processManifestItem(item, zipOutput, content);
771 return baos.toByteArray();
773 } catch (IOException ex) {
774 logger.error("IO Exception parsing zip", ex);
775 throw new ExternalTestingException(SDC_RESOLVER_ERR, 500, ex.getMessage());
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());
786 ZipEntry zi = new ZipEntry(item.getFile());
787 zipOutput.putNextEntry(zi);
788 zipOutput.write(content);
789 zipOutput.closeEntry();
793 if (item.getData() != null) {
794 for(FileData subitem: item.getData()) {
795 processManifestItem(subitem, zipOutput, contentMap);
802 * Process the archive as a CSAR file.
803 * @param content relevant extracted content.
804 * @return byte array of client to send to endpoint.
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);
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;
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;
822 try(ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
823 try(ZipOutputStream zipOutput = new ZipOutputStream(baos)) {
824 processCsarArchiveEntry(fileToGet, zipOutput, content);
825 return baos.toByteArray();
827 } catch (IOException ex) {
828 logger.error("IO Exception parsing zip", ex);
829 throw new ExternalTestingException(SDC_RESOLVER_ERR, 500, ex.getMessage());
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.
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.
848 ZipEntry zi = new ZipEntry(filename);
849 zipOutput.putNextEntry(zi);
850 zipOutput.write(content);
851 zipOutput.closeEntry();
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")) {
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);
873 logger.warn("archive {} contains imports but it is not an array. Unexpected, this is.", filename);
878 private Optional<String> getEntryDefinitionPointer(byte[] toscaMetadataFile) {
880 Properties p = new Properties();
881 p.load(new ByteArrayInputStream(toscaMetadataFile));
882 return Optional.ofNullable(p.getProperty(TOSCA_META_ENTRY_DEFINITIONS));
884 catch (IOException ex) {
885 logger.error("failed to process tosca metadata file {}", TOSCA_META, ex);
888 return Optional.empty();
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.
897 private Map<String, byte[]> extractRelevantContent(final byte[] zip) {
898 final Map<String, byte[]> zipFileAndByteMap;
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);
906 return zipFileAndByteMap.entrySet().stream()
907 .filter(stringEntry -> hasRelevantExtension(stringEntry.getKey()))
908 .collect(Collectors.toMap(Entry::getKey, Entry::getValue));
912 * Checks if the file matches with a expected extension.
914 * @param filePath the file path
915 * @return {@code true} if the file extension matches with {@link #relevantArchiveFileExtensionSet}, {@code false}
918 private boolean hasRelevantExtension(final String filePath) {
919 final String entryExtension = FilenameUtils.getExtension(filePath);
920 return StringUtils.isNotEmpty(entryExtension) && (relevantArchiveFileExtensionSet.contains(entryExtension));
924 * We need to name the byte array we add to the multipart request sent to the VTP.
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;
934 public String getFilename() {
935 return this.filename;