ffce0ada78357f6fb614fa371860f7e04827bc49
[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 KIND_KEY = "kind: ";
50     private static final String ANY_UNICODE_NEWLINE = "\\R";
51     private static final Logger logger = LoggerFactory.getLogger(HelmClientImpl.class);
52     private final PropertiesToYamlConverter propertiesToYamlConverter;
53
54     @Autowired
55     public HelmClientImpl(final PropertiesToYamlConverter propertiesToYamlConverter) {
56         this.propertiesToYamlConverter = propertiesToYamlConverter;
57     }
58
59     private static final Set<String> SUPPORTED_KINDS = Set.of(KIND_JOB, KIND_POD, KIND_SERVICE, KIND_DEPLOYMENT,
60             KIND_REPLICA_SET, KIND_DAEMON_SET, KIND_STATEFUL_SET);
61
62     /**
63      * Execute a helm install dry run
64      *
65      * @param releaseName Name of the release given to helm install
66      * @param kubeconfig kubernetes configuration file path
67      * @param helmChart path of the helm chart to install
68      *
69      * @throws HelmClientExecuteException when exception occurs on executing command
70      */
71     @Override
72     public void runHelmChartInstallWithDryRunFlag(final String releaseName, final Path kubeconfig, final Path helmChart)
73             throws HelmClientExecuteException {
74         logger.info("Running dry-run on {} to cluster {} using releaseName: {}", helmChart, kubeconfig, releaseName);
75         final ProcessBuilder processBuilder = prepareDryRunCommand(releaseName, kubeconfig, helmChart);
76         executeCommand(processBuilder);
77         logger.info("Successfully ran dry for Chart {}", helmChart);
78
79     }
80
81     /**
82      *
83      * @param releaseName Name of the release given to helm install
84      * @param kubeconfig kubernetes configuration file path
85      * @param helmChart path of the helm chart to install
86      *
87      * @return Resources for helmChart as a List of strings
88      */
89     @Override
90     public List<String> getKubeKinds(final String releaseName, final Path kubeconfig, final Path helmChart) {
91         logger.info("Retrieving kinds from chart {} using releaseName {}", helmChart, releaseName);
92         final ProcessBuilder processBuilder = prepareKubeKindCommand(releaseName, kubeconfig, helmChart);
93         final String response = executeCommand(processBuilder);
94         if (StringUtils.isEmpty(response)) {
95             logger.warn("Response is empty: {}", response);
96             return Collections.emptyList();
97         }
98         final List<String> kinds = processKinds(response);
99
100         logger.debug("Found kinds: {}", kinds);
101         return kinds;
102     }
103
104
105     @Override
106     public List<String> getKubeKindsUsingManifestCommand(final String releaseName, final Path kubeConfig)
107             throws HelmClientExecuteException {
108         logger.info("Retrieving kinds from helm release history using releaseName {}", releaseName);
109
110         final ProcessBuilder processBuilder = prepareGetKubeKindCommand(releaseName, kubeConfig);
111         final String response = executeCommand(processBuilder);
112         if (StringUtils.isEmpty(response)) {
113             logger.warn("Response is empty: {}", response);
114             return Collections.emptyList();
115         }
116         final List<String> kinds = processKinds(response);
117
118         logger.debug("Kinds found from the helm release history: {}", kinds);
119         return kinds;
120     }
121
122
123     /**
124      *
125      * @param releaseName Name of the release given to helm install
126      * @param kubeconfig kubernetes configuration file path
127      * @param helmChart path of the helm chart to install
128      * @throws HelmClientExecuteException when exception occurs on executing command
129      */
130     @Override
131     public void installHelmChart(final String releaseName, final Path kubeconfig, final Path helmChart,
132             final Map<String, String> lifeCycleParams) throws HelmClientExecuteException {
133         logger.info("Installing {} to cluster {} using releaseName: {}", helmChart, kubeconfig, releaseName);
134         final ProcessBuilder processBuilder =
135                 prepareInstallCommand(releaseName, kubeconfig, helmChart, lifeCycleParams);
136         executeCommand(processBuilder);
137         logger.info("Chart {} installed successfully", helmChart);
138
139     }
140
141     /**
142      * @param releaseName Name of the release given to helm install
143      * @param kubeConfigFilePath kubernetes configuration file path
144      * @throws HelmClientExecuteException when exception occurs on executing command
145      */
146     @Override
147     public void unInstallHelmChart(final String releaseName, final Path kubeConfigFilePath)
148             throws HelmClientExecuteException {
149         logger.info("uninstalling the release {} from cluster {}", releaseName, kubeConfigFilePath);
150         final ProcessBuilder processBuilder = prepareUnInstallCommand(releaseName, kubeConfigFilePath);
151         final String commandResponse = executeCommand(processBuilder);
152         if (!StringUtils.isEmpty(commandResponse)) {
153             if (commandResponse.contains("Release not loaded")) {
154                 throw new HelmClientExecuteException(
155                         "Unable to find the installed Helm chart by using releaseName: " + releaseName);
156             }
157         }
158
159         logger.info("Release {} uninstalled successfully", releaseName);
160     }
161
162     private ProcessBuilder prepareDryRunCommand(final String releaseName, final Path kubeconfig, final Path helmChart) {
163         final List<String> helmArguments = List.of("helm", "install", releaseName, "-n", "default",
164                 helmChart.toString(), "--dry-run", "--kubeconfig", kubeconfig.toString());
165         return new ProcessBuilder().command(helmArguments);
166     }
167
168     private ProcessBuilder prepareInstallCommand(final String releaseName, final Path kubeconfig, final Path helmChart,
169             final Map<String, String> lifeCycleParams) {
170         final List<String> commands = new ArrayList<String>(List.of("helm", "install", releaseName, "-n", "default",
171                 helmChart.toString(), "--kubeconfig", kubeconfig.toString()));
172
173         if (lifeCycleParams != null && !lifeCycleParams.isEmpty()) {
174             final String fileName = helmChart.getParent().resolve("values.yaml").toString();
175             createYamlFile(fileName, lifeCycleParams);
176             commands.add("-f ".concat(fileName));
177         }
178         final List<String> helmArguments = List.of("sh", "-c", toString(commands));
179         return new ProcessBuilder().command(helmArguments);
180     }
181
182     private void createYamlFile(final String fileName, final Map<String, String> lifeCycleParams) {
183         logger.debug("Will create the runtime values.yaml file.");
184         final String yamlContent = propertiesToYamlConverter.getValuesYamlFileContent(lifeCycleParams);
185         logger.debug("Yaml file content : {}", yamlContent);
186         try {
187             Files.write(Paths.get(fileName), yamlContent.getBytes());
188         } catch (final IOException e) {
189             logger.error("Failed to create the run time life cycle yaml file: {} " + e.getMessage());
190             throw new HelmClientExecuteException(
191                     "Failed to create the run time life cycle yaml file: {} " + e.getMessage());
192         }
193     }
194
195     private ProcessBuilder prepareUnInstallCommand(final String releaseName, final Path kubeConfig) {
196         logger.debug("Will remove tis log after checking ubeconfig path: {}", kubeConfig.toFile().getName());
197         final List<String> helmArguments = new ArrayList<>(
198                 List.of("helm", "uninstall", releaseName, "-n", "default", "--kubeconfig", kubeConfig.toString()));
199         return new ProcessBuilder().command(helmArguments);
200     }
201
202     private ProcessBuilder prepareKubeKindCommand(final String releaseName, final Path kubeconfig,
203             final Path helmChart) {
204         final List<String> commands = List.of("helm", "template", releaseName, "-n", "default", helmChart.toString(),
205                 "--dry-run", "--kubeconfig", kubeconfig.toString(), "--skip-tests", "| grep kind | uniq");
206         final List<String> helmArguments = List.of("sh", "-c", toString(commands));
207         return new ProcessBuilder().command(helmArguments);
208     }
209
210     private ProcessBuilder prepareGetKubeKindCommand(final String releaseName, final Path kubeconfig) {
211         final List<String> commands = List.of("helm", "get", "manifest", releaseName, "-n", "default", "--kubeconfig",
212                 kubeconfig.toString(), "| grep kind | uniq");
213         final List<String> helmArguments = List.of("sh", "-c", toString(commands));
214         return new ProcessBuilder().command(helmArguments);
215     }
216
217     private String executeCommand(final ProcessBuilder processBuilder) throws HelmClientExecuteException {
218         final String commandStr = toString(processBuilder);
219
220         try {
221             logger.debug("Executing cmd: {}", commandStr);
222             final Process process = processBuilder.start();
223
224             final InputStreamConsumer errors = new InputStreamConsumer(process.getErrorStream());
225             final InputStreamConsumer output = new InputStreamConsumer(process.getInputStream());
226
227             final Thread errorsConsumer = new Thread(errors);
228             final Thread outputConsumer = new Thread(output);
229             errorsConsumer.start();
230             outputConsumer.start();
231
232             process.waitFor();
233
234             errorsConsumer.join();
235             outputConsumer.join();
236
237             final int exitValue = process.exitValue();
238             if (exitValue != 0) {
239                 final String stderr = errors.getContent();
240                 if (!stderr.isEmpty()) {
241                     throw new HelmClientExecuteException("Command execution failed: " + commandStr + " " + stderr);
242                 }
243             }
244
245             final String stdout = output.getContent();
246             logger.debug("Command <{}> execution, output: {}", commandStr, stdout);
247             return stdout;
248
249         } catch (final InterruptedException interruptedException) {
250             Thread.currentThread().interrupt();
251             throw new HelmClientExecuteException(
252                     "Failed to execute the Command: " + commandStr + ", the command was interrupted",
253                     interruptedException);
254         } catch (final Exception exception) {
255             throw new HelmClientExecuteException("Failed to execute the Command: " + commandStr, exception);
256         }
257     }
258
259     private List<String> processKinds(final String response) {
260
261         logger.debug("Processing kube kinds");
262
263         final List<String> kinds = new ArrayList<>();
264         for (final String entry : response.split(ANY_UNICODE_NEWLINE)) {
265             if (entry != null) {
266                 final String line = entry.trim();
267                 if (!line.isBlank()) {
268                     final String kind = line.replace(KIND_KEY, "").trim();
269                     if (SUPPORTED_KINDS.contains(kind)) {
270                         logger.debug("Found Supported kind: {}", kind);
271                         kinds.add(kind);
272                     } else {
273                         logger.warn("kind: {} is not currently supported", kind);
274                     }
275                 }
276             }
277         }
278         return kinds;
279     }
280
281     private String toString(final ProcessBuilder processBuilder) {
282         return String.join(" ", processBuilder.command());
283     }
284
285     private String toString(final List<String> commands) {
286         return String.join(" ", commands);
287     }
288 }