5b13a44a128a596d86da11629de3c89955108902
[policy/clamp.git] /
1 /*-
2  * ========================LICENSE_START=================================
3  * Copyright (C) 2021-2024 Nordix Foundation. 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 org.apache.commons.io.IOUtils;
30 import org.apache.commons.lang3.StringUtils;
31 import org.onap.policy.clamp.acm.participant.kubernetes.exception.ServiceException;
32 import org.onap.policy.clamp.acm.participant.kubernetes.models.ChartInfo;
33 import org.onap.policy.clamp.acm.participant.kubernetes.models.HelmRepository;
34 import org.onap.policy.clamp.acm.participant.kubernetes.service.ChartStore;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
37 import org.springframework.beans.factory.annotation.Autowired;
38 import org.springframework.stereotype.Component;
39
40 /**
41  * Client to talk with Helm cli. Supports helm3 + version
42  */
43 @Component
44 public class HelmClient {
45
46     @Autowired
47     private ChartStore chartStore;
48
49     private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
50     private static final String PATH_DELIMITER = "/";
51     public static final String COMMAND_SH = "/bin/sh";
52     private static final String COMMAND_HELM = "/usr/local/bin/helm";
53     public static final String COMMAND_KUBECTL = "/usr/local/bin/kubectl";
54
55     /**
56      * Install a chart.
57      *
58      * @param chart name and version.
59      * @throws ServiceException incase of error
60      */
61     public void installChart(ChartInfo chart) throws ServiceException {
62         if (! checkNamespaceExists(chart.getNamespace())) {
63             var processBuilder = prepareCreateNamespaceCommand(chart.getNamespace());
64             executeCommand(processBuilder);
65         }
66         var processBuilder = prepareInstallCommand(chart);
67         logger.info("Installing helm chart {} from the repository {} ", chart.getChartId().getName(),
68             chart.getRepository().getRepoName());
69         executeCommand(processBuilder);
70         logger.info("Chart {} installed successfully", chart.getChartId().getName());
71     }
72
73     /**
74      * Add repository if doesn't exist.
75      * @param repo HelmRepository
76      * @return boolean true of false based on add repo success or failed
77      * @throws ServiceException incase of error
78      */
79     public boolean addRepository(HelmRepository repo) throws ServiceException {
80         if (!verifyHelmRepoAlreadyExist(repo)) {
81             logger.info("Adding repository to helm client");
82             executeCommand(prepareRepoAddCommand(repo));
83             logger.debug("Added repository {} to the helm client", repo.getRepoName());
84             return updateHelmRepo();
85         }
86         logger.info("Repository already exists, updating the repo");
87         updateHelmRepo();
88         return false;
89     }
90
91
92     /**
93      * Finds helm chart repository for the chart.
94      *
95      * @param chart ChartInfo.
96      * @return the chart repository as a string
97      * @throws ServiceException in case of error
98      * @throws IOException in case of IO errors
99      */
100     public String findChartRepository(ChartInfo chart) throws ServiceException, IOException {
101         if (updateHelmRepo()) {
102             String repository = verifyConfiguredRepo(chart);
103             if (repository != null) {
104                 logger.info("Helm chart located in the repository {} ", repository);
105                 return repository;
106             }
107         }
108         var localHelmChartDir = chartStore.getAppPath(chart.getChartId()).toString();
109         logger.info("Chart not found in helm repositories, verifying local repo {} ", localHelmChartDir);
110         if (verifyLocalHelmRepo(new File(localHelmChartDir + PATH_DELIMITER + chart.getChartId().getName()))) {
111             return localHelmChartDir;
112         }
113         return null;
114     }
115
116     /**
117      * Verify helm chart in configured repositories.
118      * @param chart chartInfo
119      * @return repo name
120      * @throws IOException incase of error
121      * @throws ServiceException incase of error
122      */
123     public String verifyConfiguredRepo(ChartInfo chart) throws IOException, ServiceException {
124         logger.info("Looking for helm chart {} in all the configured helm repositories", chart.getChartId().getName());
125         String repository = null;
126         var builder = helmRepoVerifyCommand(chart.getChartId().getName());
127         String output = executeCommand(builder);
128         repository = verifyOutput(output, chart.getChartId().getName());
129         return repository;
130     }
131
132     /**
133      * Uninstall a chart.
134      *
135      * @param chart name and version.
136      * @throws ServiceException incase of error
137      */
138     public void uninstallChart(ChartInfo chart) throws ServiceException {
139         executeCommand(prepareUnInstallCommand(chart));
140     }
141
142
143     /**
144      * Execute helm cli bash commands .
145      * @param processBuilder processbuilder
146      * @return string output
147      * @throws ServiceException incase of error.
148      */
149     public String executeCommand(ProcessBuilder processBuilder) throws ServiceException {
150         var commandStr = toString(processBuilder);
151
152         try {
153             var process = processBuilder.start();
154             process.waitFor();
155             int exitValue = process.exitValue();
156
157             if (exitValue != 0) {
158                 var error = IOUtils.toString(process.getErrorStream(), StandardCharsets.UTF_8);
159                 if (! error.isEmpty()) {
160                     throw new ServiceException("Command execution failed: " + commandStr + " " + error);
161                 }
162             }
163
164             var output = IOUtils.toString(process.getInputStream(), StandardCharsets.UTF_8);
165             logger.debug("Command <{}> execution, output: {}", commandStr, output);
166             return output;
167
168         } catch (InterruptedException ie) {
169             Thread.currentThread().interrupt();
170             throw new ServiceException("Failed to execute the Command: " + commandStr + ", the command was interrupted",
171                 ie);
172         } catch (Exception exc) {
173             throw new ServiceException("Failed to execute the Command: " + commandStr, exc);
174         }
175     }
176
177     private boolean checkNamespaceExists(String namespace) throws ServiceException {
178         logger.info("Check if namespace {} exists on the cluster", namespace);
179         String output = executeCommand(prepareVerifyNamespaceCommand(namespace));
180         return !output.isEmpty();
181     }
182
183     private String verifyOutput(String output, String value) {
184         for (var line: output.split("\\R")) {
185             if (line.contains(value)) {
186                 return line.split("/")[0];
187             }
188         }
189         return null;
190     }
191
192     private ProcessBuilder prepareRepoAddCommand(HelmRepository repo) throws ServiceException {
193         if (StringUtils.isEmpty(repo.getAddress())) {
194             throw new ServiceException("Repository Should have valid address");
195         }
196         // @formatter:off
197         List<String> helmArguments = new ArrayList<>(
198                 List.of(
199                     COMMAND_HELM,
200                         "repo",
201                         "add", repo.getRepoName(), repo.getAddress()
202                 ));
203         if (!StringUtils.isEmpty(repo.getUserName()) && !StringUtils.isEmpty(repo.getPassword())) {
204             helmArguments.addAll(List.of("--username", repo.getUserName(), "--password",  repo.getPassword()));
205         }
206         return new ProcessBuilder().command(helmArguments);
207     }
208
209     private boolean verifyHelmRepoAlreadyExist(HelmRepository repo) {
210         try {
211             logger.debug("Verify the repo already exist in helm repositories");
212             var helmArguments = List.of(COMMAND_SH, "-c", COMMAND_HELM + " repo list | grep " + repo.getRepoName());
213             String response = executeCommand(new ProcessBuilder().command(helmArguments));
214             if (StringUtils.isEmpty(response)) {
215                 return false;
216             }
217         } catch (ServiceException e) {
218             logger.debug("Repository {} not found:", repo.getRepoName(), e);
219             return false;
220         }
221         return true;
222     }
223
224     private ProcessBuilder prepareVerifyNamespaceCommand(String namespace) {
225         var helmArguments = List.of(COMMAND_SH, "-c", COMMAND_KUBECTL + " get ns | grep " + namespace);
226         return new ProcessBuilder().command(helmArguments);
227     }
228
229     private ProcessBuilder prepareInstallCommand(ChartInfo chart) {
230
231         // @formatter:off
232         List<String> helmArguments = new ArrayList<>(
233             List.of(
234                 COMMAND_HELM,
235                 "install", chart.getReleaseName(), chart.getRepository().getRepoName() + "/"
236                             + chart.getChartId().getName(),
237                 "--version", chart.getChartId().getVersion(),
238                 "--namespace", chart.getNamespace()
239             ));
240         // @formatter:on
241
242         // Verify if values.yaml/override parameters available for the chart
243         var localOverrideYaml = chartStore.getOverrideFile(chart);
244
245         if (verifyLocalHelmRepo(localOverrideYaml)) {
246             logger.info("Override yaml available for the helm chart");
247             helmArguments.addAll(List.of("--values", localOverrideYaml.getPath()));
248         }
249
250         if (chart.getOverrideParams() != null) {
251             for (var entry : chart.getOverrideParams().entrySet()) {
252                 helmArguments.addAll(List.of("--set", entry.getKey() + "=" + entry.getValue()));
253             }
254         }
255         return new ProcessBuilder().command(helmArguments);
256     }
257
258     private ProcessBuilder prepareUnInstallCommand(ChartInfo chart) {
259         return new ProcessBuilder(COMMAND_HELM, "delete", chart.getReleaseName(), "--namespace",
260             chart.getNamespace());
261     }
262
263     private ProcessBuilder prepareCreateNamespaceCommand(String namespace) {
264         return new ProcessBuilder().command(COMMAND_KUBECTL, "create", "namespace", namespace);
265     }
266
267     private ProcessBuilder helmRepoVerifyCommand(String chartName) {
268         return new ProcessBuilder().command(COMMAND_SH, "-c", COMMAND_HELM + " search repo | grep " + chartName);
269     }
270
271
272     private boolean updateHelmRepo() {
273         try {
274             logger.info("Updating local helm repositories");
275             executeCommand(new ProcessBuilder().command(COMMAND_HELM, "repo", "update"));
276             logger.debug("Helm repositories updated successfully");
277         } catch (ServiceException e) {
278             logger.error("Failed to update the helm repo: ", e);
279             return false;
280         }
281         return true;
282     }
283
284     private boolean verifyLocalHelmRepo(File localFile) {
285         return localFile.exists();
286     }
287
288     protected static String toString(ProcessBuilder processBuilder) {
289         return String.join(" ", processBuilder.command());
290     }
291 }