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 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;
56 public HelmClientImpl(final PropertiesToYamlConverter propertiesToYamlConverter) {
57 this.propertiesToYamlConverter = propertiesToYamlConverter;
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);
64 * Execute a helm install dry run
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
70 * @throws HelmClientExecuteException when exception occurs on executing command
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);
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
88 * @return Resources for helmChart as a List of strings
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();
99 final List<String> kinds = processKinds(response);
101 logger.debug("Found kinds: {}", kinds);
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);
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();
117 final List<String> kinds = processKinds(response);
119 logger.debug("Kinds found from the helm release history: {}", kinds);
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
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);
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
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);
158 logger.info("Release {} uninstalled successfully", releaseName);
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 getProcessBuilder().command(helmArguments);
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()));
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));
177 final List<String> helmArguments = List.of("sh", "-c", toString(commands));
178 return getProcessBuilder().command(helmArguments);
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);
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);
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 getProcessBuilder().command(helmArguments);
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 getProcessBuilder().command(helmArguments);
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 getProcessBuilder().command(helmArguments);
216 private String executeCommand(final ProcessBuilder processBuilder) throws HelmClientExecuteException {
217 final String commandStr = toString(processBuilder);
220 logger.debug("Executing cmd: {}", commandStr);
221 final Process process = processBuilder.start();
223 final InputStreamConsumer errors = new InputStreamConsumer(process.getErrorStream());
224 final InputStreamConsumer output = new InputStreamConsumer(process.getInputStream());
226 final Thread errorsConsumer = new Thread(errors);
227 final Thread outputConsumer = new Thread(output);
228 errorsConsumer.start();
229 outputConsumer.start();
233 errorsConsumer.join();
234 outputConsumer.join();
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);
244 final String stdout = output.getContent();
245 logger.debug("Command <{}> execution, output: {}", commandStr, stdout);
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);
258 private List<String> processKinds(final String response) {
260 logger.debug("Processing kube kinds");
262 final List<String> kinds = new ArrayList<>();
263 for (final String entry : response.split(ANY_UNICODE_NEWLINE)) {
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);
272 logger.warn("kind: {} is not currently supported", kind);
280 private String toString(final ProcessBuilder processBuilder) {
281 return String.join(" ", processBuilder.command());
284 private String toString(final List<String> commands) {
285 return String.join(" ", commands);
288 ProcessBuilder getProcessBuilder() {
289 return new ProcessBuilder();