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