001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
002 *
003 * Copyright © 2017 MicroBean.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
014 * implied.  See the License for the specific language governing
015 * permissions and limitations under the License.
016 */
017package org.microbean.helm.maven;
018
019import java.io.BufferedReader;
020import java.io.IOException;
021import java.io.InputStreamReader;
022import java.io.Reader;
023
024import java.net.URI;
025import java.net.URL;
026
027import java.nio.charset.StandardCharsets;
028
029import java.nio.file.Files;
030import java.nio.file.Path;
031import java.nio.file.Paths;
032
033import java.util.Objects;
034
035import java.util.concurrent.Callable;
036import java.util.concurrent.Future;
037
038import java.util.regex.Matcher;
039
040import javax.inject.Inject;
041
042import hapi.chart.ChartOuterClass.Chart;
043
044import hapi.release.ReleaseOuterClass.Release;
045
046import hapi.services.tiller.Tiller.InstallReleaseRequest;
047import hapi.services.tiller.Tiller.InstallReleaseResponse;
048
049import org.apache.maven.execution.MavenSession;
050
051import org.apache.maven.plugin.MojoExecutionException;
052
053import org.apache.maven.plugin.logging.Log;
054
055import org.apache.maven.plugins.annotations.Mojo;
056import org.apache.maven.plugins.annotations.Parameter;
057
058import org.apache.maven.project.MavenProject;
059
060import org.microbean.helm.ReleaseManager;
061
062import org.microbean.helm.chart.AbstractChartLoader;
063import org.microbean.helm.chart.URLChartLoader;
064
065/**
066 * <a
067 * href="https://github.com/kubernetes/helm/blob/master/docs/using_helm.md#helm-install-installing-a-package">Installs
068 * a chart and hence creates a release</a>.
069 *
070 * @author <a href="https://about.me/lairdnelson"
071 * target="_parent">Laird Nelson</a>
072 */
073@Mojo(name = "install")
074public class InstallReleaseMojo extends AbstractMutatingReleaseMojo {
075
076
077  /*
078   * Instance fields.
079   */
080
081
082  /**
083   * The {@link MavenProject} in effect.
084   */
085  private final MavenProject project;
086
087  /**
088   * The {@link MavenSession} in effect.
089   */
090  private final MavenSession session;
091
092  /**
093   * The name of the release to install.  If omitted, a release name
094   * will be generated and used instead.
095   *
096   * @see #getReleaseName()
097   *
098   * @see #setReleaseName(String)
099   *
100   * @see #validateReleaseName(String)
101   */
102  /*
103   * This field shadows the AbstractSingleReleaseMojo#releaseName
104   * field on purpose to relax its "required" nature.
105   */
106  @Parameter
107  private String releaseName;
108
109  /**
110   * The <a
111   * href="https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/">namespace</a>
112   * into which the release will be installed.
113   */
114  @Parameter
115  private String releaseNamespace;
116
117  /**
118   * Whether to reuse the release name for repeated installations.  It
119   * is strongly recommended that you not set this to {@code true} in
120   * production scenarios.
121   */
122  @Parameter(defaultValue = "false")
123  private boolean reuseReleaseName;
124
125  /**
126   * Whether a missing or non-resolvable {@code chartUrl} parameter
127   * will result in skipped execution or an error.
128   */
129  @Parameter(defaultValue = "false")
130  private boolean lenient;
131
132  /**
133   * YAML-formatted values to supply at the time of installation.
134   *
135   * If this parameter and the {@code valuesYamlUri} parameter are
136   * both specified, this parameter is preferred if its value is
137   * non-{@code null} and non-empty.
138   */
139  @Parameter(property = "helm.install.valuesYaml")
140  private String valuesYaml;
141
142  /**
143   * A URI identifying a document containing YAML-formatted values to
144   * supply at the time of installation.
145   *
146   * If this parameter and the {@code valuesYaml} parameter are both
147   * specified, this parameter is ignored if the value of the {@code
148   * valuesYaml} parameter is non-{@code null} and non-empty.
149   */
150  @Parameter(property = "helm.install.valuesYamlUri")
151  private URI valuesYamlUri;
152  
153  /**
154   * A {@link URL} representing the chart to install.  If omitted,
155   * <code>file:/${project.build.directory}/generated-sources/helm/charts/${project.artifactId}</code>
156   * will be used instead.
157   */
158  @Parameter(required = true,
159             defaultValue = "file:${project.build.directory}/generated-sources/helm/charts/${project.artifactId}",
160             property = "helm.install.chartUrl"
161  )
162  private URL chartUrl;
163
164  
165  /*
166   * Constructors.
167   */
168  
169
170  /**
171   * Creates a new {@link InstallReleaseMojo}.
172   *
173   * @param project the {@link MavenProject} in effect; must not be
174   * {@code null}
175   *
176   * @param session the {@link MavenSession} in effect; must not be
177   * {@code null}
178   *
179   * @exception NullPointerException if either {@code project} or
180   * {@code session} is {@code null}
181   */
182  @Inject
183  public InstallReleaseMojo(final MavenProject project, final MavenSession session) {
184    super();
185    Objects.requireNonNull(project);
186    Objects.requireNonNull(session);
187    this.project = project;
188    this.session = session;
189  }
190
191
192  /*
193   * Instance methods.
194   */
195
196
197  /**
198   * {@inheritDoc}
199   *
200   * <p>This implementation <a
201   * href="https://github.com/kubernetes/helm/blob/master/docs/using_helm.md#helm-install-installing-a-package">installs</a>
202   * the <a
203   * href="https://docs.helm.sh/developing_charts/#charts">chart</a>
204   * residing at the {@linkplain #getChartUrl() indicated URL} and
205   * thus creates a <a
206   * href="https://docs.helm.sh/glossary/#release">release</a>.</p>
207   */
208  @Override
209  protected void execute(final Callable<ReleaseManager> releaseManagerCallable) throws Exception {
210    Objects.requireNonNull(releaseManagerCallable);
211    final Log log = this.getLog();
212    assert log != null;
213
214    URL chartUrl = this.getChartUrl();
215    if (chartUrl == null) {
216      final Path chartPath = Paths.get(new StringBuilder(this.project.getBuild().getDirectory()).append("/generated-sources/helm/charts/").append(this.project.getArtifactId()).toString());
217      assert chartPath != null;
218      chartUrl = chartPath.toUri().toURL();
219      if (!Files.isDirectory(chartPath)) {
220        if (this.isLenient()) {
221          if (log.isWarnEnabled()) {
222            log.warn("Non-existent or unresolvable default chartUrl (" + chartUrl + "); skipping execution");
223          }
224          chartUrl = null;
225        } else {
226          throw new MojoExecutionException("Non-existent or unresolvable default chartUrl: " + chartUrl);
227        }
228      }
229    }
230    
231    if (chartUrl != null) {
232      if (log.isDebugEnabled()) {
233        log.debug("chartUrl: " + chartUrl);
234      }
235      
236      Chart.Builder chartBuilder = null;
237      try (final AbstractChartLoader<URL> chartLoader = this.createChartLoader()) {
238        if (chartLoader == null) {
239          throw new IllegalStateException("createChartLoader() == null");
240        }
241        if (log.isDebugEnabled()) {
242          log.debug("chartLoader: " + chartLoader);
243          log.debug("Loading Helm chart from " + chartUrl);
244        }
245        chartBuilder = chartLoader.load(chartUrl);
246      }
247      
248      if (chartBuilder == null) {
249        throw new IllegalStateException("chartBuilder.load(\"" + chartUrl + "\") == null");
250      }
251      
252      if (log.isInfoEnabled()) {
253        log.info("Loaded Helm chart from " + chartUrl);
254      }
255      
256      final InstallReleaseRequest.Builder requestBuilder = InstallReleaseRequest.newBuilder();
257      assert requestBuilder != null;
258      
259      requestBuilder.setDisableHooks(this.getDisableHooks());
260      requestBuilder.setDryRun(this.getDryRun());
261      
262      final String releaseName = this.getReleaseName();
263      if (releaseName != null) {
264        requestBuilder.setName(releaseName);
265      }
266      
267      final String releaseNamespace = this.getReleaseNamespace();
268      if (releaseNamespace != null) {
269        requestBuilder.setNamespace(releaseNamespace);
270      }
271      
272      requestBuilder.setReuseName(this.getReuseReleaseName());
273      requestBuilder.setTimeout(this.getTimeout());
274
275      String valuesYaml = this.getValuesYaml();
276      if (valuesYaml == null || valuesYaml.isEmpty()) {
277        final URI valuesYamlUri = this.getValuesYamlUri();
278        if (valuesYamlUri != null) {
279          final URL yamlUrl = valuesYamlUri.toURL();
280          assert yamlUrl != null;
281          try (final Reader reader = new BufferedReader(new InputStreamReader(yamlUrl.openStream(), StandardCharsets.UTF_8))) {
282            final StringBuilder sb = new StringBuilder();
283            final char[] buffer = new char[4096];
284            int charsRead = -1;
285            while ((charsRead = reader.read(buffer, 0, buffer.length)) >= 0) {
286              sb.append(buffer, 0, charsRead);
287            }
288            valuesYaml = sb.toString();
289          }
290        }
291      }
292      if (valuesYaml != null && !valuesYaml.isEmpty()) {
293        final hapi.chart.ConfigOuterClass.Config.Builder values = requestBuilder.getValuesBuilder();
294        assert values != null;
295        values.setRaw(valuesYaml);
296      }
297      
298      requestBuilder.setWait(this.getWait());
299      
300      final ReleaseManager releaseManager = releaseManagerCallable.call();
301      if (releaseManager == null) {
302        throw new IllegalStateException("releaseManagerCallable.call() == null");
303      }
304      
305      if (log.isInfoEnabled()) {
306        log.info("Installing release " + requestBuilder.getName());
307      }
308      final Future<InstallReleaseResponse> installReleaseResponseFuture = releaseManager.install(requestBuilder, chartBuilder);
309      assert installReleaseResponseFuture != null;
310      final InstallReleaseResponse installReleaseResponse = installReleaseResponseFuture.get();
311      assert installReleaseResponse != null;
312      if (log.isInfoEnabled()) {
313        final Release release = installReleaseResponse.getRelease();
314        assert release != null;
315        log.info("Installed release " + release.getName());
316      }
317    }
318  }
319
320  /**
321   * Creates and returns an {@link AbstractChartLoader} capable of
322   * loading a Helm chart from a {@link URL}.
323   *
324   * <p>This method never returns {@code null}.</p>
325   *
326   * <p>Overrides of this method must not return {@code null}.</p>
327   *
328   * <p>This implementation returns a new {@link URLChartLoader}.</p>
329   *
330   * @return a new {@link AbstractChartLoader} implementation; never
331   * {@code null}
332   */
333  protected AbstractChartLoader<URL> createChartLoader() {
334    return new URLChartLoader();
335  }
336
337  /**
338   * Returns a {@link URL} identifying a Helm chart that can be read
339   * by the {@link AbstractChartLoader} produced by the {@link
340   * #createChartLoader()} method.
341   *
342   * <p>This method may return {@code null}.</p>
343   *
344   * @return a {@link URL} to a Helm chart, or {@code null}
345   *
346   * @see #setChartUrl(URL)
347   */
348  public URL getChartUrl() {
349    return this.chartUrl;
350  }
351
352  /**
353   * Sets the {@link URL} identifying a Helm chart that can be read
354   * by the {@link AbstractChartLoader} produced by the {@link
355   * #createChartLoader()} method.
356   *
357   * @param chartUrl the {@link URL} identifying a Helm chart that can
358   * be read by the {@link AbstractChartLoader} produced by the {@link
359   * #createChartLoader()} method; may be {@code null}
360   */
361  public void setChartUrl(final URL chartUrl) {
362    this.chartUrl = chartUrl;
363  }
364
365  /**
366   * Returns {@code true} if this {@link InstallReleaseMojo} is
367   * <em>lenient</em>; if {@code true}, a missing or unresolvable
368   * {@link #getChartUrl() chartUrl} parameter will result in
369   * execution being skipped rather than a {@link
370   * MojoExecutionException} being thrown.
371   *
372   * @return {@code true} if this {@link InstallReleaseMojo} is
373   * <em>lenient</em>; {@code false} otherwise
374   */
375  public boolean isLenient() {
376    return this.lenient;
377  }
378
379  /**
380   * Sets whether this {@link InstallReleaseMojo} is <em>lenient</em>;
381   * if {@code true} is supplied, a missing or unresolvable {@link
382   * #getChartUrl() chartUrl} parameter will result in execution being
383   * skipped rather than a {@link MojoExecutionException} being
384   * thrown.
385   *
386   * @param lenient whether this {@link InstallReleaseMojo} is
387   * <em>lenient</em>; if {@code true}, a missing or unresolvable
388   * {@link #getChartUrl() chartUrl} parameter will result in
389   * execution being skipped rather than a {@link
390   * MojoExecutionException} being thrown
391   */
392  public void setLenient(final boolean lenient) {
393    this.lenient = lenient;
394  }
395  
396  /**
397   * Returns the <a
398   * href="https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/">namespace</a>
399   * into which the release will be installed.
400   *
401   * <p>This method may return {@code null}.</p>
402   *
403   * @return the <a
404   * href="https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/">namespace</a>
405   * into which the release will be installed, or {@code null}
406   *
407   * @see #setReleaseNamespace(String)
408   */
409  public String getReleaseNamespace() {
410    return this.releaseNamespace;
411  }
412
413  /**
414   * Sets the <a
415   * href="https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/">namespace</a>
416   * into which the release will be installed.
417   *
418   * @param releaseNamespace the <a
419   * href="https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/">namespace</a>
420   * into which the release will be installed; may be {@code null}
421   *
422   * @see #getReleaseNamespace()
423   */
424  public void setReleaseNamespace(final String releaseNamespace) {
425    this.releaseNamespace = releaseNamespace;
426  }
427
428  /**
429   * Returns {@code true} if the {@linkplain #getReleaseName()
430   * supplied release name} should be reused across installations.
431   *
432   * @return {@code true} if the {@linkplain #getReleaseName()
433   * supplied release name} should be reused across installations;
434   * {@code false} otherwise
435   *
436   * @see #setReuseReleaseName(boolean)
437   */
438  public boolean getReuseReleaseName() {
439    return this.reuseReleaseName;
440  }
441
442  /**
443   * Sets whether the {@linkplain #getReleaseName() supplied release
444   * name} should be reused across installations.
445   *
446   * @param reuseReleaseName whether the {@linkplain #getReleaseName()
447   * supplied release name} should be reused across installations
448   *
449   * @see #getReuseReleaseName()
450   */
451  public void setReuseReleaseName(final boolean reuseReleaseName) {
452    this.reuseReleaseName = reuseReleaseName;
453  }
454
455  /**
456   * Returns a YAML {@link String} representing the values to use to
457   * customize the installation.
458   *
459   * <p>This method may return {@code null}.</p>
460   *
461   * <p>Overrides of this method may return {@code null}.</p>
462   *
463   * @return a YAML {@link String} representing the values to use to
464   * customize the installation, or {@code null}
465   *
466   * @see #setValuesYaml(String)
467   */
468  public String getValuesYaml() {
469    return this.valuesYaml;
470  }
471
472  /**
473   * Installs a YAML {@link String} representing the values to use to
474   * customize the installation.
475   *
476   * @param valuesYaml the YAML {@link String} representing the values to use to
477   * customize the installation; may be {@code null}
478   *
479   * @see #getValuesYaml()
480   */
481  public void setValuesYaml(final String valuesYaml) {
482    this.valuesYaml = valuesYaml;
483  }
484
485  /**
486   * Returns a {@link URI} identifying a YAML document containing the
487   * values to use to customize the installation.
488   *
489   * <p>This method may return {@code null}.</p>
490   *
491   * <p>Overrides of this method may return {@code null}.</p>
492   *
493   * @return a {@link URI} identifying a YAML document containing the
494   * values to use to customize the installation, or {@code null}
495   *
496   * @see #setValuesYamlUri(URI)
497   */
498  public URI getValuesYamlUri() {
499    return this.valuesYamlUri;
500  }
501
502  /**
503   * Sets the {@link URI} identifying a YAML document containing the
504   * values to use to customize the installation.
505   *
506   * @param valuesYamlUri the {@link URI} identifying a YAML document
507   * containing the values to use to customize the installation; may
508   * be {@code null}
509   *
510   * @see #getValuesYamlUri()
511   */
512  public void setValuesYamlUri(final URI valuesYamlUri) {
513    this.valuesYamlUri = valuesYamlUri;
514  }
515  
516  /**
517   * {@inheritDoc}
518   *
519   * <p>This implementation allows the supplied {@code name} to be
520   * {@code null} or {@linkplain String#isEmpty() empty}.</p>
521   */
522  @Override
523  protected void validateReleaseName(final String name) {
524    if (name != null && !name.isEmpty()) {
525      super.validateReleaseName(name);
526    }
527  }
528 
529}