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}