001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 002 * 003 * Copyright © 2017-2018 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.BufferedInputStream; 020import java.io.ByteArrayOutputStream; 021import java.io.InputStream; 022import java.io.IOException; 023 024import java.net.URI; 025import java.net.URL; 026 027import java.util.Arrays; 028import java.util.ArrayList; 029import java.util.Base64; 030import java.util.HashMap; 031import java.util.List; 032import java.util.Map; 033import java.util.Objects; 034 035import java.util.regex.Matcher; 036import java.util.regex.Pattern; 037 038import com.github.zafarkhaja.semver.Version; 039 040import io.fabric8.kubernetes.client.DefaultKubernetesClient; 041import io.fabric8.kubernetes.client.KubernetesClient; 042import io.fabric8.kubernetes.client.KubernetesClientException; 043 044import io.fabric8.kubernetes.client.dsl.Resource; 045 046import io.fabric8.kubernetes.api.model.Container; 047import io.fabric8.kubernetes.api.model.ContainerPort; 048import io.fabric8.kubernetes.api.model.EnvVar; 049import io.fabric8.kubernetes.api.model.HTTPGetAction; 050import io.fabric8.kubernetes.api.model.IntOrString; 051import io.fabric8.kubernetes.api.model.ObjectMeta; 052import io.fabric8.kubernetes.api.model.PodSpec; 053import io.fabric8.kubernetes.api.model.PodTemplateSpec; 054import io.fabric8.kubernetes.api.model.Probe; 055import io.fabric8.kubernetes.api.model.Secret; 056import io.fabric8.kubernetes.api.model.SecretVolumeSource; 057import io.fabric8.kubernetes.api.model.Service; 058import io.fabric8.kubernetes.api.model.ServicePort; 059import io.fabric8.kubernetes.api.model.ServiceSpec; 060import io.fabric8.kubernetes.api.model.Status; 061import io.fabric8.kubernetes.api.model.Volume; 062import io.fabric8.kubernetes.api.model.VolumeMount; 063 064import io.fabric8.kubernetes.api.model.extensions.Deployment; 065import io.fabric8.kubernetes.api.model.extensions.DeploymentSpec; 066import io.fabric8.kubernetes.api.model.extensions.DoneableDeployment; 067 068import org.microbean.development.annotation.Experimental; 069 070/** 071 * A class that idiomatically but faithfully emulates the 072 * Tiller-installing behavior of the {@code helm init} command. 073 * 074 * <p>In general, this class follows the logic as expressed in <a 075 * href="https://github.com/kubernetes/helm/blob/master/cmd/helm/installer/install.go">the 076 * {@code install.go} source code from the Helm project</a>, 077 * problematic or not. The intent is to have an installer, usable as 078 * an idiomatic Java library, that behaves just like {@code helm 079 * init}.</p> 080 * 081 * <p><strong>Note:</strong> This class is experimental and its API is 082 * subject to change without notice.</p> 083 * 084 * @author <a href="https://about.me/lairdnelson" 085 * target="_parent">Laird Nelson</a> 086 * 087 * @see #init() 088 * 089 * @see <a 090 * href="https://github.com/kubernetes/helm/blob/master/cmd/helm/installer/install.go">The 091 * <code>install.go</code> source code from the Helm project</a> 092 */ 093@Experimental 094public class TillerInstaller { 095 096 097 /* 098 * Static fields. 099 */ 100 101 102 /* 103 * Atomic static fields. 104 */ 105 106 private static final Integer ONE = Integer.valueOf(1); 107 108 private static final ImagePullPolicy DEFAULT_IMAGE_PULL_POLICY = ImagePullPolicy.IF_NOT_PRESENT; 109 110 private static final String DEFAULT_NAME = "tiller"; 111 112 private static final String DEFAULT_NAMESPACE = "kube-system"; 113 114 private static final String TILLER_TLS_CERTS_PATH = "/etc/certs"; 115 116 /** 117 * The version of Tiller to install. 118 */ 119 public static final String VERSION = "2.8.1"; 120 121 /* 122 * Derivative static fields. 123 */ 124 125 private static final Pattern TILLER_VERSION_PATTERN = Pattern.compile(":v(.+)$"); 126 127 private static final String DEFAULT_IMAGE_NAME = "gcr.io/kubernetes-helm/" + DEFAULT_NAME + ":v" + VERSION; 128 129 private static final String DEFAULT_DEPLOYMENT_NAME = DEFAULT_NAME + "-deploy"; 130 131 private static final String SECRET_NAME = DEFAULT_NAME + "-secret"; 132 133 134 /* 135 * Instance fields. 136 */ 137 138 139 private final KubernetesClient kubernetesClient; 140 141 private final String tillerNamespace; 142 143 144 /* 145 * Constructors. 146 */ 147 148 149 /** 150 * Creates a new {@link TillerInstaller}, using a new {@link 151 * DefaultKubernetesClient}. 152 * 153 * @see #TillerInstaller(KubernetesClient) 154 */ 155 public TillerInstaller() { 156 this(new DefaultKubernetesClient()); 157 } 158 159 /** 160 * Creates a new {@link TillerInstaller}. 161 * 162 * @param kubernetesClient the {@link KubernetesClient} to use to 163 * communicate with Kubernetes; must not be {@code null} 164 * 165 * @exception NullPointerException if {@code kubernetesClient} is 166 * {@code null} 167 */ 168 public TillerInstaller(final KubernetesClient kubernetesClient) { 169 super(); 170 Objects.requireNonNull(kubernetesClient); 171 this.kubernetesClient = kubernetesClient; 172 final String tillerNamespace = System.getenv("TILLER_NAMESPACE"); 173 if (tillerNamespace == null || tillerNamespace.isEmpty()) { 174 this.tillerNamespace = DEFAULT_NAMESPACE; 175 } else { 176 this.tillerNamespace = tillerNamespace; 177 } 178 } 179 180 181 /* 182 * Instance methods. 183 */ 184 185 186 187 public void init() { 188 try { 189 this.init(false, null, null, null, null, null, null, null, false, false, false, null, null, null); 190 } catch (final IOException willNotHappen) { 191 throw new AssertionError(willNotHappen); 192 } 193 } 194 195 public void init(final boolean upgrade) { 196 try { 197 this.init(upgrade, null, null, null, null, null, null, null, false, false, false, null, null, null); 198 } catch (final IOException willNotHappen) { 199 throw new AssertionError(willNotHappen); 200 } 201 } 202 203 /** 204 * Attempts to {@linkplain #install(String, String, String, Map, 205 * String, String, ImagePullPolicy, boolean, boolean, boolean, URI, 206 * URI, URI) install} Tiller into the Kubernetes cluster, silently 207 * returning if Tiller is already installed and {@code upgrade} is 208 * {@code false}, or {@linkplain #upgrade(String, String, String, 209 * String, String, ImagePullPolicy, Map) upgrading} the Tiller 210 * installation if {@code upgrade} is {@code true} and a newer 211 * version of Tiller is available. 212 * 213 * @param upgrade whether or not to attempt an upgrade if Tiller is 214 * already installed 215 * 216 * @param namespace the Kubernetes namespace into which Tiller will 217 * be installed, if it is not already installed; may be {@code null} 218 * in which case a default will be used 219 * 220 * @param deploymentName the name that the Kubernetes Deployment 221 * representing Tiller will have; may be {@code null}; {@code 222 * tiller-deploy} by default 223 * 224 * @param serviceName the name that the Kubernetes Service 225 * representing Tiller will have; may be {@code null}; {@code 226 * tiller-deploy} (yes, {@code tiller-deploy}) by default 227 * 228 * @param labels the Kubernetes Labels that will be applied to 229 * various Kubernetes resources representing Tiller; may be {@code 230 * null} in which case a {@link Map} consisting of a label of {@code 231 * app} with a value of {@code helm} and a label of {@code name} 232 * with a value of {@code tiller} will be used instead 233 * 234 * @param serviceAccountName the name of the Kubernetes Service 235 * Account that Tiller should use; may be {@code null} in which case 236 * the default Service Account will be used instead 237 * 238 * @param imageName the name of the Docker image that contains the 239 * Tiller code; may be {@code null} in which case the Java {@link 240 * String} <code>"gcr.io/kubernetes-helm/tiller:v" + {@value 241 * #VERSION}</code> will be used instead 242 * 243 * @param imagePullPolicy an {@link ImagePullPolicy} specifying how 244 * the Tiller image should be pulled; may be {@code null} in which 245 * case {@link ImagePullPolicy#IF_NOT_PRESENT} will be used instead 246 * 247 * @param hostNetwork the value to be used for the {@linkplain 248 * PodSpec#setHostNetwork(Boolean) <code>hostNetwork</code> 249 * property} of the Tiller Pod's {@link PodSpec} 250 * 251 * @param tls whether Tiller's conversations with Kubernetes will be 252 * encrypted using TLS 253 * 254 * @param verifyTls whether, if and only if {@code tls} is {@code 255 * true}, additional TLS-related verification will be performed 256 * 257 * @param tlsKeyUri a {@link URI} to the public key used during TLS 258 * communication with Kubernetes; may be {@code null} if {@code tls} 259 * is {@code false} 260 * 261 * @param tlsCertUri a {@link URI} to the certificate used during 262 * TLS communication with Kubernetes; may be {@code null} if {@code 263 * tls} is {@code false} 264 * 265 * @param tlsCaCertUri a {@link URI} to the certificate authority 266 * used during TLS communication with Kubernetes; may be {@code 267 * null} if {@code tls} is {@code false} 268 * 269 * @exception IOException if a communication error occurs 270 * 271 * @see #init(boolean, String, String, String, Map, Map, String, 272 * String, ImagePullPolicy, int, boolean, boolean, boolean, URI, 273 * URI, URI) 274 * 275 * @see #install(String, String, String, Map, Map, String, String, 276 * ImagePullPolicy, int, boolean, boolean, boolean, URI, URI, URI) 277 * 278 * @see #upgrade(String, String, String, String, String, 279 * ImagePullPolicy, Map) 280 */ 281 public void init(final boolean upgrade, 282 String namespace, 283 String deploymentName, 284 String serviceName, 285 Map<String, String> labels, 286 String serviceAccountName, 287 String imageName, 288 final ImagePullPolicy imagePullPolicy, 289 final boolean hostNetwork, 290 final boolean tls, 291 final boolean verifyTls, 292 final URI tlsKeyUri, 293 final URI tlsCertUri, 294 final URI tlsCaCertUri) 295 throws IOException { 296 this.init(upgrade, 297 namespace, 298 deploymentName, 299 serviceName, 300 labels, 301 null, 302 serviceAccountName, 303 imageName, 304 imagePullPolicy, 305 0, 306 hostNetwork, 307 tls, 308 verifyTls, 309 tlsKeyUri, 310 tlsCertUri, 311 tlsCaCertUri); 312 } 313 314 /** 315 * Attempts to {@linkplain #install(String, String, String, Map, 316 * String, String, ImagePullPolicy, boolean, boolean, boolean, URI, 317 * URI, URI) install} Tiller into the Kubernetes cluster, silently 318 * returning if Tiller is already installed and {@code upgrade} is 319 * {@code false}, or {@linkplain #upgrade(String, String, String, 320 * String, String, ImagePullPolicy, Map) upgrading} the Tiller 321 * installation if {@code upgrade} is {@code true} and a newer 322 * version of Tiller is available. 323 * 324 * @param upgrade whether or not to attempt an upgrade if Tiller is 325 * already installed 326 * 327 * @param namespace the Kubernetes namespace into which Tiller will 328 * be installed, if it is not already installed; may be {@code null} 329 * in which case a default will be used 330 * 331 * @param deploymentName the name that the Kubernetes Deployment 332 * representing Tiller will have; may be {@code null}; {@code 333 * tiller-deploy} by default 334 * 335 * @param serviceName the name that the Kubernetes Service 336 * representing Tiller will have; may be {@code null}; {@code 337 * tiller-deploy} (yes, {@code tiller-deploy}) by default 338 * 339 * @param labels the Kubernetes Labels that will be applied to 340 * various Kubernetes resources representing Tiller; may be {@code 341 * null} in which case a {@link Map} consisting of a label of {@code 342 * app} with a value of {@code helm} and a label of {@code name} 343 * with a value of {@code tiller} will be used instead 344 * 345 * @param nodeSelector a {@link Map} representing labels that will 346 * be written as a node selector; may be {@code null} 347 * 348 * @param serviceAccountName the name of the Kubernetes Service 349 * Account that Tiller should use; may be {@code null} in which case 350 * the default Service Account will be used instead 351 * 352 * @param imageName the name of the Docker image that contains the 353 * Tiller code; may be {@code null} in which case the Java {@link 354 * String} <code>"gcr.io/kubernetes-helm/tiller:v" + {@value 355 * #VERSION}</code> will be used instead 356 * 357 * @param imagePullPolicy an {@link ImagePullPolicy} specifying how 358 * the Tiller image should be pulled; may be {@code null} in which 359 * case {@link ImagePullPolicy#IF_NOT_PRESENT} will be used instead 360 * 361 * @param maxHistory the maximum number of release versions stored 362 * per release; a value that is less than or equal to zero means 363 * there is effectively no limit 364 * 365 * @param hostNetwork the value to be used for the {@linkplain 366 * PodSpec#setHostNetwork(Boolean) <code>hostNetwork</code> 367 * property} of the Tiller Pod's {@link PodSpec} 368 * 369 * @param tls whether Tiller's conversations with Kubernetes will be 370 * encrypted using TLS 371 * 372 * @param verifyTls whether, if and only if {@code tls} is {@code 373 * true}, additional TLS-related verification will be performed 374 * 375 * @param tlsKeyUri a {@link URI} to the public key used during TLS 376 * communication with Kubernetes; may be {@code null} if {@code tls} 377 * is {@code false} 378 * 379 * @param tlsCertUri a {@link URI} to the certificate used during 380 * TLS communication with Kubernetes; may be {@code null} if {@code 381 * tls} is {@code false} 382 * 383 * @param tlsCaCertUri a {@link URI} to the certificate authority 384 * used during TLS communication with Kubernetes; may be {@code 385 * null} if {@code tls} is {@code false} 386 * 387 * @exception IOException if a communication error occurs 388 * 389 * @see #install(String, String, String, Map, Map, String, String, 390 * ImagePullPolicy, int, boolean, boolean, boolean, URI, URI, URI) 391 * 392 * @see #upgrade(String, String, String, String, String, 393 * ImagePullPolicy, Map) 394 */ 395 public void init(final boolean upgrade, 396 String namespace, 397 String deploymentName, 398 String serviceName, 399 Map<String, String> labels, 400 Map<String, String> nodeSelector, 401 String serviceAccountName, 402 String imageName, 403 final ImagePullPolicy imagePullPolicy, 404 final int maxHistory, 405 final boolean hostNetwork, 406 final boolean tls, 407 final boolean verifyTls, 408 final URI tlsKeyUri, 409 final URI tlsCertUri, 410 final URI tlsCaCertUri) 411 throws IOException { 412 namespace = normalizeNamespace(namespace); 413 deploymentName = normalizeDeploymentName(deploymentName); 414 serviceName = normalizeServiceName(serviceName); 415 labels = normalizeLabels(labels); 416 serviceAccountName = normalizeServiceAccountName(serviceAccountName); 417 imageName = normalizeImageName(imageName); 418 419 try { 420 this.install(namespace, 421 deploymentName, 422 serviceName, 423 labels, 424 nodeSelector, 425 serviceAccountName, 426 imageName, 427 imagePullPolicy, 428 maxHistory, 429 hostNetwork, 430 tls, 431 verifyTls, 432 tlsKeyUri, 433 tlsCertUri, 434 tlsCaCertUri); 435 } catch (final KubernetesClientException kubernetesClientException) { 436 final Status status = kubernetesClientException.getStatus(); 437 if (status == null || !"AlreadyExists".equals(status.getReason())) { 438 throw kubernetesClientException; 439 } else if (upgrade) { 440 this.upgrade(namespace, 441 deploymentName, 442 serviceName, 443 serviceAccountName, 444 imageName, 445 imagePullPolicy, 446 labels); 447 } 448 } 449 } 450 451 public void install() { 452 try { 453 this.install(null, null, null, null, null, null, null, null, 0, false, false, false, null, null, null); 454 } catch (final IOException willNotHappen) { 455 throw new AssertionError(willNotHappen); 456 } 457 } 458 459 public void install(String namespace, 460 final String deploymentName, 461 final String serviceName, 462 Map<String, String> labels, 463 final String serviceAccountName, 464 final String imageName, 465 final ImagePullPolicy imagePullPolicy, 466 final boolean hostNetwork, 467 final boolean tls, 468 final boolean verifyTls, 469 final URI tlsKeyUri, 470 final URI tlsCertUri, 471 final URI tlsCaCertUri) 472 throws IOException { 473 this.install(namespace, 474 deploymentName, 475 serviceName, 476 labels, 477 null, 478 serviceAccountName, 479 imageName, 480 imagePullPolicy, 481 0, // maxHistory 482 hostNetwork, 483 tls, 484 verifyTls, 485 tlsKeyUri, 486 tlsCertUri, 487 tlsCaCertUri); 488 } 489 490 public void install(String namespace, 491 final String deploymentName, 492 final String serviceName, 493 Map<String, String> labels, 494 final Map<String, String> nodeSelector, 495 final String serviceAccountName, 496 final String imageName, 497 final ImagePullPolicy imagePullPolicy, 498 final int maxHistory, 499 final boolean hostNetwork, 500 final boolean tls, 501 final boolean verifyTls, 502 final URI tlsKeyUri, 503 final URI tlsCertUri, 504 final URI tlsCaCertUri) 505 throws IOException { 506 namespace = normalizeNamespace(namespace); 507 labels = normalizeLabels(labels); 508 final Deployment deployment = 509 this.createDeployment(namespace, 510 normalizeDeploymentName(deploymentName), 511 labels, 512 nodeSelector, 513 normalizeServiceAccountName(serviceAccountName), 514 normalizeImageName(imageName), 515 imagePullPolicy, 516 maxHistory, 517 hostNetwork, 518 tls, 519 verifyTls); 520 521 this.kubernetesClient.extensions().deployments().inNamespace(namespace).create(deployment); 522 523 final Service service = this.createService(namespace, normalizeServiceName(serviceName), labels); 524 this.kubernetesClient.services().inNamespace(namespace).create(service); 525 526 if (tls) { 527 final Secret secret = 528 this.createSecret(namespace, 529 tlsKeyUri, 530 tlsCertUri, 531 tlsCaCertUri, 532 labels); 533 this.kubernetesClient.secrets().inNamespace(namespace).create(secret); 534 } 535 536 } 537 538 public void upgrade() { 539 this.upgrade(null, null, null, null, null, null, null, false); 540 } 541 542 public void upgrade(String namespace, 543 final String deploymentName, 544 String serviceName, 545 final String serviceAccountName, 546 final String imageName, 547 final ImagePullPolicy imagePullPolicy, 548 final Map<String, String> labels) { 549 this.upgrade(namespace, 550 deploymentName, 551 serviceName, 552 serviceAccountName, 553 imageName, 554 imagePullPolicy, 555 labels, 556 false); 557 } 558 559 public void upgrade(String namespace, 560 final String deploymentName, 561 String serviceName, 562 final String serviceAccountName, 563 final String imageName, 564 final ImagePullPolicy imagePullPolicy, 565 final Map<String, String> labels, 566 final boolean force) { 567 namespace = normalizeNamespace(namespace); 568 serviceName = normalizeServiceName(serviceName); 569 570 final Resource<Deployment, DoneableDeployment> resource = this.kubernetesClient.extensions() 571 .deployments() 572 .inNamespace(namespace) 573 .withName(normalizeDeploymentName(deploymentName)); 574 assert resource != null; 575 576 if (!force) { 577 final String serverTillerImage = resource.get().getSpec().getTemplate().getSpec().getContainers().get(0).getImage(); 578 assert serverTillerImage != null; 579 580 if (isServerTillerVersionGreaterThanClientTillerVersion(serverTillerImage)) { 581 throw new IllegalStateException(serverTillerImage + " is newer than " + VERSION + "; use force=true to force downgrade"); 582 } 583 } 584 585 resource.edit() 586 .editSpec() 587 .editTemplate() 588 .editSpec() 589 .editContainer(0) 590 .withImage(normalizeImageName(imageName)) 591 .withImagePullPolicy(normalizeImagePullPolicy(imagePullPolicy)) 592 .and() 593 .withServiceAccountName(normalizeServiceAccountName(serviceAccountName)) 594 .endSpec() 595 .endTemplate() 596 .endSpec() 597 .done(); 598 599 // TODO: this way of emulating install.go's check to see if the 600 // Service exists...not sure it's right 601 final Service service = this.kubernetesClient.services() 602 .inNamespace(namespace) 603 .withName(serviceName) 604 .get(); 605 if (service == null) { 606 this.createService(namespace, serviceName, normalizeLabels(labels)); 607 } 608 609 } 610 611 protected Service createService(final String namespace, 612 final String serviceName, 613 Map<String, String> labels) { 614 labels = normalizeLabels(labels); 615 616 final Service service = new Service(); 617 618 final ObjectMeta metadata = new ObjectMeta(); 619 metadata.setNamespace(normalizeNamespace(namespace)); 620 metadata.setName(normalizeServiceName(serviceName)); 621 metadata.setLabels(labels); 622 623 service.setMetadata(metadata); 624 service.setSpec(this.createServiceSpec(labels)); 625 626 return service; 627 } 628 629 protected Deployment createDeployment(String namespace, 630 final String deploymentName, 631 Map<String, String> labels, 632 final String serviceAccountName, 633 final String imageName, 634 final ImagePullPolicy imagePullPolicy, 635 final boolean hostNetwork, 636 final boolean tls, 637 final boolean verifyTls) { 638 return this.createDeployment(namespace, 639 deploymentName, 640 labels, 641 null, 642 serviceAccountName, 643 imageName, 644 imagePullPolicy, 645 0, 646 hostNetwork, 647 tls, 648 verifyTls); 649 } 650 651 protected Deployment createDeployment(String namespace, 652 final String deploymentName, 653 Map<String, String> labels, 654 final Map<String, String> nodeSelector, 655 final String serviceAccountName, 656 final String imageName, 657 final ImagePullPolicy imagePullPolicy, 658 final int maxHistory, 659 final boolean hostNetwork, 660 final boolean tls, 661 final boolean verifyTls) { 662 namespace = normalizeNamespace(namespace); 663 labels = normalizeLabels(labels); 664 665 final Deployment deployment = new Deployment(); 666 667 final ObjectMeta metadata = new ObjectMeta(); 668 metadata.setNamespace(namespace); 669 metadata.setName(normalizeDeploymentName(deploymentName)); 670 metadata.setLabels(labels); 671 deployment.setMetadata(metadata); 672 673 deployment.setSpec(this.createDeploymentSpec(labels, 674 nodeSelector, 675 serviceAccountName, 676 imageName, 677 imagePullPolicy, 678 maxHistory, 679 namespace, 680 hostNetwork, 681 tls, 682 verifyTls)); 683 return deployment; 684 } 685 686 protected Secret createSecret(final String namespace, 687 final URI tlsKeyUri, 688 final URI tlsCertUri, 689 final URI tlsCaCertUri, 690 final Map<String, String> labels) 691 throws IOException { 692 693 final Secret secret = new Secret(); 694 secret.setType("Opaque"); 695 696 final Map<String, String> secretData = new HashMap<>(); 697 698 try (final InputStream tlsKeyStream = read(tlsKeyUri)) { 699 if (tlsKeyStream != null) { 700 secretData.put("tls.key", Base64.getEncoder().encodeToString(toByteArray(tlsKeyStream))); 701 } 702 } 703 704 try (final InputStream tlsCertStream = read(tlsCertUri)) { 705 if (tlsCertStream != null) { 706 secretData.put("tls.crt", Base64.getEncoder().encodeToString(toByteArray(tlsCertStream))); 707 } 708 } 709 710 try (final InputStream tlsCaCertStream = read(tlsCaCertUri)) { 711 if (tlsCaCertStream != null) { 712 secretData.put("ca.crt", Base64.getEncoder().encodeToString(toByteArray(tlsCaCertStream))); 713 } 714 } 715 716 secret.setData(secretData); 717 718 final ObjectMeta metadata = new ObjectMeta(); 719 metadata.setNamespace(normalizeNamespace(namespace)); 720 metadata.setName(SECRET_NAME); 721 metadata.setLabels(normalizeLabels(labels)); 722 secret.setMetadata(metadata); 723 724 return secret; 725 } 726 727 protected DeploymentSpec createDeploymentSpec(final Map<String, String> labels, 728 final String serviceAccountName, 729 final String imageName, 730 final ImagePullPolicy imagePullPolicy, 731 final String namespace, 732 final boolean hostNetwork, 733 final boolean tls, 734 final boolean verifyTls) { 735 return this.createDeploymentSpec(labels, 736 null, 737 serviceAccountName, 738 imageName, 739 imagePullPolicy, 740 0, 741 namespace, 742 hostNetwork, 743 tls, 744 verifyTls); 745 } 746 747 protected DeploymentSpec createDeploymentSpec(final Map<String, String> labels, 748 final Map<String, String> nodeSelector, 749 final String serviceAccountName, 750 final String imageName, 751 final ImagePullPolicy imagePullPolicy, 752 final int maxHistory, 753 final String namespace, 754 final boolean hostNetwork, 755 final boolean tls, 756 final boolean verifyTls) { 757 758 final DeploymentSpec deploymentSpec = new DeploymentSpec(); 759 final PodTemplateSpec podTemplateSpec = new PodTemplateSpec(); 760 final ObjectMeta metadata = new ObjectMeta(); 761 metadata.setLabels(normalizeLabels(labels)); 762 podTemplateSpec.setMetadata(metadata); 763 final PodSpec podSpec = new PodSpec(); 764 podSpec.setServiceAccountName(normalizeServiceAccountName(serviceAccountName)); 765 podSpec.setContainers(Arrays.asList(this.createContainer(imageName, imagePullPolicy, maxHistory, namespace, tls, verifyTls))); 766 podSpec.setHostNetwork(Boolean.valueOf(hostNetwork)); 767 if (nodeSelector != null && !nodeSelector.isEmpty()) { 768 podSpec.setNodeSelector(nodeSelector); 769 } 770 if (tls) { 771 final Volume volume = new Volume(); 772 volume.setName(DEFAULT_NAME + "-certs"); 773 final SecretVolumeSource secretVolumeSource = new SecretVolumeSource(); 774 secretVolumeSource.setSecretName(SECRET_NAME); 775 volume.setSecret(secretVolumeSource); 776 podSpec.setVolumes(Arrays.asList(volume)); 777 } 778 podTemplateSpec.setSpec(podSpec); 779 deploymentSpec.setTemplate(podTemplateSpec); 780 return deploymentSpec; 781 } 782 783 protected Container createContainer(final String imageName, 784 final ImagePullPolicy imagePullPolicy, 785 final String namespace, 786 final boolean tls, 787 final boolean verifyTls) { 788 return this.createContainer(imageName, imagePullPolicy, 0, namespace, tls, verifyTls); 789 } 790 791 protected Container createContainer(final String imageName, 792 final ImagePullPolicy imagePullPolicy, 793 final int maxHistory, 794 final String namespace, 795 final boolean tls, 796 final boolean verifyTls) { 797 final Container container = new Container(); 798 container.setName(DEFAULT_NAME); 799 container.setImage(normalizeImageName(imageName)); 800 container.setImagePullPolicy(normalizeImagePullPolicy(imagePullPolicy)); 801 802 final List<ContainerPort> containerPorts = new ArrayList<>(2); 803 804 ContainerPort containerPort = new ContainerPort(); 805 containerPort.setContainerPort(Integer.valueOf(44134)); 806 containerPort.setName(DEFAULT_NAME); 807 containerPorts.add(containerPort); 808 809 containerPort = new ContainerPort(); 810 containerPort.setContainerPort(Integer.valueOf(44135)); 811 containerPort.setName("http"); 812 containerPorts.add(containerPort); 813 814 container.setPorts(containerPorts); 815 816 final List<EnvVar> env = new ArrayList<>(); 817 818 final EnvVar tillerNamespace = new EnvVar(); 819 tillerNamespace.setName("TILLER_NAMESPACE"); 820 tillerNamespace.setValue(normalizeNamespace(namespace)); 821 env.add(tillerNamespace); 822 823 final EnvVar tillerHistoryMax = new EnvVar(); 824 tillerHistoryMax.setName("TILLER_HISTORY_MAX"); 825 tillerHistoryMax.setValue(String.valueOf(maxHistory)); 826 env.add(tillerHistoryMax); 827 828 if (tls) { 829 final EnvVar tlsVerify = new EnvVar(); 830 tlsVerify.setName("TILLER_TLS_VERIFY"); 831 tlsVerify.setValue(verifyTls ? "1" : ""); 832 env.add(tlsVerify); 833 834 final EnvVar tlsEnable = new EnvVar(); 835 tlsEnable.setName("TILLER_TLS_ENABLE"); 836 tlsEnable.setValue("1"); 837 env.add(tlsEnable); 838 839 final EnvVar tlsCerts = new EnvVar(); 840 tlsCerts.setName("TILLER_TLS_CERTS"); 841 tlsCerts.setValue(TILLER_TLS_CERTS_PATH); 842 env.add(tlsCerts); 843 } 844 845 container.setEnv(env); 846 847 848 final IntOrString port44135 = new IntOrString(Integer.valueOf(44135)); 849 850 final HTTPGetAction livenessHttpGetAction = new HTTPGetAction(); 851 livenessHttpGetAction.setPath("/liveness"); 852 livenessHttpGetAction.setPort(port44135); 853 final Probe livenessProbe = new Probe(); 854 livenessProbe.setHttpGet(livenessHttpGetAction); 855 livenessProbe.setInitialDelaySeconds(ONE); 856 livenessProbe.setTimeoutSeconds(ONE); 857 container.setLivenessProbe(livenessProbe); 858 859 final HTTPGetAction readinessHttpGetAction = new HTTPGetAction(); 860 readinessHttpGetAction.setPath("/readiness"); 861 readinessHttpGetAction.setPort(port44135); 862 final Probe readinessProbe = new Probe(); 863 readinessProbe.setHttpGet(readinessHttpGetAction); 864 readinessProbe.setInitialDelaySeconds(ONE); 865 readinessProbe.setTimeoutSeconds(ONE); 866 container.setReadinessProbe(readinessProbe); 867 868 if (tls) { 869 final VolumeMount volumeMount = new VolumeMount(); 870 volumeMount.setName(DEFAULT_NAME + "-certs"); 871 volumeMount.setReadOnly(true); 872 volumeMount.setMountPath(TILLER_TLS_CERTS_PATH); 873 container.setVolumeMounts(Arrays.asList(volumeMount)); 874 } 875 876 return container; 877 } 878 879 protected ServiceSpec createServiceSpec(final Map<String, String> labels) { 880 final ServiceSpec serviceSpec = new ServiceSpec(); 881 serviceSpec.setType("ClusterIP"); 882 883 final ServicePort servicePort = new ServicePort(); 884 servicePort.setName(DEFAULT_NAME); 885 servicePort.setPort(Integer.valueOf(44134)); 886 servicePort.setTargetPort(new IntOrString(DEFAULT_NAME)); 887 serviceSpec.setPorts(Arrays.asList(servicePort)); 888 889 serviceSpec.setSelector(normalizeLabels(labels)); 890 return serviceSpec; 891 } 892 893 protected final String normalizeNamespace(String namespace) { 894 if (namespace == null || namespace.isEmpty()) { 895 namespace = this.tillerNamespace; 896 if (namespace == null || namespace.isEmpty()) { 897 namespace = DEFAULT_NAMESPACE; 898 } 899 } 900 return namespace; 901 } 902 903 904 /* 905 * Static methods. 906 */ 907 908 909 protected static final Map<String, String> normalizeLabels(Map<String, String> labels) { 910 if (labels == null) { 911 labels = new HashMap<>(7); 912 } 913 if (!labels.containsKey("app")) { 914 labels.put("app", "helm"); 915 } 916 if (!labels.containsKey("name")) { 917 labels.put("name", DEFAULT_NAME); 918 } 919 return labels; 920 } 921 922 protected static final String normalizeDeploymentName(final String deploymentName) { 923 if (deploymentName == null || deploymentName.isEmpty()) { 924 return DEFAULT_DEPLOYMENT_NAME; 925 } else { 926 return deploymentName; 927 } 928 } 929 930 protected static final String normalizeImageName(final String imageName) { 931 if (imageName == null || imageName.isEmpty()) { 932 return DEFAULT_IMAGE_NAME; 933 } else { 934 return imageName; 935 } 936 } 937 938 private static final String normalizeImagePullPolicy(ImagePullPolicy imagePullPolicy) { 939 if (imagePullPolicy == null) { 940 imagePullPolicy = DEFAULT_IMAGE_PULL_POLICY; 941 } 942 assert imagePullPolicy != null; 943 return imagePullPolicy.toString(); 944 } 945 946 protected static final String normalizeServiceAccountName(final String serviceAccountName) { 947 return serviceAccountName == null ? "" : serviceAccountName; 948 } 949 950 protected static final String normalizeServiceName(final String serviceName) { 951 if (serviceName == null || serviceName.isEmpty()) { 952 return DEFAULT_DEPLOYMENT_NAME; // yes, DEFAULT_*DEPLOYMENT*_NAME 953 } else { 954 return serviceName; 955 } 956 } 957 958 private static final InputStream read(final URI uri) throws IOException { 959 final InputStream returnValue; 960 if (uri == null) { 961 returnValue = null; 962 } else { 963 final URL url = uri.toURL(); 964 assert url != null; 965 final InputStream uriStream = url.openStream(); 966 if (uriStream == null) { 967 returnValue = null; 968 } else if (uriStream instanceof BufferedInputStream) { 969 returnValue = (BufferedInputStream)uriStream; 970 } else { 971 returnValue = new BufferedInputStream(uriStream); 972 } 973 } 974 return returnValue; 975 } 976 977 private static final byte[] toByteArray(final InputStream inputStream) throws IOException { 978 // Interesting historical anecdotes at https://stackoverflow.com/a/1264737/208288. 979 byte[] returnValue = null; 980 if (inputStream != null) { 981 final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 982 returnValue = new byte[4096]; // arbitrary size 983 int bytesRead; 984 while ((bytesRead = inputStream.read(returnValue, 0, returnValue.length)) != -1) { 985 buffer.write(returnValue, 0, bytesRead); 986 } 987 buffer.flush(); 988 returnValue = buffer.toByteArray(); 989 } 990 return returnValue; 991 } 992 993 private static final boolean isServerTillerVersionGreaterThanClientTillerVersion(final String serverTillerImage) { 994 boolean returnValue = false; 995 if (serverTillerImage != null) { 996 final Matcher matcher = TILLER_VERSION_PATTERN.matcher(serverTillerImage); 997 assert matcher != null; 998 if (matcher.find()) { 999 final String versionSpecifier = matcher.group(1); 1000 if (versionSpecifier != null) { 1001 final Version serverTillerVersion = Version.valueOf(versionSpecifier); 1002 assert serverTillerVersion != null; 1003 final Version clientTillerVersion = Version.valueOf(VERSION); 1004 assert clientTillerVersion != null; 1005 returnValue = serverTillerVersion.compareTo(clientTillerVersion) > 0; 1006 } 1007 } 1008 } 1009 return returnValue; 1010 } 1011 1012 1013 1014 /* 1015 * Inner and nested classes. 1016 */ 1017 1018 1019 /** 1020 * An {@code enum} representing valid values for a Kubernetes {@code 1021 * imagePullPolicy} field. 1022 * 1023 * @author <a href="https://about.me/lairdnelson" 1024 * target="_parent">Laird Nelson</a> 1025 */ 1026 public static enum ImagePullPolicy { 1027 1028 1029 /** 1030 * An {@link ImagePullPolicy} indicating that a Docker image 1031 * should always be pulled. 1032 */ 1033 ALWAYS("Always"), 1034 1035 /** 1036 * An {@link ImagePullPolicy} indicating that a Docker image 1037 * should be pulled only if it is not already cached locally. 1038 */ 1039 IF_NOT_PRESENT("IfNotPresent"), 1040 1041 /** 1042 * An {@link ImagePullPolicy} indicating that a Docker image 1043 * should never be pulled. 1044 */ 1045 NEVER("Never"); 1046 1047 /** 1048 * The actual valid Kubernetes value for this {@link 1049 * ImagePullPolicy}. 1050 * 1051 * <p>This field is never {@code null}.</p> 1052 */ 1053 private final String value; 1054 1055 1056 /* 1057 * Constructors. 1058 */ 1059 1060 1061 /** 1062 * Creates a new {@link ImagePullPolicy}. 1063 * 1064 * @param value the valid Kubernetes value for this {@link 1065 * ImagePullPolicy}; must not be {@code null} 1066 * 1067 * @exception NullPointerException if {@code value} is {@code 1068 * null} 1069 */ 1070 ImagePullPolicy(final String value) { 1071 Objects.requireNonNull(value); 1072 this.value = value; 1073 } 1074 1075 1076 /* 1077 * Instance methods. 1078 */ 1079 1080 1081 /** 1082 * Returns the valid Kubernetes value for this {@link 1083 * ImagePullPolicy}. 1084 * 1085 * <p>This method never returns {@code null}.</p> 1086 * 1087 * @return the valid Kubernetes value for this {@link 1088 * ImagePullPolicy}; never {@code null} 1089 */ 1090 @Override 1091 public final String toString() { 1092 return this.value; 1093 } 1094 } 1095 1096}