af991678ee5f33af791afff136e6abcecfd7c282
[so/adapters/so-cnf-adapter.git] /
1 /*-
2  * ============LICENSE_START=======================================================
3  *  Copyright (C) 2023 Nordix Foundation.
4  * ================================================================================
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  *
17  * SPDX-License-Identifier: Apache-2.0
18  * ============LICENSE_END=========================================================
19  */
20
21 package org.onap.so.cnfm.lcm.bpmn.flows.extclients.helm;
22
23 import static org.onap.so.cnfm.lcm.bpmn.flows.Constants.KIND_DAEMON_SET;
24 import static org.onap.so.cnfm.lcm.bpmn.flows.Constants.KIND_DEPLOYMENT;
25 import static org.onap.so.cnfm.lcm.bpmn.flows.Constants.KIND_JOB;
26 import static org.onap.so.cnfm.lcm.bpmn.flows.Constants.KIND_POD;
27 import static org.onap.so.cnfm.lcm.bpmn.flows.Constants.KIND_REPLICA_SET;
28 import static org.onap.so.cnfm.lcm.bpmn.flows.Constants.KIND_SERVICE;
29 import static org.onap.so.cnfm.lcm.bpmn.flows.Constants.KIND_STATEFUL_SET;
30 import java.io.IOException;
31 import java.nio.file.Files;
32 import java.nio.file.Path;
33 import java.nio.file.Paths;
34 import java.util.ArrayList;
35 import java.util.Collections;
36 import java.util.List;
37 import java.util.Map;
38 import java.util.Set;
39 import org.jvnet.jaxb2_commons.lang.StringUtils;
40 import org.onap.so.cnfm.lcm.bpmn.flows.exceptions.HelmClientExecuteException;
41 import org.onap.so.cnfm.lcm.bpmn.flows.utils.PropertiesToYamlConverter;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
44 import org.springframework.beans.factory.annotation.Autowired;
45 import org.springframework.stereotype.Service;
46
47 @Service
48 public class HelmClientImpl implements HelmClient {
49     private static final String DEFAULT_NAMESPACE = "default";
50     private static final String KIND_KEY = "kind: ";
51     private static final String ANY_UNICODE_NEWLINE = "\\R";
52     private static final Logger logger = LoggerFactory.getLogger(HelmClientImpl.class);
53     private final PropertiesToYamlConverter propertiesToYamlConverter;
54
55     @Autowired
56     public HelmClientImpl(final PropertiesToYamlConverter propertiesToYamlConverter) {
57         this.propertiesToYamlConverter = propertiesToYamlConverter;
58     }
59
60     private static final Set<String> SUPPORTED_KINDS = Set.of(KIND_JOB, KIND_POD, KIND_SERVICE, KIND_DEPLOYMENT,
61             KIND_REPLICA_SET, KIND_DAEMON_SET, KIND_STATEFUL_SET);
62
63     /**
64      * Execute a helm install dry run
65      *
66      * @param releaseName Name of the release given to helm install
67      * @param kubeconfig kubernetes configuration file path
68      * @param helmChart path of the helm chart to install
69      *
70      * @throws HelmClientExecuteException when exception occurs on executing command
71      */
72     @Override
73     public void runHelmChartInstallWithDryRunFlag(final String releaseName, final Path kubeconfig, final Path helmChart)
74             throws HelmClientExecuteException {
75         logger.info("Running dry-run on {} to cluster {} using releaseName: {}", helmChart, kubeconfig, releaseName);
76         final ProcessBuilder processBuilder = prepareDryRunCommand(releaseName, kubeconfig, helmChart);
77         executeCommand(processBuilder);
78         logger.info("Successfully ran dry for Chart {}", helmChart);
79
80     }
81
82     /**
83      *
84      * @param releaseName Name of the release given to helm install
85      * @param kubeconfig kubernetes configuration file path
86      * @param helmChart path of the helm chart to install
87      *
88      * @return Resources for helmChart as a List of strings
89      */
90     @Override
91     public List<String> getKubeKinds(final String releaseName, final Path kubeconfig, final Path helmChart) {
92         logger.info("Retrieving kinds from chart {} using releaseName {}", helmChart, releaseName);
93         final ProcessBuilder processBuilder = prepareKubeKindCommand(releaseName, kubeconfig, helmChart);
94         final String response = executeCommand(processBuilder);
95         if (StringUtils.isEmpty(response)) {
96             logger.warn("Response is empty: {}", response);
97             return Collections.emptyList();
98         }
99         final List<String> kinds = processKinds(response);
100
101         logger.debug("Found kinds: {}", kinds);
102         return kinds;
103     }
104
105
106     @Override
107     public List<String> getKubeKindsUsingManifestCommand(final String releaseName, final Path kubeConfig)
108             throws HelmClientExecuteException {
109         logger.info("Retrieving kinds from helm release history using releaseName {}", releaseName);
110
111         final ProcessBuilder processBuilder = prepareGetKubeKindCommand(releaseName, kubeConfig);
112         final String response = executeCommand(processBuilder);
113         if (StringUtils.isEmpty(response)) {
114             logger.warn("Response is empty: {}", response);
115             return Collections.emptyList();
116         }
117         final List<String> kinds = processKinds(response);
118
119         logger.debug("Kinds found from the helm release history: {}", kinds);
120         return kinds;
121     }
122
123
124     /**
125      *
126      * @param releaseName Name of the release given to helm install
127      * @param kubeconfig kubernetes configuration file path
128      * @param helmChart path of the helm chart to install
129      * @throws HelmClientExecuteException when exception occurs on executing command
130      */
131     @Override
132     public void installHelmChart(final String releaseName, final Path kubeconfig, final Path helmChart,
133             final Map<String, String> lifeCycleParams) throws HelmClientExecuteException {
134         logger.info("Installing {} to cluster {} using releaseName: {}", helmChart, kubeconfig, releaseName);
135         final ProcessBuilder processBuilder =
136                 prepareInstallCommand(releaseName, kubeconfig, helmChart, lifeCycleParams);
137         executeCommand(processBuilder);
138         logger.info("Chart {} installed successfully", helmChart);
139
140     }
141
142     /**
143      * @param releaseName Name of the release given to helm install
144      * @param kubeConfigFilePath kubernetes configuration file path
145      * @throws HelmClientExecuteException when exception occurs on executing command
146      */
147     @Override
148     public void unInstallHelmChart(final String releaseName, final Path kubeConfigFilePath)
149             throws HelmClientExecuteException {
150         logger.info("uninstalling the release {} from cluster {}", releaseName, kubeConfigFilePath);
151         final ProcessBuilder processBuilder = prepareUnInstallCommand(releaseName, kubeConfigFilePath);
152         final String commandResponse = executeCommand(processBuilder);
153         if (!StringUtils.isEmpty(commandResponse) && commandResponse.contains("Release not loaded")) {
154             throw new HelmClientExecuteException(
155                     "Unable to find the installed Helm chart by using releaseName: " + releaseName);
156         }
157
158         logger.info("Release {} uninstalled successfully", releaseName);
159     }
160
161     private ProcessBuilder prepareDryRunCommand(final String releaseName, final Path kubeconfig, final Path helmChart) {
162         final List<String> helmArguments = List.of("helm", "install", releaseName, "-n", DEFAULT_NAMESPACE,
163                 helmChart.toString(), "--dry-run", "--kubeconfig", kubeconfig.toString());
164         return new ProcessBuilder().command(helmArguments);
165     }
166
167     private ProcessBuilder prepareInstallCommand(final String releaseName, final Path kubeconfig, final Path helmChart,
168             final Map<String, String> lifeCycleParams) {
169         final List<String> commands = new ArrayList<>(List.of("helm", "install", releaseName, "-n", DEFAULT_NAMESPACE,
170                 helmChart.toString(), "--kubeconfig", kubeconfig.toString()));
171
172         if (lifeCycleParams != null && !lifeCycleParams.isEmpty()) {
173             final String fileName = helmChart.getParent().resolve("values.yaml").toString();
174             createYamlFile(fileName, lifeCycleParams);
175             commands.add("-f ".concat(fileName));
176         }
177         final List<String> helmArguments = List.of("sh", "-c", toString(commands));
178         return new ProcessBuilder().command(helmArguments);
179     }
180
181     private void createYamlFile(final String fileName, final Map<String, String> lifeCycleParams) {
182         logger.debug("Will create the runtime values.yaml file.");
183         final String yamlContent = propertiesToYamlConverter.getValuesYamlFileContent(lifeCycleParams);
184         logger.debug("Yaml file content : {}", yamlContent);
185         try {
186             Files.write(Paths.get(fileName), yamlContent.getBytes());
187         } catch (final IOException ioException) {
188             throw new HelmClientExecuteException(
189                     "Failed to create the run time life cycle yaml file: {} " + ioException.getMessage(), ioException);
190         }
191     }
192
193     private ProcessBuilder prepareUnInstallCommand(final String releaseName, final Path kubeConfig) {
194         logger.debug("Will remove tis log after checking ubeconfig path: {}", kubeConfig.toFile().getName());
195         final List<String> helmArguments = new ArrayList<>(List.of("helm", "uninstall", releaseName, "-n",
196                 DEFAULT_NAMESPACE, "--kubeconfig", kubeConfig.toString()));
197         return new ProcessBuilder().command(helmArguments);
198     }
199
200     private ProcessBuilder prepareKubeKindCommand(final String releaseName, final Path kubeconfig,
201             final Path helmChart) {
202         final List<String> commands =
203                 List.of("helm", "template", releaseName, "-n", DEFAULT_NAMESPACE, helmChart.toString(), "--dry-run",
204                         "--kubeconfig", kubeconfig.toString(), "--skip-tests", "| grep kind | uniq");
205         final List<String> helmArguments = List.of("sh", "-c", toString(commands));
206         return new ProcessBuilder().command(helmArguments);
207     }
208
209     private ProcessBuilder prepareGetKubeKindCommand(final String releaseName, final Path kubeconfig) {
210         final List<String> commands = List.of("helm", "get", "manifest", releaseName, "-n", DEFAULT_NAMESPACE,
211                 "--kubeconfig", kubeconfig.toString(), "| grep kind | uniq");
212         final List<String> helmArguments = List.of("sh", "-c", toString(commands));
213         return new ProcessBuilder().command(helmArguments);
214     }
215
216     private String executeCommand(final ProcessBuilder processBuilder) throws HelmClientExecuteException {
217         final String commandStr = toString(processBuilder);
218
219         try {
220             logger.debug("Executing cmd: {}", commandStr);
221             final Process process = processBuilder.start();
222
223             final InputStreamConsumer errors = new InputStreamConsumer(process.getErrorStream());
224             final InputStreamConsumer output = new InputStreamConsumer(process.getInputStream());
225
226             final Thread errorsConsumer = new Thread(errors);
227             final Thread outputConsumer = new Thread(output);
228             errorsConsumer.start();
229             outputConsumer.start();
230
231             process.waitFor();
232
233             errorsConsumer.join();
234             outputConsumer.join();
235
236             final int exitValue = process.exitValue();
237             if (exitValue != 0) {
238                 final String stderr = errors.getContent();
239                 if (!stderr.isEmpty()) {
240                     throw new HelmClientExecuteException("Command execution failed: " + commandStr + " " + stderr);
241                 }
242             }
243
244             final String stdout = output.getContent();
245             logger.debug("Command <{}> execution, output: {}", commandStr, stdout);
246             return stdout;
247
248         } catch (final InterruptedException interruptedException) {
249             Thread.currentThread().interrupt();
250             throw new HelmClientExecuteException(
251                     "Failed to execute the Command: " + commandStr + ", the command was interrupted",
252                     interruptedException);
253         } catch (final Exception exception) {
254             throw new HelmClientExecuteException("Failed to execute the Command: " + commandStr, exception);
255         }
256     }
257
258     private List<String> processKinds(final String response) {
259
260         logger.debug("Processing kube kinds");
261
262         final List<String> kinds = new ArrayList<>();
263         for (final String entry : response.split(ANY_UNICODE_NEWLINE)) {
264             if (entry != null) {
265                 final String line = entry.trim();
266                 if (!line.isBlank()) {
267                     final String kind = line.replace(KIND_KEY, "").trim();
268                     if (SUPPORTED_KINDS.contains(kind)) {
269                         logger.debug("Found Supported kind: {}", kind);
270                         kinds.add(kind);
271                     } else {
272                         logger.warn("kind: {} is not currently supported", kind);
273                     }
274                 }
275             }
276         }
277         return kinds;
278     }
279
280     private String toString(final ProcessBuilder processBuilder) {
281         return String.join(" ", processBuilder.command());
282     }
283
284     private String toString(final List<String> commands) {
285         return String.join(" ", commands);
286     }
287 }