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}