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}