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}