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