da8d455b8c0003dfd647fc776e2fb3ac485582ad
[policy/clamp.git] /
1 /*-
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
10  *
11  *      http://www.apache.org/licenses/LICENSE-2.0
12  *
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===================================
19  */
20
21 package org.onap.policy.clamp.acm.participant.kubernetes.helm;
22
23 import java.io.File;
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;
40
41 /**
42  * Client to talk with Helm cli. Supports helm3 + version
43  */
44 @Component
45 @NoArgsConstructor
46 public class HelmClient {
47
48     private ChartStore chartStore;
49
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";
55
56     @Autowired
57     public HelmClient(ChartStore chartStore) {
58         this.chartStore = chartStore;
59     }
60
61     /**
62      * Install a chart.
63      *
64      * @param chart name and version.
65      * @throws ServiceException in case of error
66      */
67     public void installChart(ChartInfo chart) throws ServiceException {
68         if (!checkNamespaceExists(chart.getNamespace())) {
69             var processBuilder = prepareCreateNamespaceCommand(chart.getNamespace());
70             executeCommand(processBuilder);
71         }
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());
77     }
78
79     /**
80      * Add a repository if it doesn't exist.
81      *
82      * @param repo HelmRepository
83      * @return boolean true of false based on added repo success or failed
84      * @throws ServiceException in case of error
85      */
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();
92         }
93         logger.info("Repository already exists, updating the repo");
94         updateHelmRepo();
95         return false;
96     }
97
98
99     /**
100      * Finds helm chart repository for the chart.
101      *
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
106      */
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);
112                 return repository;
113             }
114         }
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;
119         }
120         return null;
121     }
122
123     /**
124      * Verify the helm chart in configured repositories.
125      *
126      * @param chart chartInfo
127      * @return repo name
128      * @throws ServiceException in case of error
129      */
130     public String verifyConfiguredRepo(ChartInfo chart) throws ServiceException {
131         logger.info("Looking for helm chart {} in all the configured helm repositories", chart.getChartId().getName());
132         String repository;
133         var builder = helmRepoVerifyCommand(chart.getChartId().getName());
134         String output = executeCommand(builder);
135         repository = verifyOutput(output, chart.getChartId().getName());
136         return repository;
137     }
138
139     /**
140      * Uninstall a chart.
141      *
142      * @param chart name and version.
143      * @throws ServiceException in case of error
144      */
145     public void uninstallChart(ChartInfo chart) throws ServiceException {
146         executeCommand(prepareUnInstallCommand(chart));
147     }
148
149
150     /**
151      * Execute helm cli bash commands.
152      *
153      * @param processBuilder process builder object
154      * @return string output
155      * @throws ServiceException in case of error.
156      */
157     public String executeCommand(ProcessBuilder processBuilder) throws ServiceException {
158         var commandStr = toString(processBuilder);
159
160         try {
161             var process = processBuilder.start();
162             process.waitFor();
163             int exitValue = process.exitValue();
164
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);
169                 }
170             }
171
172             var output = IOUtils.toString(process.getInputStream(), StandardCharsets.UTF_8);
173             logger.debug("Command <{}> execution, output: {}", commandStr, output);
174             return output;
175
176         } catch (InterruptedException ie) {
177             Thread.currentThread().interrupt();
178             throw new ServiceException("Failed to execute the Command: " + commandStr + ", the command was interrupted",
179                 ie);
180         } catch (Exception exc) {
181             throw new ServiceException("Failed to execute the Command: " + commandStr, exc);
182         }
183     }
184
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();
189     }
190
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];
195             }
196         }
197         return null;
198     }
199
200     private ProcessBuilder prepareRepoAddCommand(HelmRepository repo) throws ServiceException {
201         if (StringUtils.isEmpty(repo.getAddress())) {
202             throw new ServiceException("Repository Should have valid address");
203         }
204         // @formatter:off
205         List<String> helmArguments = new ArrayList<>(
206                 List.of(
207                     COMMAND_HELM,
208                         "repo",
209                         "add", repo.getRepoName(), repo.getAddress()
210                 ));
211         if (!StringUtils.isEmpty(repo.getUserName()) && !StringUtils.isEmpty(repo.getPassword())) {
212             helmArguments.addAll(List.of("--username", repo.getUserName(), "--password",  repo.getPassword()));
213         }
214         return new ProcessBuilder().command(helmArguments);
215     }
216
217     private boolean verifyHelmRepoAlreadyExist(HelmRepository repo) {
218         try {
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)) {
223                 return false;
224             }
225         } catch (ServiceException e) {
226             logger.debug("Repository {} not found:", repo.getRepoName(), e);
227             return false;
228         }
229         return true;
230     }
231
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);
235     }
236
237     private ProcessBuilder prepareInstallCommand(ChartInfo chart) {
238
239         // @formatter:off
240         List<String> helmArguments = new ArrayList<>(
241             List.of(
242                 COMMAND_HELM,
243                 "install", chart.getReleaseName(), chart.getRepository().getRepoName() + "/"
244                             + chart.getChartId().getName(),
245                 "--version", chart.getChartId().getVersion(),
246                 "--namespace", chart.getNamespace()
247             ));
248         // @formatter:on
249
250         // Verify if values.yaml/override parameters available for the chart
251         var localOverrideYaml = chartStore.getOverrideFile(chart);
252
253         if (verifyLocalHelmRepo(localOverrideYaml)) {
254             logger.info("Override yaml available for the helm chart");
255             helmArguments.addAll(List.of("--values", localOverrideYaml.getPath()));
256         }
257
258         if (chart.getOverrideParams() != null) {
259             for (var entry : chart.getOverrideParams().entrySet()) {
260                 helmArguments.addAll(List.of("--set", entry.getKey() + "=" + entry.getValue()));
261             }
262         }
263         return new ProcessBuilder().command(helmArguments);
264     }
265
266     private ProcessBuilder prepareUnInstallCommand(ChartInfo chart) {
267         return new ProcessBuilder(COMMAND_HELM, "delete", chart.getReleaseName(), "--namespace",
268             chart.getNamespace());
269     }
270
271     private ProcessBuilder prepareCreateNamespaceCommand(String namespace) {
272         return new ProcessBuilder().command(COMMAND_KUBECTL, "create", "namespace", namespace);
273     }
274
275     private ProcessBuilder helmRepoVerifyCommand(String chartName) {
276         return new ProcessBuilder().command(COMMAND_SH, "-c", COMMAND_HELM + " search repo | grep " + chartName);
277     }
278
279
280     private boolean updateHelmRepo() {
281         try {
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);
287             return false;
288         }
289         return true;
290     }
291
292     private boolean verifyLocalHelmRepo(File localFile) {
293         return localFile.exists();
294     }
295
296     protected static String toString(ProcessBuilder processBuilder) {
297         return String.join(" ", processBuilder.command());
298     }
299 }