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}