6a1b9865425af1a2ac1c2fb880327399d31536e2
[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.BufferedReader;
24 import java.io.File;
25 import java.io.IOException;
26 import java.io.InputStreamReader;
27 import java.lang.invoke.MethodHandles;
28 import java.nio.charset.StandardCharsets;
29 import java.util.ArrayList;
30 import java.util.List;
31 import java.util.Map;
32 import org.apache.commons.io.IOUtils;
33 import org.onap.policy.clamp.controlloop.participant.kubernetes.exception.ServiceException;
34 import org.onap.policy.clamp.controlloop.participant.kubernetes.models.ChartInfo;
35 import org.onap.policy.clamp.controlloop.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
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         var processBuilder = prepareCreateNamespaceCommand(chart.getNamespace());
60         try {
61             executeCommand(processBuilder);
62         } catch (ServiceException e) {
63             logger.warn("Namespace not created", e);
64         }
65         processBuilder = prepareInstallCommand(chart);
66         logger.info("Installing helm chart {} from the repository {} ", chart.getChartId().getName(),
67             chart.getRepository());
68         executeCommand(processBuilder);
69         logger.info("Chart {} installed successfully", chart.getChartId().getName());
70     }
71
72     /**
73      * Finds helm chart repository for the chart.
74      *
75      * @param chart ChartInfo.
76      * @return the chart repository as a string
77      * @throws ServiceException in case of error
78      * @throws IOException in case of IO errors
79      */
80     public String findChartRepository(ChartInfo chart) throws ServiceException, IOException {
81         updateHelmRepo();
82         String repository = verifyConfiguredRepo(chart);
83         if (repository != null) {
84             return repository;
85         }
86         var localHelmChartDir = chartStore.getAppPath(chart.getChartId()).toString();
87         logger.info("Chart not found in helm repositories, verifying local repo {} ", localHelmChartDir);
88         if (verifyLocalHelmRepo(new File(localHelmChartDir + "/" + chart.getChartId().getName()))) {
89             repository = localHelmChartDir;
90         }
91
92         return repository;
93     }
94
95     /**
96      * Verify helm chart in configured repositories.
97      * @param chart chartInfo
98      * @return repo name
99      * @throws IOException incase of error
100      * @throws ServiceException incase of error
101      */
102     public String verifyConfiguredRepo(ChartInfo chart) throws IOException, ServiceException {
103         logger.info("Looking for helm chart {} in all the configured helm repositories", chart.getChartId().getName());
104         String repository = null;
105         var builder = helmRepoVerifyCommand(chart.getChartId().getName());
106         String output = executeCommand(builder);
107         try (var reader = new BufferedReader(new InputStreamReader(IOUtils.toInputStream(output,
108             StandardCharsets.UTF_8)))) {
109             String line = reader.readLine();
110             while (line != null) {
111                 if (line.contains(chart.getChartId().getName())) {
112                     repository = line.split("/")[0];
113                     logger.info("Helm chart located in the repository {} ", repository);
114                     return repository;
115                 }
116                 line = reader.readLine();
117             }
118         }
119         return repository;
120     }
121
122     /**
123      * Uninstall a chart.
124      *
125      * @param chart name and version.
126      * @throws ServiceException incase of error
127      */
128     public void uninstallChart(ChartInfo chart) throws ServiceException {
129         executeCommand(prepareUnInstallCommand(chart));
130     }
131
132
133     /**
134      * Execute helm cli bash commands .
135      * @param processBuilder processbuilder
136      * @return string output
137      * @throws ServiceException incase of error.
138      */
139     public static String executeCommand(ProcessBuilder processBuilder) throws ServiceException {
140         var commandStr = toString(processBuilder);
141
142         try {
143             var process = processBuilder.start();
144             process.waitFor();
145             int exitValue = process.exitValue();
146
147             if (exitValue != 0) {
148                 var error = IOUtils.toString(process.getErrorStream(), StandardCharsets.UTF_8);
149                 if (! error.isEmpty()) {
150                     throw new ServiceException("Command execution failed: " + commandStr + " " + error);
151                 }
152             }
153
154             var output = IOUtils.toString(process.getInputStream(), StandardCharsets.UTF_8);
155             logger.debug("Command <{}> execution, output: {}", commandStr, output);
156             return output;
157
158         } catch (InterruptedException ie) {
159             Thread.currentThread().interrupt();
160             throw new ServiceException("Failed to execute the Command: " + commandStr + ", the command was interrupted",
161                 ie);
162         } catch (Exception exc) {
163             throw new ServiceException("Failed to execute the Command: " + commandStr, exc);
164         }
165     }
166
167     private ProcessBuilder prepareInstallCommand(ChartInfo chart) {
168
169         // @formatter:off
170         List<String> helmArguments = new ArrayList<>(
171             List.of(
172                 "helm",
173                 "install", chart.getReleaseName(), chart.getRepository() + "/" + chart.getChartId().getName(),
174                 "--version", chart.getChartId().getVersion(),
175                 "--namespace", chart.getNamespace()
176             )
177         );
178         // @formatter:on
179
180         // Verify if values.yaml/override parameters available for the chart
181         var localOverrideYaml = chartStore.getOverrideFile(chart);
182
183         if (verifyLocalHelmRepo(localOverrideYaml)) {
184             logger.info("Override yaml available for the helm chart");
185             helmArguments.addAll(List.of("--values", localOverrideYaml.getPath()));
186         }
187
188         if (chart.getOverrideParams() != null) {
189             for (Map.Entry<String, String> entry : chart.getOverrideParams().entrySet()) {
190                 helmArguments.addAll(List.of("--set", entry.getKey() + "=" + entry.getValue()));
191             }
192         }
193         return new ProcessBuilder().command(helmArguments);
194     }
195
196     private ProcessBuilder prepareUnInstallCommand(ChartInfo chart) {
197         return new ProcessBuilder("helm", "delete", chart.getReleaseName(), "--namespace",
198             chart.getNamespace());
199     }
200
201     private ProcessBuilder prepareCreateNamespaceCommand(String namespace) {
202         return new ProcessBuilder().command("kubectl", "create", "namespace", namespace);
203     }
204
205     private ProcessBuilder helmRepoVerifyCommand(String chartName) {
206         return new ProcessBuilder().command("sh", "-c", "helm search repo | grep " + chartName);
207     }
208
209
210     private void updateHelmRepo() throws ServiceException {
211         logger.info("Updating local helm repositories before verifying the chart");
212         executeCommand(new ProcessBuilder().command("helm", "repo", "update"));
213         logger.debug("Helm repositories updated successfully");
214     }
215
216     private boolean verifyLocalHelmRepo(File localFile) {
217         return localFile.exists();
218     }
219
220     protected static String toString(ProcessBuilder processBuilder) {
221         return String.join(" ", processBuilder.command());
222     }
223 }