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;
018
019import java.io.Closeable;
020import java.io.IOException;
021
022import java.util.Iterator;
023import java.util.Objects;
024
025import java.util.concurrent.Future;
026import java.util.concurrent.FutureTask;
027
028import java.util.regex.Matcher;
029import java.util.regex.Pattern;
030import java.util.regex.PatternSyntaxException;
031
032import hapi.chart.ChartOuterClass.Chart;
033
034import hapi.release.ReleaseOuterClass.Release;
035
036import hapi.services.tiller.ReleaseServiceGrpc.ReleaseServiceBlockingStub;
037import hapi.services.tiller.ReleaseServiceGrpc.ReleaseServiceFutureStub;
038import hapi.services.tiller.Tiller.GetHistoryRequest;
039import hapi.services.tiller.Tiller.GetHistoryRequestOrBuilder;
040import hapi.services.tiller.Tiller.GetHistoryResponse;
041import hapi.services.tiller.Tiller.GetReleaseContentRequest;
042import hapi.services.tiller.Tiller.GetReleaseContentRequestOrBuilder;
043import hapi.services.tiller.Tiller.GetReleaseContentResponse;
044import hapi.services.tiller.Tiller.GetReleaseStatusRequest;
045import hapi.services.tiller.Tiller.GetReleaseStatusRequestOrBuilder;
046import hapi.services.tiller.Tiller.GetReleaseStatusResponse;
047import hapi.services.tiller.Tiller.InstallReleaseRequest;
048import hapi.services.tiller.Tiller.InstallReleaseRequestOrBuilder;
049import hapi.services.tiller.Tiller.InstallReleaseResponse;
050import hapi.services.tiller.Tiller.ListReleasesRequest;
051import hapi.services.tiller.Tiller.ListReleasesRequestOrBuilder;
052import hapi.services.tiller.Tiller.ListReleasesResponse;
053import hapi.services.tiller.Tiller.RollbackReleaseRequest;
054import hapi.services.tiller.Tiller.RollbackReleaseRequestOrBuilder;
055import hapi.services.tiller.Tiller.RollbackReleaseResponse;
056import hapi.services.tiller.Tiller.TestReleaseRequest;
057import hapi.services.tiller.Tiller.TestReleaseRequestOrBuilder;
058import hapi.services.tiller.Tiller.TestReleaseResponse;
059import hapi.services.tiller.Tiller.UninstallReleaseRequest;
060import hapi.services.tiller.Tiller.UninstallReleaseRequestOrBuilder;
061import hapi.services.tiller.Tiller.UninstallReleaseResponse;
062import hapi.services.tiller.Tiller.UpdateReleaseRequest;
063import hapi.services.tiller.Tiller.UpdateReleaseRequestOrBuilder;
064import hapi.services.tiller.Tiller.UpdateReleaseResponse;
065
066import org.microbean.helm.chart.MissingDependenciesException;
067import org.microbean.helm.chart.Requirements;
068
069/**
070 * A manager of <a href="https://docs.helm.sh/glossary/#release">Helm releases</a>.
071 *
072 * @author <a href="https://about.me/lairdnelson/"
073 * target="_parent">Laird Nelson</a>
074 */
075public class ReleaseManager implements Closeable {
076
077
078  /*
079   * Static fields.
080   */
081
082  
083  /**
084   * A {@link Pattern} specifying the constraints that a Helm release
085   * name should satisfy.
086   *
087   * <p>Because Helm release names are often used in hostnames, they
088   * should conform to <a
089   * href="https://tools.ietf.org/html/rfc1123#page-13">RFC 1123</a>.
090   * This {@link Pattern} reifies those constraints.</p>
091   *
092   * @see #validateReleaseName(String)
093   *
094   * @see <a href="https://tools.ietf.org/html/rfc1123#page-13">RFC
095   * 1123</a>
096   */
097  public static final Pattern RFC_1123_PATTERN = Pattern.compile("^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$");
098
099
100  /*
101   * Instance fields.
102   */
103
104  
105  /**
106   * The {@link Tiller} instance used to communicate with Helm's
107   * back-end Tiller component.
108   *
109   * <p>This field is never {@code null}.</p>
110   *
111   * @see Tiller
112   */
113  private final Tiller tiller;
114
115
116  /*
117   * Constructors.
118   */
119
120
121  /**
122   * Creates a new {@link ReleaseManager}.
123   *
124   * @param tiller the {@link Tiller} instance representing a
125   * connection to the <a
126   * href="https://docs.helm.sh/architecture/#components">Tiller
127   * server</a>; must not be {@code null}
128   *
129   * @exception NullPointerException if {@code tiller} is {@code null}
130   *
131   * @see Tiller
132   */
133  public ReleaseManager(final Tiller tiller) {
134    super();
135    Objects.requireNonNull(tiller);
136    this.tiller = tiller;
137  }
138
139
140  /*
141   * Instance methods.
142   */
143
144
145  /**
146   * Returns the {@link Tiller} instance used to communicate with
147   * Helm's back-end Tiller component.
148   *
149   * <p>This method never returns {@code null}.</p>
150   *
151   * @return a non-{@code null} {@link Tiller}
152   *
153   * @see #ReleaseManager(Tiller)
154   *
155   * @see Tiller
156   */
157  protected final Tiller getTiller() {
158    return this.tiller;
159  }
160  
161  /**
162   * Calls {@link Tiller#close() close()} on the {@link Tiller}
163   * instance {@linkplain #ReleaseManager(Tiller) supplied at
164   * construction time}.
165   *
166   * @exception IOException if an error occurs
167   */
168  @Override
169  public void close() throws IOException {
170    this.getTiller().close();
171  }
172
173  /**
174   * Returns the content that made up a given Helm release.
175   *
176   * <p>This method never returns {@code null}.</p>
177   *
178   * <p>Overrides of this method must not return {@code null}.</p>
179   *
180   * @param request the {@link GetReleaseContentRequest} describing
181   * the release; must not be {@code null}
182   *
183   * @return a {@link Future} containing a {@link
184   * GetReleaseContentResponse} that has the information requested;
185   * never {@code null}
186   *
187   * @exception NullPointerException if {@code request} is {@code
188   * null}
189   */
190  public Future<GetReleaseContentResponse> getContent(final GetReleaseContentRequest request) throws IOException {
191    Objects.requireNonNull(request);
192    validate(request);
193
194    final ReleaseServiceFutureStub stub = this.getTiller().getReleaseServiceFutureStub();
195    assert stub != null;
196    return stub.getReleaseContent(request);
197  }
198
199  /**
200   * Returns the history of a given Helm release.
201   *
202   * <p>This method never returns {@code null}.</p>
203   *
204   * <p>Overrides of this method must not return {@code null}.</p>
205   *
206   * @param request the {@link GetHistoryRequest}
207   * describing the release; must not be {@code null}
208   *
209   * @return a {@link Future} containing a {@link
210   * GetHistoryResponse} that has the information requested;
211   * never {@code null}
212   *
213   * @exception NullPointerException if {@code request} is {@code
214   * null}
215   */
216  public Future<GetHistoryResponse> getHistory(final GetHistoryRequest request) throws IOException {
217    Objects.requireNonNull(request);
218    validate(request);
219
220    final ReleaseServiceFutureStub stub = this.getTiller().getReleaseServiceFutureStub();
221    assert stub != null;
222    return stub.getHistory(request);
223  }
224
225  /**
226   * Returns the status of a given Helm release.
227   *
228   * <p>This method never returns {@code null}.</p>
229   *
230   * <p>Overrides of this method must not return {@code null}.</p>
231   *
232   * @param request the {@link GetReleaseStatusRequest} describing the
233   * release; must not be {@code null}
234   *
235   * @return a {@link Future} containing a {@link
236   * GetReleaseStatusResponse} that has the information requested;
237   * never {@code null}
238   *
239   * @exception NullPointerException if {@code request} is {@code
240   * null}
241   */
242  public Future<GetReleaseStatusResponse> getStatus(final GetReleaseStatusRequest request) throws IOException {
243    Objects.requireNonNull(request);
244    validate(request);
245
246    final ReleaseServiceFutureStub stub = this.getTiller().getReleaseServiceFutureStub();
247    assert stub != null;
248    return stub.getReleaseStatus(request);
249  }   
250  
251  /**
252   * Installs a release.
253   *
254   * <p>This method never returns {@code null}.</p>
255   *
256   * <p>Overrides of this method must not return {@code null}.</p>
257   *
258   * @param requestBuilder the {@link
259   * hapi.services.tiller.Tiller.InstallReleaseRequest.Builder} representing the
260   * installation request; must not be {@code null} and must
261   * {@linkplain #validate(Tiller.InstallReleaseRequestOrBuilder) pass
262   * validation}; its {@link
263   * hapi.services.tiller.Tiller.InstallReleaseRequest.Builder#setChart(hapi.chart.ChartOuterClass.Chart.Builder)}
264   * method will be called with the supplied {@code chartBuilder} as
265   * its argument value
266   *
267   * @param chartBuilder a {@link
268   * hapi.chart.ChartOuterClass.Chart.Builder} representing the Helm
269   * chart to install; must not be {@code null}
270   *
271   * @return a {@link Future} containing a {@link
272   * InstallReleaseResponse} that has the information requested; never
273   * {@code null}
274   *
275   * @exception MissingDependenciesException if the supplied {@code
276   * chartBuilder} has a {@code requirements.yaml} resource in it that
277   * mentions subcharts that it does not contain
278   * 
279   * @exception NullPointerException if {@code request} is {@code
280   * null}
281   *
282   * @see org.microbean.helm.chart.AbstractChartLoader
283   */
284  public Future<InstallReleaseResponse> install(final InstallReleaseRequest.Builder requestBuilder,
285                                                final Chart.Builder chartBuilder)
286    throws IOException {
287    Objects.requireNonNull(requestBuilder);
288    Objects.requireNonNull(chartBuilder);
289    validate(requestBuilder);
290
291    // Note that the mere act of calling getValuesBuilder() has the
292    // convenient if surprising side effect of initializing the
293    // values-related innards of requestBuilder if they haven't yet
294    // been set such that, for example, requestBuilder.getValues()
295    // will no longer return null under any circumstances.  If instead
296    // here we called requestBuilder.getValues(), null *would* be
297    // returned.  For *our* code, this is fine, but Tiller's code
298    // crashes when there's a null in the values slot.
299    requestBuilder.setChart(Requirements.apply(chartBuilder, requestBuilder.getValuesBuilder()));
300    
301    String releaseNamespace = requestBuilder.getNamespace();
302    if (releaseNamespace == null || releaseNamespace.isEmpty()) {
303      final io.fabric8.kubernetes.client.Config configuration = this.getTiller().getConfiguration();
304      if (configuration == null) {
305        requestBuilder.setNamespace("default");
306      } else {
307        releaseNamespace = configuration.getNamespace();
308        if (releaseNamespace == null || releaseNamespace.isEmpty()) {
309          requestBuilder.setNamespace("default");
310        } else {
311          requestBuilder.setNamespace(releaseNamespace);
312        }
313      }
314    }
315    
316    final ReleaseServiceFutureStub stub = this.getTiller().getReleaseServiceFutureStub();
317    assert stub != null;
318    return stub.installRelease(requestBuilder.build());
319  }
320
321  /**
322   * Returns information about Helm releases.
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   * @param request the {@link ListReleasesRequest} describing the
329   * releases to be returned; must not be {@code null}
330   *
331   * @return an {@link Iterator} of {@link ListReleasesResponse}
332   * objects comprising the information requested; never {@code null}
333   *
334   * @exception NullPointerException if {@code request} is {@code
335   * null}
336   *
337   * @exception PatternSyntaxException if the {@link
338   * ListReleasesRequestOrBuilder#getFilter()} return value is
339   * non-{@code null}, non-{@linkplain String#isEmpty() empty} but not
340   * a {@linkplain Pattern#compile(String) valid regular expression}
341   */
342  public Iterator<ListReleasesResponse> list(final ListReleasesRequest request) {
343    Objects.requireNonNull(request);
344    validate(request);
345
346    final ReleaseServiceBlockingStub stub = this.getTiller().getReleaseServiceBlockingStub();
347    assert stub != null;
348    return stub.listReleases(request);
349  }
350
351  /**
352   * Rolls back a previously installed release.
353   *
354   * <p>This method never returns {@code null}.</p>
355   *
356   * <p>Overrides of this method must not return {@code null}.</p>
357   *
358   * @param request the {@link RollbackReleaseRequest} describing the
359   * release; must not be {@code null}
360   *
361   * @return a {@link Future} containing a {@link
362   * RollbackReleaseResponse} that has the information requested;
363   * never {@code null}
364   *
365   * @exception NullPointerException if {@code request} is {@code
366   * null}
367   */
368  public Future<RollbackReleaseResponse> rollback(final RollbackReleaseRequest request)
369    throws IOException {
370    Objects.requireNonNull(request);
371    validate(request);
372
373    final ReleaseServiceFutureStub stub = this.getTiller().getReleaseServiceFutureStub();
374    assert stub != null;
375    return stub.rollbackRelease(request);
376  }
377
378  /**
379   * Returns information about tests run on a given Helm release.
380   *
381   * <p>This method never returns {@code null}.</p>
382   *
383   * <p>Overrides of this method must not return {@code null}.</p>
384   *
385   * @param request the {@link TestReleaseRequest} describing the
386   * release to be tested; must not be {@code null}
387   *
388   * @return an {@link Iterator} of {@link TestReleaseResponse}
389   * objects comprising the information requested; never {@code null}
390   *
391   * @exception NullPointerException if {@code request} is {@code
392   * null}
393   */
394  public Iterator<TestReleaseResponse> test(final TestReleaseRequest request) {
395    Objects.requireNonNull(request);
396    validate(request);
397
398    final ReleaseServiceBlockingStub stub = this.getTiller().getReleaseServiceBlockingStub();
399    assert stub != null;
400    return stub.runReleaseTest(request);
401  }
402
403  /**
404   * Uninstalls (deletes) a previously installed release.
405   *
406   * <p>This method never returns {@code null}.</p>
407   *
408   * <p>Overrides of this method must not return {@code null}.</p>
409   *
410   * @param request the {@link UninstallReleaseRequest} describing the
411   * release; must not be {@code null}
412   *
413   * @return a {@link Future} containing a {@link
414   * UninstallReleaseResponse} that has the information requested;
415   * never {@code null}
416   *
417   * @exception NullPointerException if {@code request} is {@code
418   * null}
419   */
420  public Future<UninstallReleaseResponse> uninstall(final UninstallReleaseRequest request)
421    throws IOException {
422    Objects.requireNonNull(request);
423    validate(request);
424
425    final ReleaseServiceFutureStub stub = this.getTiller().getReleaseServiceFutureStub();
426    assert stub != null;
427    return stub.uninstallRelease(request);
428  }
429
430  /**
431   * Updates a release.
432   *
433   * <p>This method never returns {@code null}.</p>
434   *
435   * <p>Overrides of this method must not return {@code null}.</p>
436   *
437   * @param requestBuilder the {@link
438   * hapi.services.tiller.Tiller.UpdateReleaseRequest.Builder}
439   * representing the installation request; must not be {@code null}
440   * and must {@linkplain
441   * #validate(Tiller.UpdateReleaseRequestOrBuilder) pass validation};
442   * its {@link
443   * hapi.services.tiller.Tiller.UpdateReleaseRequest.Builder#setChart(hapi.chart.ChartOuterClass.Chart.Builder)}
444   * method will be called with the supplied {@code chartBuilder} as
445   * its argument value
446   *
447   * @param chartBuilder a {@link
448   * hapi.chart.ChartOuterClass.Chart.Builder} representing the Helm
449   * chart with which to update the release; must not be {@code null}
450   *
451   * @return a {@link Future} containing a {@link
452   * UpdateReleaseResponse} that has the information requested; never
453   * {@code null}
454   *
455   * @exception NullPointerException if {@code request} is {@code
456   * null}
457   *
458   * @see org.microbean.helm.chart.AbstractChartLoader
459   */
460  public Future<UpdateReleaseResponse> update(final UpdateReleaseRequest.Builder requestBuilder,
461                                              final Chart.Builder chartBuilder)
462    throws IOException {
463    Objects.requireNonNull(requestBuilder);
464    Objects.requireNonNull(chartBuilder);
465    validate(requestBuilder);
466    
467    // Note that the mere act of calling getValuesBuilder() has the
468    // convenient if surprising side effect of initializing the
469    // values-related innards of requestBuilder if they haven't yet
470    // been set such that, for example, requestBuilder.getValues()
471    // will no longer return null under any circumstances.  If instead
472    // here we called requestBuilder.getValues(), null *would* be
473    // returned.  For *our* code, this is fine, but Tiller's code
474    // crashes when there's a null in the values slot.
475    requestBuilder.setChart(Requirements.apply(chartBuilder, requestBuilder.getValuesBuilder()));
476
477    final ReleaseServiceFutureStub stub = this.getTiller().getReleaseServiceFutureStub();
478    assert stub != null;
479    return stub.updateRelease(requestBuilder.build());
480  }
481
482  /**
483   * Validates the supplied {@link GetReleaseContentRequestOrBuilder}.
484   *
485   * @param request the request to validate
486   *
487   * @exception NullPointerException if {@code request} is {@code null}
488   *
489   * @exception IllegalArgumentException if {@code request} is invalid
490   *
491   * @see #validateReleaseName(String)
492   */
493  protected void validate(final GetReleaseContentRequestOrBuilder request) {
494    Objects.requireNonNull(request);
495    validateReleaseName(request.getName());
496  }
497
498  /**
499   * Validates the supplied {@link GetHistoryRequestOrBuilder}.
500   *
501   * @param request the request to validate
502   *
503   * @exception NullPointerException if {@code request} is {@code null}
504   *
505   * @exception IllegalArgumentException if {@code request} is invalid
506   *
507   * @see #validateReleaseName(String)
508   */
509  protected void validate(final GetHistoryRequestOrBuilder request) {
510    Objects.requireNonNull(request);
511    validateReleaseName(request.getName());
512  }
513
514  /**
515   * Validates the supplied {@link GetReleaseStatusRequestOrBuilder}.
516   *
517   * @param request the request to validate
518   *
519   * @exception NullPointerException if {@code request} is {@code null}
520   *
521   * @exception IllegalArgumentException if {@code request} is invalid
522   *
523   * @see #validateReleaseName(String)
524   */
525  protected void validate(final GetReleaseStatusRequestOrBuilder request) {
526    Objects.requireNonNull(request);
527    validateReleaseName(request.getName());
528  }
529
530  /**
531   * Validates the supplied {@link InstallReleaseRequestOrBuilder}.
532   *
533   * @param request the request to validate
534   *
535   * @exception NullPointerException if {@code request} is {@code null}
536   *
537   * @exception IllegalArgumentException if {@code request} is invalid
538   *
539   * @see #validateReleaseName(String)
540   */
541  protected void validate(final InstallReleaseRequestOrBuilder request) {
542    Objects.requireNonNull(request);
543    validateReleaseName(request.getName());
544  }
545
546  /**
547   * Validates the supplied {@link ListReleasesRequestOrBuilder}.
548   *
549   * @param request the request to validate
550   *
551   * @exception NullPointerException if {@code request} is {@code null}
552   *
553   * @exception IllegalArgumentException if {@code request} is invalid
554   *
555   * @see #validateReleaseName(String)
556   */
557  protected void validate(final ListReleasesRequestOrBuilder request) {
558    Objects.requireNonNull(request);
559    final String filter = request.getFilter();
560    if (filter != null && !filter.isEmpty()) {
561      Pattern.compile(filter);
562    }
563  }
564
565  /**
566   * Validates the supplied {@link RollbackReleaseRequestOrBuilder}.
567   *
568   * @param request the request to validate
569   *
570   * @exception NullPointerException if {@code request} is {@code null}
571   *
572   * @exception IllegalArgumentException if {@code request} is invalid
573   *
574   * @see #validateReleaseName(String)
575   */
576  protected void validate(final RollbackReleaseRequestOrBuilder request) {
577    Objects.requireNonNull(request);
578    validateReleaseName(request.getName());
579  }
580
581  /**
582   * Validates the supplied {@link TestReleaseRequestOrBuilder}.
583   *
584   * @param request the request to validate
585   *
586   * @exception NullPointerException if {@code request} is {@code null}
587   *
588   * @exception IllegalArgumentException if {@code request} is invalid
589   *
590   * @see #validateReleaseName(String)
591   */
592  protected void validate(final TestReleaseRequestOrBuilder request) {
593    Objects.requireNonNull(request);
594    validateReleaseName(request.getName());
595  }
596
597  /**
598   * Validates the supplied {@link UninstallReleaseRequestOrBuilder}.
599   *
600   * @param request the request to validate
601   *
602   * @exception NullPointerException if {@code request} is {@code null}
603   *
604   * @exception IllegalArgumentException if {@code request} is invalid
605   *
606   * @see #validateReleaseName(String)
607   */
608  protected void validate(final UninstallReleaseRequestOrBuilder request) {
609    Objects.requireNonNull(request);
610    validateReleaseName(request.getName());
611  }
612
613  /**
614   * Validates the supplied {@link UpdateReleaseRequestOrBuilder}.
615   *
616   * @param request the request to validate
617   *
618   * @exception NullPointerException if {@code request} is {@code null}
619   *
620   * @exception IllegalArgumentException if {@code request} is invalid
621   *
622   * @see #validateReleaseName(String)
623   */
624  protected void validate(final UpdateReleaseRequestOrBuilder request) {
625    Objects.requireNonNull(request);
626    validateReleaseName(request.getName());
627  }
628
629  /**
630   * Ensures that the supplied {@code name} is a valid Helm release
631   * name.
632   *
633   * <p>Because Helm release names are often used in hostnames, they
634   * should conform to <a
635   * href="https://tools.ietf.org/html/rfc1123#page-13">RFC 1123</a>.
636   * This method performs that validation by default, using the {@link
637   * #RFC_1123_PATTERN} field.</p>
638   *
639   * @param name the name to validate; may be {@code null} or
640   * {@linkplain String#isEmpty()} since Tiller will generate a valid
641   * name in such a case using the <a
642   * href="https://github.com/technosophos/moniker">{@code
643   * moniker}</a> project; if non-{@code null} must match the pattern
644   * represented by the value of the {@link #RFC_1123_PATTERN} field
645   *
646   * @see #RFC_1123_PATTERN
647   *
648   * @see <a href="https://tools.ietf.org/html/rfc1123#page-13">RFC
649   * 1123</a>
650   */
651  protected void validateReleaseName(final String name) {
652    if (name != null && !name.isEmpty()) {
653      final Matcher matcher = RFC_1123_PATTERN.matcher(name);
654      assert matcher != null;
655      if (!matcher.matches()) {
656        throw new IllegalArgumentException("Invalid release name: " + name + "; must match " + RFC_1123_PATTERN.toString());
657      }
658    }
659  }
660
661}