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