From 678d1a99d9a1d621fc54bd08ff673937c94bf468 Mon Sep 17 00:00:00 2001 From: Jakub Balhar Date: Mon, 26 Oct 2020 13:33:08 +0100 Subject: [PATCH] In Memory implementation of the Caching API (#904) In Memory implementation of the Caching API Signed-off-by: Jakub Balhar --- caching-service/README.md | 69 ++++ caching-service/build.gradle | 82 +++++ caching-service/cachingServiceStructure.png | Bin 0 -> 59217 bytes .../zowe/apiml/caching/CachingService.java | 25 ++ .../apiml/caching/api/CachingController.java | 256 ++++++++++++++ .../caching/config/MessageConfiguration.java | 25 ++ .../caching/config/StorageConfiguration.java | 25 ++ .../apiml/caching/config/SwaggerConfig.java | 57 ++++ .../exceptions/CachingPayloadException.java | 16 + .../zowe/apiml/caching/model/KeyValue.java | 22 ++ .../zowe/apiml/caching/service/Storage.java | 64 ++++ .../service/inmemory/InMemoryStorage.java | 77 +++++ .../src/main/resources/application.yml | 82 +++++ caching-service/src/main/resources/banner.txt | 1 + .../main/resources/caching-log-messages.yml | 59 ++++ .../caching/api/CachingControllerTest.java | 312 ++++++++++++++++++ .../service/inmemory/InMemoryStorageTest.java | 128 +++++++ .../src/test/resources/application.yml | 82 +++++ .../client/api/ZaasClientTestController.java | 3 + docs/local-configuration.md | 5 + gradle/coverage.gradle | 1 + gradle/license.gradle | 1 + gradle/publish.gradle | 1 + integration-tests/build.gradle | 1 + .../CachingApiIntegrationTest.java | 170 ++++++++++ package.json | 5 +- settings.gradle | 1 + zaas-client/README.md | 9 + zaas-client/build.gradle | 1 + .../DefaultZaasClientConfiguration.java | 9 +- .../main/resources/component-scripts/start.sh | 21 ++ 31 files changed, 1601 insertions(+), 9 deletions(-) create mode 100644 caching-service/README.md create mode 100644 caching-service/build.gradle create mode 100644 caching-service/cachingServiceStructure.png create mode 100644 caching-service/src/main/java/org/zowe/apiml/caching/CachingService.java create mode 100644 caching-service/src/main/java/org/zowe/apiml/caching/api/CachingController.java create mode 100644 caching-service/src/main/java/org/zowe/apiml/caching/config/MessageConfiguration.java create mode 100644 caching-service/src/main/java/org/zowe/apiml/caching/config/StorageConfiguration.java create mode 100644 caching-service/src/main/java/org/zowe/apiml/caching/config/SwaggerConfig.java create mode 100644 caching-service/src/main/java/org/zowe/apiml/caching/exceptions/CachingPayloadException.java create mode 100644 caching-service/src/main/java/org/zowe/apiml/caching/model/KeyValue.java create mode 100644 caching-service/src/main/java/org/zowe/apiml/caching/service/Storage.java create mode 100644 caching-service/src/main/java/org/zowe/apiml/caching/service/inmemory/InMemoryStorage.java create mode 100644 caching-service/src/main/resources/application.yml create mode 100644 caching-service/src/main/resources/banner.txt create mode 100644 caching-service/src/main/resources/caching-log-messages.yml create mode 100644 caching-service/src/test/java/org/zowe/apiml/caching/api/CachingControllerTest.java create mode 100644 caching-service/src/test/java/org/zowe/apiml/caching/service/inmemory/InMemoryStorageTest.java create mode 100644 caching-service/src/test/resources/application.yml create mode 100644 integration-tests/src/test/java/org/zowe/apiml/cachingservice/CachingApiIntegrationTest.java rename discoverable-client/src/main/java/org/zowe/apiml/client/configuration/ZaasClientConfig.java => zaas-client/src/main/java/org/zowe/apiml/zaasclient/config/DefaultZaasClientConfiguration.java (91%) diff --git a/caching-service/README.md b/caching-service/README.md new file mode 100644 index 0000000000..6295410190 --- /dev/null +++ b/caching-service/README.md @@ -0,0 +1,69 @@ +# Caching Service + +To support the High Availability of all components within Zowe, components either need to be stateless, or offload the state to a location accessible by all instances of the service, including those which just started. At the current time, some services are not, and cannot be stateless. For these services, we introduce the Caching service. + +The Caching service aims to provide an API which offers the possibility to store, retrieve and delete data associated with keys. The service will be used only by internal Zowe applications and will not be exposed to the internet. The Caching service needs to support a hot-reload scenario in which a client service requests all available service data. + +The initial implementation of the service will depend on VSAM to store the key/value pairs, as VSAM is a native z/OS solution to storing key/value pairs. Eventually, there will be other implementations for solutions such as MQs. As such, this needs to be taken into account for the initial design document. + +## Architecture + +Internal architecture needs to take into consideration, namely the fact that there will be multiple storage solutions. The API, on the other hand, remains the same throughout various storage implementations. + +![Diagram](cachingServiceStructure.png "Architecture of the service") + +## How to use + +The Caching Service is built on top of the spring enabler, which means that it is dynamically registered to the API Mediation Layer. It appears in the API Catalog under the tile "Zowe Applications". + +There are REST APIs available to create, delete, and update key-value pairs in the cache, as well as APIs to read a specific key-value pair or all key-value pairs in the cache. + +## Storage + +There are multiple storage solutions supported by the Caching Service with the option to +add custom implementation. [Additional Storage Support](#additional-storage-support) explains +what needs to be done to implement custom solution. + +### In Memory + +This storage is useful for testing and integration verification. Don't use it in production. +The key/value pairs are stored only in the memory of one instance of the service and therefore +won't persist. + +### VSAM + +TO BE DONE + +### Additional Storage Support + +To add a new implementation it is necessary to provide the library with the implementation +of the Storage.class and properly configure the Spring with the used implementation. + + @ConditionalOnProperty( + value = "caching.storage", + havingValue = "custom" + ) + @Bean + public Storage custom() { + return new CustomStorage(); + } + +The example above shows the Configuration within the library that will use different storage than the default InMemory one. + +It is possible to provide the custom implementation via the -Dloader.path property provided on startup of the Caching service. + +## How do you run for local development + +The Caching Service is a Spring Boot application. You can either add it as a run configuration and run it together with other services, or the npm command to run API ML also runs the mock. + +Command to run full set of api-layer: `npm run api-layer`. If you are looking for the Continuous Integration set up run: `npm run api-layer-ci` + +In local usage, the Caching Service will run at `https://localhost:10016`. The API path is `/cachingservice/api/v1/cache/${path-params-as-needed}`. +For example, `https://localhost:10016/cachingservice/api/v1/cache/my-key` retrieves the cache entry using the key 'my-key'. + +## Configuration properties + +The Caching Service uses the standard `application.yml` structure for configuration. + +`apiml.service.routes` only specifies one API route as there is no need for web socket or UI routes. +`caching.storage` this property is reserved for the setup of the proper storage within the Caching Service. diff --git a/caching-service/build.gradle b/caching-service/build.gradle new file mode 100644 index 0000000000..c497bd4b6f --- /dev/null +++ b/caching-service/build.gradle @@ -0,0 +1,82 @@ +buildscript { + repositories mavenRepositories + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") + classpath("gradle.plugin.com.gorylenko.gradle-git-properties:gradle-git-properties:${gradleGitPropertiesVersion}") + } +} + +normalization { + runtimeClasspath { + ignore("**/*git.properties*") + ignore("**/*build-info.properties*") + } +} + +apply plugin: 'org.springframework.boot' +apply plugin: 'com.gorylenko.gradle-git-properties' + +springBoot { + // This statement tells the Gradle Spring Boot plugin to generate a file + // build/resources/main/META-INF/build-info.properties that is picked up by Spring Boot to display + // via /info endpoint + buildInfo { + properties { + // Generate extra build info: + additionalProperties = [ + by: System.properties['user.name'], + operatingSystem: "${System.properties['os.name']} (${System.properties['os.version']})", + number: System.getenv('BUILD_NUMBER') ? System.getenv('BUILD_NUMBER') : "n/a", + machine: InetAddress.localHost.hostName + ] + } + } +} + +gitProperties { + gitPropertiesDir = new File("${project.rootDir}/${name}/build/resources/main/META-INF") +} + +dependencies { + compile project(':common-service-core') + compile project(':zaas-client') + + implementation project(':onboarding-enabler-spring') + + compile libraries.jjwt + compile libraries.jjwt_impl + compile libraries.jjwt_jackson + + implementation libraries.springFox + implementation libraries.spring_boot_starter + implementation libraries.spring_boot_starter_actuator + implementation libraries.spring_boot_starter_web + implementation libraries.spring_boot_starter_websocket + + implementation libraries.bootstrap + implementation libraries.jquery + + implementation libraries.gson + compileOnly libraries.lombok + annotationProcessor libraries.lombok + + testImplementation libraries.spring_boot_starter_test +} + + +bootJar.archiveName = "${bootJar.baseName}.jar" + +jar { + enabled = true + archiveName = "${jar.baseName}-thin.jar" + + def libClassPathEntries = configurations.runtimeClasspath.files.collect { + "lib/" + it.getName() + } + doFirst { + manifest { + attributes "Class-Path": libClassPathEntries.join(" "), + "Main-Class": "org.zowe.apiml.caching.CachingService" + } + } +} diff --git a/caching-service/cachingServiceStructure.png b/caching-service/cachingServiceStructure.png new file mode 100644 index 0000000000000000000000000000000000000000..8ad397b90edb0f9f08ccb184bba2a3259e105974 GIT binary patch literal 59217 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 literal 0 HcmV?d00001 diff --git a/caching-service/src/main/java/org/zowe/apiml/caching/CachingService.java b/caching-service/src/main/java/org/zowe/apiml/caching/CachingService.java new file mode 100644 index 0000000000..56e457169c --- /dev/null +++ b/caching-service/src/main/java/org/zowe/apiml/caching/CachingService.java @@ -0,0 +1,25 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package org.zowe.apiml.caching; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.zowe.apiml.enable.EnableApiDiscovery; + +@SpringBootApplication +@EnableApiDiscovery +public class CachingService { + + public static void main(String[] args) { + SpringApplication app = new SpringApplication(CachingService.class); + app.setLogStartupInfo(false); + app.run(args); + } +} diff --git a/caching-service/src/main/java/org/zowe/apiml/caching/api/CachingController.java b/caching-service/src/main/java/org/zowe/apiml/caching/api/CachingController.java new file mode 100644 index 0000000000..157842a861 --- /dev/null +++ b/caching-service/src/main/java/org/zowe/apiml/caching/api/CachingController.java @@ -0,0 +1,256 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package org.zowe.apiml.caching.api; + +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.zowe.apiml.caching.exceptions.CachingPayloadException; +import org.zowe.apiml.caching.model.KeyValue; +import org.zowe.apiml.caching.service.Storage; +import org.zowe.apiml.message.core.Message; +import org.zowe.apiml.message.core.MessageService; +import org.zowe.apiml.zaasclient.config.DefaultZaasClientConfiguration; +import org.zowe.apiml.zaasclient.exception.ZaasClientErrorCodes; +import org.zowe.apiml.zaasclient.exception.ZaasClientException; +import org.zowe.apiml.zaasclient.service.ZaasClient; +import org.zowe.apiml.zaasclient.service.ZaasToken; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import java.util.Arrays; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1") +@Import(DefaultZaasClientConfiguration.class) +public class CachingController { + private static final String TOKEN_COOKIE_PREFIX = "apimlAuthenticationToken"; + private static final String KEY_NOT_IN_CACHE_MESSAGE = "org.zowe.apiml.cache.keyNotInCache"; + + private final Storage storage; + private final ZaasClient zaasClient; + private final MessageService messageService; + + @GetMapping(value = "/cache/{key}", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + @ApiOperation(value = "Retrieves a specific value in the cache", + notes = "Value returned is for the provided {key}") + @ResponseBody + public ResponseEntity getValue(@PathVariable String key, HttpServletRequest request) { + String serviceId; + try { + ZaasToken token = queryTokenFromRequest(request); + serviceId = token.getUserId(); + } catch (ZaasClientException e) { + return handleZaasClientException(e, request); + } + + if (key == null) { + return noKeyProvidedResponse(serviceId); + } + + KeyValue readPair = storage.read(serviceId, key); + + if (readPair == null) { + Message message = messageService.createMessage(KEY_NOT_IN_CACHE_MESSAGE, key, serviceId); + return new ResponseEntity<>(message.mapToView(), HttpStatus.NOT_FOUND); + } + + return new ResponseEntity<>(readPair, HttpStatus.OK); + } + + @GetMapping(value = "/cache", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + @ApiOperation(value = "Retrieves all values in the cache", + notes = "Values returned for the calling service") + @ResponseBody + public ResponseEntity getAllValues(HttpServletRequest request) { + String serviceId; + try { + ZaasToken token = queryTokenFromRequest(request); + serviceId = token.getUserId(); + } catch (ZaasClientException e) { + return handleZaasClientException(e, request); + } + + return new ResponseEntity<>(storage.readForService(serviceId), HttpStatus.OK); + } + + @PostMapping(value = "/cache", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + @ApiOperation(value = "Create a new key in the cache", + notes = "A new key-value pair will be added to the cache") + @ResponseBody + public ResponseEntity createKey(@RequestBody KeyValue keyValue, HttpServletRequest request) { + String serviceId; + try { + ZaasToken token = queryTokenFromRequest(request); + serviceId = token.getUserId(); + + checkForInvalidPayload(keyValue); + } catch (ZaasClientException e) { + return handleZaasClientException(e, request); + } catch (CachingPayloadException e) { + return invalidPayloadResponse(e, keyValue); + } + + KeyValue createdPair = storage.create(serviceId, keyValue); + + if (createdPair == null) { + Message message = messageService.createMessage("org.zowe.apiml.cache.keyCollision", keyValue.getKey()); + return new ResponseEntity<>(message.mapToView(), HttpStatus.CONFLICT); + } + + return new ResponseEntity<>(HttpStatus.CREATED); + } + + @PutMapping(value = "/cache", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + @ApiOperation(value = "Update key in the cache", + notes = "Value at the key in the provided key-value pair will be updated to the provided value") + @ResponseBody + public ResponseEntity update(@RequestBody KeyValue keyValue, HttpServletRequest request) { + String serviceId; + try { + ZaasToken token = queryTokenFromRequest(request); + serviceId = token.getUserId(); + + checkForInvalidPayload(keyValue); + } catch (ZaasClientException e) { + return handleZaasClientException(e, request); + } catch (CachingPayloadException e) { + return invalidPayloadResponse(e, keyValue); + } + + KeyValue updatedPair = storage.update(serviceId, keyValue); + + if (updatedPair == null) { + Message message = messageService.createMessage(KEY_NOT_IN_CACHE_MESSAGE, keyValue.getKey(), serviceId); + return new ResponseEntity<>(message.mapToView(), HttpStatus.NOT_FOUND); + } + + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + @DeleteMapping(value = "/cache/{key}", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + @ApiOperation(value = "Delete key from the cache", + notes = "Will delete key-value pair for the provided {key}") + @ResponseBody + public ResponseEntity delete(@PathVariable String key, HttpServletRequest request) { + String serviceId; + try { + ZaasToken token = queryTokenFromRequest(request); + serviceId = token.getUserId(); + } catch (ZaasClientException e) { + return handleZaasClientException(e, request); + } + + if (key == null) { + return noKeyProvidedResponse(serviceId); + } + + KeyValue deletedPair = storage.delete(serviceId, key); + + if (deletedPair == null) { + Message message = messageService.createMessage(KEY_NOT_IN_CACHE_MESSAGE, key, serviceId); + return new ResponseEntity<>(message.mapToView(), HttpStatus.NOT_FOUND); + } + + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + private void checkForInvalidPayload(KeyValue keyValue) throws CachingPayloadException { + if (keyValue == null) { + throw new CachingPayloadException("No KeyValue provided in the payload"); + } + + if (keyValue.getValue() == null) { + throw new CachingPayloadException("No value provided in the payload"); + } + + String key = keyValue.getKey(); + if (key == null) { + throw new CachingPayloadException("No key provided in the payload"); + } + if (!StringUtils.isAlphanumeric(key)) { + throw new CachingPayloadException("Key is not alphanumeric"); + } + } + + private ResponseEntity invalidPayloadResponse(CachingPayloadException e, KeyValue keyValue) { + Message message = messageService.createMessage("org.zowe.apiml.cache.invalidPayload", keyValue, e.getMessage()); + return new ResponseEntity<>(message.mapToView(), HttpStatus.BAD_REQUEST); + } + + private ResponseEntity noKeyProvidedResponse(String serviceId) { + Message message = messageService.createMessage("org.zowe.apiml.cache.keyNotProvided", serviceId); + return new ResponseEntity<>(message.mapToView(), HttpStatus.BAD_REQUEST); + } + + private ZaasToken queryTokenFromRequest(HttpServletRequest request) throws ZaasClientException { + String jwtToken = getJwtTokenFromCookie(request); + ZaasToken zaasToken = zaasClient.query(jwtToken); + + if (zaasToken == null) { + throw new ZaasClientException(ZaasClientErrorCodes.INVALID_JWT_TOKEN, "Queried token is null"); + } + if (zaasToken.isExpired()) { + throw new ZaasClientException(ZaasClientErrorCodes.EXPIRED_JWT_EXCEPTION, "Queried token is expired"); + } + + return zaasToken; + } + + private String getJwtTokenFromCookie(HttpServletRequest request) { + Cookie[] cookies = request.getCookies(); + if (cookies == null) { + return null; + } + return Arrays.stream(cookies) + .filter(cookie -> cookie.getName().equals(TOKEN_COOKIE_PREFIX)) + .filter(cookie -> !cookie.getValue().isEmpty()) + .findFirst() + .map(Cookie::getValue) + .orElse(null); + } + + private ResponseEntity handleZaasClientException(ZaasClientException e, HttpServletRequest request) { + String requestUrl = request.getRequestURL().toString(); + Message message; + HttpStatus statusCode; + + switch (e.getErrorCode()) { + case TOKEN_NOT_PROVIDED: + statusCode = HttpStatus.BAD_REQUEST; + message = messageService.createMessage("org.zowe.apiml.security.query.tokenNotProvided", requestUrl); + break; + case INVALID_JWT_TOKEN: + statusCode = HttpStatus.UNAUTHORIZED; + message = messageService.createMessage("org.zowe.apiml.security.query.invalidToken", requestUrl); + break; + case EXPIRED_JWT_EXCEPTION: + statusCode = HttpStatus.UNAUTHORIZED; + message = messageService.createMessage("org.zowe.apiml.security.expiredToken", requestUrl); + break; + case SERVICE_UNAVAILABLE: + statusCode = HttpStatus.NOT_FOUND; + message = messageService.createMessage("org.zowe.apiml.cache.gatewayUnavailable", requestUrl, e.getMessage()); + break; + default: + statusCode = HttpStatus.INTERNAL_SERVER_ERROR; + message = messageService.createMessage("org.zowe.apiml.common.internalRequestError", requestUrl, e.getMessage(), e.getCause()); + break; + } + + return new ResponseEntity<>(message.mapToView(), statusCode); + } +} diff --git a/caching-service/src/main/java/org/zowe/apiml/caching/config/MessageConfiguration.java b/caching-service/src/main/java/org/zowe/apiml/caching/config/MessageConfiguration.java new file mode 100644 index 0000000000..ab1392f20e --- /dev/null +++ b/caching-service/src/main/java/org/zowe/apiml/caching/config/MessageConfiguration.java @@ -0,0 +1,25 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package org.zowe.apiml.caching.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.zowe.apiml.message.core.MessageService; +import org.zowe.apiml.message.yaml.YamlMessageServiceInstance; + +@Configuration +public class MessageConfiguration { + @Bean + public MessageService messageService() { + MessageService messageService = YamlMessageServiceInstance.getInstance(); + messageService.loadMessages("/caching-log-messages.yml"); + return messageService; + } +} diff --git a/caching-service/src/main/java/org/zowe/apiml/caching/config/StorageConfiguration.java b/caching-service/src/main/java/org/zowe/apiml/caching/config/StorageConfiguration.java new file mode 100644 index 0000000000..fc6a02ea42 --- /dev/null +++ b/caching-service/src/main/java/org/zowe/apiml/caching/config/StorageConfiguration.java @@ -0,0 +1,25 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package org.zowe.apiml.caching.config; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.zowe.apiml.caching.service.Storage; +import org.zowe.apiml.caching.service.inmemory.InMemoryStorage; + +@Configuration +public class StorageConfiguration { + @ConditionalOnMissingBean(Storage.class) + @Bean + public Storage inMemory() { + return new InMemoryStorage(); + } +} diff --git a/caching-service/src/main/java/org/zowe/apiml/caching/config/SwaggerConfig.java b/caching-service/src/main/java/org/zowe/apiml/caching/config/SwaggerConfig.java new file mode 100644 index 0000000000..292528e008 --- /dev/null +++ b/caching-service/src/main/java/org/zowe/apiml/caching/config/SwaggerConfig.java @@ -0,0 +1,57 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package org.zowe.apiml.caching.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +import java.util.Collections; + +@Configuration +@EnableSwagger2 +public class SwaggerConfig { + + @Value("${apiml.service.title}") + private String apiTitle; + + @Value("${apiml.service.apiInfo[0].version}") + private String apiVersion; + + @Value("${apiml.service.description}") + private String apiDescription; + + @Bean + public Docket api() { + return new Docket(DocumentationType.SWAGGER_2) + .select() + .apis(RequestHandlerSelectors.any()) + .paths(PathSelectors.ant("/api/v1/**")) + .build() + .apiInfo( + new ApiInfo( + apiTitle, + apiDescription, + apiVersion, + null, + null, + null, + null, + Collections.emptyList() + ) + ); + } +} diff --git a/caching-service/src/main/java/org/zowe/apiml/caching/exceptions/CachingPayloadException.java b/caching-service/src/main/java/org/zowe/apiml/caching/exceptions/CachingPayloadException.java new file mode 100644 index 0000000000..b123884551 --- /dev/null +++ b/caching-service/src/main/java/org/zowe/apiml/caching/exceptions/CachingPayloadException.java @@ -0,0 +1,16 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package org.zowe.apiml.caching.exceptions; + +public class CachingPayloadException extends Exception { + public CachingPayloadException(String message) { + super(message); + } +} diff --git a/caching-service/src/main/java/org/zowe/apiml/caching/model/KeyValue.java b/caching-service/src/main/java/org/zowe/apiml/caching/model/KeyValue.java new file mode 100644 index 0000000000..490e8b277d --- /dev/null +++ b/caching-service/src/main/java/org/zowe/apiml/caching/model/KeyValue.java @@ -0,0 +1,22 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package org.zowe.apiml.caching.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@JsonInclude(JsonInclude.Include.NON_EMPTY) +@Data +public class KeyValue { + private final String key; + private final String value; +} diff --git a/caching-service/src/main/java/org/zowe/apiml/caching/service/Storage.java b/caching-service/src/main/java/org/zowe/apiml/caching/service/Storage.java new file mode 100644 index 0000000000..7c44a11aa4 --- /dev/null +++ b/caching-service/src/main/java/org/zowe/apiml/caching/service/Storage.java @@ -0,0 +1,64 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package org.zowe.apiml.caching.service; + +import org.zowe.apiml.caching.model.KeyValue; + +import java.util.Map; + +/** + * Every supported storage backend needs to have an implementation of the Storage. + */ +public interface Storage { + /** + * Store new KeyValue pair in the storage. If there is a key collision null is returned. + * + * @param serviceId Id of the service to store the value for + * @param toCreate KeyValue pair to be created. + * @return The stored KeyValue pair or null. + */ + KeyValue create(String serviceId, KeyValue toCreate); + + /** + * Returns the keys associated with the provided keys. + * + * @param serviceId Id of the service to read value for + * @param key key to lookup + * @return KeyValue associated with the value + */ + KeyValue read(String serviceId, String key); + + /** + * Replaces the value for the given key with the new value. If there is no existing key/value pair null is returned. + * + * @param serviceId Id of the service to store the value for. + * @param toUpdate Value to store instead of the original one. + * @return Updated key/value pair or null. + */ + KeyValue update(String serviceId, KeyValue toUpdate); + + /** + * Delete the key/value pair if it exists within the context of the service. If there is none existing null + * is returned. + * + * @param serviceId Id of the service to delete the value for. + * @param toDelete Key to delete from the storage. + * @return Deleted key/value pair or null. + */ + KeyValue delete(String serviceId, String toDelete); + + /** + * Return all the key/value pairs for given service id. + * + * @param serviceId Id of the service to load all key/value pairs + * @return Map with the key/value pairs or null if there is none existing. + */ + Map readForService(String serviceId); +} diff --git a/caching-service/src/main/java/org/zowe/apiml/caching/service/inmemory/InMemoryStorage.java b/caching-service/src/main/java/org/zowe/apiml/caching/service/inmemory/InMemoryStorage.java new file mode 100644 index 0000000000..85bf3be181 --- /dev/null +++ b/caching-service/src/main/java/org/zowe/apiml/caching/service/inmemory/InMemoryStorage.java @@ -0,0 +1,77 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package org.zowe.apiml.caching.service.inmemory; + +import org.zowe.apiml.caching.model.KeyValue; +import org.zowe.apiml.caching.service.Storage; + +import java.util.HashMap; +import java.util.Map; + +public class InMemoryStorage implements Storage { + private Map> storage = new HashMap<>(); + + public InMemoryStorage() { + } + + protected InMemoryStorage(Map> storage) { + this.storage = storage; + } + + @Override + public KeyValue create(String serviceId, KeyValue toCreate) { + storage.computeIfAbsent(serviceId, k -> new HashMap<>()); + Map serviceStorage = storage.get(serviceId); + serviceStorage.put(toCreate.getKey(), toCreate); + return toCreate; + } + + @Override + public KeyValue read(String serviceId, String key) { + Map serviceSpecificStorage = storage.get(serviceId); + if (serviceSpecificStorage == null) { + return null; + } + + return serviceSpecificStorage.get(key); + } + + @Override + public KeyValue update(String serviceId, KeyValue toUpdate) { + String keyToUpdate = toUpdate.getKey(); + if (isKeyNotInCache(serviceId, keyToUpdate)) { + return null; + } + + Map serviceStorage = storage.get(serviceId); + serviceStorage.put(keyToUpdate, toUpdate); + return toUpdate; + } + + @Override + public KeyValue delete(String serviceId, String toDelete) { + if (isKeyNotInCache(serviceId, toDelete)) { + return null; + } + + Map serviceSpecificStorage = storage.get(serviceId); + return serviceSpecificStorage.remove(toDelete); + } + + @Override + public Map readForService(String serviceId) { + return storage.get(serviceId); + } + + private boolean isKeyNotInCache(String serviceId, String keyToTest) { + Map serviceSpecificStorage = storage.get(serviceId); + return serviceSpecificStorage == null || serviceSpecificStorage.get(keyToTest) == null; + } +} diff --git a/caching-service/src/main/resources/application.yml b/caching-service/src/main/resources/application.yml new file mode 100644 index 0000000000..2b01989ef9 --- /dev/null +++ b/caching-service/src/main/resources/application.yml @@ -0,0 +1,82 @@ +spring: + application: + name: Caching service + +apiml: + enabled: true + service: + preferIpAddress: false + + serviceId: cachingservice + title: Caching service for internal usage. + description: Service that provides caching API. + + discoveryServiceUrls: https://localhost:10011/eureka/ + + scheme: https + + hostname: localhost + port: 10016 + baseUrl: ${apiml.service.scheme}://${apiml.service.hostname}:${apiml.service.port} + contextPath: /${apiml.service.serviceId} + + homePageRelativeUrl: ${apiml.service.contextPath} + statusPageRelativeUrl: ${apiml.service.contextPath}/application/info + healthCheckRelativeUrl: ${apiml.service.contextPath}/application/health + + routes: + - gateway-url: "api/v1" + service-url: ${apiml.service.contextPath}/api/v1 + apiInfo: + - apiId: org.zowe.cachingservice + version: 1.0.0 + gatewayUrl: api/v1 + swaggerUrl: ${apiml.service.scheme}://${apiml.service.hostname}:${apiml.service.port}${apiml.service.contextPath}/v2/api-docs + documentationUrl: https://www.zowe.org + catalog: + tile: + id: zowe + title: Zowe Applications + description: Applications which are part of Zowe. + version: 1.0.0 + ssl: + enabled: true + verifySslCertificatesOfServices: true + protocol: ${server.ssl.protocol} + keyStoreType: ${server.ssl.keyStoreType} + trustStoreType: ${server.ssl.trustStoreType} + + keyAlias: ${server.ssl.keyAlias} + keyPassword: ${server.ssl.keyPassword} + keyStore: ${server.ssl.keyStore} + keyStorePassword: ${server.ssl.keyStorePassword} + trustStore: ${server.ssl.trustStore} + trustStorePassword: ${server.ssl.trustStorePassword} + customMetadata: + apiml: + enableUrlEncodedCharacters: true + gatewayPort: 10010 + gatewayAuthEndpoint: /api/v1/gateway/auth + corsEnabled: false + +server: + port: ${apiml.service.port} + + servlet: + contextPath: /${apiml.service.serviceId} + + ssl: + enabled: true + clientAuth: want + protocol: TLSv1.2 + enabled-protocols: TLSv1.2 + ciphers: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384 + keyStoreType: PKCS12 + trustStoreType: PKCS12 + + keyAlias: localhost + keyPassword: password + keyStore: keystore/localhost/localhost.keystore.p12 + keyStorePassword: password + trustStore: keystore/localhost/localhost.truststore.p12 + trustStorePassword: password diff --git a/caching-service/src/main/resources/banner.txt b/caching-service/src/main/resources/banner.txt new file mode 100644 index 0000000000..dc1287451e --- /dev/null +++ b/caching-service/src/main/resources/banner.txt @@ -0,0 +1 @@ +Caching Service diff --git a/caching-service/src/main/resources/caching-log-messages.yml b/caching-service/src/main/resources/caching-log-messages.yml new file mode 100644 index 0000000000..fe0e5a532a --- /dev/null +++ b/caching-service/src/main/resources/caching-log-messages.yml @@ -0,0 +1,59 @@ +messages: + # General messages (100 - 199) + - key: org.zowe.apiml.security.expiredToken + number: ZWEAT100 + type: ERROR + text: "Token is expired for URL '%s'" + reason: "The validity of the token is expired." + action: "Obtain a new token by performing an authentication request." + + # Query messages (130 - 140) + - key: org.zowe.apiml.security.query.invalidToken + number: ZWEAG130 + type: ERROR + text: "Token is not valid for URL '%s'" + reason: "The token is not valid." + action: "Provide a valid token." + + - key: org.zowe.apiml.security.query.tokenNotProvided + number: ZWEAG131 + type: ERROR + text: "No authorization token provided for URL '%s'" + reason: "No authorization token is provided." + action: "Provide a valid authorization token." + + - key: org.zowe.apiml.cache.invalidPayload + number: ZWECS130 + type: ERROR + text: "Payload '%s' is not valid: '%s'." + reason: "The payload is not in valid JSON format." + action: "Provide a payload in JSON format." + + - key: org.zowe.apiml.cache.keyNotInCache + number: ZWECS131 + type: ERROR + text: "Key '%s' is not in the cache for service '%s'" + reason: "Cache does not contain the provided key." + action: "Add a key-value pair to the cache using the key or operate on an existing key in the cache." + + - key: org.zowe.apiml.cache.keyNotProvided + number: ZWECS132 + type: ERROR + text: "No cache key provided." + reason: "No cache key was provided." + action: "Provide a key that is in the cache." + + - key: org.zowe.apiml.cache.keyCollision + number: ZWECS133 + type: ERROR + text: "Adding key '%s' resulted in a collision in the cache." + reason: "Key is already in the cache." + action: "Update or delete the key, or add a different key." + + # Service specific messages (700 - 799) + - key: org.zowe.apiml.cache.gatewayUnavailable + number: ZWECS700 + type: ERROR + text: "Gateway service is not available at URL '%s'. Error returned: '%s'" + reason: "The gateway service is not available." + action: "Make sure that the gateway service is running and is accessible by the URL provided in the message." diff --git a/caching-service/src/test/java/org/zowe/apiml/caching/api/CachingControllerTest.java b/caching-service/src/test/java/org/zowe/apiml/caching/api/CachingControllerTest.java new file mode 100644 index 0000000000..dd63022a25 --- /dev/null +++ b/caching-service/src/test/java/org/zowe/apiml/caching/api/CachingControllerTest.java @@ -0,0 +1,312 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package org.zowe.apiml.caching.api; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockHttpServletRequest; +import org.zowe.apiml.caching.model.KeyValue; +import org.zowe.apiml.caching.service.Storage; +import org.zowe.apiml.message.api.ApiMessageView; +import org.zowe.apiml.message.core.MessageService; +import org.zowe.apiml.message.yaml.YamlMessageService; +import org.zowe.apiml.zaasclient.exception.ZaasClientErrorCodes; +import org.zowe.apiml.zaasclient.exception.ZaasClientException; +import org.zowe.apiml.zaasclient.service.ZaasClient; +import org.zowe.apiml.zaasclient.service.ZaasToken; + +import javax.servlet.http.Cookie; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsNull.nullValue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class CachingControllerTest { + private static final String SERVICE_ID = "test-service"; + private static final String KEY = "key"; + private static final String VALUE = "value"; + + private static final KeyValue KEY_VALUE = new KeyValue(KEY, VALUE); + private static final ZaasToken TOKEN = new ZaasToken(); + + private MockHttpServletRequest mockRequest; + + private Storage mockStorage; + private ZaasClient mockZaasClient; + private final MessageService messageService = new YamlMessageService("/caching-log-messages.yml"); + private CachingController underTest; + + @BeforeEach + void setUp() throws ZaasClientException { + mockRequest = new MockHttpServletRequest(); + mockStorage = mock(Storage.class); + mockZaasClient = mock(ZaasClient.class); + when(mockZaasClient.query(any())).thenReturn(TOKEN); + + TOKEN.setUserId(SERVICE_ID); + underTest = new CachingController(mockStorage, mockZaasClient, messageService); + } + + @Test + void givenStorageReturnsValidValue_whenGetByKey_thenReturnProperValue() { + when(mockStorage.read(SERVICE_ID, KEY)).thenReturn(KEY_VALUE); + + ResponseEntity response = underTest.getValue(KEY, mockRequest); + assertThat(response.getStatusCode(), is(HttpStatus.OK)); + + KeyValue body = (KeyValue) response.getBody(); + assertThat(body.getValue(), is(VALUE)); + } + + @Test + void givenNoKey_whenGetByKey_thenResponseBadRequest() { + ApiMessageView expectedBody = messageService.createMessage("org.zowe.apiml.cache.keyNotProvided", SERVICE_ID).mapToView(); + + ResponseEntity response = underTest.getValue(null, mockRequest); + assertThat(response.getStatusCode(), is(HttpStatus.BAD_REQUEST)); + assertThat(response.getBody(), is(expectedBody)); + } + + @Test + void givenStoreWithNoKey_whenGetByKey_thenResponseNotFound() { + ApiMessageView expectedBody = messageService.createMessage("org.zowe.apiml.cache.keyNotInCache", KEY, SERVICE_ID).mapToView(); + when(mockStorage.read(any(), any())).thenReturn(null); + + ResponseEntity response = underTest.getValue(KEY, mockRequest); + assertThat(response.getStatusCode(), is(HttpStatus.NOT_FOUND)); + assertThat(response.getBody(), is(expectedBody)); + } + + @Test + void givenStorageReturnsValidValues_whenGetByService_thenReturnProperValues() { + Map values = new HashMap<>(); + values.put(KEY, new KeyValue("key2", VALUE)); + when(mockStorage.readForService(SERVICE_ID)).thenReturn(values); + + ResponseEntity response = underTest.getAllValues(mockRequest); + assertThat(response.getStatusCode(), is(HttpStatus.OK)); + + Map result = (Map) response.getBody(); + assertThat(result, is(values)); + } + + @Test + void givenStorage_whenCreateKey_thenResponseCreated() { + when(mockStorage.create(SERVICE_ID, KEY_VALUE)).thenReturn(KEY_VALUE); + + ResponseEntity response = underTest.createKey(KEY_VALUE, mockRequest); + assertThat(response.getStatusCode(), is(HttpStatus.CREATED)); + assertThat(response.getBody(), is(nullValue())); + } + + @Test + void givenStorageWithExistingKey_whenCreateKey_thenResponseConflict() { + when(mockStorage.create(SERVICE_ID, KEY_VALUE)).thenReturn(null); + ApiMessageView expectedBody = messageService.createMessage("org.zowe.apiml.cache.keyCollision", KEY).mapToView(); + + ResponseEntity response = underTest.createKey(KEY_VALUE, mockRequest); + assertThat(response.getStatusCode(), is(HttpStatus.CONFLICT)); + assertThat(response.getBody(), is(expectedBody)); + } + + @Test + void givenStorageWithKey_whenUpdateKey_thenResponseNoContent() { + when(mockStorage.update(SERVICE_ID, KEY_VALUE)).thenReturn(KEY_VALUE); + + ResponseEntity response = underTest.update(KEY_VALUE, mockRequest); + assertThat(response.getStatusCode(), is(HttpStatus.NO_CONTENT)); + assertThat(response.getBody(), is(nullValue())); + } + + @Test + void givenStorageWithNoKey_whenUpdateKey_thenResponseNotFound() { + when(mockStorage.update(SERVICE_ID, KEY_VALUE)).thenReturn(null); + ApiMessageView expectedBody = messageService.createMessage("org.zowe.apiml.cache.keyNotInCache", KEY, SERVICE_ID).mapToView(); + + ResponseEntity response = underTest.update(KEY_VALUE, mockRequest); + assertThat(response.getStatusCode(), is(HttpStatus.NOT_FOUND)); + assertThat(response.getBody(), is(expectedBody)); + } + + @Test + void givenStorageWithKey_whenDeleteKey_thenResponseNoContent() { + when(mockStorage.delete(any(), any())).thenReturn(KEY_VALUE); + + ResponseEntity response = underTest.delete(KEY, mockRequest); + assertThat(response.getStatusCode(), is(HttpStatus.NO_CONTENT)); + assertThat(response.getBody(), is(nullValue())); + } + + @Test + void givenNoKey_whenDeleteKey_thenResponseBadRequest() { + ApiMessageView expectedBody = messageService.createMessage("org.zowe.apiml.cache.keyNotProvided").mapToView(); + + ResponseEntity response = underTest.delete(null, mockRequest); + assertThat(response.getStatusCode(), is(HttpStatus.BAD_REQUEST)); + assertThat(response.getBody(), is(expectedBody)); + } + + @Test + void givenStorageWithNoKey_whenDeleteKey_thenResponseNotFound() { + ApiMessageView expectedBody = messageService.createMessage("org.zowe.apiml.cache.keyNotInCache", KEY, SERVICE_ID).mapToView(); + when(mockStorage.delete(any(), any())).thenReturn(null); + + ResponseEntity response = underTest.delete(KEY, mockRequest); + assertThat(response.getStatusCode(), is(HttpStatus.NOT_FOUND)); + assertThat(response.getBody(), is(expectedBody)); + } + + @Test + void givenNoPayload_whenValidatePayload_thenResponseBadRequest() { + ApiMessageView expectedBody = messageService.createMessage("org.zowe.apiml.cache.invalidPayload", + null, "No KeyValue provided in the payload").mapToView(); + + ResponseEntity response = underTest.createKey(null, mockRequest); + assertThat(response.getStatusCode(), is(HttpStatus.BAD_REQUEST)); + assertThat(response.getBody(), is(expectedBody)); + } + + @ParameterizedTest + @MethodSource("provideStringsForGivenBadKeyValue") + void givenBadKeyValue_whenValidatePayload_thenResponseBadRequest(String key, String value, String errMessage) { + KeyValue keyValue = new KeyValue(key, value); + ApiMessageView expectedBody = messageService.createMessage("org.zowe.apiml.cache.invalidPayload", + keyValue, errMessage).mapToView(); + + ResponseEntity response = underTest.createKey(keyValue, mockRequest); + assertThat(response.getStatusCode(), is(HttpStatus.BAD_REQUEST)); + assertThat(response.getBody(), is(expectedBody)); + } + + private static Stream provideStringsForGivenBadKeyValue() { + return Stream.of( + Arguments.of("key", null, "No value provided in the payload"), + Arguments.of(null, "value", "No key provided in the payload"), + Arguments.of("key ", "value", "Key is not alphanumeric") + ); + } + + @Test + void givenNoToken_whenQueryToken_thenResponseBadRequest() throws ZaasClientException { + ApiMessageView expectedBody = messageService.createMessage("org.zowe.apiml.security.query.tokenNotProvided", + mockRequest.getRequestURL().toString()).mapToView(); + when(mockZaasClient.query(any())).thenThrow(new ZaasClientException(ZaasClientErrorCodes.TOKEN_NOT_PROVIDED)); + + ResponseEntity response = underTest.getValue(KEY, mockRequest); + assertThat(response.getStatusCode(), is(HttpStatus.BAD_REQUEST)); + assertThat(response.getBody(), is(expectedBody)); + } + + @Test + void givenInvalidToken_whenQueryToken_thenResponseUnauthorized() throws ZaasClientException { + ApiMessageView expectedBody = messageService.createMessage("org.zowe.apiml.security.query.invalidToken", + mockRequest.getRequestURL().toString()).mapToView(); + when(mockZaasClient.query(any())).thenThrow(new ZaasClientException(ZaasClientErrorCodes.INVALID_JWT_TOKEN)); + + ResponseEntity response = underTest.getValue(KEY, mockRequest); + assertThat(response.getStatusCode(), is(HttpStatus.UNAUTHORIZED)); + assertThat(response.getBody(), is(expectedBody)); + } + + @Test + void givenExpiredToken_whenQueryToken_thenResponseUnauthorized() throws ZaasClientException { + ApiMessageView expectedBody = messageService.createMessage("org.zowe.apiml.security.expiredToken", + mockRequest.getRequestURL().toString()).mapToView(); + when(mockZaasClient.query(any())).thenThrow(new ZaasClientException(ZaasClientErrorCodes.EXPIRED_JWT_EXCEPTION)); + + ResponseEntity response = underTest.getValue(KEY, mockRequest); + assertThat(response.getStatusCode(), is(HttpStatus.UNAUTHORIZED)); + assertThat(response.getBody(), is(expectedBody)); + } + + @Test + void givenNoGateway_whenQueryToken_thenResponseUnauthorized() throws ZaasClientException { + ZaasClientException zaasException = new ZaasClientException(ZaasClientErrorCodes.SERVICE_UNAVAILABLE, "This is an error"); + ApiMessageView expectedBody = messageService.createMessage("org.zowe.apiml.cache.gatewayUnavailable", + mockRequest.getRequestURL().toString(), zaasException.getMessage()).mapToView(); + when(mockZaasClient.query(any())).thenThrow(zaasException); + + ResponseEntity response = underTest.getValue(KEY, mockRequest); + assertThat(response.getStatusCode(), is(HttpStatus.NOT_FOUND)); + assertThat(response.getBody(), is(expectedBody)); + } + + @Test + void givenRandomError_whenQueryToken_thenResponseInternalError() throws ZaasClientException { + Throwable errCause = new Exception("This is an error"); + ZaasClientException zaasException = new ZaasClientException(ZaasClientErrorCodes.GENERIC_EXCEPTION, errCause); + ApiMessageView expectedBody = messageService.createMessage("org.zowe.apiml.common.internalRequestError", + mockRequest.getRequestURL().toString(), zaasException.getMessage(), zaasException.getCause()).mapToView(); + when(mockZaasClient.query(any())).thenThrow(zaasException); + + ResponseEntity response = underTest.getValue(KEY, mockRequest); + assertThat(response.getStatusCode(), is(HttpStatus.INTERNAL_SERVER_ERROR)); + assertThat(response.getBody(), is(expectedBody)); + } + + @Test + void givenToken_whenTokenQueryReturnsNull_thenResponseUnauthorized() throws ZaasClientException { + ApiMessageView expectedBody = messageService.createMessage("org.zowe.apiml.security.query.invalidToken", + mockRequest.getRequestURL().toString()).mapToView(); + when(mockZaasClient.query(any())).thenReturn(null); + + ResponseEntity response = underTest.getValue(KEY, mockRequest); + assertThat(response.getStatusCode(), is(HttpStatus.UNAUTHORIZED)); + assertThat(response.getBody(), is(expectedBody)); + } + + @Test + void givenToken_whenTokenQueryReturnsExpiredToken_thenResponseUnauthorized() throws ZaasClientException { + ApiMessageView expectedBody = messageService.createMessage("org.zowe.apiml.security.expiredToken", + mockRequest.getRequestURL().toString()).mapToView(); + ZaasToken expiredToken = new ZaasToken(); + expiredToken.setExpired(true); + when(mockZaasClient.query(any())).thenReturn(expiredToken); + + ResponseEntity response = underTest.getValue(KEY, mockRequest); + assertThat(response.getStatusCode(), is(HttpStatus.UNAUTHORIZED)); + assertThat(response.getBody(), is(expectedBody)); + } + + @ParameterizedTest + @MethodSource("cookieTestProvider") + void givenCookieWithWrongAuthentication_whenQueryToken_thenResponseUnauthorized + (String cookieName, String cookieValue, String queryToken) throws ZaasClientException { + ApiMessageView expectedBody = messageService.createMessage("org.zowe.apiml.security.query.invalidToken", + mockRequest.getRequestURL().toString()).mapToView(); + + Cookie[] cookies = new Cookie[]{new Cookie(cookieName, cookieValue)}; + mockRequest.setCookies(cookies); + when(mockZaasClient.query(queryToken)).thenReturn(null); + + ResponseEntity response = underTest.getAllValues(mockRequest); + assertThat(response.getStatusCode(), is(HttpStatus.UNAUTHORIZED)); + assertThat(response.getBody(), is(expectedBody)); + } + + private static Stream cookieTestProvider() { + return Stream.of( + Arguments.of("my", "cookie", null), + Arguments.of("apimlAuthenticationToken", "bad_token", "bad_token"), + Arguments.of("apimlAuthenticationToken", "", null) + ); + } +} diff --git a/caching-service/src/test/java/org/zowe/apiml/caching/service/inmemory/InMemoryStorageTest.java b/caching-service/src/test/java/org/zowe/apiml/caching/service/inmemory/InMemoryStorageTest.java new file mode 100644 index 0000000000..748a1c46eb --- /dev/null +++ b/caching-service/src/test/java/org/zowe/apiml/caching/service/inmemory/InMemoryStorageTest.java @@ -0,0 +1,128 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package org.zowe.apiml.caching.service.inmemory; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.zowe.apiml.caching.model.KeyValue; + +import java.util.HashMap; +import java.util.Map; + +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; + +public class InMemoryStorageTest { + private InMemoryStorage underTest; + + private Map> testingStorage; + private final String serviceId = "acme"; + + @BeforeEach + void setUp() { + testingStorage = new HashMap<>(); + underTest = new InMemoryStorage(testingStorage); + } + + @Test + void givenDefaultStorageConstructor_whenStorageConstructed_thenCanUseStorage() { + underTest = new InMemoryStorage(); + underTest.create(serviceId, new KeyValue("key", "value")); + + KeyValue result = underTest.read(serviceId, "key"); + assertThat(result.getKey(), is("key")); + assertThat(result.getValue(), is("value")); + } + + @Test + void givenThereIsNoValueForService_whenValueIsStored_thenItIsStored() { + underTest.create(serviceId, new KeyValue("username", "ValidName")); + + KeyValue result = testingStorage.get(serviceId).get("username"); + assertThat(result.getKey(), is("username")); + assertThat(result.getValue(), is("ValidName")); + } + + @Test + void givenThereIsValueForService_whenValueIsUpdated_thenItIsReplaced() { + Map serviceStorage = new HashMap<>(); + testingStorage.put(serviceId, serviceStorage); + serviceStorage.put("username", new KeyValue("username", "Name 1")); + underTest.update(serviceId, new KeyValue("username", "ValidName")); + + KeyValue result = testingStorage.get(serviceId).get("username"); + assertThat(result.getKey(), is("username")); + assertThat(result.getValue(), is("ValidName")); + } + + @Test + void givenThereIsNoServiceCache_whenValueIsUpdated_thenNullIsReturned() { + KeyValue result = underTest.update(serviceId, new KeyValue("username", "Name 1")); + assertThat(result, is(nullValue())); + } + + @Test + void givenThereIsNoKey_whenValueIsUpdated_thenNullIsReturned() { + testingStorage.put(serviceId, new HashMap<>()); + KeyValue result = underTest.update(serviceId, new KeyValue("bad key", "Name 1")); + assertThat(result, is(nullValue())); + } + + @Test + void givenValueWasAlreadyAddedToTheStorage_whenRequested_thenItWillBeReturned() { + Map serviceStorage = new HashMap<>(); + testingStorage.put(serviceId, serviceStorage); + serviceStorage.put("username", new KeyValue("username", "Name 1")); + + KeyValue result = underTest.read(serviceId, "username"); + assertThat(result.getKey(), is("username")); + assertThat(result.getValue(), is("Name 1")); + } + + @Test + void givenNoValueWasStoredForTheService_whenRequested_thenNullWillBeReturned() { + KeyValue result = underTest.read(serviceId, "username"); + assertThat(result, is(nullValue())); + } + + @Test + void givenServiceHasStoredValues_whenLoadingAllForService_thenAllAreReturned() { + Map serviceStorage = new HashMap<>(); + testingStorage.put(serviceId, serviceStorage); + serviceStorage.put("username", new KeyValue("username", "Name 1")); + + Map result = underTest.readForService(serviceId); + assertThat(result.containsKey("username"), is(true)); + } + + @Test + void givenKeyDoesntExist_whenDeletionRequested_thenNullIsReturned() { + testingStorage.put(serviceId, new HashMap<>()); + KeyValue result = underTest.delete(serviceId, "nonexistent"); + assertThat(result, is(nullValue())); + } + + @Test + void givenServiceStorageDoesntExist_whenDeletionRequest_thenNullIsReturned() { + KeyValue result = underTest.delete(serviceId, "nonexistent"); + assertThat(result, is(nullValue())); + } + + @Test + void givenKeyExists_whenDeletionRequested_thenKeyValueIsReturnedAndKeyIsRemoved() { + Map serviceStorage = new HashMap<>(); + testingStorage.put(serviceId, serviceStorage); + serviceStorage.put("username", new KeyValue("username", "Name 1")); + + underTest.delete(serviceId, "username"); + assertThat(serviceStorage.containsKey("username"), is(false)); + } +} diff --git a/caching-service/src/test/resources/application.yml b/caching-service/src/test/resources/application.yml new file mode 100644 index 0000000000..e6cedc2582 --- /dev/null +++ b/caching-service/src/test/resources/application.yml @@ -0,0 +1,82 @@ +spring: + application: + name: Caching service + +apiml: + enabled: true + service: + preferIpAddress: false + + serviceId: cachingservice + title: Caching service for the internal usage. + description: Service which provides caching API + + discoveryServiceUrls: https://localhost:10011/eureka/ + + scheme: https + + hostname: localhost + port: 10016 + baseUrl: ${apiml.service.scheme}://${apiml.service.hostname}:${apiml.service.port} + contextPath: /${apiml.service.serviceId} + + homePageRelativeUrl: ${apiml.service.contextPath} + statusPageRelativeUrl: ${apiml.service.contextPath}/application/info + healthCheckRelativeUrl: ${apiml.service.contextPath}/application/health + + routes: + - gateway-url: "api/v1" + service-url: ${apiml.service.contextPath}/api/v1 + apiInfo: + - apiId: org.zowe.cachingservice + version: 1.0.0 + gatewayUrl: api/v1 + swaggerUrl: ${apiml.service.scheme}://${apiml.service.hostname}:${apiml.service.port}${apiml.service.contextPath}/v1/api-docs + documentationUrl: https://www.zowe.org + catalog: + tile: + id: zowe + title: Zowe applications + description: Applications which are part of the Zowe + version: 1.0.0 + ssl: + enabled: true + verifySslCertificatesOfServices: true + protocol: ${server.ssl.protocol} + keyStoreType: ${server.ssl.keyStoreType} + trustStoreType: ${server.ssl.trustStoreType} + + keyAlias: ${server.ssl.keyAlias} + keyPassword: ${server.ssl.keyPassword} + keyStore: ${server.ssl.keyStore} + keyStorePassword: ${server.ssl.keyStorePassword} + trustStore: ${server.ssl.trustStore} + trustStorePassword: ${server.ssl.trustStorePassword} + customMetadata: + apiml: + enableUrlEncodedCharacters: true + gatewayPort: 10010 + gatewayAuthEndpoint: /api/v1/gateway/auth + corsEnabled: false + +server: + port: 10016 + + servlet: + contextPath: / + + ssl: + enabled: true + clientAuth: want + protocol: TLSv1.2 + enabled-protocols: TLSv1.2 + ciphers: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384 + keyStoreType: PKCS12 + trustStoreType: PKCS12 + + keyAlias: localhost + keyPassword: password + keyStore: ../keystore/localhost/localhost.keystore.p12 + keyStorePassword: password + trustStore: ../keystore/localhost/localhost.truststore.p12 + trustStorePassword: password diff --git a/discoverable-client/src/main/java/org/zowe/apiml/client/api/ZaasClientTestController.java b/discoverable-client/src/main/java/org/zowe/apiml/client/api/ZaasClientTestController.java index 184f9d528f..342961be87 100644 --- a/discoverable-client/src/main/java/org/zowe/apiml/client/api/ZaasClientTestController.java +++ b/discoverable-client/src/main/java/org/zowe/apiml/client/api/ZaasClientTestController.java @@ -15,9 +15,11 @@ import lombok.Data; import lombok.NoArgsConstructor; import org.apache.commons.lang3.StringUtils; +import org.springframework.context.annotation.Import; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import org.zowe.apiml.zaasclient.config.DefaultZaasClientConfiguration; import org.zowe.apiml.zaasclient.exception.ZaasClientException; import org.zowe.apiml.zaasclient.exception.ZaasConfigurationException; import org.zowe.apiml.zaasclient.service.ZaasClient; @@ -28,6 +30,7 @@ value = "/api/v1/zaasClient", consumes = "application/json", tags = {"Zaas client test call"}) +@Import(DefaultZaasClientConfiguration.class) public class ZaasClientTestController { private ZaasClient zaasClient; diff --git a/docs/local-configuration.md b/docs/local-configuration.md index a230df6ea5..f169769eeb 100644 --- a/docs/local-configuration.md +++ b/docs/local-configuration.md @@ -41,6 +41,11 @@ java -jar discovery-service/build/libs/discovery-service.jar --spring.config.add java -jar api-catalog-services/build/libs/api-catalog-services.jar --spring.config.additional-location=file:./config/local/api-catalog-service.yml ``` +### Caching API + +```shell +java -jar caching-service/build/libs/caching-service.jar +``` ### Sample Application - Discoverable Client diff --git a/gradle/coverage.gradle b/gradle/coverage.gradle index 38a271cace..c7ece37a15 100644 --- a/gradle/coverage.gradle +++ b/gradle/coverage.gradle @@ -7,6 +7,7 @@ ext.javaProjectsWithUnitTests = [ 'discovery-service', 'apiml-common', 'apiml-utility', + 'caching-service', 'gateway-service', 'onboarding-enabler-java', 'onboarding-enabler-spring', diff --git a/gradle/license.gradle b/gradle/license.gradle index 36c15f45aa..cb317cacd3 100644 --- a/gradle/license.gradle +++ b/gradle/license.gradle @@ -3,6 +3,7 @@ ext.projectsNeedLicense = [ 'api-catalog-ui', 'apiml-common', 'apiml-security-common', + 'caching-service', 'common-service-core', 'discoverable-client', 'discovery-service', diff --git a/gradle/publish.gradle b/gradle/publish.gradle index debd813240..f5d416036d 100644 --- a/gradle/publish.gradle +++ b/gradle/publish.gradle @@ -7,6 +7,7 @@ ext.javaLibraries = [ 'apiml-utility', 'apiml-common', 'apiml-security-common', + 'caching-service', 'discovery-service', 'gateway-service', 'security-service-client-spring', diff --git a/integration-tests/build.gradle b/integration-tests/build.gradle index f17a93ed3e..7088558b90 100644 --- a/integration-tests/build.gradle +++ b/integration-tests/build.gradle @@ -27,6 +27,7 @@ dependencies { compile libraries.jjwt compile(project(':apiml-security-common')) compile(project(':zaas-client')) + compile(project(':caching-service')) testCompile libraries.junit testCompile libraries.hamcrest diff --git a/integration-tests/src/test/java/org/zowe/apiml/cachingservice/CachingApiIntegrationTest.java b/integration-tests/src/test/java/org/zowe/apiml/cachingservice/CachingApiIntegrationTest.java new file mode 100644 index 0000000000..0e8592d723 --- /dev/null +++ b/integration-tests/src/test/java/org/zowe/apiml/cachingservice/CachingApiIntegrationTest.java @@ -0,0 +1,170 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.cachingservice; +import io.restassured.RestAssured; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.*; +import org.zowe.apiml.caching.model.KeyValue; +import org.zowe.apiml.gatewayservice.SecurityUtils; +import org.zowe.apiml.util.categories.NotForMainframeTest; +import org.zowe.apiml.util.categories.TestsNotMeantForZowe; +import org.zowe.apiml.util.http.HttpRequestUtils; + +import java.net.URI; + +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static org.apache.http.HttpStatus.*; +import static org.hamcrest.Matchers.isEmptyString; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsNot.not; + +@TestsNotMeantForZowe +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@NotForMainframeTest +class CachingApiIntegrationTest { + + private static final URI CACHING_PATH = HttpRequestUtils.getUriFromGateway("/cachingservice/api/v1/cache"); + private final static String COOKIE_NAME = "apimlAuthenticationToken"; + private static String jwtToken; + + @BeforeAll + static void setup() { + RestAssured.useRelaxedHTTPSValidation(); + jwtToken = generateToken(); + } + + @Test + @Order(1) + void givenValidKeyValue_whenCallingCreateEndpoint_thenStoreIt() { + KeyValue keyValue = new KeyValue("testKey", "testValue"); + + given() + .contentType(JSON) + .body(keyValue) + .cookie(COOKIE_NAME, jwtToken) + .when() + .post(CACHING_PATH) + .then() + .statusCode(is(SC_CREATED)); + } + + @Test + @Order(2) + void givenEmptyBody_whenCallingCreateEndpoint_thenReturn400() { + given() + .contentType(JSON) + .cookie(COOKIE_NAME, jwtToken) + .when() + .post(CACHING_PATH) + .then() + .statusCode(is(SC_BAD_REQUEST)); + } + + @Test + @Order(3) + void givenValidKeyParameter_whenCallingGetEndpoint_thenReturnKeyValueEntry() { + given() + .contentType(JSON) + .cookie(COOKIE_NAME, jwtToken) + .when() + .get(CACHING_PATH + "/testKey") + .then() + .body(not(isEmptyString())) + .statusCode(is(SC_OK)); + } + + @Test + @Order(4) + void givenValidKeyParameter_whenCallingGetAllEndpoint_thenAllTheStoredEntries() { + KeyValue keyValue = new KeyValue("testKey2", "testValue2"); + + given() + .contentType(JSON) + .body(keyValue) + .cookie(COOKIE_NAME, jwtToken) + .when() + .post(CACHING_PATH) + .then() + .statusCode(is(SC_CREATED)); + + given() + .contentType(JSON) + .cookie(COOKIE_NAME, jwtToken) + .when() + .get(CACHING_PATH) + .then() + .body("testKey", Matchers.is(not(isEmptyString())), + "testKey2", Matchers.is(not(isEmptyString()))) + .statusCode(is(SC_OK)); + } + + @Test + @Order(5) + void givenNonExistingKeyParameter_whenCallingGetEndpoint_thenReturnKeyNotFound() { + given() + .contentType(JSON) + .cookie(COOKIE_NAME, jwtToken) + .when() + .get(CACHING_PATH + "/invalidKey") + .then() + .body(not(isEmptyString())) + .statusCode(is(SC_NOT_FOUND)); + } + + @Test + @Order(6) + void givenValidKeyParameter_whenCallingUpdateEndpoint_thenReturnUpdateValue() { + KeyValue newValue = new KeyValue("testKey", "newValue"); + + given() + .contentType(JSON) + .body(newValue) + .cookie(COOKIE_NAME, jwtToken) + .when() + .put(CACHING_PATH) + .then() + .statusCode(is(SC_NO_CONTENT)); + + given() + .contentType(JSON) + .cookie(COOKIE_NAME, jwtToken) + .when() + .get(CACHING_PATH + "/testKey") + .then() + .body("value", Matchers.is("newValue")) + .statusCode(is(SC_OK)); + } + + @Test + @Order(7) + void givenValidKeyParameter_whenCallingDeleteEndpoint_thenDeleteKeyValueFromStore() { + given() + .contentType(JSON) + .cookie(COOKIE_NAME, jwtToken) + .when() + .delete(CACHING_PATH + "/testKey") + .then() + .statusCode(is(SC_NO_CONTENT)); + + given() + .contentType(JSON) + .cookie(COOKIE_NAME, jwtToken) + .when() + .get(CACHING_PATH + "/testKey") + .then() + .statusCode(is(SC_NOT_FOUND)); + } + + private static String generateToken() { + return SecurityUtils.gatewayToken(); + } +} diff --git a/package.json b/package.json index ef603a1b44..b4d3306083 100644 --- a/package.json +++ b/package.json @@ -6,12 +6,13 @@ "doc": "docs" }, "scripts": { - "api-layer": "concurrently --names \"GS,DS,AC,DC,ZO\" -c cyan,yellow,white,blue,green npm:gateway-service npm:discovery-service npm:api-catalog-service npm:discoverable-client npm:mock-zosmf", - "api-layer-ci": "concurrently --names \"GS,DS,AC,DC,ZO\" -c cyan,yellow,white,blue,green npm:gateway-service-ci npm:discovery-service npm:api-catalog-service npm:discoverable-client npm:mock-zosmf", + "api-layer": "concurrently --names \"GS,DS,AC,DC,ZO,CS\" -c cyan,yellow,white,blue,green npm:gateway-service npm:discovery-service npm:api-catalog-service npm:discoverable-client npm:mock-zosmf npm:caching-service", + "api-layer-ci": "concurrently --names \"GS,DS,AC,DC,ZO,CS\" -c cyan,yellow,white,blue,green npm:gateway-service-ci npm:discovery-service npm:api-catalog-service npm:discoverable-client npm:mock-zosmf npm:caching-service", "api-layer-core": "concurrently --names \"GW,DS,AC\" -c cyan,yellow,white npm:gateway-service npm:discovery-service npm:api-catalog-service", "api-layer-without-gateway": "concurrently --names \"DS,AC,DC\" -c yellow,white,blue npm:discovery-service npm:api-catalog-service npm:discoverable-client", "api-layer-without-discovery": "concurrently --names \"GW,AC,DC\" -c cyan,white,blue npm:gateway-service npm:api-catalog-service npm:discoverable-client", "api-layer-without-catalog": "concurrently --names \"GW,DS,DC\" -c cyan,yellow,blue npm:gateway-service npm:discovery-service npm:discoverable-client", + "caching-service": "java -jar caching-service/build/libs/caching-service.jar", "gateway-service": "java -jar gateway-service/build/libs/gateway-service.jar --spring.config.additional-location=file:./config/local/gateway-service.yml --apiml.security.ssl.verifySslCertificatesOfServices=true", "gateway-service-ci": "java -jar gateway-service/build/libs/gateway-service.jar --spring.config.additional-location=file:./config/local/gateway-service.yml --apiml.security.ssl.verifySslCertificatesOfServices=true --spring.profiles.include=diag --apiml.security.x509.enabled=true", "gateway-service-debug": "java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,address=5010,suspend=y -jar gateway-service/build/libs/gateway-service.jar --spring.config.additional-location=file:./config/local/gateway-service.yml", diff --git a/settings.gradle b/settings.gradle index e41ac93977..e81832e644 100644 --- a/settings.gradle +++ b/settings.gradle @@ -19,6 +19,7 @@ include 'discovery-service' include 'apiml-utility' include 'apiml-common' include 'apiml-security-common' +include 'caching-service' include 'gateway-service' include 'common-service-core' include 'discoverable-client' diff --git a/zaas-client/README.md b/zaas-client/README.md index 7d08902546..d2d7735196 100644 --- a/zaas-client/README.md +++ b/zaas-client/README.md @@ -157,6 +157,15 @@ To use this library use the procedure described in this article. } ``` + Alternatively, you can import `DefaultZaasClientCongfiguration` to use the default configuration file structure: + + ```java + @Import(DefaultZaasClientConfiguration.class) + public class SampleZaasClientImplementation { + private ZaasClient zaasClient; + } + ``` + 4. Create an instance of `ZaasClient` in your class and provide the `configProperties` object like the following: ```java diff --git a/zaas-client/build.gradle b/zaas-client/build.gradle index 787f0e45ee..b8be40c878 100644 --- a/zaas-client/build.gradle +++ b/zaas-client/build.gradle @@ -1,6 +1,7 @@ dependencies { annotationProcessor libraries.lombok + compile "org.springframework.boot:spring-boot-starter-web:${springBootVersion}" compile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.11' compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.10.1' compile group: 'org.slf4j', name: 'slf4j-api', version: '1.7.30' diff --git a/discoverable-client/src/main/java/org/zowe/apiml/client/configuration/ZaasClientConfig.java b/zaas-client/src/main/java/org/zowe/apiml/zaasclient/config/DefaultZaasClientConfiguration.java similarity index 91% rename from discoverable-client/src/main/java/org/zowe/apiml/client/configuration/ZaasClientConfig.java rename to zaas-client/src/main/java/org/zowe/apiml/zaasclient/config/DefaultZaasClientConfiguration.java index 89637d8f08..8fa8ec4b47 100644 --- a/discoverable-client/src/main/java/org/zowe/apiml/client/configuration/ZaasClientConfig.java +++ b/zaas-client/src/main/java/org/zowe/apiml/zaasclient/config/DefaultZaasClientConfiguration.java @@ -7,18 +7,15 @@ * * Copyright Contributors to the Zowe Project. */ -package org.zowe.apiml.client.configuration; +package org.zowe.apiml.zaasclient.config; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.zowe.apiml.zaasclient.config.ConfigProperties; import org.zowe.apiml.zaasclient.exception.ZaasConfigurationException; import org.zowe.apiml.zaasclient.service.ZaasClient; import org.zowe.apiml.zaasclient.service.internal.ZaasClientImpl; -@Configuration -public class ZaasClientConfig { +public class DefaultZaasClientConfiguration { @Value("${apiml.service.hostname}") private String host; @@ -47,10 +44,8 @@ public class ZaasClientConfig { @Value("${apiml.service.ssl.trustStoreType}") private String trustStoreType; - @Bean public ConfigProperties getConfigProperties() { - ConfigProperties configProperties = new ConfigProperties(); configProperties.setApimlHost(host); configProperties.setApimlPort(port); diff --git a/zowe-install/src/main/resources/component-scripts/start.sh b/zowe-install/src/main/resources/component-scripts/start.sh index 870f034c78..c1a5566bef 100644 --- a/zowe-install/src/main/resources/component-scripts/start.sh +++ b/zowe-install/src/main/resources/component-scripts/start.sh @@ -139,3 +139,24 @@ _BPX_JOBNAME=${ZOWE_PREFIX}${GATEWAY_CODE} java -Xms32m -Xmx256m -Xquickstart \ -Djava.protocol.handler.pkgs=com.ibm.crypto.provider \ -cp ${ROOT_DIR}"/components/api-mediation/gateway-service.jar":/usr/include/java_classes/IRRRacf.jar \ org.springframework.boot.loader.PropertiesLauncher & + +if [[ ! -z "$ZOWE_CACHING_SERVICE_START" ]] +then + CACHING_CODE=CS + _BPX_JOBNAME=${ZOWE_PREFIX}${CACHING_CODE} java -Xms16m -Xmx512m -Xquickstart \ + -Dibm.serversocket.recover=true \ + -Dfile.encoding=UTF-8 \ + -Djava.io.tmpdir=/tmp \ + -Dspring.profiles.include=$LOG_LEVEL \ + -Dserver.ssl.enabled=true \ + -Dserver.ssl.keyStore=${KEYSTORE} \ + -Dserver.ssl.keyStoreType=${KEYSTORE_TYPE} \ + -Dserver.ssl.keyStorePassword=${KEYSTORE_PASSWORD} \ + -Dserver.ssl.keyAlias=${KEY_ALIAS} \ + -Dserver.ssl.keyPassword=${KEYSTORE_PASSWORD} \ + -Dserver.ssl.trustStore=${TRUSTSTORE} \ + -Dserver.ssl.trustStoreType=${KEYSTORE_TYPE} \ + -Dserver.ssl.trustStorePassword=${KEYSTORE_PASSWORD} \ + -Djava.protocol.handler.pkgs=com.ibm.crypto.provider \ + -jar ${ROOT_DIR}"/components/api-mediation/caching-service.jar" & +fi