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