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