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