diff options
author | liamfallon <liam.fallon@est.tech> | 2021-06-07 16:41:12 +0100 |
---|---|---|
committer | liamfallon <liam.fallon@est.tech> | 2021-06-09 14:34:20 +0100 |
commit | 030ba1a000fc78e4c8778be469a41b14538a58ea (patch) | |
tree | 234ef346f04a282628d25fb85647e75a93530fc2 /participant/participant-impl | |
parent | 20c7487f77d0728d270b2bed34c2c798d17cc12d (diff) |
Initial commit for kubernetes participant in CLAMP
Spring application that exposes REST end points for installing, uninstalling, onboarding
and deleting of helm charts to/from local directory.
CL runtime can also trigger installation and uninstallation of helm charts from both local and configured helm repositories.
Junits will be committed as a separate review.
Issue-ID: POLICY-3240
Change-Id: I7633b6fd6ad41fc8fa55d3722e44f1b2ec132e50
Signed-off-by: zrrmmua <ramesh.murugan.iyer@est.tech>
Signed-off-by: liamfallon <liam.fallon@est.tech>
Diffstat (limited to 'participant/participant-impl')
19 files changed, 1668 insertions, 2 deletions
diff --git a/participant/participant-impl/participant-impl-kubernetes/pom.xml b/participant/participant-impl/participant-impl-kubernetes/pom.xml index a85c5fdbf..504d91d78 100644 --- a/participant/participant-impl/participant-impl-kubernetes/pom.xml +++ b/participant/participant-impl/participant-impl-kubernetes/pom.xml @@ -19,7 +19,7 @@ --> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> @@ -30,5 +30,116 @@ <artifactId>policy-clamp-participant-impl-kubernetes</artifactId> <name>${project.artifactId}</name> - <description>Kubernetes participant, that allows microservices running in Kubernetes to partake in control loops</description> + <description>Kubernetes participant, that allows k8s pods to partake in control loops</description> + + <properties> + <springboot.version>2.5.0</springboot.version> + <immutable.version>2.8.8</immutable.version> + <springfox.version>3.0.0</springfox.version> + </properties> + + <dependencyManagement> + <dependencies> + <!-- Spring Boot BOM --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-dependencies</artifactId> + <version>${springboot.version}</version> + <type>pom</type> + <scope>import</scope> + </dependency> + </dependencies> + </dependencyManagement> + + <dependencies> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-web</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-webflux</artifactId> + </dependency> + <dependency> + <groupId>org.apache.tomcat.embed</groupId> + <artifactId>tomcat-embed-core</artifactId> + </dependency> + <dependency> + <groupId>org.immutables</groupId> + <artifactId>value</artifactId> + <version>${immutable.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.immutables</groupId> + <artifactId>gson</artifactId> + <version>${immutable.version}</version> + </dependency> + <dependency> + <groupId>org.json</groupId> + <artifactId>json</artifactId> + </dependency> + <dependency> + <groupId>org.projectlombok</groupId> + <artifactId>lombok</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>io.springfox</groupId> + <artifactId>springfox-swagger2</artifactId> + <version>${springfox.version}</version> + </dependency> + <dependency> + <groupId>io.springfox</groupId> + <artifactId>springfox-swagger-ui</artifactId> + <version>${springfox.version}</version> + <scope>runtime</scope> + </dependency> + <dependency> + <groupId>commons-fileupload</groupId> + <artifactId>commons-fileupload</artifactId> + <version>1.4</version> + </dependency> + <dependency> + <groupId>org.onap.policy.clamp.participant</groupId> + <artifactId>policy-clamp-participant-intermediary</artifactId> + <version>${project.version}</version> + </dependency> + + </dependencies> + + <build> + <resources> + <!-- Output the version of the control loop system --> + <resource> + <directory>src/main/resources</directory> + <filtering>true</filtering> + <includes> + <include>**/version.txt</include> + </includes> + </resource> + <resource> + <directory>src/main/resources</directory> + <filtering>false</filtering> + <excludes> + <exclude>**/version.txt</exclude> + </excludes> + </resource> + </resources> + <plugins> + <plugin> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-maven-plugin</artifactId> + <version>${springboot.version}</version> + <executions> + <execution> + <goals> + <goal>repackage</goal> + </goals> + <phase>package</phase> + </execution> + </executions> + </plugin> + </plugins> + </build> </project> diff --git a/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/Application.java b/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/Application.java new file mode 100644 index 000000000..ffa0bceb9 --- /dev/null +++ b/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/Application.java @@ -0,0 +1,40 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2021 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.clamp.controlloop.participant.kubernetes; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Starter. + * + */ +@SpringBootApplication +public class Application { + /** + * Main class. + * @param args args + */ + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} + diff --git a/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/configurations/BeanFactory.java b/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/configurations/BeanFactory.java new file mode 100644 index 000000000..3199d0cd9 --- /dev/null +++ b/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/configurations/BeanFactory.java @@ -0,0 +1,71 @@ +/*- + * ========================LICENSE_START================================= + * Copyright (C) 2021 Nordix Foundation. All rights reserved. + * ====================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================LICENSE_END=================================== + */ + +package org.onap.policy.clamp.controlloop.participant.kubernetes.configurations; + +import org.apache.catalina.connector.Connector; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; +import org.springframework.boot.web.servlet.server.ServletWebServerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.multipart.MultipartResolver; +import org.springframework.web.multipart.commons.CommonsMultipartResolver; + +/** + * Bean Factory class for helm client. + */ +@Configuration +public class BeanFactory { + + @Value("${server.http-port}") + private int httpPort = 0; + + /** + * Method to create servlet container bean. + * @return webserver factory + */ + @Bean + public ServletWebServerFactory servletContainer() { + var tomcat = new TomcatServletWebServerFactory(); + if (httpPort > 0) { + tomcat.addAdditionalTomcatConnectors(getHttpConnector(httpPort)); + } + return tomcat; + } + + private static Connector getHttpConnector(int httpPort) { + var connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL); + connector.setScheme("http"); + connector.setPort(httpPort); + connector.setSecure(false); + return connector; + } + + /** + * Method to create multipartResolver bean. + * @return MultipartResolver + */ + @Bean(name = "multipartResolver") + public MultipartResolver multipartResolver() { + var multipartResolver = new CommonsMultipartResolver(); + multipartResolver.setMaxUploadSize(100000); + return multipartResolver; + } + +} diff --git a/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/configurations/ParametersConfig.java b/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/configurations/ParametersConfig.java new file mode 100644 index 000000000..5f2a4e4ad --- /dev/null +++ b/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/configurations/ParametersConfig.java @@ -0,0 +1,41 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2021 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.clamp.controlloop.participant.kubernetes.configurations; + +import org.onap.policy.clamp.controlloop.common.exception.ControlLoopException; +import org.onap.policy.clamp.controlloop.participant.kubernetes.parameters.ParticipantK8sParameterHandler; +import org.onap.policy.clamp.controlloop.participant.kubernetes.parameters.ParticipantK8sParameters; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ParametersConfig { + + @Value("${participant.file}") + private String file; + + @Bean + public ParticipantK8sParameters participantK8sParameters() throws ControlLoopException { + return new ParticipantK8sParameterHandler().toParticipantK8sParameters(file); + } +} + diff --git a/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/configurations/ParticipantIntermediaryConfig.java b/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/configurations/ParticipantIntermediaryConfig.java new file mode 100644 index 000000000..d8c39925b --- /dev/null +++ b/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/configurations/ParticipantIntermediaryConfig.java @@ -0,0 +1,48 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2021 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.clamp.controlloop.participant.kubernetes.configurations; + +import org.onap.policy.clamp.controlloop.participant.intermediary.api.ParticipantIntermediaryApi; +import org.onap.policy.clamp.controlloop.participant.intermediary.api.ParticipantIntermediaryFactory; +import org.onap.policy.clamp.controlloop.participant.kubernetes.handler.ControlLoopElementHandler; +import org.onap.policy.clamp.controlloop.participant.kubernetes.parameters.ParticipantK8sParameters; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ParticipantIntermediaryConfig { + + /** + * Create ParticipantIntermediaryApi. + * + * @param parameters the K8s Participant Parameters + * @param clElementHandler the ControlLoop Element Handler + * @return ParticipantIntermediaryApi + */ + @Bean + public ParticipantIntermediaryApi participantIntermediaryApi(ParticipantK8sParameters parameters, + ControlLoopElementHandler clElementHandler) { + ParticipantIntermediaryApi intermediaryApi = new ParticipantIntermediaryFactory().createApiImplementation(); + intermediaryApi.init(parameters.getIntermediaryParameters()); + intermediaryApi.registerControlLoopElementListener(clElementHandler); + return intermediaryApi; + } +} diff --git a/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/controller/ChartController.java b/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/controller/ChartController.java new file mode 100644 index 000000000..427b06fc5 --- /dev/null +++ b/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/controller/ChartController.java @@ -0,0 +1,166 @@ +/*- + * ========================LICENSE_START================================= + * Copyright (C) 2021 Nordix Foundation. All rights reserved. + * ====================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================LICENSE_END=================================== + */ + +package org.onap.policy.clamp.controlloop.participant.kubernetes.controller; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import java.io.IOException; +import java.util.ArrayList; +import org.onap.policy.clamp.controlloop.participant.kubernetes.exception.ServiceException; +import org.onap.policy.clamp.controlloop.participant.kubernetes.models.ChartInfo; +import org.onap.policy.clamp.controlloop.participant.kubernetes.models.ChartList; +import org.onap.policy.clamp.controlloop.participant.kubernetes.models.InstallationInfo; +import org.onap.policy.clamp.controlloop.participant.kubernetes.service.ChartService; +import org.onap.policy.common.utils.coder.CoderException; +import org.onap.policy.common.utils.coder.StandardCoder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +@RestController("chartController") +@RequestMapping("helm") +@Api(tags = {"chart"}) +public class ChartController { + + @Autowired + private ChartService chartService; + + private static final StandardCoder CODER = new StandardCoder(); + + /** + * REST endpoint to get all the charts. + * + * @return List of charts installed + */ + @GetMapping(path = "/charts", produces = MediaType.APPLICATION_JSON_VALUE) + @ApiOperation(value = "Return all Charts") + @ApiResponses(value = {@ApiResponse(code = 200, message = "chart List")}) + public ResponseEntity<ChartList> getAllCharts() { + return new ResponseEntity<>(ChartList.builder().charts(new ArrayList<>(chartService.getAllCharts())).build(), + HttpStatus.OK); + } + + /** + * REST endpoint to install a helm chart. + * + * @param info Info of the chart to be installed + * @return Status of the install operation + * @throws ServiceException incase of error + */ + @PostMapping(path = "/install", consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE) + @ApiOperation(value = "Install the chart") + @ApiResponses(value = {@ApiResponse(code = 201, message = "chart Installed")}) + public ResponseEntity<Object> installChart(@RequestBody InstallationInfo info) + throws ServiceException, IOException { + ChartInfo chart = chartService.getChart(info.getName(), info.getVersion()); + if (chart == null) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + chartService.installChart(chart); + return new ResponseEntity<>(HttpStatus.CREATED); + } + + /** + * REST endpoint to uninstall a specific chart. + * + * @param name name of the chart + * @param version version of the chart + * @return Status of operation + * @throws ServiceException incase of error. + */ + @DeleteMapping(path = "/uninstall/{name}/{version}", produces = MediaType.APPLICATION_JSON_VALUE) + @ApiOperation(value = "Uninstall the Chart") + @ApiResponses(value = {@ApiResponse(code = 201, message = "chart Uninstalled")}) + public ResponseEntity<Object> uninstallChart(@PathVariable("name") String name, + @PathVariable("version") String version) throws ServiceException { + ChartInfo chart = chartService.getChart(name, version); + if (chart == null) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + chartService.uninstallChart(chart); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + /** + * REST endpoint to onboard a chart. + * + * @param chartFile Multipart file for the helm chart + * @param infoJson AppInfo of the chart + * @return Status of onboard operation + * @throws ServiceException incase of error + * @throws IOException incase of IO error + */ + @PostMapping(path = "/charts", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE) + @ApiOperation(value = "Onboard the Chart") + @ApiResponses(value = {@ApiResponse(code = 201, message = "Chart Onboarded")}) + public ResponseEntity<String> onboardChart(@RequestPart("chart") MultipartFile chartFile, + @RequestParam(name = "values", required = false) MultipartFile overrideFile, + @RequestParam("info") String infoJson) throws ServiceException, IOException { + + ChartInfo info; + try { + info = CODER.decode(infoJson, ChartInfo.class); + } catch (CoderException e) { + throw new ServiceException("Error parsing the chart information", e); + } + + chartService.saveChart(info, chartFile, overrideFile); + return new ResponseEntity<>(HttpStatus.OK); + } + + /** + * REST endpoint to delete a specific helm chart. + * + * @param name name of the chart + * @param version version of the chart + * @return Status of operation + * @throws ServiceException incase of error. + */ + @DeleteMapping(path = "/charts/{name}/{version}") + @ApiOperation(value = "Delete the chart") + @ApiResponses(value = {@ApiResponse(code = 204, message = "Chart Deleted")}) + public ResponseEntity<Object> deleteChart(@PathVariable("name") String name, + @PathVariable("version") String version) throws ServiceException { + + ChartInfo chart = chartService.getChart(name, version); + if (chart == null) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + chartService.deleteChart(chart); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } +} diff --git a/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/exception/ServiceException.java b/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/exception/ServiceException.java new file mode 100644 index 000000000..9a825cf75 --- /dev/null +++ b/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/exception/ServiceException.java @@ -0,0 +1,32 @@ +/*- + * ========================LICENSE_START================================= + * Copyright (C) 2021 Nordix Foundation. All rights reserved. + * ====================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================LICENSE_END=================================== + */ + +package org.onap.policy.clamp.controlloop.participant.kubernetes.exception; + +public class ServiceException extends Exception { + + private static final long serialVersionUID = 6810785674716590648L; + + public ServiceException(String message) { + super(message); + } + + public ServiceException(String message, Exception originalException) { + super(message, originalException); + } +} diff --git a/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/handler/ControlLoopElementHandler.java b/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/handler/ControlLoopElementHandler.java new file mode 100644 index 000000000..5f1dcb8d4 --- /dev/null +++ b/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/handler/ControlLoopElementHandler.java @@ -0,0 +1,147 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2021 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.clamp.controlloop.participant.kubernetes.handler; + + +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import org.onap.policy.clamp.controlloop.models.controlloop.concepts.ControlLoopElement; +import org.onap.policy.clamp.controlloop.models.controlloop.concepts.ControlLoopOrderedState; +import org.onap.policy.clamp.controlloop.models.controlloop.concepts.ControlLoopState; +import org.onap.policy.clamp.controlloop.participant.intermediary.api.ControlLoopElementListener; +import org.onap.policy.clamp.controlloop.participant.intermediary.api.ParticipantIntermediaryApi; +import org.onap.policy.clamp.controlloop.participant.kubernetes.exception.ServiceException; +import org.onap.policy.clamp.controlloop.participant.kubernetes.models.ChartInfo; +import org.onap.policy.clamp.controlloop.participant.kubernetes.service.ChartService; +import org.onap.policy.models.base.PfModelException; +import org.onap.policy.models.tosca.authorative.concepts.ToscaNodeTemplate; +import org.onap.policy.models.tosca.authorative.concepts.ToscaServiceTemplate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * This class handles implementation of controlLoopElement updates. + */ +@Component +public class ControlLoopElementHandler implements ControlLoopElementListener { + private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + @Autowired + private ChartService chartService; + + @Autowired + private ParticipantIntermediaryApi intermediaryApi; + + // Map of CLElement Id and installed Helm charts + private final Map<UUID, ChartInfo> chartMap = new HashMap<>(); + + /** + * Callback method to handle a control loop element state change. + * + * @param controlLoopElementId the ID of the control loop element + * @param currentState the current state of the control loop element + * @param newState the state to which the control loop element is changing to + */ + @Override + public synchronized void controlLoopElementStateChange(UUID controlLoopElementId, ControlLoopState currentState, + ControlLoopOrderedState newState) { + switch (newState) { + case UNINITIALISED: + ChartInfo chart = chartMap.get(controlLoopElementId); + if (chart != null) { + LOGGER.info("Helm deployment to be deleted {} ", chart.getReleaseName()); + try { + chartService.uninstallChart(chart); + intermediaryApi.updateControlLoopElementState(controlLoopElementId, newState, + ControlLoopState.UNINITIALISED); + } catch (ServiceException se) { + LOGGER.warn("deletion of Helm deployment failed", se); + } + } + break; + case PASSIVE: + intermediaryApi.updateControlLoopElementState(controlLoopElementId, newState, ControlLoopState.PASSIVE); + break; + case RUNNING: + intermediaryApi.updateControlLoopElementState(controlLoopElementId, newState, ControlLoopState.RUNNING); + break; + default: + LOGGER.warn("cannot transition from state {} to state {}", currentState, newState); + break; + } + } + + + /** + * Callback method to handle an update on a control loop element. + * + * @param element the information on the control loop element + * @param controlLoopDefinition toscaServiceTemplate + * @throws PfModelException in case of an exception + */ + @Override + public synchronized void controlLoopElementUpdate(ControlLoopElement element, + ToscaServiceTemplate controlLoopDefinition) throws PfModelException { + + for (Map.Entry<String, ToscaNodeTemplate> nodeTemplate : controlLoopDefinition.getToscaTopologyTemplate() + .getNodeTemplates().entrySet()) { + + // Fetching the node template of corresponding CL element + if (element.getDefinition().getName().equals(nodeTemplate.getKey()) + && nodeTemplate.getValue().getProperties().containsKey("chart")) { + @SuppressWarnings("unchecked") + Map<String, Object> chartData = + (Map<String, Object>) nodeTemplate.getValue().getProperties().get("chart"); + + LOGGER.info("Installation request received for the Helm Chart {} ", chartData); + var chart = new ChartInfo(String.valueOf(chartData.get("release_name")), + String.valueOf(chartData.get("chart_name")), String.valueOf(chartData.get("version")), + String.valueOf(chartData.get("namespace"))); + try { + var repositoryValue = chartData.get("repository"); + if (repositoryValue != null) { + chart.setRepository(String.valueOf(repositoryValue)); + } + chartService.installChart(chart); + chartMap.put(element.getId(), chart); + } catch (IOException | ServiceException ise) { + LOGGER.warn("installation of Helm chart failed", ise); + } + } + } + } + + /** + * Overridden method. + * + * @param controlLoopElementId controlLoopElement id + * @throws PfModelException incase of error + */ + @Override + public synchronized void handleStatistics(UUID controlLoopElementId) throws PfModelException { + // TODO Implement statistics functionality + } +} diff --git a/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/helm/HelmClient.java b/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/helm/HelmClient.java new file mode 100644 index 000000000..456122f3d --- /dev/null +++ b/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/helm/HelmClient.java @@ -0,0 +1,201 @@ +/*- + * ========================LICENSE_START================================= + * Copyright (C) 2021 Nordix Foundation. All rights reserved. + * ====================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================LICENSE_END=================================== + */ + +package org.onap.policy.clamp.controlloop.participant.kubernetes.helm; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.invoke.MethodHandles; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.apache.commons.io.IOUtils; +import org.onap.policy.clamp.controlloop.participant.kubernetes.exception.ServiceException; +import org.onap.policy.clamp.controlloop.participant.kubernetes.models.ChartInfo; +import org.onap.policy.clamp.controlloop.participant.kubernetes.service.ChartStore; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Client to talk with Helm cli. Supports helm3 + version + */ +@Component +public class HelmClient { + + @Autowired + private ChartStore chartStore; + + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + + /** + * Install a chart. + * + * @param chart name and version. + * @throws ServiceException incase of error + */ + public void installChart(ChartInfo chart) throws ServiceException { + var processBuilder = prepareCreateNamespaceCommand(chart.getNamespace()); + try { + executeCommand(processBuilder); + } catch (ServiceException e) { + logger.warn("Namespace not created", e); + } + processBuilder = prepareInstallCommand(chart); + logger.info("Installing helm chart {} from the repository {} ", chart.getChartName(), chart.getRepository()); + executeCommand(processBuilder); + logger.info("Chart {} installed successfully", chart.getChartName()); + } + + /** + * Finds helm chart repository for the chart. + * + * @param chart ChartInfo. + * @throws ServiceException incase of error + */ + public String findChartRepository(ChartInfo chart) throws ServiceException, IOException { + updateHelmRepo(); + logger.info("Looking for helm chart {} in all the configured helm repositories", chart.getChartName()); + String repository = null; + + var process = helmRepoVerifyCommand(chart.getChartName()).start(); + + try (var reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String line = reader.readLine(); + while (line != null) { + if (line.contains(chart.getChartName())) { + repository = line.split("/")[0]; + logger.info("Helm chart located in the repository {} ", repository); + return repository; + } + line = reader.readLine(); + } + } + + var localHelmChartDir = chartStore.getAppPath(chart.getChartName(), chart.getVersion()).toString(); + logger.info("Chart not found in helm repositories, verifying local repo {} ", localHelmChartDir); + if (verifyLocalHelmRepo(localHelmChartDir + "/" + chart.getChartName())) { + repository = localHelmChartDir; + } + + return repository; + } + + /** + * Uninstall a chart. + * + * @param chart name and version. + * @throws ServiceException incase of error + */ + public void uninstallChart(ChartInfo chart) throws ServiceException { + executeCommand(prepareUnInstallCommand(chart)); + } + + static String executeCommand(ProcessBuilder processBuilder) throws ServiceException { + var commandStr = toString(processBuilder); + + processBuilder.redirectInput(ProcessBuilder.Redirect.DISCARD); + + try { + var process = processBuilder.start(); + process.waitFor(); + int exitValue = process.exitValue(); + + if (exitValue != 0) { + var error = IOUtils.toString(process.getErrorStream(), StandardCharsets.UTF_8); + throw new ServiceException("Command execution failed: " + commandStr + " " + error); + } + var output = IOUtils.toString(process.getInputStream(), StandardCharsets.UTF_8); + logger.debug("Command <{}> execution, output: {}", commandStr, output); + return output; + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + throw new ServiceException( + "Failed to execute the Command: " + commandStr + ", the command was interrupted", ie); + } catch (Exception exc) { + throw new ServiceException("Failed to execute the Command: " + commandStr, exc); + } + } + + private ProcessBuilder prepareInstallCommand(ChartInfo chart) { + + // @formatter:off + List<String> helmArguments = new ArrayList<>( + Arrays.asList( + "helm", + "install", chart.getReleaseName(), chart.getRepository() + "/" + chart.getChartName(), + "--version", chart.getVersion(), + "--namespace", chart.getNamespace() + ) + ); + // @formatter:on + + // Verify if values.yaml available for the chart + var overrideFile = chartStore.getOverrideFile(chart).getPath(); + if (verifyLocalHelmRepo(overrideFile)) { + logger.info("Override yaml file available for the helm chart"); + helmArguments.addAll(Arrays.asList("--values", overrideFile)); + } + + return new ProcessBuilder().command(helmArguments); + } + + private ProcessBuilder prepareUnInstallCommand(ChartInfo chart) { + return new ProcessBuilder("helm", "delete", chart.getReleaseName(), "--namespace", chart.getNamespace()); + } + + private ProcessBuilder prepareCreateNamespaceCommand(String namespace) { + return new ProcessBuilder().command("kubectl", "create", "namespace", namespace); + } + + private ProcessBuilder helmRepoVerifyCommand(String chartName) { + return new ProcessBuilder().command("bash", "-c", "helm search repo | grep " + chartName); + } + + private ProcessBuilder localRepoVerifyCommand(String localFile) { + return new ProcessBuilder().command("bash", "-c", "ls " + localFile); + } + + private void updateHelmRepo() throws ServiceException { + logger.info("Updating local helm repositories before verifying the chart"); + List<String> helmArguments = Arrays.asList("helm", "repo", "update"); + + executeCommand(new ProcessBuilder().command(helmArguments)); + logger.debug("Helm repositories updated successfully"); + } + + private boolean verifyLocalHelmRepo(String localFile) { + var isVerified = false; + var processBuilder = localRepoVerifyCommand(localFile); + try { + executeCommand(processBuilder); + isVerified = true; + } catch (ServiceException e) { + logger.error("Unable to verify file in local repository", e); + } + return isVerified; + } + + protected static String toString(ProcessBuilder processBuilder) { + return String.join(" ", processBuilder.command()); + } +} diff --git a/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/models/ChartInfo.java b/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/models/ChartInfo.java new file mode 100644 index 000000000..6bfb7aed5 --- /dev/null +++ b/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/models/ChartInfo.java @@ -0,0 +1,45 @@ +/*- + * ========================LICENSE_START================================= + * Copyright (C) 2021 Nordix Foundation. All rights reserved. + * ====================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================LICENSE_END=================================== + */ + +package org.onap.policy.clamp.controlloop.participant.kubernetes.models; + +import lombok.Data; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.immutables.gson.Gson; + +@Data +@RequiredArgsConstructor +@Gson.TypeAdapters +public class ChartInfo { + + @NonNull + private String releaseName; + + @NonNull + private String chartName; + + @NonNull + private String version; + + @NonNull + private String namespace; + + private String repository; + +} diff --git a/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/models/ChartList.java b/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/models/ChartList.java new file mode 100644 index 000000000..c86bff58a --- /dev/null +++ b/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/models/ChartList.java @@ -0,0 +1,31 @@ +/*- + * ========================LICENSE_START================================= + * Copyright (C) 2021 Nordix Foundation. All rights reserved. + * ====================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================LICENSE_END=================================== + */ + +package org.onap.policy.clamp.controlloop.participant.kubernetes.models; + +import java.util.Collection; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Builder +public class ChartList { + private Collection<ChartInfo> charts; +} diff --git a/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/models/InstallationInfo.java b/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/models/InstallationInfo.java new file mode 100644 index 000000000..b21e93a01 --- /dev/null +++ b/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/models/InstallationInfo.java @@ -0,0 +1,29 @@ +/*- + * ========================LICENSE_START================================= + * Copyright (C) 2021 Nordix Foundation. All rights reserved. + * ====================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================LICENSE_END=================================== + */ + +package org.onap.policy.clamp.controlloop.participant.kubernetes.models; + +import lombok.Getter; +import org.immutables.gson.Gson; + +@Getter +@Gson.TypeAdapters +public class InstallationInfo { + private String name; + private String version; +} diff --git a/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/parameters/ParticipantK8sParameterHandler.java b/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/parameters/ParticipantK8sParameterHandler.java new file mode 100644 index 000000000..1a7dc35e8 --- /dev/null +++ b/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/parameters/ParticipantK8sParameterHandler.java @@ -0,0 +1,73 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2021 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.clamp.controlloop.participant.kubernetes.parameters; + +import java.io.File; +import javax.ws.rs.core.Response; +import org.onap.policy.clamp.controlloop.common.exception.ControlLoopException; +import org.onap.policy.common.parameters.BeanValidationResult; +import org.onap.policy.common.utils.coder.Coder; +import org.onap.policy.common.utils.coder.CoderException; +import org.onap.policy.common.utils.coder.StandardCoder; + +/** + * This class handles reading, parsing and validating of control loop participant parameters from JSON files. + */ +public class ParticipantK8sParameterHandler { + private static final Coder CODER = new StandardCoder(); + + /** + * Read the parameters from the path of the file. + * + * @param path path of the config file. + * @return the parameters read from the configuration file + * @throws ControlLoopException on parameter exceptions + */ + public ParticipantK8sParameters toParticipantK8sParameters(String path) throws ControlLoopException { + ParticipantK8sParameters parameters = null; + // Read the parameters + try { + // Read the parameters from JSON + var file = new File(path); + parameters = CODER.decode(file, ParticipantK8sParameters.class); + } catch (final CoderException e) { + final String errorMessage = + "error reading parameters from \"" + path + "\"\n" + "(" + e.getClass().getSimpleName() + ")"; + throw new ControlLoopException(Response.Status.NOT_ACCEPTABLE, errorMessage, e); + } + + // The JSON processing returns null if there is an empty file + if (parameters == null) { + final String errorMessage = "no parameters found in \"" + path + "\""; + throw new ControlLoopException(Response.Status.NOT_ACCEPTABLE, errorMessage); + } + + // validate the parameters + final BeanValidationResult validationResult = parameters.validate(); + if (!validationResult.isValid()) { + String returnMessage = + "validation error(s) on parameters from \"" + path + "\"\n" + validationResult.getResult(); + throw new ControlLoopException(Response.Status.NOT_ACCEPTABLE, returnMessage); + } + + return parameters; + } +} diff --git a/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/parameters/ParticipantK8sParameters.java b/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/parameters/ParticipantK8sParameters.java new file mode 100644 index 000000000..65b32433c --- /dev/null +++ b/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/parameters/ParticipantK8sParameters.java @@ -0,0 +1,59 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2021 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.clamp.controlloop.participant.kubernetes.parameters; + +import javax.validation.constraints.NotBlank; +import lombok.Getter; +import org.onap.policy.clamp.controlloop.participant.intermediary.parameters.ParticipantIntermediaryParameters; +import org.onap.policy.common.parameters.ParameterGroupImpl; +import org.onap.policy.common.parameters.annotations.NotNull; +import org.onap.policy.common.parameters.annotations.Valid; +import org.onap.policy.models.provider.PolicyModelsProviderParameters; + +/** + * Class to hold all parameters needed for the kubernetes participant. + * + */ +@NotNull +@NotBlank +@Getter +public class ParticipantK8sParameters extends ParameterGroupImpl { + public static final String DEFAULT_LOCAL_CHART_DIR = "/var/helm-manager/local-charts"; + public static final String DEFAULT_INFO_FILE_NAME = "CHART_INFO.json"; + + @Valid + private ParticipantIntermediaryParameters intermediaryParameters; + @Valid + private PolicyModelsProviderParameters databaseProviderParameters; + + + private String localChartDirectory = DEFAULT_LOCAL_CHART_DIR; + private String infoFileName = DEFAULT_INFO_FILE_NAME; + + /** + * Create the kubernetes participant parameter group. + * + * @param name the parameter group name + */ + public ParticipantK8sParameters(final String name) { + super(name); + } +} diff --git a/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/service/ChartService.java b/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/service/ChartService.java new file mode 100644 index 000000000..6accac339 --- /dev/null +++ b/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/service/ChartService.java @@ -0,0 +1,122 @@ +/*- + * ========================LICENSE_START================================= + * Copyright (C) 2021 Nordix Foundation. All rights reserved. + * ====================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================LICENSE_END=================================== + */ + +package org.onap.policy.clamp.controlloop.participant.kubernetes.service; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.util.Collection; +import org.onap.policy.clamp.controlloop.participant.kubernetes.exception.ServiceException; +import org.onap.policy.clamp.controlloop.participant.kubernetes.helm.HelmClient; +import org.onap.policy.clamp.controlloop.participant.kubernetes.models.ChartInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +@Service +public class ChartService { + private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + @Autowired + private ChartStore chartStore; + + @Autowired + private HelmClient helmClient; + + /** + * Get all the installed charts. + * @return list of charts. + */ + public Collection<ChartInfo> getAllCharts() { + return chartStore.getAllCharts(); + } + + /** + * Get specific chart info. + * @param name name of the app + * @param version version of the app + * @return chart + * @throws ServiceException incase of error. + */ + public ChartInfo getChart(String name, String version) throws ServiceException { + return chartStore.getChart(name, version); + } + + /** + * Save a helm chart. + * @param chartInfo name and version of the app. + * @param chartFile Helm chart file + * @return chart details of the helm chart + * @throws IOException incase of IO error + * @throws ServiceException incase of error + */ + public ChartInfo saveChart(ChartInfo chartInfo, MultipartFile chartFile, MultipartFile overrideFile) + throws IOException, ServiceException { + return chartStore.saveChart(chartInfo, chartFile, overrideFile); + } + + /** + * Delete a helm chart. + * @param chart name and version of the chart. + */ + public void deleteChart(ChartInfo chart) { + chartStore.deleteChart(chart); + } + + /** + * Install a helm chart. + * @param chart name and version. + * @throws ServiceException incase of error + */ + public void installChart(ChartInfo chart) throws ServiceException, IOException { + if (chart.getRepository() == null) { + String repository = findChartRepo(chart); + if (repository == null) { + logger.error("Chart repository could not be found. Skipping chart Installation " + + "for the chart {} ", chart.getChartName()); + return; + } else { + chart.setRepository(repository); + } + } + helmClient.installChart(chart); + } + + /** + * Finds helm chart repository for a given chart. + * @param chart chartInfo. + * @throws ServiceException incase of error + */ + public String findChartRepo(ChartInfo chart) throws ServiceException, IOException { + logger.info("Fetching helm chart repository for the given chart {} ", chart.getChartName()); + return helmClient.findChartRepository(chart); + } + + /** + * Uninstall a helm chart. + * @param chart name and version + * @throws ServiceException incase of error. + */ + public void uninstallChart(ChartInfo chart) throws ServiceException { + logger.info("Uninstalling helm deployment {}", chart.getReleaseName()); + helmClient.uninstallChart(chart); + } + +} diff --git a/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/service/ChartStore.java b/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/service/ChartStore.java new file mode 100644 index 000000000..2d0ce7a83 --- /dev/null +++ b/participant/participant-impl/participant-impl-kubernetes/src/main/java/org/onap/policy/clamp/controlloop/participant/kubernetes/service/ChartStore.java @@ -0,0 +1,215 @@ +/*- + * ========================LICENSE_START================================= + * Copyright (C) 2021 Nordix Foundation. All rights reserved. + * ====================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================LICENSE_END=================================== + */ + +package org.onap.policy.clamp.controlloop.participant.kubernetes.service; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.lang.invoke.MethodHandles; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.onap.policy.clamp.controlloop.participant.kubernetes.exception.ServiceException; +import org.onap.policy.clamp.controlloop.participant.kubernetes.models.ChartInfo; +import org.onap.policy.clamp.controlloop.participant.kubernetes.parameters.ParticipantK8sParameters; +import org.onap.policy.common.utils.coder.CoderException; +import org.onap.policy.common.utils.coder.StandardCoder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.FileSystemUtils; +import org.springframework.web.multipart.MultipartFile; + +@Component +public class ChartStore { + private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + private static final StandardCoder STANDARD_CODER = new StandardCoder(); + + @Autowired + private ParticipantK8sParameters participantK8sParameters; + + /** + * The chartStore map contains chart name as key & ChartInfo as value. + */ + private Map<String, ChartInfo> localChartMap = new ConcurrentHashMap<>(); + + /** + * Constructor method. + */ + public ChartStore() { + this.restoreFromLocalFileSystem(); + } + + /** + * Get local helm chart file. + * + * @param chart ChartInfo + * @return the chart file. + */ + public File getHelmChartFile(ChartInfo chart) { + var appPath = getAppPath(chart.getChartName(), chart.getVersion()); + return new File(appPath.toFile(), chart.getChartName()); + } + + /** + * Get the override yaml file. + * + * @param chart ChartInfo + * @return the override yaml file + */ + public File getOverrideFile(ChartInfo chart) { + var appPath = getAppPath(chart.getChartName(), chart.getVersion()); + return new File(appPath.toFile(), "values.yaml"); + } + + + /** + * Saves the helm chart. + * + * @param chartInfo chartInfo + * @param chartFile helm chart file. + * @return chart + * @throws IOException incase of IO error + * @throws ServiceException incase of error. + */ + public synchronized ChartInfo saveChart(ChartInfo chartInfo, MultipartFile chartFile, MultipartFile overrideFile) + throws IOException, ServiceException { + if (localChartMap.containsKey(key(chartInfo.getChartName(), chartInfo.getVersion()))) { + throw new ServiceException("Chart already exist"); + } + var appPath = getAppPath(chartInfo.getChartName(), chartInfo.getVersion()); + Files.createDirectories(appPath); + + chartFile.transferTo(getHelmChartFile(chartInfo)); + if (overrideFile != null) { + overrideFile.transferTo(getOverrideFile(chartInfo)); + } + + localChartMap.put(key(chartInfo), chartInfo); + storeChartInFile(chartInfo); + return chartInfo; + } + + /** + * Get the chart info. + * + * @param name name of the chart + * @param version version of the chart + * @return chart + */ + public synchronized ChartInfo getChart(String name, String version) { + return localChartMap.get(key(name, version)); + } + + /** + * Get all the charts installed. + * + * @return list of charts. + */ + public synchronized List<ChartInfo> getAllCharts() { + return new ArrayList<>(localChartMap.values()); + } + + /** + * Delete a chart. + * + * @param chart chart info + */ + public synchronized void deleteChart(ChartInfo chart) { + var appPath = getAppPath(chart.getChartName(), chart.getVersion()); + try { + FileSystemUtils.deleteRecursively(appPath); + } catch (IOException exc) { + LOGGER.warn("Could not delete chart from local file system : {}", appPath, exc); + } + + localChartMap.remove(key(chart)); + } + + /** + * Fetch the local chart directory of specific chart. + * + * @param chartName name of the chart + * @param chartVersion version of the chart + * @return path + */ + public Path getAppPath(String chartName, String chartVersion) { + return Path.of(participantK8sParameters.getLocalChartDirectory(), chartName, chartVersion); + } + + private void storeChartInFile(ChartInfo chart) { + try (var out = new PrintStream(new FileOutputStream(getFile(chart)))) { + out.print(STANDARD_CODER.encode(chart)); + } catch (Exception exc) { + LOGGER.warn("Could not store chart: {} {}", chart.getChartName(), exc); + } + } + + private File getFile(ChartInfo chart) { + var appPath = getAppPath(chart.getChartName(), chart.getVersion()).toString(); + return Path.of(appPath, participantK8sParameters.getInfoFileName()).toFile(); + } + + private synchronized void restoreFromLocalFileSystem() { + Path localChartDirectoryPath = Paths.get(participantK8sParameters.getLocalChartDirectory()); + + try { + Files.createDirectories(localChartDirectoryPath); + restoreFromLocalFileSystem(localChartDirectoryPath); + } catch (IOException ioe) { + LOGGER.warn("Could not restore charts from local file system: {}", ioe); + } + } + + private synchronized void restoreFromLocalFileSystem(Path localChartDirectoryPath) + throws IOException { + + Files.walkFileTree(localChartDirectoryPath, new SimpleFileVisitor<Path>() { + @Override + public FileVisitResult visitFile(Path localChartFile, BasicFileAttributes attrs) throws IOException { + try { + ChartInfo chart = STANDARD_CODER.decode(localChartFile.toFile(), ChartInfo.class); + localChartMap.put(key(chart), chart); + return FileVisitResult.CONTINUE; + } catch (CoderException ce) { + throw new IOException("Error decoding chart file", ce); + } + } + }); + } + + private String key(ChartInfo chart) { + return key(chart.getChartName(), chart.getVersion()); + } + + private String key(String chartName, String chartVersion) { + return chartName + "_" + chartVersion; + } + +} diff --git a/participant/participant-impl/participant-impl-kubernetes/src/main/resources/config/KubernetesParticipantConfig.json b/participant/participant-impl/participant-impl-kubernetes/src/main/resources/config/KubernetesParticipantConfig.json new file mode 100644 index 000000000..620e05552 --- /dev/null +++ b/participant/participant-impl/participant-impl-kubernetes/src/main/resources/config/KubernetesParticipantConfig.json @@ -0,0 +1,56 @@ +{ + "name": "ControlLoopParticipantK8s", + "localChartDirectory": "/var/helm-manager/local-charts", + "infoFileName": "CHART_INFO.json", + + "intermediaryParameters":{ + "name":"Participant parameters", + "reportingTimeInterval":120000, + "description":"Participant Description", + "participantId":{ + "name":"K8sParticipant0", + "version":"1.0.0" + }, + "participantType":{ + "name":"org.onap.k8s.controlloop.K8SControlLoopParticipant", + "version":"2.3.4" + }, + "clampControlLoopTopics":{ + "topicSources":[ + { + "topic":"POLICY-CLRUNTIME-PARTICIPANT", + "servers":[ + "localhost" + ], + "topicCommInfrastructure":"dmaap", + "fetchTimeout":15000 + } + ], + "topicSinks":[ + { + "topic":"POLICY-CLRUNTIME-PARTICIPANT", + "servers":[ + "localhost" + ], + "topicCommInfrastructure":"dmaap" + }, + { + "topic":"POLICY-NOTIFICATION", + "servers":[ + "localhost" + ], + "topicCommInfrastructure":"dmaap" + } + ] + } + }, + "databaseProviderParameters":{ + "name":"PolicyProviderParameterGroup", + "implementation":"org.onap.policy.models.provider.impl.DatabasePolicyModelsProviderImpl", + "databaseDriver":"org.mariadb.jdbc.Driver", + "databaseUrl":"jdbc:mariadb://localhost:3306/controlloop", + "databaseUser":"admin", + "databasePassword":"passme", + "persistenceUnit":"ToscaConceptTest" + } +} diff --git a/participant/participant-impl/participant-impl-kubernetes/src/main/resources/config/application.yaml b/participant/participant-impl/participant-impl-kubernetes/src/main/resources/config/application.yaml new file mode 100644 index 000000000..b4240036b --- /dev/null +++ b/participant/participant-impl/participant-impl-kubernetes/src/main/resources/config/application.yaml @@ -0,0 +1,25 @@ +spring: + profiles: + active: prod + +participant: + file: src/main/resources/config/KubernetesParticipantConfig.json +management: + endpoints: + web: + exposure: + include: "loggers,logfile,health,info,metrics,threaddump,heapdump" +server: + # Configuration of the HTTP/REST server. The parameters are defined and handled by the springboot framework. + # See springboot documentation. + http-port : 8083 + +logging: + # Configuration of logging + level: + ROOT: INFO + org.springframework: ERROR + org.springframework.data: ERROR + org.springframework.web.reactive.function.client.ExchangeFunctions: ERROR + file: + name: /var/log/helm-manager/application.log diff --git a/participant/participant-impl/participant-impl-kubernetes/src/test/resources/KubernetesHelm.yaml b/participant/participant-impl/participant-impl-kubernetes/src/test/resources/KubernetesHelm.yaml new file mode 100644 index 000000000..3212b5ad2 --- /dev/null +++ b/participant/participant-impl/participant-impl-kubernetes/src/test/resources/KubernetesHelm.yaml @@ -0,0 +1,154 @@ +# ============LICENSE_START======================================================= +# Copyright (C) 2021 Nordix Foundation. +# ================================================================================ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# ============LICENSE_END========================================================= +tosca_definitions_version: tosca_simple_yaml_1_3 +data_types: + onap.datatypes.ToscaConceptIdentifier: + derived_from: tosca.datatypes.Root + properties: + name: + type: string + required: true + version: + type: string + required: true +node_types: + org.onap.policy.clamp.controlloop.Participant: + version: 1.0.1 + derived_from: tosca.nodetypes.Root + properties: + provider: + type: string + requred: false + org.onap.policy.clamp.controlloop.ControlLoopElement: + version: 1.0.1 + derived_from: tosca.nodetypes.Root + properties: + provider: + type: string + requred: false + participant_id: + type: onap.datatypes.ToscaConceptIdentifier + requred: true + org.onap.policy.clamp.controlloop.ControlLoop: + version: 1.0.1 + derived_from: tosca.nodetypes.Root + properties: + provider: + type: string + requred: false + elements: + type: list + required: true + entry_schema: + type: onap.datatypes.ToscaConceptIdentifier + org.onap.policy.clamp.controlloop.K8SMicroserviceControlLoopElement: + version: 1.0.1 + derived_from: org.onap.policy.clamp.controlloop.ControlLoopElement + properties: + chart: + type: string + required: true + configs: + type: list + required: false + requirements: + type: string + requred: false + templates: + type: list + required: false + entry_schema: + values: + type: string + requred: true +topology_template: + node_templates: + org.onap.k8s.controlloop.K8SControlLoopParticipant: + version: 2.3.4 + type: org.onap.policy.clamp.controlloop.Participant + type_version: 1.0.1 + description: Participant for K8S + properties: + provider: ONAP + + org.onap.domain.database.HelloWorld_K8SMicroserviceControlLoopElement: + # Chart from any chart repository configured on helm client. + version: 1.2.3 + type: org.onap.policy.clamp.controlloop.K8SMicroserviceControlLoopElement + type_version: 1.0.0 + description: Control loop element for the K8S microservice for Hello World + properties: + provider: ONAP + participant_id: + name: org.onap.k8s.controlloop.K8SControlLoopParticipant + version: 2.3.4 + chart: + release_name: helloworld + chart_name: hello + version: 0.1.0 + repository: chartMuseum + namespace: onap + + org.onap.domain.database.PMSH_K8SMicroserviceControlLoopElement: + # Chart from local file system + version: 1.2.3 + type: org.onap.policy.clamp.controlloop.K8SMicroserviceControlLoopElement + type_version: 1.0.0 + description: Control loop element for the K8S microservice for PMSH + properties: + provider: ONAP + participant_id: + name: org.onap.k8s.controlloop.K8SControlLoopParticipant + version: 2.3.4 + chart: + release_name: pmshmicroservice + chart_name: test + version: 1.0.1 + namespace: onap + + org.onap.domain.database.Local_K8SMicroserviceControlLoopElement: + # Chart installation without passing repository name + version: 1.2.3 + type: org.onap.policy.clamp.controlloop.K8SMicroserviceControlLoopElement + type_version: 1.0.0 + description: Control loop element for the K8S microservice for local chart + properties: + provider: ONAP + participant_id: + name: org.onap.k8s.controlloop.K8SControlLoopParticipant + version: 2.3.4 + chart: + release_name: nginxms + chart_name: nginx-ingress + version: 0.9.1 + namespace: onap + + org.onap.domain.sample.GenericK8s_ControlLoopDefinition: + version: 1.2.3 + type: org.onap.policy.clamp.controlloop.ControlLoop + type_version: 1.0.0 + description: Control loop for Hello World + properties: + provider: ONAP + elements: + - name: org.onap.domain.database.HelloWorld_K8SMicroserviceControlLoopElement + version: 1.2.3 + - name: org.onap.domain.database.PMSH_K8SMicroserviceControlLoopElement + version: 1.2.3 + - name: org.onap.domain.database.Local_K8SMicroserviceControlLoopElement + version: 1.2.3 |