2 * ========================LICENSE_START=================================
3 * Copyright (C) 2021-2025 OpenInfra Foundation Europe. All rights reserved.
4 * ======================================================================
5 * Modifications Copyright (C) 2021 AT&T Intellectual Property. All rights reserved.
6 * ======================================================================
7 * Licensed under the Apache License, Version 2.0 (the "License");
8 * you may not use this file except in compliance with the License.
9 * You may obtain a copy of the License at
11 * http://www.apache.org/licenses/LICENSE-2.0
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
18 * ========================LICENSE_END===================================
21 package org.onap.policy.clamp.acm.participant.kubernetes.helm;
24 import java.io.IOException;
25 import java.lang.invoke.MethodHandles;
26 import java.nio.charset.StandardCharsets;
27 import java.util.ArrayList;
28 import java.util.List;
29 import lombok.NoArgsConstructor;
30 import org.apache.commons.io.IOUtils;
31 import org.apache.commons.lang3.StringUtils;
32 import org.onap.policy.clamp.acm.participant.kubernetes.exception.ServiceException;
33 import org.onap.policy.clamp.acm.participant.kubernetes.models.ChartInfo;
34 import org.onap.policy.clamp.acm.participant.kubernetes.models.HelmRepository;
35 import org.onap.policy.clamp.acm.participant.kubernetes.service.ChartStore;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
38 import org.springframework.beans.factory.annotation.Autowired;
39 import org.springframework.stereotype.Component;
42 * Client to talk with Helm cli. Supports helm3 + version
46 public class HelmClient {
48 private ChartStore chartStore;
50 private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
51 private static final String PATH_DELIMITER = "/";
52 public static final String COMMAND_SH = "/bin/sh";
53 private static final String COMMAND_HELM = "/usr/local/bin/helm";
54 public static final String COMMAND_KUBECTL = "/usr/local/bin/kubectl";
57 public HelmClient(ChartStore chartStore) {
58 this.chartStore = chartStore;
64 * @param chart name and version.
65 * @throws ServiceException in case of error
67 public void installChart(ChartInfo chart) throws ServiceException {
68 if (!checkNamespaceExists(chart.getNamespace())) {
69 var processBuilder = prepareCreateNamespaceCommand(chart.getNamespace());
70 executeCommand(processBuilder);
72 var processBuilder = prepareInstallCommand(chart);
73 logger.info("Installing helm chart {} from the repository {} ", chart.getChartId().getName(),
74 chart.getRepository().getRepoName());
75 executeCommand(processBuilder);
76 logger.info("Chart {} installed successfully", chart.getChartId().getName());
80 * Add a repository if it doesn't exist.
82 * @param repo HelmRepository
83 * @return boolean true of false based on added repo success or failed
84 * @throws ServiceException in case of error
86 public boolean addRepository(HelmRepository repo) throws ServiceException {
87 if (!verifyHelmRepoAlreadyExist(repo)) {
88 logger.info("Adding repository to helm client");
89 executeCommand(prepareRepoAddCommand(repo));
90 logger.debug("Added repository {} to the helm client", repo.getRepoName());
91 return updateHelmRepo();
93 logger.info("Repository already exists, updating the repo");
100 * Finds helm chart repository for the chart.
102 * @param chart ChartInfo.
103 * @return the chart repository as a string
104 * @throws ServiceException in case of error
105 * @throws IOException in case of IO errors
107 public String findChartRepository(ChartInfo chart) throws ServiceException, IOException {
108 if (updateHelmRepo()) {
109 String repository = verifyConfiguredRepo(chart);
110 if (repository != null) {
111 logger.info("Helm chart located in the repository {} ", repository);
115 var localHelmChartDir = chartStore.getAppPath(chart.getChartId()).toString();
116 logger.info("Chart not found in helm repositories, verifying local repo {} ", localHelmChartDir);
117 if (verifyLocalHelmRepo(new File(localHelmChartDir + PATH_DELIMITER + chart.getChartId().getName()))) {
118 return localHelmChartDir;
124 * Verify the helm chart in configured repositories.
126 * @param chart chartInfo
128 * @throws ServiceException in case of error
130 public String verifyConfiguredRepo(ChartInfo chart) throws ServiceException {
131 logger.info("Looking for helm chart {} in all the configured helm repositories", chart.getChartId().getName());
133 var builder = helmRepoVerifyCommand(chart.getChartId().getName());
134 String output = executeCommand(builder);
135 repository = verifyOutput(output, chart.getChartId().getName());
142 * @param chart name and version.
143 * @throws ServiceException in case of error
145 public void uninstallChart(ChartInfo chart) throws ServiceException {
146 executeCommand(prepareUnInstallCommand(chart));
151 * Execute helm cli bash commands.
153 * @param processBuilder process builder object
154 * @return string output
155 * @throws ServiceException in case of error.
157 public String executeCommand(ProcessBuilder processBuilder) throws ServiceException {
158 var commandStr = toString(processBuilder);
161 var process = processBuilder.start();
163 int exitValue = process.exitValue();
165 if (exitValue != 0) {
166 var error = IOUtils.toString(process.getErrorStream(), StandardCharsets.UTF_8);
167 if (!error.isEmpty()) {
168 throw new ServiceException("Command execution failed: " + commandStr + " " + error);
172 var output = IOUtils.toString(process.getInputStream(), StandardCharsets.UTF_8);
173 logger.debug("Command <{}> execution, output: {}", commandStr, output);
176 } catch (InterruptedException ie) {
177 Thread.currentThread().interrupt();
178 throw new ServiceException("Failed to execute the Command: " + commandStr + ", the command was interrupted",
180 } catch (Exception exc) {
181 throw new ServiceException("Failed to execute the Command: " + commandStr, exc);
185 private boolean checkNamespaceExists(String namespace) throws ServiceException {
186 logger.info("Check if namespace {} exists on the cluster", namespace);
187 String output = executeCommand(prepareVerifyNamespaceCommand(namespace));
188 return !output.isEmpty();
191 private String verifyOutput(String output, String value) {
192 for (var line : output.split("\\R")) {
193 if (line.contains(value)) {
194 return line.split("/")[0];
200 private ProcessBuilder prepareRepoAddCommand(HelmRepository repo) throws ServiceException {
201 if (StringUtils.isEmpty(repo.getAddress())) {
202 throw new ServiceException("Repository Should have valid address");
205 List<String> helmArguments = new ArrayList<>(
209 "add", repo.getRepoName(), repo.getAddress()
211 if (!StringUtils.isEmpty(repo.getUserName()) && !StringUtils.isEmpty(repo.getPassword())) {
212 helmArguments.addAll(List.of("--username", repo.getUserName(), "--password", repo.getPassword()));
214 return new ProcessBuilder().command(helmArguments);
217 private boolean verifyHelmRepoAlreadyExist(HelmRepository repo) {
219 logger.debug("Verify the repo already exist in helm repositories");
220 var helmArguments = List.of(COMMAND_SH, "-c", COMMAND_HELM + " repo list | grep " + repo.getRepoName());
221 String response = executeCommand(new ProcessBuilder().command(helmArguments));
222 if (StringUtils.isEmpty(response)) {
225 } catch (ServiceException e) {
226 logger.debug("Repository {} not found:", repo.getRepoName(), e);
232 private ProcessBuilder prepareVerifyNamespaceCommand(String namespace) {
233 var helmArguments = List.of(COMMAND_SH, "-c", COMMAND_KUBECTL + " get ns | grep " + namespace);
234 return new ProcessBuilder().command(helmArguments);
237 private ProcessBuilder prepareInstallCommand(ChartInfo chart) {
240 List<String> helmArguments = new ArrayList<>(
243 "install", chart.getReleaseName(), chart.getRepository().getRepoName() + "/"
244 + chart.getChartId().getName(),
245 "--version", chart.getChartId().getVersion(),
246 "--namespace", chart.getNamespace()
250 // Verify if values.yaml/override parameters available for the chart
251 var localOverrideYaml = chartStore.getOverrideFile(chart);
253 if (verifyLocalHelmRepo(localOverrideYaml)) {
254 logger.info("Override yaml available for the helm chart");
255 helmArguments.addAll(List.of("--values", localOverrideYaml.getPath()));
258 if (chart.getOverrideParams() != null) {
259 for (var entry : chart.getOverrideParams().entrySet()) {
260 helmArguments.addAll(List.of("--set", entry.getKey() + "=" + entry.getValue()));
263 return new ProcessBuilder().command(helmArguments);
266 private ProcessBuilder prepareUnInstallCommand(ChartInfo chart) {
267 return new ProcessBuilder(COMMAND_HELM, "delete", chart.getReleaseName(), "--namespace",
268 chart.getNamespace());
271 private ProcessBuilder prepareCreateNamespaceCommand(String namespace) {
272 return new ProcessBuilder().command(COMMAND_KUBECTL, "create", "namespace", namespace);
275 private ProcessBuilder helmRepoVerifyCommand(String chartName) {
276 return new ProcessBuilder().command(COMMAND_SH, "-c", COMMAND_HELM + " search repo | grep " + chartName);
280 private boolean updateHelmRepo() {
282 logger.info("Updating local helm repositories");
283 executeCommand(new ProcessBuilder().command(COMMAND_HELM, "repo", "update"));
284 logger.debug("Helm repositories updated successfully");
285 } catch (ServiceException e) {
286 logger.error("Failed to update the helm repo: ", e);
292 private boolean verifyLocalHelmRepo(File localFile) {
293 return localFile.exists();
296 protected static String toString(ProcessBuilder processBuilder) {
297 return String.join(" ", processBuilder.command());