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 javax.inject.Inject;
039
040import hapi.chart.ChartOuterClass.Chart;
041
042import hapi.release.ReleaseOuterClass.Release;
043
044import hapi.services.tiller.Tiller.UpdateReleaseRequest;
045import hapi.services.tiller.Tiller.UpdateReleaseResponse;
046
047import org.apache.maven.execution.MavenSession;
048
049import org.apache.maven.plugin.MojoExecutionException;
050
051import org.apache.maven.plugin.logging.Log;
052
053import org.apache.maven.plugins.annotations.Mojo;
054import org.apache.maven.plugins.annotations.Parameter;
055
056import org.apache.maven.project.MavenProject;
057
058import org.microbean.helm.ReleaseManager;
059
060import org.microbean.helm.chart.AbstractChartLoader;
061import org.microbean.helm.chart.URLChartLoader;
062
063/**
064 * <a
065 * href="https://docs.helm.sh/using_helm/#helm-upgrade-and-helm-rollback-upgrading-a-release-and-recovering-on-failure">Updates
066 * a release</a> with a new chart.
067 *
068 * @author <a href="https://about.me/lairdnelson"
069 * target="_parent">Laird Nelson</a>
070 */
071@Mojo(name = "update")
072public class UpdateReleaseMojo extends AbstractForceableMutatingReleaseMojo {
073
074
075  /*
076   * Instance fields.
077   */
078
079
080  /**
081   * The {@link MavenProject} in effect.
082   *
083   * <p>This field is never {@code null}.</p>
084   *
085   * @see #UpdateReleaseMojo(MavenProject, MavenSession)
086   */
087  private final MavenProject project;
088
089  /**
090   * The {@link MavenSession} in effect.
091   *
092   * <p>This field is never {@code null}.</p>
093   *
094   * @see #UpdateReleaseMojo(MavenProject, MavenSession)
095   */
096  private final MavenSession session;
097
098  /**
099   * A {@link URL} to the new chart to update the release with.  If
100   * omitted,
101   * <code>file:/${project.build.directory}/generated-sources/helm/charts/${project.artifactId}</code>
102   * will be used instead.
103   */
104  @Parameter(property = "helm.update.chartUrl")
105  private URL chartUrl;
106
107  /**
108   * Whether values should be reset to the values built in to the
109   * chart.  Ignored if {@codde reuseValues} is {@code true}.
110   */
111  @Parameter(defaultValue = "false", property = "helm.update.resetValues")
112  private boolean resetValues;
113
114  /**
115   * Whether values should be reused from the prior release, merged
116   * together with any additional values specified in the {@code
117   * valuesYaml} parameter.  Ignored if {@code resetValues} is {@code
118   * true}.
119   */
120  @Parameter(property = "helm.update.reuseValues")
121  private boolean reuseValues;
122
123  /**
124   * New values in YAML format to use when updating the release.  May
125   * be combined with the effects of the {@code resetValues} and
126   * {@code reuseValues} parameters.
127   *
128   * If this parameter and the {@code valuesYamlUri} parameter are
129   * both specified, this parameter is preferred if its value is
130   * non-{@code null} and non-empty.
131   */
132  @Parameter(property = "helm.update.valuesYaml")
133  private String valuesYaml;
134
135  /**
136   * A URI identifying a document containing YAML-formatted values to
137   * supply at the time of updating the release.
138   *
139   * If this parameter and the {@code valuesYaml} parameter are both
140   * specified, this parameter is ignored if the value of the {@code
141   * valuesYaml} parameter is non-{@code null} and non-empty.
142   */
143  @Parameter(property = "helm.update.valuesYamlUri")
144  private URI valuesYamlUri;
145  
146  
147  /*
148   * Constructors.
149   */
150  
151
152  /**
153   * Creates a new {@link UpdateReleaseMojo}.
154   *
155   * @param project the {@link MavenProject} in effect; must not be
156   * {@code null}
157   *
158   * @param session the {@link MavenSession} in effect; must not be
159   * {@code null}
160   *
161   * @exception NullPointerException if either {@code project} or
162   * {@code session} is {@code null}
163   */
164  @Inject
165  public UpdateReleaseMojo(final MavenProject project, final MavenSession session) {
166    super();
167    Objects.requireNonNull(project);
168    Objects.requireNonNull(session);
169    this.project = project;
170    this.session = session;
171  }
172
173
174  /*
175   * Instance methods.
176   */
177
178
179  /**
180   * {@inheritDoc}
181   *
182   * <p>This implementation <a
183   * href="https://docs.helm.sh/using_helm/#helm-upgrade-and-helm-rollback-upgrading-a-release-and-recovering-on-failure">updates</a>
184   * the release named by the {@linkplain #getReleaseName() supplied
185   * release name} with a new <a
186   * href="https://docs.helm.sh/developing_charts/#charts">chart</a>
187   * residing at the {@linkplain #getChartUrl() indicated URL}.</p>
188   */
189  @Override
190  protected void execute(final Callable<ReleaseManager> releaseManagerCallable) throws Exception {
191    Objects.requireNonNull(releaseManagerCallable);
192    final Log log = this.getLog();
193    assert log != null;
194
195    URL chartUrl = this.getChartUrl();
196    if (chartUrl == null) {
197      final Path chartDirectoryPath = Paths.get(new StringBuilder(this.project.getBuild().getDirectory()).append("/generated-sources/helm/charts/").append(this.project.getArtifactId()).toString());
198      assert chartDirectoryPath != null;
199      chartUrl = chartDirectoryPath.toUri().toURL();
200      if (!Files.isDirectory(chartDirectoryPath)) {
201        throw new MojoExecutionException("Non-existent chartUrl: " + chartUrl);
202      }
203    }
204    assert chartUrl != null;
205    if (log.isDebugEnabled()) {
206      log.debug("chartUrl: " + chartUrl);
207    }
208
209    Chart.Builder chartBuilder = null;
210    try (final AbstractChartLoader<URL> chartLoader = this.createChartLoader()) {
211      if (chartLoader == null) {
212        throw new IllegalStateException("createChartLoader() == null");
213      }
214      if (log.isDebugEnabled()) {
215        log.debug("chartLoader: " + chartLoader);
216        log.debug("Loading Helm chart from " + chartUrl);
217      }
218      chartBuilder = chartLoader.load(chartUrl);
219    }
220
221    if (chartBuilder == null) {
222      throw new IllegalStateException("chartBuilder.load(\"" + chartUrl + "\") == null");
223    }
224
225    if (log.isInfoEnabled()) {
226      log.info("Loaded Helm chart from " + chartUrl);
227    }
228
229    final UpdateReleaseRequest.Builder requestBuilder = UpdateReleaseRequest.newBuilder();
230    assert requestBuilder != null;
231
232    requestBuilder.setDisableHooks(this.getDisableHooks());
233    requestBuilder.setDryRun(this.getDryRun());
234    requestBuilder.setForce(this.getForce());
235    requestBuilder.setRecreate(this.getRecreate());
236
237    final String releaseName = this.getReleaseName();
238    if (releaseName != null) {
239      requestBuilder.setName(releaseName);
240    }
241
242    requestBuilder.setResetValues(this.getResetValues());
243    requestBuilder.setReuseValues(this.getReuseValues());
244    requestBuilder.setTimeout(this.getTimeout());
245
246    String valuesYaml = this.getValuesYaml();
247    if (valuesYaml == null || valuesYaml.isEmpty()) {
248      final URI valuesYamlUri = this.getValuesYamlUri();
249      if (valuesYamlUri != null) {
250        final URL yamlUrl = valuesYamlUri.toURL();
251        assert yamlUrl != null;
252        try (final Reader reader = new BufferedReader(new InputStreamReader(yamlUrl.openStream(), StandardCharsets.UTF_8))) {
253          final StringBuilder sb = new StringBuilder();
254          final char[] buffer = new char[4096];
255          int charsRead = -1;
256          while ((charsRead = reader.read(buffer, 0, buffer.length)) >= 0) {
257            sb.append(buffer, 0, charsRead);
258          }
259          valuesYaml = sb.toString();
260        }
261      }
262    }
263    if (valuesYaml != null && !valuesYaml.isEmpty()) {
264      final hapi.chart.ConfigOuterClass.Config.Builder values = requestBuilder.getValuesBuilder();
265      assert values != null;
266      values.setRaw(valuesYaml);
267    }
268    
269    requestBuilder.setWait(this.getWait());
270
271    final ReleaseManager releaseManager = releaseManagerCallable.call();
272    if (releaseManager == null) {
273      throw new IllegalStateException("releaseManagerCallable.call() == null");
274    }
275
276    if (log.isInfoEnabled()) {
277      log.info("Updating release " + requestBuilder.getName());
278    }
279    final Future<UpdateReleaseResponse> updateReleaseResponseFuture = releaseManager.update(requestBuilder, chartBuilder);
280    assert updateReleaseResponseFuture != null;
281    final UpdateReleaseResponse updateReleaseResponse = updateReleaseResponseFuture.get();
282    assert updateReleaseResponse != null;
283    if (log.isInfoEnabled()) {
284      final Release release = updateReleaseResponse.getRelease();
285      assert release != null;
286      log.info("Updated release " + release.getName());
287    }
288    
289  }
290
291  /**
292   * Returns a {@link URL} identifying a Helm chart that can be read
293   * by the {@link AbstractChartLoader} produced by the {@link
294   * #createChartLoader()} method.
295   *
296   * <p>This method may return {@code null}.</p>
297   *
298   * @return a {@link URL} to a Helm chart, or {@code null}
299   *
300   * @see #setChartUrl(URL)
301   */
302  public URL getChartUrl() {
303    return this.chartUrl;
304  }
305  
306  /**
307   * Sets the {@link URL} identifying a Helm chart that can be read
308   * by the {@link AbstractChartLoader} produced by the {@link
309   * #createChartLoader()} method.
310   *
311   * @param chartUrl the {@link URL} identifying a Helm chart that can
312   * be read by the {@link AbstractChartLoader} produced by the {@link
313   * #createChartLoader()} method; may be {@code null}
314   */
315  public void setChartUrl(final URL chartUrl) {
316    this.chartUrl = chartUrl;
317  }
318
319  /**
320   * Returns {@code true} if, during the update, values should be
321   * reset to the values built in to the {@linkplain #getChartUrl()
322   * new chart}.
323   *
324   * @return {@code true} if, during the update, values should be
325   * reset to the values built in to the {@linkplain #getChartUrl()
326   * new chart}; {@code false} otherwise
327   *
328   * @see #setResetValues(boolean)
329   */
330  public boolean getResetValues() {
331    return this.resetValues;
332  }
333
334  /**
335   * Sets whether, during the update, values should be
336   * reset to the values built in to the {@linkplain #getChartUrl()
337   * new chart}.
338   *
339   * @param resetValues if {@code true}, during the update values will
340   * be reset to the values built in to the {@linkplain #getChartUrl()
341   * new chart}
342   *
343   * @see #getResetValues()
344   */
345  public void setResetValues(final boolean resetValues) {
346    this.resetValues = resetValues;
347  }
348
349  /**
350   * Returns {@code true} if, during the update, any new values should
351   * be merged with those present in the prior version of the release
352   * being updated.
353   *
354   * @return {@code true} if, during the update, any new values should
355   * be merged with those present in the prior version of the release
356   * being updated; {@code false} otherwise
357   *
358   * @see #setReuseValues(boolean)
359   */
360  public boolean getReuseValues() {
361    return this.reuseValues;
362  }
363
364  /**
365   * Sets whether, during the update, any new values should
366   * be merged with those present in the prior version of the release
367   * being updated.
368   *
369   * @param reuseValues if {@code true} during the update any new
370   * values will be merged with those present in the prior version of
371   * the release being updated
372   *
373   * @see #getReuseValues()
374   */
375  public void setReuseValues(final boolean reuseValues) {
376    this.reuseValues = reuseValues;
377  }
378
379  /**
380   * Returns a YAML {@link String} representing the values to use to
381   * customize the update.
382   *
383   * <p>This method may return {@code null}.</p>
384   *
385   * <p>Overrides of this method may return {@code null}.</p>
386   *
387   * @return a YAML {@link String} representing the values to use to
388   * customize the update, or {@code null}
389   *
390   * @see #setValuesYaml(String)
391   */
392  public String getValuesYaml() {
393    return this.valuesYaml;
394  }
395
396  /**
397   * Installs a YAML {@link String} representing the values to use to
398   * customize the update.
399   *
400   * @param valuesYaml the YAML {@link String} representing the values to use to
401   * customize the update; may be {@code null}
402   *
403   * @see #getValuesYaml()
404   */
405  public void setValuesYaml(final String valuesYaml) {
406    this.valuesYaml = valuesYaml;
407  }
408
409  /**
410   * Returns a {@link URI} identifying a YAML document containing the
411   * values to use to customize the installation.
412   *
413   * <p>This method may return {@code null}.</p>
414   *
415   * <p>Overrides of this method may return {@code null}.</p>
416   *
417   * @return a {@link URI} identifying a YAML document containing the
418   * values to use to customize the installation, or {@code null}
419   *
420   * @see #setValuesYamlUri(URI)
421   */
422  public URI getValuesYamlUri() {
423    return this.valuesYamlUri;
424  }
425
426  /**
427   * Sets the {@link URI} identifying a YAML document containing the
428   * values to use to customize the installation.
429   *
430   * @param valuesYamlUri the {@link URI} identifying a YAML document
431   * containing the values to use to customize the installation; may
432   * be {@code null}
433   *
434   * @see #getValuesYamlUri()
435   */
436  public void setValuesYamlUri(final URI valuesYamlUri) {
437    this.valuesYamlUri = valuesYamlUri;
438  }
439
440  /**
441   * Creates and returns an {@link AbstractChartLoader} capable of
442   * loading a Helm chart from a {@link URL}.
443   *
444   * <p>This method never returns {@code null}.</p>
445   *
446   * <p>Overrides of this method must not return {@code null}.</p>
447   *
448   * <p>This implementation returns a new {@link URLChartLoader}.</p>
449   *
450   * @return a new {@link AbstractChartLoader} implementation; never
451   * {@code null}
452   */
453  protected AbstractChartLoader<URL> createChartLoader() {
454    return new URLChartLoader();
455  }
456
457}