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
9 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 * SPDX-License-Identifier: Apache-2.0
18 * ============LICENSE_END=========================================================
21 package org.onap.so.cnfm.lcm.bpmn.flows.extclients.helm;
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;
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;
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;
55 public HelmClientImpl(final PropertiesToYamlConverter propertiesToYamlConverter) {
56 this.propertiesToYamlConverter = propertiesToYamlConverter;
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);
63 * Execute a helm install dry run
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
69 * @throws HelmClientExecuteException when exception occurs on executing command
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);
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
87 * @return Resources for helmChart as a List of strings
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();
98 final List<String> kinds = processKinds(response);
100 logger.debug("Found kinds: {}", kinds);
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);
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();
116 final List<String> kinds = processKinds(response);
118 logger.debug("Kinds found from the helm release history: {}", kinds);
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
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);
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
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);
159 logger.info("Release {} uninstalled successfully", releaseName);
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);
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()));
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));
178 final List<String> helmArguments = List.of("sh", "-c", toString(commands));
179 return new ProcessBuilder().command(helmArguments);
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);
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());
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);
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);
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);
217 private String executeCommand(final ProcessBuilder processBuilder) throws HelmClientExecuteException {
218 final String commandStr = toString(processBuilder);
221 logger.debug("Executing cmd: {}", commandStr);
222 final Process process = processBuilder.start();
224 final InputStreamConsumer errors = new InputStreamConsumer(process.getErrorStream());
225 final InputStreamConsumer output = new InputStreamConsumer(process.getInputStream());
227 final Thread errorsConsumer = new Thread(errors);
228 final Thread outputConsumer = new Thread(output);
229 errorsConsumer.start();
230 outputConsumer.start();
234 errorsConsumer.join();
235 outputConsumer.join();
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);
245 final String stdout = output.getContent();
246 logger.debug("Command <{}> execution, output: {}", commandStr, stdout);
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);
259 private List<String> processKinds(final String response) {
261 logger.debug("Processing kube kinds");
263 final List<String> kinds = new ArrayList<>();
264 for (final String entry : response.split(ANY_UNICODE_NEWLINE)) {
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);
273 logger.warn("kind: {} is not currently supported", kind);
281 private String toString(final ProcessBuilder processBuilder) {
282 return String.join(" ", processBuilder.command());
285 private String toString(final List<String> commands) {
286 return String.join(" ", commands);