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.util.ArrayList;
020import java.util.Collection;
021import java.util.Iterator;
022import java.util.List;
023import java.util.Objects;
024
025import java.util.concurrent.Callable;
026
027import hapi.release.TestRunOuterClass.TestRun;
028
029import hapi.services.tiller.Tiller.TestReleaseRequest;
030import hapi.services.tiller.Tiller.TestReleaseResponse;
031
032import org.apache.maven.plugin.MojoFailureException;
033
034import org.apache.maven.plugin.logging.Log;
035
036import org.apache.maven.plugins.annotations.Mojo;
037import org.apache.maven.plugins.annotations.Parameter;
038
039import org.microbean.helm.ReleaseManager;
040
041/**
042 * Runs tests against a Helm release.
043 *
044 * @author <a href="https://about.me/lairdnelson"
045 * target="_parent">Laird Nelson</a>
046 */
047@Mojo(name = "test")
048public class TestReleaseMojo extends AbstractSingleReleaseMojo {
049
050
051  /*
052   * Instance fields.
053   */
054  
055
056  /**
057   * The timeout, in seconds, to use for Kubernetes operations; set to
058   * {@code 300} by default for parity with the {@code helm} command
059   * line program.
060   */
061  @Parameter(defaultValue = "300")
062  private long timeout; // in seconds
063
064  /**
065   * Whether test Pods should be deleted after the test completes.
066   */
067  @Parameter
068  private boolean cleanup;
069
070  /**
071   * A {@link List} of <a
072   * href="apidocs/org/microbean/helm/maven/ReleaseTestListener.html">{@code
073   * ReleaseTestListener}</a>s that will be notified of the test
074   * results.
075   */
076  @Parameter(alias = "releaseTestListenersList")
077  private List<ReleaseTestListener> releaseTestListeners;
078  
079
080  /*
081   * Constructors.
082   */
083  
084
085  /**
086   * Creates a new {@link TestReleaseMojo}.
087   */
088  public TestReleaseMojo() {
089    super();
090  }
091
092
093  /*
094   * Protected instance methods.
095   */
096  
097
098  /**
099   * {@inheritDoc}
100   *
101   * <p>This implementation {@linkplain
102   * ReleaseManager#test(hapi.services.tiller.Tiller.TestReleaseRequest)
103   * runs tests against a release}.</p>
104   */
105  @Override
106  protected void execute(final Callable<ReleaseManager> releaseManagerCallable) throws Exception {
107    Objects.requireNonNull(releaseManagerCallable);
108    final Log log = this.getLog();
109    assert log != null;
110
111    final TestReleaseRequest.Builder requestBuilder = TestReleaseRequest.newBuilder();
112    assert requestBuilder != null;
113
114    requestBuilder.setCleanup(this.getCleanup());
115
116    final String releaseName = this.getReleaseName();
117    if (releaseName != null) {
118      requestBuilder.setName(releaseName);
119    }
120
121    requestBuilder.setTimeout(this.getTimeout());
122    
123    final ReleaseManager releaseManager = releaseManagerCallable.call();
124    if (releaseManager == null) {
125      throw new IllegalStateException("releaseManagerCallable.call() == null");
126    }
127
128    if (log.isInfoEnabled()) {
129      log.info("Testing release " + releaseName);
130    }
131    
132    final Iterator<TestReleaseResponse> testReleaseResponses = releaseManager.test(requestBuilder.build());
133    assert testReleaseResponses != null;
134    if (testReleaseResponses.hasNext()) {
135      final List<ReleaseTestListener> listeners = this.getReleaseTestListenersList();
136      while (testReleaseResponses.hasNext()) {
137        final TestReleaseResponse response = testReleaseResponses.next();
138        assert response != null;
139        if (listeners != null && !listeners.isEmpty()) {
140          final ReleaseTestEvent event = new ReleaseTestEvent(this, response);
141          for (final ReleaseTestListener listener : listeners) {
142            if (listener != null) {
143              listener.releaseTested(event);
144            }
145          }
146        }
147        final TestRun.Status status = response.getStatus();
148        if (TestRun.Status.FAILURE.equals(status)) {
149          throw new MojoFailureException(response.getMsg());
150        }
151      }
152    }
153    
154  }
155
156
157  /*
158   * Public instance methods.
159   */
160
161  /**
162   * Returns {@code true} if test Pods should be deleted after the
163   * tests complete.
164   *
165   * @return {@code true} if test Pods should be deleted after the
166   * tests complete; {@code false} otherwise
167   *
168   * @see #setCleanup(boolean)
169   */  
170  public boolean getCleanup() {
171    return this.cleanup;
172  }
173
174  /**
175   * Sets whether test Pods should be deleted after the
176   * tests complete.
177   *
178   * @param cleanup if {@code true}, test Pods will be deleted after
179   * the tests complete
180   *
181   * @see #getCleanup()
182   */
183  public void setCleanup(final boolean cleanup) {
184    this.cleanup = cleanup;
185  }
186
187  /**
188   * Returns the timeout value, in seconds, for Kubernetes operations.
189   *
190   * @return the timeout value, in seconds, for Kubernetes operations
191   *
192   * @see #setTimeout(long)
193   */
194  public long getTimeout() {
195    return this.timeout;
196  }
197
198  /**
199   * Sets the timeout value, in seconds, for Kubernetes operations.
200   *
201   * @param timeout the timeout value, in seconds, for Kubernetes
202   * operations
203   *
204   * @see #getTimeout()
205   */
206  public void setTimeout(final long timeout) {
207    this.timeout = timeout;
208  }
209
210  /**
211   * Adds a {@link ReleaseTestListener} that will be {@linkplain
212   * ReleaseTestListener#releaseTested(ReleaseTestEvent) notified when
213   * the tests complete}.
214   *
215   * @param listener the {@link ReleaseTestListener} to add; may be
216   * {@code null} in which case no action will be taken
217   *
218   * @see #removeReleaseTestListener(ReleaseTestListener)
219   *
220   * @see #getReleaseTestListenersList()
221   */
222  public void addReleaseTestListener(final ReleaseTestListener listener) {
223    if (listener != null) {
224      if (this.releaseTestListeners == null) {
225        this.releaseTestListeners = new ArrayList<>();      
226      }
227      this.releaseTestListeners.add(listener);
228    }
229  }
230
231  /**
232   * Removes a {@link ReleaseTestListener} from this {@link
233   * TestReleaseMojo}.
234   *
235   * @param listener the {@link ReleaseTestListener} to remove; may be
236   * {@code null} in which case no action will be taken
237   *
238   * @see #addReleaseTestListener(ReleaseTestListener)
239   *
240   * @see #getReleaseTestListenersList()
241   */
242  public void removeReleaseTestListener(final ReleaseTestListener listener) {
243    if (listener != null && this.releaseTestListeners != null) {
244      this.releaseTestListeners.remove(listener);
245    }
246  }
247
248  /**
249   * Invokes the {@link #getReleaseTestListenersList()} method and
250   * {@linkplain Collection#toArray(Object[]) converts its return
251   * value to an array}.
252   *
253   * <p>This method never returns {@code null}.</p>
254   *
255   * <p>Overrides of this method must not return {@code null}.</p>
256   *
257   * @return a non-{@code null} array of {@link ReleaseTestListener}s
258   *
259   * @see #getReleaseTestListenersList()
260   */
261  public ReleaseTestListener[] getReleaseTestListeners() {
262    final Collection<ReleaseTestListener> listeners = this.getReleaseTestListenersList();
263    if (listeners == null || listeners.isEmpty()) {
264      return new ReleaseTestListener[0];
265    } else {
266      return listeners.toArray(new ReleaseTestListener[listeners.size()]);
267    }
268  }
269
270  /**
271   * Returns the {@link List} of {@link ReleaseTestListener}s whose
272   * elements will be {@linkplain
273   * ReleaseTestListener#releaseTested(ReleaseTestEvent) notified when
274   * the tests complete}.
275   *
276   * <p>This method may return {@code null}.</p>
277   *
278   * <p>Overrides of this method are permitted to return {@code null}.</p>
279   *
280   * @return a {@link List} of {@link ReleaseTestListener}s, or {@code null}
281   *
282   * @see #setReleaseTestListenersList(List)
283   *
284   * @see #addReleaseTestListener(ReleaseTestListener)
285   *
286   * @see #removeReleaseTestListener(ReleaseTestListener)
287   */
288  public List<ReleaseTestListener> getReleaseTestListenersList() {
289    return this.releaseTestListeners;
290  }
291
292  /**
293   * Installs the {@link List} of {@link ReleaseTestListener}s whose
294   * elements will be {@linkplain
295   * ReleaseTestListener#releaseTested(ReleaseTestEvent) notified when
296   * the tests complete}.
297   *
298   * @param releaseTestListeners the {@link List} of {@link ReleaseTestListener}s whose
299   * elements will be {@linkplain
300   * ReleaseTestListener#releaseTested(ReleaseTestEvent) notified when
301   * the tests complete}; may be {@code null}
302   *
303   * @see #getReleaseTestListenersList()
304   *
305   * @see #addReleaseTestListener(ReleaseTestListener)
306   *
307   * @see #removeReleaseTestListener(ReleaseTestListener)
308   */
309  public void setReleaseTestListenersList(final List<ReleaseTestListener> releaseTestListeners) {
310    this.releaseTestListeners = releaseTestListeners;
311  }  
312
313}