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