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   * The maximum number of characters a <a
085   * href="https://github.com/kubernetes/community/blob/master/contributors/design-proposals/architecture/identifiers.md#definitions">Kubernetes
086   * identifier of type {@code DNS_SUBDOMAIN}</a> is permitted to contain
087   * ({@value}).
088   *
089   * @see <a
090   * href="https://github.com/kubernetes/community/blob/master/contributors/design-proposals/architecture/identifiers.md#definitions">Kubernetes
091   * identifier definitions</a>
092   */
093  public static final int DNS_SUBDOMAIN_MAX_LENGTH = 253;
094
095  /**
096   * A {@link Pattern} specifying the constraints that a non-{@code
097   * null}, non-{@linkplain String#isEmpty() empty} Helm release name
098   * should satisfy.
099   *
100   * <p>Because Helm release names are often used in hostnames, they
101   * should conform to <a
102   * href="https://tools.ietf.org/html/rfc1123#page-13">RFC 1123</a>.
103   * This {@link Pattern} reifies those constraints.</p>
104   *
105   * @see #validateReleaseName(String)
106   *
107   * @see <a href="https://tools.ietf.org/html/rfc1123#page-13">RFC
108   * 1123</a>
109   *
110   * @see <a
111   * href="https://github.com/kubernetes/community/blob/master/contributors/design-proposals/architecture/identifiers.md#definitions">Kubernetes
112   * identifier definitions</a>
113   */
114  public static final Pattern DNS_SUBDOMAIN_PATTERN = Pattern.compile("^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$");
115
116  /**
117   * The maximum number of characters a <a
118   * href="https://github.com/kubernetes/community/blob/master/contributors/design-proposals/architecture/identifiers.md#definitions">Kubernetes
119   * identifier of type {@code DNS_LABEL}</a> is permitted to contain
120   * ({@value}).
121   *
122   * @see <a
123   * href="https://github.com/kubernetes/community/blob/master/contributors/design-proposals/architecture/identifiers.md#definitions">Kubernetes
124   * identifier definitions</a>
125   */
126  public static final int DNS_LABEL_MAX_LENGTH = 63;
127
128  /**
129   * A {@link Pattern} specifying the constraints that a non-{@code
130   * null}, non-{@linkplain String#isEmpty() empty} Kubernetes
131   * namespace should satisfy.
132   *
133   * @see #validateNamespace(String)
134   *
135   * @see <a href="https://tools.ietf.org/html/rfc1123#page-13">RFC
136   * 1123</a>
137   *
138   * @see <a
139   * href="https://github.com/kubernetes/community/blob/master/contributors/design-proposals/architecture/identifiers.md#definitions">Kubernetes
140   * identifier definitions</a>
141   */
142  public static final Pattern DNS_LABEL_PATTERN = Pattern.compile("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$");
143
144  /**
145   * The maximum number of characters a Helm release name is permitted
146   * to contain ({@value}).
147   *
148   * @see <a href="https://github.com/kubernetes/helm/pull/1560">Helm pull request #1560</a>
149   */
150  public static final int HELM_RELEASE_NAME_MAX_LENGTH = 53;
151  
152  /**
153   * An alias for the {@link #DNS_SUBDOMAIN_PATTERN} field.
154   *
155   * @see #DNS_SUBDOMAIN_PATTERN
156   */
157  public static final Pattern RFC_1123_PATTERN = DNS_SUBDOMAIN_PATTERN;
158
159
160  /*
161   * Instance fields.
162   */
163
164  
165  /**
166   * The {@link Tiller} instance used to communicate with Helm's
167   * back-end Tiller component.
168   *
169   * <p>This field is never {@code null}.</p>
170   *
171   * @see Tiller
172   */
173  private final Tiller tiller;
174
175
176  /*
177   * Constructors.
178   */
179
180
181  /**
182   * Creates a new {@link ReleaseManager}.
183   *
184   * @param tiller the {@link Tiller} instance representing a
185   * connection to the <a
186   * href="https://docs.helm.sh/architecture/#components">Tiller
187   * server</a>; must not be {@code null}
188   *
189   * @exception NullPointerException if {@code tiller} is {@code null}
190   *
191   * @see Tiller
192   */
193  public ReleaseManager(final Tiller tiller) {
194    super();
195    Objects.requireNonNull(tiller);
196    this.tiller = tiller;
197  }
198
199
200  /*
201   * Instance methods.
202   */
203
204
205  /**
206   * Returns the {@link Tiller} instance used to communicate with
207   * Helm's back-end Tiller component.
208   *
209   * <p>This method never returns {@code null}.</p>
210   *
211   * @return a non-{@code null} {@link Tiller}
212   *
213   * @see #ReleaseManager(Tiller)
214   *
215   * @see Tiller
216   */
217  protected final Tiller getTiller() {
218    return this.tiller;
219  }
220  
221  /**
222   * Calls {@link Tiller#close() close()} on the {@link Tiller}
223   * instance {@linkplain #ReleaseManager(Tiller) supplied at
224   * construction time}.
225   *
226   * @exception IOException if an error occurs
227   */
228  @Override
229  public void close() throws IOException {
230    this.getTiller().close();
231  }
232
233  /**
234   * Returns the content that made up a given Helm release.
235   *
236   * <p>This method never returns {@code null}.</p>
237   *
238   * <p>Overrides of this method must not return {@code null}.</p>
239   *
240   * @param request the {@link GetReleaseContentRequest} describing
241   * the release; must not be {@code null}
242   *
243   * @return a {@link Future} containing a {@link
244   * GetReleaseContentResponse} that has the information requested;
245   * never {@code null}
246   *
247   * @exception NullPointerException if {@code request} is {@code
248   * null}
249   */
250  public Future<GetReleaseContentResponse> getContent(final GetReleaseContentRequest request) throws IOException {
251    Objects.requireNonNull(request);
252    validate(request);
253
254    final ReleaseServiceFutureStub stub = this.getTiller().getReleaseServiceFutureStub();
255    assert stub != null;
256    return stub.getReleaseContent(request);
257  }
258
259  /**
260   * Returns the history of a given Helm release.
261   *
262   * <p>This method never returns {@code null}.</p>
263   *
264   * <p>Overrides of this method must not return {@code null}.</p>
265   *
266   * @param request the {@link GetHistoryRequest}
267   * describing the release; must not be {@code null}
268   *
269   * @return a {@link Future} containing a {@link
270   * GetHistoryResponse} that has the information requested;
271   * never {@code null}
272   *
273   * @exception NullPointerException if {@code request} is {@code
274   * null}
275   */
276  public Future<GetHistoryResponse> getHistory(final GetHistoryRequest request) throws IOException {
277    Objects.requireNonNull(request);
278    validate(request);
279
280    final ReleaseServiceFutureStub stub = this.getTiller().getReleaseServiceFutureStub();
281    assert stub != null;
282    return stub.getHistory(request);
283  }
284
285  /**
286   * Returns the status of a given Helm release.
287   *
288   * <p>This method never returns {@code null}.</p>
289   *
290   * <p>Overrides of this method must not return {@code null}.</p>
291   *
292   * @param request the {@link GetReleaseStatusRequest} describing the
293   * release; must not be {@code null}
294   *
295   * @return a {@link Future} containing a {@link
296   * GetReleaseStatusResponse} that has the information requested;
297   * never {@code null}
298   *
299   * @exception NullPointerException if {@code request} is {@code
300   * null}
301   */
302  public Future<GetReleaseStatusResponse> getStatus(final GetReleaseStatusRequest request) throws IOException {
303    Objects.requireNonNull(request);
304    validate(request);
305
306    final ReleaseServiceFutureStub stub = this.getTiller().getReleaseServiceFutureStub();
307    assert stub != null;
308    return stub.getReleaseStatus(request);
309  }   
310  
311  /**
312   * Installs a release.
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   * @param requestBuilder the {@link
319   * hapi.services.tiller.Tiller.InstallReleaseRequest.Builder} representing the
320   * installation request; must not be {@code null} and must
321   * {@linkplain #validate(Tiller.InstallReleaseRequestOrBuilder) pass
322   * validation}; its {@link
323   * hapi.services.tiller.Tiller.InstallReleaseRequest.Builder#setChart(hapi.chart.ChartOuterClass.Chart.Builder)}
324   * method will be called with the supplied {@code chartBuilder} as
325   * its argument value
326   *
327   * @param chartBuilder a {@link
328   * hapi.chart.ChartOuterClass.Chart.Builder} representing the Helm
329   * chart to install; must not be {@code null}
330   *
331   * @return a {@link Future} containing a {@link
332   * InstallReleaseResponse} that has the information requested; never
333   * {@code null}
334   *
335   * @exception MissingDependenciesException if the supplied {@code
336   * chartBuilder} has a {@code requirements.yaml} resource in it that
337   * mentions subcharts that it does not contain
338   * 
339   * @exception NullPointerException if {@code request} is {@code
340   * null}
341   *
342   * @see org.microbean.helm.chart.AbstractChartLoader
343   */
344  public Future<InstallReleaseResponse> install(final InstallReleaseRequest.Builder requestBuilder,
345                                                final Chart.Builder chartBuilder)
346    throws IOException {
347    Objects.requireNonNull(requestBuilder);
348    Objects.requireNonNull(chartBuilder);
349    validate(requestBuilder);
350
351    // Note that the mere act of calling getValuesBuilder() has the
352    // convenient if surprising side effect of initializing the
353    // values-related innards of requestBuilder if they haven't yet
354    // been set such that, for example, requestBuilder.getValues()
355    // will no longer return null under any circumstances.  If instead
356    // here we called requestBuilder.getValues(), null *would* be
357    // returned.  For *our* code, this is fine, but Tiller's code
358    // crashes when there's a null in the values slot.
359    requestBuilder.setChart(Requirements.apply(chartBuilder, requestBuilder.getValuesBuilder()));
360    
361    String releaseNamespace = requestBuilder.getNamespace();
362    if (releaseNamespace == null || releaseNamespace.isEmpty()) {
363      final io.fabric8.kubernetes.client.Config configuration = this.getTiller().getConfiguration();
364      if (configuration == null) {
365        requestBuilder.setNamespace("default");
366      } else {
367        releaseNamespace = configuration.getNamespace();
368        if (releaseNamespace == null || releaseNamespace.isEmpty()) {
369          requestBuilder.setNamespace("default");
370        } else {
371          this.validateNamespace(releaseNamespace);
372          requestBuilder.setNamespace(releaseNamespace);
373        }
374      }
375    } else {
376      this.validateNamespace(releaseNamespace);
377    }
378    
379    final ReleaseServiceFutureStub stub = this.getTiller().getReleaseServiceFutureStub();
380    assert stub != null;
381    return stub.installRelease(requestBuilder.build());
382  }
383
384  /**
385   * Returns information about Helm releases.
386   *
387   * <p>This method never returns {@code null}.</p>
388   *
389   * <p>Overrides of this method must not return {@code null}.</p>
390   *
391   * @param request the {@link ListReleasesRequest} describing the
392   * releases to be returned; must not be {@code null}
393   *
394   * @return an {@link Iterator} of {@link ListReleasesResponse}
395   * objects comprising the information requested; never {@code null}
396   *
397   * @exception NullPointerException if {@code request} is {@code
398   * null}
399   *
400   * @exception PatternSyntaxException if the {@link
401   * ListReleasesRequestOrBuilder#getFilter()} return value is
402   * non-{@code null}, non-{@linkplain String#isEmpty() empty} but not
403   * a {@linkplain Pattern#compile(String) valid regular expression}
404   */
405  public Iterator<ListReleasesResponse> list(final ListReleasesRequest request) {
406    Objects.requireNonNull(request);
407    validate(request);
408
409    final ReleaseServiceBlockingStub stub = this.getTiller().getReleaseServiceBlockingStub();
410    assert stub != null;
411    return stub.listReleases(request);
412  }
413
414  /**
415   * Rolls back a previously installed release.
416   *
417   * <p>This method never returns {@code null}.</p>
418   *
419   * <p>Overrides of this method must not return {@code null}.</p>
420   *
421   * @param request the {@link RollbackReleaseRequest} describing the
422   * release; must not be {@code null}
423   *
424   * @return a {@link Future} containing a {@link
425   * RollbackReleaseResponse} that has the information requested;
426   * never {@code null}
427   *
428   * @exception NullPointerException if {@code request} is {@code
429   * null}
430   */
431  public Future<RollbackReleaseResponse> rollback(final RollbackReleaseRequest request)
432    throws IOException {
433    Objects.requireNonNull(request);
434    validate(request);
435
436    final ReleaseServiceFutureStub stub = this.getTiller().getReleaseServiceFutureStub();
437    assert stub != null;
438    return stub.rollbackRelease(request);
439  }
440
441  /**
442   * Returns information about tests run on a given Helm release.
443   *
444   * <p>This method never returns {@code null}.</p>
445   *
446   * <p>Overrides of this method must not return {@code null}.</p>
447   *
448   * @param request the {@link TestReleaseRequest} describing the
449   * release to be tested; must not be {@code null}
450   *
451   * @return an {@link Iterator} of {@link TestReleaseResponse}
452   * objects comprising the information requested; never {@code null}
453   *
454   * @exception NullPointerException if {@code request} is {@code
455   * null}
456   */
457  public Iterator<TestReleaseResponse> test(final TestReleaseRequest request) {
458    Objects.requireNonNull(request);
459    validate(request);
460
461    final ReleaseServiceBlockingStub stub = this.getTiller().getReleaseServiceBlockingStub();
462    assert stub != null;
463    return stub.runReleaseTest(request);
464  }
465
466  /**
467   * Uninstalls (deletes) a previously installed release.
468   *
469   * <p>This method never returns {@code null}.</p>
470   *
471   * <p>Overrides of this method must not return {@code null}.</p>
472   *
473   * @param request the {@link UninstallReleaseRequest} describing the
474   * release; must not be {@code null}
475   *
476   * @return a {@link Future} containing a {@link
477   * UninstallReleaseResponse} that has the information requested;
478   * never {@code null}
479   *
480   * @exception NullPointerException if {@code request} is {@code
481   * null}
482   */
483  public Future<UninstallReleaseResponse> uninstall(final UninstallReleaseRequest request)
484    throws IOException {
485    Objects.requireNonNull(request);
486    validate(request);
487
488    final ReleaseServiceFutureStub stub = this.getTiller().getReleaseServiceFutureStub();
489    assert stub != null;
490    return stub.uninstallRelease(request);
491  }
492
493  /**
494   * Updates a release.
495   *
496   * <p>This method never returns {@code null}.</p>
497   *
498   * <p>Overrides of this method must not return {@code null}.</p>
499   *
500   * @param requestBuilder the {@link
501   * hapi.services.tiller.Tiller.UpdateReleaseRequest.Builder}
502   * representing the installation request; must not be {@code null}
503   * and must {@linkplain
504   * #validate(Tiller.UpdateReleaseRequestOrBuilder) pass validation};
505   * its {@link
506   * hapi.services.tiller.Tiller.UpdateReleaseRequest.Builder#setChart(hapi.chart.ChartOuterClass.Chart.Builder)}
507   * method will be called with the supplied {@code chartBuilder} as
508   * its argument value
509   *
510   * @param chartBuilder a {@link
511   * hapi.chart.ChartOuterClass.Chart.Builder} representing the Helm
512   * chart with which to update the release; must not be {@code null}
513   *
514   * @return a {@link Future} containing a {@link
515   * UpdateReleaseResponse} that has the information requested; never
516   * {@code null}
517   *
518   * @exception NullPointerException if {@code request} is {@code
519   * null}
520   *
521   * @see org.microbean.helm.chart.AbstractChartLoader
522   */
523  public Future<UpdateReleaseResponse> update(final UpdateReleaseRequest.Builder requestBuilder,
524                                              final Chart.Builder chartBuilder)
525    throws IOException {
526    Objects.requireNonNull(requestBuilder);
527    Objects.requireNonNull(chartBuilder);
528    validate(requestBuilder);
529    
530    // Note that the mere act of calling getValuesBuilder() has the
531    // convenient if surprising side effect of initializing the
532    // values-related innards of requestBuilder if they haven't yet
533    // been set such that, for example, requestBuilder.getValues()
534    // will no longer return null under any circumstances.  If instead
535    // here we called requestBuilder.getValues(), null *would* be
536    // returned.  For *our* code, this is fine, but Tiller's code
537    // crashes when there's a null in the values slot.
538    requestBuilder.setChart(Requirements.apply(chartBuilder, requestBuilder.getValuesBuilder()));
539
540    final ReleaseServiceFutureStub stub = this.getTiller().getReleaseServiceFutureStub();
541    assert stub != null;
542    return stub.updateRelease(requestBuilder.build());
543  }
544
545  /**
546   * Validates the supplied {@link GetReleaseContentRequestOrBuilder}.
547   *
548   * @param request the request to validate
549   *
550   * @exception NullPointerException if {@code request} is {@code null}
551   *
552   * @exception IllegalArgumentException if {@code request} is invalid
553   *
554   * @see #validateReleaseName(String)
555   */
556  protected void validate(final GetReleaseContentRequestOrBuilder request) {
557    Objects.requireNonNull(request);
558    validateReleaseName(request.getName());
559  }
560
561  /**
562   * Validates the supplied {@link GetHistoryRequestOrBuilder}.
563   *
564   * @param request the request to validate
565   *
566   * @exception NullPointerException if {@code request} is {@code null}
567   *
568   * @exception IllegalArgumentException if {@code request} is invalid
569   *
570   * @see #validateReleaseName(String)
571   */
572  protected void validate(final GetHistoryRequestOrBuilder request) {
573    Objects.requireNonNull(request);
574    validateReleaseName(request.getName());
575  }
576
577  /**
578   * Validates the supplied {@link GetReleaseStatusRequestOrBuilder}.
579   *
580   * @param request the request to validate
581   *
582   * @exception NullPointerException if {@code request} is {@code null}
583   *
584   * @exception IllegalArgumentException if {@code request} is invalid
585   *
586   * @see #validateReleaseName(String)
587   */
588  protected void validate(final GetReleaseStatusRequestOrBuilder request) {
589    Objects.requireNonNull(request);
590    validateReleaseName(request.getName());
591  }
592
593  /**
594   * Validates the supplied {@link InstallReleaseRequestOrBuilder}.
595   *
596   * @param request the request to validate
597   *
598   * @exception NullPointerException if {@code request} is {@code null}
599   *
600   * @exception IllegalArgumentException if {@code request} is invalid
601   *
602   * @see #validateReleaseName(String)
603   */
604  protected void validate(final InstallReleaseRequestOrBuilder request) {
605    Objects.requireNonNull(request);
606    validateReleaseName(request.getName());
607  }
608
609  /**
610   * Validates the supplied {@link ListReleasesRequestOrBuilder}.
611   *
612   * @param request the request to validate
613   *
614   * @exception NullPointerException if {@code request} is {@code null}
615   *
616   * @exception IllegalArgumentException if {@code request} is invalid
617   *
618   * @see #validateReleaseName(String)
619   */
620  protected void validate(final ListReleasesRequestOrBuilder request) {
621    Objects.requireNonNull(request);
622    final String filter = request.getFilter();
623    if (filter != null && !filter.isEmpty()) {
624      Pattern.compile(filter);
625    }
626  }
627
628  /**
629   * Validates the supplied {@link RollbackReleaseRequestOrBuilder}.
630   *
631   * @param request the request to validate
632   *
633   * @exception NullPointerException if {@code request} is {@code null}
634   *
635   * @exception IllegalArgumentException if {@code request} is invalid
636   *
637   * @see #validateReleaseName(String)
638   */
639  protected void validate(final RollbackReleaseRequestOrBuilder request) {
640    Objects.requireNonNull(request);
641    validateReleaseName(request.getName());
642  }
643
644  /**
645   * Validates the supplied {@link TestReleaseRequestOrBuilder}.
646   *
647   * @param request the request to validate
648   *
649   * @exception NullPointerException if {@code request} is {@code null}
650   *
651   * @exception IllegalArgumentException if {@code request} is invalid
652   *
653   * @see #validateReleaseName(String)
654   */
655  protected void validate(final TestReleaseRequestOrBuilder request) {
656    Objects.requireNonNull(request);
657    validateReleaseName(request.getName());
658  }
659
660  /**
661   * Validates the supplied {@link UninstallReleaseRequestOrBuilder}.
662   *
663   * @param request the request to validate
664   *
665   * @exception NullPointerException if {@code request} is {@code null}
666   *
667   * @exception IllegalArgumentException if {@code request} is invalid
668   *
669   * @see #validateReleaseName(String)
670   */
671  protected void validate(final UninstallReleaseRequestOrBuilder request) {
672    Objects.requireNonNull(request);
673    validateReleaseName(request.getName());
674  }
675
676  /**
677   * Validates the supplied {@link UpdateReleaseRequestOrBuilder}.
678   *
679   * @param request the request to validate
680   *
681   * @exception NullPointerException if {@code request} is {@code null}
682   *
683   * @exception IllegalArgumentException if {@code request} is invalid
684   *
685   * @see #validateReleaseName(String)
686   */
687  protected void validate(final UpdateReleaseRequestOrBuilder request) {
688    Objects.requireNonNull(request);
689    validateReleaseName(request.getName());
690  }
691
692  /**
693   * Ensures that the supplied {@code name} is a valid Helm release
694   * name.
695   *
696   * <p>Because frequently Helm releases are not required to be named
697   * by the end user, a {@code null} or {@linkplain String#isEmpty()
698   * empty} {@code name} is valid.</p>
699   *
700   * <p>Because Helm release names are often used in hostnames, they
701   * should conform to <a
702   * href="https://tools.ietf.org/html/rfc1123#page-13">RFC 1123</a>.
703   * This method performs that validation by default, using the {@link
704   * #DNS_SUBDOMAIN_PATTERN} field.</p>
705   *
706   * @param name the name to validate; may be {@code null} or
707   * {@linkplain String#isEmpty()} since Tiller will generate a valid
708   * name in such a case using the <a
709   * href="https://github.com/technosophos/moniker">{@code
710   * moniker}</a> project; if non-{@code null} must match the pattern
711   * represented by the value of the {@link #DNS_SUBDOMAIN_PATTERN} field
712   *
713   * @see #DNS_SUBDOMAIN_PATTERN
714   *
715   * @see <a href="https://tools.ietf.org/html/rfc1123#page-13">RFC
716   * 1123</a>
717   */
718  protected void validateReleaseName(final String name) {    
719    if (name != null) {
720      final int nameLength = name.length();
721      if (nameLength > HELM_RELEASE_NAME_MAX_LENGTH) {
722        throw new IllegalArgumentException("Invalid release name: " + name + "; length is greater than " + HELM_RELEASE_NAME_MAX_LENGTH + " characters: " + nameLength);
723      } else if (nameLength > 0) {
724        final Matcher matcher = DNS_SUBDOMAIN_PATTERN.matcher(name);
725        assert matcher != null;
726        if (!matcher.matches()) {
727          throw new IllegalArgumentException("Invalid release name: " + name + "; must match " + DNS_SUBDOMAIN_PATTERN.toString());
728        }
729      }
730    }
731  }
732
733  /**
734   * Ensures that the supplied {@code namespace} is a valid namespace.
735   *
736   * <p>Namespaces <a
737   * href="https://github.com/kubernetes/community/blob/master/contributors/design-proposals/architecture/identifiers.md#general-design">must
738   * conform</a> to <a
739   * href="https://tools.ietf.org/html/rfc1123#page-13">RFC 1123</a>.
740   * This method performs that validation by default, using the {@link
741   * #DNS_SUBDOMAIN_PATTERN} field.</p>
742   *
743   * @param namespace the namespace to validate; may be {@code null} or
744   * {@linkplain String#isEmpty()}
745   *
746   * @see #DNS_LABEL_PATTERN
747   *
748   * @see <a href="https://tools.ietf.org/html/rfc1123#page-13">RFC
749   * 1123</a>
750   */
751  protected void validateNamespace(final String namespace) {
752    if (namespace != null) {
753      final int namespaceLength = namespace.length();
754      if (namespaceLength > DNS_LABEL_MAX_LENGTH) {
755        throw new IllegalArgumentException("Invalid namespace: " + namespace + "; length is greater than " + DNS_LABEL_MAX_LENGTH + " characters: " + namespaceLength);
756      } else if (namespaceLength > 0) {
757        final Matcher matcher = DNS_LABEL_PATTERN.matcher(namespace);
758        assert matcher != null;
759        if (!matcher.matches()) {
760          throw new IllegalArgumentException("Invalid namespace: " + namespace + "; must match " + DNS_LABEL_PATTERN.toString());
761        }
762      }
763    }
764  }
765
766}