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&mdash;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&mdash;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}