001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 002 * 003 * Copyright © 2017-2018 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.IOException; 020 021import java.util.Map; 022import java.util.Objects; 023 024import java.util.concurrent.Callable; 025 026import java.util.regex.Matcher; 027 028import io.fabric8.kubernetes.client.Config; 029import io.fabric8.kubernetes.client.DefaultKubernetesClient; 030 031import org.apache.maven.plugin.MojoExecutionException; 032import org.apache.maven.plugin.MojoFailureException; 033 034import org.apache.maven.plugin.logging.Log; 035 036import org.apache.maven.plugins.annotations.Parameter; 037 038import org.microbean.helm.ReleaseManager; 039import org.microbean.helm.Tiller; 040 041/** 042 * An {@link AbstractHelmMojo} that provides other <a 043 * href="https://microbean.github.io/microbean-helm/">Helm</a>-related 044 * <a 045 * href="https://maven.apache.org/developers/mojo-api-specification.html">mojo</a> 046 * implementations the ability to work with a {@link ReleaseManager}. 047 * 048 * @author <a href="https://about.me/lairdnelson" 049 * target="_parent">Laird Nelson</a> 050 * 051 * @see #execute(Callable) 052 */ 053public abstract class AbstractReleaseMojo extends AbstractHelmMojo { 054 055 056 /* 057 * Instance fields. 058 */ 059 060 061 /** 062 * Whether to skip execution. 063 */ 064 @Parameter(defaultValue = "false", property = "helm.skip") 065 private boolean skip; 066 067 /** 068 * The <a 069 * href="https://static.javadoc.io/io.fabric8/kubernetes-client/3.1.8/io/fabric8/kubernetes/client/Config.html">{@code 070 * Config}</a> describing how a <a 071 * href="https://static.javadoc.io/io.fabric8/kubernetes-client/3.1.8/io/fabric8/kubernetes/client/DefaultKubernetesClient.html">{@code 072 * DefaultKubernetesClient}</a> should connect to a Kubernetes 073 * cluster. 074 */ 075 @Parameter 076 private Config clientConfiguration; 077 078 /** 079 * The Kubernetes cluster namespace in which Tiller may be found. 080 */ 081 @Parameter(defaultValue = "kube-system", property = "tiller.namespace") 082 private String tillerNamespace; 083 084 /** 085 * The port on which Tiller may be reached. 086 */ 087 @Parameter(defaultValue = "44134", property = "tiller.port") 088 private int tillerPort; 089 090 /** 091 * The Kubernetes labels normally found on Tiller pods. 092 */ 093 @Parameter(property = "tiller.labels") 094 private Map<String, String> tillerLabels; 095 096 097 /* 098 * Constructors. 099 */ 100 101 102 /** 103 * Creates a new {@link AbstractReleaseMojo}. 104 */ 105 protected AbstractReleaseMojo() { 106 super(); 107 } 108 109 110 /* 111 * Public instance methods. 112 */ 113 114 115 /** 116 * {@linkplain #getSkip() Skips} execution if instructed, or calls 117 * the {@link #execute(Callable)} method with a {@link Callable} 118 * containing the results of an invocation of the {@link 119 * #createReleaseManager(Tiller)} method. 120 * 121 * @exception MojoExecutionException if there was a problem 122 * executing this mojo 123 * 124 * @exception MojoFailureException if the mojo executed properly, 125 * but the job it was to perform failed in some way 126 */ 127 @Override 128 public void execute() throws MojoExecutionException, MojoFailureException { 129 final Log log = this.getLog(); 130 assert log != null; 131 132 if (this.getSkip()) { 133 if (log.isDebugEnabled()) { 134 log.debug("Skipping execution by request."); 135 } 136 return; 137 } 138 139 final ReleaseManagerCallable releaseManagerCallable = new ReleaseManagerCallable(); 140 Throwable throwable = null; 141 try { 142 this.execute(releaseManagerCallable); 143 } catch (final InterruptedException interruptedException) { 144 Thread.currentThread().interrupt(); 145 final MojoExecutionException mojoExecutionException = new MojoExecutionException(interruptedException.getMessage(), interruptedException); 146 throwable = mojoExecutionException; 147 throw mojoExecutionException; 148 } catch (final RuntimeException | MojoExecutionException | MojoFailureException throwMe) { 149 throwable = throwMe; 150 throw throwMe; 151 } catch (final Exception otherStuff) { 152 final MojoExecutionException mojoExecutionException = new MojoExecutionException(otherStuff.getMessage(), otherStuff); 153 throwable = mojoExecutionException; 154 throw mojoExecutionException; 155 } finally { 156 if (releaseManagerCallable.releaseManager != null) { 157 try { 158 releaseManagerCallable.releaseManager.close(); 159 } catch (final IOException ioException) { 160 if (throwable != null) { 161 throwable.addSuppressed(ioException); 162 } else { 163 throw new MojoExecutionException(ioException.getMessage(), ioException); 164 } 165 } 166 } 167 } 168 } 169 170 /** 171 * Returns the {@link Config} describing how a {@link 172 * DefaultKubernetesClient} is to connect to a Kubernetes cluster. 173 * 174 * <p>This method may return {@code null}.</p> 175 * 176 * <p>Overrides of this method may return {@code null}.</p> 177 * 178 * @return a {@link Config}, or {@code null} 179 * 180 * @see #setClientConfiguration(Config) 181 */ 182 public Config getClientConfiguration() { 183 return this.clientConfiguration; 184 } 185 186 /** 187 * Installs the {@link Config} describing how a {@link 188 * DefaultKubernetesClient} is to connect to a Kubernetes cluster. 189 * 190 * @param config the {@link Config} to use; may be {@code null} 191 * 192 * @see #getClientConfiguration() 193 */ 194 public void setClientConfiguration(final Config config) { 195 this.clientConfiguration = config; 196 } 197 198 /** 199 * Returns {@code true} if this {@link AbstractReleaseMojo} should 200 * not execute. 201 * 202 * @return {@code true} if this {@link AbstractReleaseMojo} should 203 * not execute; {@code false} otherwise 204 * 205 * @see #setSkip(boolean) 206 */ 207 public boolean getSkip() { 208 return this.skip; 209 } 210 211 /** 212 * Controls whether this {@link AbstractReleaseMojo} should execute. 213 * 214 * @param skip if {@code true}, this {@link AbstractReleaseMojo} 215 * will not execute 216 * 217 * @see #getSkip() 218 */ 219 public void setSkip(final boolean skip) { 220 this.skip = skip; 221 } 222 223 /** 224 * Returns the Kubernetes namespace in which Tiller may be found. 225 * 226 * <p>This method may return {@code null}.</p> 227 * 228 * @return the Kubernetes namespace in which Tiller may be found, or 229 * {@code null} 230 */ 231 public String getTillerNamespace() { 232 return this.tillerNamespace; 233 } 234 235 /** 236 * Sets the Kubernetes namespace in which Tiller may be found. 237 * 238 * @param tillerNamespace the Kubernetes namespace in which Tiller 239 * may be found; may be {@code null} in which case {@code 240 * kube-system} will be used by the {@link 241 * #createTiller(DefaultKubernetesClient)} method instead 242 */ 243 public void setTillerNamespace(String tillerNamespace) { 244 this.tillerNamespace = tillerNamespace; 245 } 246 247 /** 248 * Returns the port on which Tiller may be found. 249 * 250 * @return the port on which Tiller may be found; normally {@code 251 * 44134} 252 */ 253 public int getTillerPort() { 254 return this.tillerPort; 255 } 256 257 /** 258 * Sets the port on which Tiller may be found. 259 * 260 * @param tillerPort the port on which Tiller may be found; normally 261 * {@code 44134} 262 */ 263 public void setTillerPort(final int tillerPort) { 264 this.tillerPort = tillerPort; 265 } 266 267 /** 268 * Returns the Kubernetes labels that Tiller Pods have. 269 * 270 * <p>This method may return {@code null}.</p> 271 * 272 * @return the Kubernetes labels that Tiller Pods have, or {@code 273 * null} 274 */ 275 public Map<String, String> getTillerLabels() { 276 return this.tillerLabels; 277 } 278 279 /** 280 * Sets the Kubernetes labels that Tiller Pods have. 281 * 282 * <p>Tiller Pods are normally labeled with {@code app = helm} and 283 * {@code name = tiller}.</p> 284 * 285 * @param tillerLabels a {@link Map} containing the labels; may be 286 * {@code null} 287 */ 288 public void setTillerLabels(final Map<String, String> tillerLabels) { 289 this.tillerLabels = tillerLabels; 290 } 291 292 293 /* 294 * Protected instance methods. 295 */ 296 297 298 /** 299 * Performs a release-oriented task using a {@link ReleaseManager} 300 * {@linkplain Callable#call() available} from the supplied {@link 301 * Callable}. 302 * 303 * @param releaseManagerCallable the {@link Callable} that will 304 * provide a {@link ReleaseManager}; must not be {@code null} 305 * 306 * @exception Exception if an error occurs 307 */ 308 protected abstract void execute(final Callable<ReleaseManager> releaseManagerCallable) throws Exception; 309 310 /** 311 * Creates a {@link DefaultKubernetesClient} for communicating with 312 * Kubernetes clusters. 313 * 314 * <p>This method never returns {@code null}.</p> 315 * 316 * <p>Overrides of this method must not return {@code null}.</p> 317 * 318 * <p>The default implementation calls the {@link 319 * #getClientConfiguration()} method and {@linkplain 320 * DefaultKubernetesClient#DefaultKubernetesClient(Config) uses its 321 * return value}, unless it is {@code null}, in which case a new 322 * {@link DefaultKubernetesClient} is created via its {@linkplain 323 * DefaultKubernetesClient#DefaultKubernetesClient() no-argument 324 * constructor}.</p> 325 * 326 * @return a new, non-{@code null} {@link DefaultKubernetesClient} 327 * 328 * @exception IOException if there was a problem creating the client 329 */ 330 protected DefaultKubernetesClient createClient() throws IOException { 331 final DefaultKubernetesClient client; 332 final Config config = this.getClientConfiguration(); 333 if (config == null) { 334 client = new DefaultKubernetesClient(); 335 } else { 336 client = new DefaultKubernetesClient(config); 337 } 338 return client; 339 } 340 341 /** 342 * Creates a {@link Tiller} and returns it. 343 * 344 * <p>This method never returns {@code null}.</p> 345 * 346 * <p>Overrides of this method must not return {@code null}.</p> 347 * 348 * <p>This implementation passes the supplied {@link 349 * DefaultKubernetesClient} to the <a 350 * href="https://microbean.github.io/microbean-helm/apidocs/org/microbean/helm/Tiller.html#Tiller-T-">appropriate 351 * <code>Tiller</code> constructor</a>.</p> 352 * 353 * @param client the {@link DefaultKubernetesClient} to use to 354 * communicate with a Kubernetes cluster; must not be {@code null} 355 * 356 * @return a new {@link Tiller}; never {@code null} 357 * 358 * @exception NullPointerException if {@code client} is {@code null} 359 * 360 * @exception IOException if there was a problem creating a {@link 361 * Tiller} 362 */ 363 protected Tiller createTiller(final DefaultKubernetesClient client) throws IOException { 364 Objects.requireNonNull(client); 365 return new Tiller(client, this.getTillerNamespace(), this.getTillerPort(), this.getTillerLabels()); 366 } 367 368 /** 369 * Creates a {@link ReleaseManager} and returns it. 370 * 371 * <p>This method never returns {@code null}.</p> 372 * 373 * <p>Overrides of this method must not return {@code null}.</p> 374 * 375 * <p>This implementation passes the supplied {@link 376 * Tiller} to the {@linkplain ReleaseManager#ReleaseManager(Tiller) 377 * appropriate <code>ReleaseManager</code> constructor}.</p> 378 * 379 * @param tiller the {@link Tiller} to use to communicate with a 380 * Tiller server; must not be {@code null} 381 * 382 * @return a new {@link ReleaseManager}; never {@code null} 383 * 384 * @exception NullPointerException if {@code tiller} is {@code null} 385 * 386 * @exception IOException if there was a problem creating a {@link 387 * ReleaseManager} 388 */ 389 protected ReleaseManager createReleaseManager(final Tiller tiller) throws IOException { 390 Objects.requireNonNull(tiller); 391 return new ReleaseManager(tiller); 392 } 393 394 /** 395 * Validates a <a 396 * href="https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/">Kubernetes 397 * namespace</a> for correctness. 398 * 399 * <p>The default implementation checks the supplied {@code 400 * namespace} to see if it is less than or equal to {@value 401 * ReleaseManager#DNS_LABEL_MAX_LENGTH} characters, and if it 402 * {@linkplain Matcher#matches() matches} the value of the {@link 403 * ReleaseManager#DNS_LABEL_PATTERN} field.</p> 404 * 405 * @param namespace the <a 406 * href="https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/">namespace</a> 407 * to validate; may be {@code null} 408 * 409 * @exception IllegalArgumentException if {@code namespace} is 410 * invalid 411 */ 412 protected void validateNamespace(final String namespace) { 413 if (namespace != null) { 414 final int namespaceLength = namespace.length(); 415 if (namespaceLength > ReleaseManager.DNS_LABEL_MAX_LENGTH) { 416 throw new IllegalArgumentException("Invalid namespace: " + namespace + "; length is greater than " + ReleaseManager.DNS_LABEL_MAX_LENGTH + " characters: " + namespaceLength); 417 } else if (namespaceLength > 0) { 418 final Matcher matcher = ReleaseManager.DNS_LABEL_PATTERN.matcher(namespace); 419 assert matcher != null; 420 if (!matcher.matches()) { 421 throw new IllegalArgumentException("Invalid namespace: " + namespace + "; must match " + ReleaseManager.DNS_LABEL_PATTERN.toString()); 422 } 423 } 424 } 425 } 426 427 428 /* 429 * Inner and nested classes. 430 */ 431 432 433 /** 434 * A {@link Callable} whose {@link #call()} method yields the same 435 * {@link ReleaseManager} for every invocation, {@linkplain 436 * AbstractReleaseMojo#createReleaseManager(Tiller) creating one} if 437 * necessary. 438 * 439 * @author <a href="https://about.me/lairdnelson" 440 * target="_parent">Laird Nelson</a> 441 * 442 * @see AbstractReleaseMojo#createReleaseManager(Tiller) 443 * 444 * @see AbstractReleaseMojo#createTiller(DefaultKubernetesClient) 445 * 446 * @see AbstractReleaseMojo#createClient() 447 */ 448 private final class ReleaseManagerCallable implements Callable<ReleaseManager> { 449 450 451 /* 452 * Instance fields. 453 */ 454 455 456 /** 457 * The {@link ReleaseManager} to return from the {@link #call()} 458 * method. 459 * 460 * <p>This field may be {@code null}.</p> 461 * 462 * @sed #call() 463 */ 464 private ReleaseManager releaseManager; 465 466 467 /* 468 * Constructors. 469 */ 470 471 472 /** 473 * Creates a new {@link ReleaseManagerCallable}. 474 */ 475 private ReleaseManagerCallable() { 476 super(); 477 } 478 479 480 /* 481 * Instance methods. 482 */ 483 484 485 /** 486 * Returns a {@link ReleaseManager}, {@linkplain 487 * AbstractReleaseMojo#createReleaseManager(Tiller) creating one} 488 * if necessary. 489 * 490 * <p>This method never returns {@code null}.</p> 491 * 492 * @return a {@link ReleaseManager}; never {@code null} 493 * 494 * @exception IOException if there was a problem creating a {@link 495 * ReleaseManager} 496 */ 497 @Override 498 public final ReleaseManager call() throws IOException { 499 if (this.releaseManager == null) { 500 this.releaseManager = createReleaseManager(createTiller(createClient())); 501 } 502 return this.releaseManager; 503 } 504 505 } 506 507}