001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
002 *
003 * Copyright © 2017 MicroBean.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
014 * implied.  See the License for the specific language governing
015 * permissions and limitations under the License.
016 */
017package org.microbean.helm;
018
019import java.io.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 io.fabric8.kubernetes.client.DefaultKubernetesClient;
036import io.fabric8.kubernetes.client.KubernetesClient;
037import io.fabric8.kubernetes.client.KubernetesClientException;
038
039import io.fabric8.kubernetes.api.model.Container;
040import io.fabric8.kubernetes.api.model.ContainerPort;
041import io.fabric8.kubernetes.api.model.EnvVar;
042import io.fabric8.kubernetes.api.model.HTTPGetAction;
043import io.fabric8.kubernetes.api.model.IntOrString;
044import io.fabric8.kubernetes.api.model.ObjectMeta;
045import io.fabric8.kubernetes.api.model.PodSpec;
046import io.fabric8.kubernetes.api.model.PodTemplateSpec;
047import io.fabric8.kubernetes.api.model.Probe;
048import io.fabric8.kubernetes.api.model.Secret;
049import io.fabric8.kubernetes.api.model.SecretVolumeSource;
050import io.fabric8.kubernetes.api.model.Service;
051import io.fabric8.kubernetes.api.model.ServicePort;
052import io.fabric8.kubernetes.api.model.ServiceSpec;
053import io.fabric8.kubernetes.api.model.Status;
054import io.fabric8.kubernetes.api.model.Volume;
055import io.fabric8.kubernetes.api.model.VolumeMount;
056
057import io.fabric8.kubernetes.api.model.extensions.Deployment;
058import io.fabric8.kubernetes.api.model.extensions.DeploymentSpec;
059
060import org.microbean.development.annotation.Experimental;
061
062/**
063 * A class that idiomatically but faithfully emulates the
064 * Tiller-installing behavior of the {@code helm init} command.
065 *
066 * <p>In general, this class follows the logic as expressed in <a
067 * href="https://github.com/kubernetes/helm/blob/master/cmd/helm/installer/install.go">the
068 * {@code install.go} source code from the Helm project</a>,
069 * problematic or not.  The intent is to have an installer, usable as
070 * an idiomatic Java library, that behaves just like {@code helm
071 * init}.</p>
072 *
073 * <p><strong>Note:</strong> This class is experimental and its API is
074 * subject to change without notice.</p>
075 *
076 * @author <a href="https://about.me/lairdnelson"
077 * target="_parent">Laird Nelson</a>
078 *
079 * @see #init()
080 *
081 * @see <a
082 * href="https://github.com/kubernetes/helm/blob/master/cmd/helm/installer/install.go">The
083 * <code>install.go</code> source code from the Helm project</a>
084 */
085@Experimental
086public class TillerInstaller {
087
088
089  /*
090   * Static fields.
091   */
092
093  
094  /*
095   * Atomic static fields.
096   */
097  
098  private static final Integer ONE = Integer.valueOf(1);
099
100  private static final ImagePullPolicy DEFAULT_IMAGE_PULL_POLICY = ImagePullPolicy.IF_NOT_PRESENT;
101  
102  private static final String DEFAULT_NAME = "tiller";
103
104  private static final String DEFAULT_NAMESPACE = "kube-system";
105
106  private static final String TILLER_TLS_CERTS_PATH = "/etc/certs";
107
108  /**
109   * The version of Tiller to install.
110   */
111  public static final String VERSION = "2.6.2";
112
113  /*
114   * Derivative static fields.
115   */
116  
117  private static final String DEFAULT_IMAGE_NAME = "gcr.io/kubernetes-helm/" + DEFAULT_NAME + ":v" + VERSION;
118
119  private static final String DEFAULT_DEPLOYMENT_NAME = DEFAULT_NAME + "-deploy";
120
121  private static final String SECRET_NAME = DEFAULT_NAME + "-secret";
122
123
124  /*
125   * Instance fields.
126   */
127
128  
129  private final KubernetesClient kubernetesClient;
130
131  private final String tillerNamespace;
132
133
134  /*
135   * Constructors.
136   */
137
138
139  /**
140   * Creates a new {@link TillerInstaller}, using a new {@link
141   * DefaultKubernetesClient}.
142   *
143   * @see #TillerInstaller(KubernetesClient)
144   */
145  public TillerInstaller() {
146    this(new DefaultKubernetesClient());
147  }
148
149  /**
150   * Creates a new {@link TillerInstaller}.
151   *
152   * @param kubernetesClient the {@link KubernetesClient} to use to
153   * communicate with Kubernetes; must not be {@code null}
154   *
155   * @exception NullPointerException if {@code kubernetesClient} is
156   * {@code null}
157   */
158  public TillerInstaller(final KubernetesClient kubernetesClient) {
159    super();
160    Objects.requireNonNull(kubernetesClient);
161    this.kubernetesClient = kubernetesClient;
162    final String tillerNamespace = System.getenv("TILLER_NAMESPACE");
163    if (tillerNamespace == null || tillerNamespace.isEmpty()) {
164      this.tillerNamespace = DEFAULT_NAMESPACE;
165    } else {
166      this.tillerNamespace = tillerNamespace;
167    }
168  }
169
170
171  /*
172   * Instance methods.
173   */
174  
175
176  
177  public void init() {
178    try {
179      this.init(false, null, null, null, null, null, null, null, false, false, false, null, null, null);
180    } catch (final IOException willNotHappen) {
181      throw new AssertionError(willNotHappen);
182    }
183  }
184  
185  public void init(final boolean upgrade) {
186    try {
187      this.init(upgrade, null, null, null, null, null, null, null, false, false, false, null, null, null);
188    } catch (final IOException willNotHappen) {
189      throw new AssertionError(willNotHappen);
190    }
191  }
192
193  /**
194   * Attempts to {@linkplain #install(String, String, String, Map,
195   * String, String, ImagePullPolicy, boolean, boolean, boolean, URI,
196   * URI, URI) install} Tiller into the Kubernetes cluster, silently
197   * returning if Tiller is already installed and {@code upgrade} is
198   * {@code false}, or {@linkplain #upgrade(String, String, String,
199   * String, String, ImagePullPolicy, Map) upgrading} the Tiller
200   * installation if {@code upgrade} is {@code true} and a newer
201   * version of Tiller is available.
202   *
203   * @param upgrade whether or not to attempt an upgrade if Tiller is
204   * already installed
205   *
206   * @param namespace the Kubernetes namespace into which Tiller will
207   * be installed, if it is not already installed; may be {@code null}
208   * in which case a default will be used
209   *
210   * @param deploymentName the name that the Kubernetes Deployment
211   * representing Tiller will have; may be {@code null}; {@code
212   * tiller-deploy} by default
213   *
214   * @param serviceName the name that the Kubernetes Service
215   * representing Tiller will have; may be {@code null}; {@code
216   * tiller-deploy} (yes, {@code tiller-deploy}) by default
217   *
218   * @param labels the Kubernetes Labels that will be applied to
219   * various Kubernetes resources representing Tiller; may be {@code
220   * null} in which case a {@link Map} consisting of a label of {@code
221   * app} with a value of {@code helm} and a label of {@code name}
222   * with a value of {@code tiller} will be used instead
223   *
224   * @param serviceAccountName the name of the Kubernetes Service
225   * Account that Tiller should use; may be {@code null} in which case
226   * the default Service Account will be used instead
227   *
228   * @param imageName the name of the Docker image that contains the
229   * Tiller code; may be {@code null} in which case the Java {@link
230   * String} <code>"gcr.io/kubernetes-helm/tiller:v" + {@value
231   * #VERSION}</code> will be used instead
232   *
233   * @param imagePullPolicy an {@link ImagePullPolicy} specifying how
234   * the Tiller image should be pulled; may be {@code null} in which
235   * case {@link ImagePullPolicy#IF_NOT_PRESENT} will be used instead
236   *
237   * @param hostNetwork the value to be used for the {@linkplain
238   * PodSpec#setHostNetwork(Boolean) <code>hostNetwork</code>
239   * property} of the Tiller Pod's {@link PodSpec}
240   *
241   * @param tls whether Tiller's conversations with Kubernetes will be
242   * encrypted using TLS
243   *
244   * @param verifyTls whether, if and only if {@code tls} is {@code
245   * true}, additional TLS-related verification will be performed
246   *
247   * @param tlsKeyUri a {@link URI} to the public key used during TLS
248   * communication with Kubernetes; may be {@code null} if {@code tls}
249   * is {@code false}
250   *
251   * @param tlsCertUri a {@link URI} to the certificate used during
252   * TLS communication with Kubernetes; may be {@code null} if {@code
253   * tls} is {@code false}
254   *
255   * @param tlsCaCertUri a {@link URI} to the certificate authority
256   * used during TLS communication with Kubernetes; may be {@code
257   * null} if {@code tls} is {@code false}
258   *
259   * @exception IOException if a communication error occurs
260   *
261   * @see #install(String, String, String, Map, String, String,
262   * ImagePullPolicy, boolean, boolean, boolean, URI, URI, URI)
263   *
264   * @see #upgrade(String, String, String, String, String,
265   * ImagePullPolicy, Map)
266   */
267  public void init(final boolean upgrade,
268                   String namespace,
269                   String deploymentName,
270                   String serviceName,
271                   Map<String, String> labels,
272                   String serviceAccountName,
273                   String imageName,
274                   final ImagePullPolicy imagePullPolicy,
275                   final boolean hostNetwork,
276                   final boolean tls,
277                   final boolean verifyTls,
278                   final URI tlsKeyUri,
279                   final URI tlsCertUri,
280                   final URI tlsCaCertUri)
281    throws IOException {
282    namespace = normalizeNamespace(namespace);
283    deploymentName = normalizeDeploymentName(deploymentName);
284    serviceName = normalizeServiceName(serviceName);
285    labels = normalizeLabels(labels);
286    serviceAccountName = normalizeServiceAccountName(serviceAccountName);
287    imageName = normalizeImageName(imageName);
288    
289    try {
290      this.install(namespace,
291                   deploymentName,
292                   serviceName,
293                   labels,
294                   serviceAccountName,
295                   imageName,
296                   imagePullPolicy,
297                   hostNetwork,
298                   tls,
299                   verifyTls,
300                   tlsKeyUri,
301                   tlsCertUri,
302                   tlsCaCertUri);
303    } catch (final KubernetesClientException kubernetesClientException) {
304      final Status status = kubernetesClientException.getStatus();
305      if (status == null || !"AlreadyExists".equals(status.getReason())) {
306        throw kubernetesClientException;
307      } else if (upgrade) {
308        this.upgrade(namespace,
309                     deploymentName,
310                     serviceName,
311                     serviceAccountName,
312                     imageName,
313                     imagePullPolicy,
314                     labels);
315      }
316    }
317  }
318
319  public void install() {
320    try {
321      this.install(null, null, null, null, null, null, null, false, false, false, null, null, null);
322    } catch (final IOException willNotHappen) {
323      throw new AssertionError(willNotHappen);
324    }
325  }
326
327  public void install(String namespace,
328                      final String deploymentName,
329                      final String serviceName,
330                      Map<String, String> labels,
331                      final String serviceAccountName,
332                      final String imageName,
333                      final ImagePullPolicy imagePullPolicy,
334                      final boolean hostNetwork,
335                      final boolean tls,
336                      final boolean verifyTls,
337                      final URI tlsKeyUri,
338                      final URI tlsCertUri,
339                      final URI tlsCaCertUri)
340    throws IOException {
341    namespace = normalizeNamespace(namespace);
342    labels = normalizeLabels(labels);
343    final Deployment deployment =
344      this.createDeployment(namespace,
345                            normalizeDeploymentName(deploymentName),
346                            labels,
347                            normalizeServiceAccountName(serviceAccountName),
348                            normalizeImageName(imageName),
349                            imagePullPolicy,
350                            hostNetwork,
351                            tls,
352                            verifyTls);
353        
354    this.kubernetesClient.extensions().deployments().inNamespace(namespace).create(deployment);
355
356    final Service service = this.createService(namespace, normalizeServiceName(serviceName), labels);
357    this.kubernetesClient.services().inNamespace(namespace).create(service);
358    
359    if (tls) {
360      final Secret secret =
361        this.createSecret(namespace,
362                          tlsKeyUri,
363                          tlsCertUri,
364                          tlsCaCertUri,
365                          labels);
366      this.kubernetesClient.secrets().inNamespace(namespace).create(secret);
367    }
368    
369  }
370
371  public void upgrade() {
372    this.upgrade(null, null, null, null, null, null, null);
373  }
374  
375  public void upgrade(String namespace,
376                      final String deploymentName,
377                      String serviceName,
378                      final String serviceAccountName,
379                      final String imageName,
380                      final ImagePullPolicy imagePullPolicy,
381                      final Map<String, String> labels) {
382    namespace = normalizeNamespace(namespace);
383    serviceName = normalizeServiceName(serviceName);
384
385    this.kubernetesClient.extensions()
386      .deployments()
387      .inNamespace(namespace)
388      .withName(normalizeDeploymentName(deploymentName))
389      .edit()
390      .editSpec()
391      .editTemplate()
392      .editSpec()
393      .editContainer(0)
394      .withImage(normalizeImageName(imageName))
395      .withImagePullPolicy(normalizeImagePullPolicy(imagePullPolicy))
396      .and()
397      .withServiceAccountName(normalizeServiceAccountName(serviceAccountName))
398      .endSpec()
399      .endTemplate()
400      .endSpec()
401      .done();
402
403    // TODO: this way of emulating install.go's check to see if the
404    // Service exists...not sure it's right
405    final Service service = this.kubernetesClient.services()
406      .inNamespace(namespace)
407      .withName(serviceName)
408      .get();
409    if (service == null) {
410      this.createService(namespace, serviceName, normalizeLabels(labels));
411    }
412    
413  }
414  
415  protected Service createService(final String namespace,
416                                  final String serviceName,
417                                  Map<String, String> labels) {
418    labels = normalizeLabels(labels);
419
420    final Service service = new Service();
421    
422    final ObjectMeta metadata = new ObjectMeta();
423    metadata.setNamespace(normalizeNamespace(namespace));
424    metadata.setName(normalizeServiceName(serviceName));
425    metadata.setLabels(labels);
426    
427    service.setMetadata(metadata);
428    service.setSpec(this.createServiceSpec(labels));
429
430    return service;
431  }
432
433  protected Deployment createDeployment(String namespace,
434                                        final String deploymentName,
435                                        Map<String, String> labels,
436                                        final String serviceAccountName,
437                                        final String imageName,
438                                        final ImagePullPolicy imagePullPolicy,
439                                        final boolean hostNetwork,
440                                        final boolean tls,
441                                        final boolean verifyTls) {
442    namespace = normalizeNamespace(namespace);
443    labels = normalizeLabels(labels);
444
445    final Deployment deployment = new Deployment();
446
447    final ObjectMeta metadata = new ObjectMeta();
448    metadata.setNamespace(namespace);
449    metadata.setName(normalizeDeploymentName(deploymentName));
450    metadata.setLabels(labels);
451    deployment.setMetadata(metadata);
452
453    deployment.setSpec(this.createDeploymentSpec(labels, serviceAccountName, imageName, imagePullPolicy, namespace, hostNetwork, tls, verifyTls));
454    return deployment;
455  }
456
457  protected Secret createSecret(final String namespace,
458                                final URI tlsKeyUri,
459                                final URI tlsCertUri,
460                                final URI tlsCaCertUri,
461                                final Map<String, String> labels)
462    throws IOException {
463    
464    final Secret secret = new Secret();
465    secret.setType("Opaque");
466
467    final Map<String, String> secretData = new HashMap<>();
468    
469    try (final InputStream tlsKeyStream = read(tlsKeyUri)) {
470      if (tlsKeyStream != null) {
471        secretData.put("tls.key", Base64.getEncoder().encodeToString(toByteArray(tlsKeyStream)));
472      }
473    }
474
475    try (final InputStream tlsCertStream = read(tlsCertUri)) {
476      if (tlsCertStream != null) {
477        secretData.put("tls.crt", Base64.getEncoder().encodeToString(toByteArray(tlsCertStream)));
478      }
479    }
480    
481    try (final InputStream tlsCaCertStream = read(tlsCaCertUri)) {
482      if (tlsCaCertStream != null) {
483        secretData.put("ca.crt", Base64.getEncoder().encodeToString(toByteArray(tlsCaCertStream)));
484      }
485    }
486
487    secret.setData(secretData);
488
489    final ObjectMeta metadata = new ObjectMeta();
490    metadata.setNamespace(normalizeNamespace(namespace));
491    metadata.setName(SECRET_NAME);
492    metadata.setLabels(normalizeLabels(labels));
493    secret.setMetadata(metadata);
494    
495    return secret;
496  }
497  
498  protected DeploymentSpec createDeploymentSpec(final Map<String, String> labels,
499                                                final String serviceAccountName,
500                                                final String imageName,
501                                                final ImagePullPolicy imagePullPolicy,
502                                                final String namespace,
503                                                final boolean hostNetwork,
504                                                final boolean tls,
505                                                final boolean verifyTls) {    
506    final DeploymentSpec deploymentSpec = new DeploymentSpec();
507    final PodTemplateSpec podTemplateSpec = new PodTemplateSpec();
508    final ObjectMeta metadata = new ObjectMeta();
509    metadata.setLabels(normalizeLabels(labels));
510    podTemplateSpec.setMetadata(metadata);
511    final PodSpec podSpec = new PodSpec();
512    podSpec.setServiceAccountName(normalizeServiceAccountName(serviceAccountName));
513    podSpec.setContainers(Arrays.asList(this.createContainer(imageName, imagePullPolicy, namespace, tls, verifyTls)));
514    podSpec.setHostNetwork(Boolean.valueOf(hostNetwork));
515    final Map<String, String> nodeSelector = new HashMap<>();
516    nodeSelector.put("beta.kubernetes.io/os", "linux");
517    podSpec.setNodeSelector(nodeSelector);    
518    if (tls) {
519      final Volume volume = new Volume();
520      volume.setName(DEFAULT_NAME + "-certs");
521      final SecretVolumeSource secretVolumeSource = new SecretVolumeSource();
522      secretVolumeSource.setSecretName(SECRET_NAME);
523      volume.setSecret(secretVolumeSource);
524      podSpec.setVolumes(Arrays.asList(volume));
525    }
526    podTemplateSpec.setSpec(podSpec);
527    deploymentSpec.setTemplate(podTemplateSpec);    
528    return deploymentSpec;
529  }
530
531  protected Container createContainer(final String imageName,
532                                      final ImagePullPolicy imagePullPolicy,
533                                      final String namespace,
534                                      final boolean tls,
535                                      final boolean verifyTls) {
536    final Container container = new Container();
537    container.setName(DEFAULT_NAME);
538    container.setImage(normalizeImageName(imageName));
539    container.setImagePullPolicy(normalizeImagePullPolicy(imagePullPolicy));
540
541    final ContainerPort containerPort = new ContainerPort();
542    containerPort.setContainerPort(Integer.valueOf(44134));
543    containerPort.setName(DEFAULT_NAME);
544    container.setPorts(Arrays.asList(containerPort));
545
546    final List<EnvVar> env = new ArrayList<>();
547    
548    final EnvVar tillerNamespace = new EnvVar();
549    tillerNamespace.setName("TILLER_NAMESPACE");
550    tillerNamespace.setValue(normalizeNamespace(namespace));
551    env.add(tillerNamespace);
552
553    if (tls) {
554      final EnvVar tlsVerify = new EnvVar();
555      tlsVerify.setName("TILLER_TLS_VERIFY");
556      tlsVerify.setValue(verifyTls ? "1" : "");
557      env.add(tlsVerify);
558      
559      final EnvVar tlsEnable = new EnvVar();
560      tlsEnable.setName("TILLER_TLS_ENABLE");
561      tlsEnable.setValue("1");
562      env.add(tlsEnable);
563
564      final EnvVar tlsCerts = new EnvVar();
565      tlsCerts.setName("TILLER_TLS_CERTS");
566      tlsCerts.setValue(TILLER_TLS_CERTS_PATH);
567      env.add(tlsCerts);
568    }
569    
570    container.setEnv(env);
571    
572
573    final IntOrString port44135 = new IntOrString(Integer.valueOf(44135));
574    
575    final HTTPGetAction livenessHttpGetAction = new HTTPGetAction();
576    livenessHttpGetAction.setPath("/liveness");
577    livenessHttpGetAction.setPort(port44135);
578    final Probe livenessProbe = new Probe();
579    livenessProbe.setHttpGet(livenessHttpGetAction);
580    livenessProbe.setInitialDelaySeconds(ONE);
581    livenessProbe.setTimeoutSeconds(ONE);
582    container.setLivenessProbe(livenessProbe);
583
584    final HTTPGetAction readinessHttpGetAction = new HTTPGetAction();
585    readinessHttpGetAction.setPath("/readiness");
586    readinessHttpGetAction.setPort(port44135);
587    final Probe readinessProbe = new Probe();
588    readinessProbe.setHttpGet(readinessHttpGetAction);
589    readinessProbe.setInitialDelaySeconds(ONE);
590    readinessProbe.setTimeoutSeconds(ONE);
591    container.setReadinessProbe(readinessProbe);
592
593    if (tls) {
594      final VolumeMount volumeMount = new VolumeMount();
595      volumeMount.setName(DEFAULT_NAME + "-certs");
596      volumeMount.setReadOnly(true);
597      volumeMount.setMountPath(TILLER_TLS_CERTS_PATH);
598      container.setVolumeMounts(Arrays.asList(volumeMount));
599    }
600
601    return container;
602  }
603
604  protected ServiceSpec createServiceSpec(final Map<String, String> labels) {
605    final ServiceSpec serviceSpec = new ServiceSpec();
606    serviceSpec.setType("ClusterIP");
607
608    final ServicePort servicePort = new ServicePort();
609    servicePort.setName(DEFAULT_NAME);
610    servicePort.setPort(Integer.valueOf(44134));
611    servicePort.setTargetPort(new IntOrString(DEFAULT_NAME));
612    serviceSpec.setPorts(Arrays.asList(servicePort));
613
614    serviceSpec.setSelector(normalizeLabels(labels));
615    return serviceSpec;
616  }
617
618  protected final String normalizeNamespace(String namespace) {
619    if (namespace == null || namespace.isEmpty()) {
620      namespace = this.tillerNamespace;
621      if (namespace == null || namespace.isEmpty()) {
622        namespace = DEFAULT_NAMESPACE;
623      }
624    }
625    return namespace;
626  }
627
628
629  /*
630   * Static methods.
631   */
632
633  
634  protected static final Map<String, String> normalizeLabels(Map<String, String> labels) {
635    if (labels == null) {
636      labels = new HashMap<>(7);
637    }
638    if (!labels.containsKey("app")) {
639      labels.put("app", "helm");
640    }
641    if (!labels.containsKey("name")) {
642      labels.put("name", DEFAULT_NAME);
643    }
644    return labels;
645  }
646  
647  protected static final String normalizeDeploymentName(final String deploymentName) {
648    if (deploymentName == null || deploymentName.isEmpty()) {
649      return DEFAULT_DEPLOYMENT_NAME;
650    } else {
651      return deploymentName;
652    }
653  }
654  
655  protected static final String normalizeImageName(final String imageName) {
656    if (imageName == null || imageName.isEmpty()) {
657      return DEFAULT_IMAGE_NAME;
658    } else {
659      return imageName;
660    }
661  }
662  
663  private static final String normalizeImagePullPolicy(ImagePullPolicy imagePullPolicy) {
664    if (imagePullPolicy == null) {
665      imagePullPolicy = DEFAULT_IMAGE_PULL_POLICY;      
666    }
667    assert imagePullPolicy != null;
668    return imagePullPolicy.toString();
669  }
670
671  protected static final String normalizeServiceAccountName(final String serviceAccountName) {
672    return serviceAccountName == null ? "" : serviceAccountName;
673  }
674  
675  protected static final String normalizeServiceName(final String serviceName) {
676    if (serviceName == null || serviceName.isEmpty()) {
677      return DEFAULT_DEPLOYMENT_NAME; // yes, DEFAULT_*DEPLOYMENT*_NAME
678    } else {
679      return serviceName;
680    }
681  }
682
683  private static final InputStream read(final URI uri) throws IOException {
684    final InputStream returnValue;
685    if (uri == null) {
686      returnValue = null;
687    } else {
688      final URL url = uri.toURL();
689      assert url != null;
690      final InputStream uriStream = url.openStream();
691      if (uriStream == null) {
692        returnValue = null;
693      } else if (uriStream instanceof BufferedInputStream) {
694        returnValue = (BufferedInputStream)uriStream;
695      } else {
696        returnValue = new BufferedInputStream(uriStream);
697      }
698    }
699    return returnValue;
700  }
701
702  private static final byte[] toByteArray(final InputStream inputStream) throws IOException {
703    // Interesting historical anecdotes at https://stackoverflow.com/a/1264737/208288.
704    byte[] returnValue = null;
705    if (inputStream != null) {
706      final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
707      returnValue = new byte[4096]; // arbitrary size
708      int bytesRead;
709      while ((bytesRead = inputStream.read(returnValue, 0, returnValue.length)) != -1) {
710        buffer.write(returnValue, 0, bytesRead);
711      }      
712      buffer.flush();      
713      returnValue = buffer.toByteArray();
714    }
715    return returnValue;
716  }
717
718
719  /*
720   * Inner and nested classes.
721   */
722
723
724  /**
725   * An {@code enum} representing valid values for a Kubernetes {@code
726   * imagePullPolicy} field.
727   *
728   * @author <a href="https://about.me/lairdnelson"
729   * target="_parent">Laird Nelson</a>
730   */
731  public static enum ImagePullPolicy {
732
733
734    /**
735     * An {@link ImagePullPolicy} indicating that a Docker image
736     * should always be pulled.
737     */
738    ALWAYS("Always"),
739
740    /**
741     * An {@link ImagePullPolicy} indicating that a Docker image
742     * should be pulled only if it is not already cached locally.
743     */
744    IF_NOT_PRESENT("IfNotPresent"),
745
746    /**
747     * An {@link ImagePullPolicy} indicating that a Docker image
748     * should never be pulled.
749     */
750    NEVER("Never");
751
752    /**
753     * The actual valid Kubernetes value for this {@link
754     * ImagePullPolicy}.
755     *
756     * <p>This field is never {@code null}.</p>
757     */
758    private final String value;
759
760
761    /*
762     * Constructors.
763     */
764
765
766    /**
767     * Creates a new {@link ImagePullPolicy}.
768     *
769     * @param value the valid Kubernetes value for this {@link
770     * ImagePullPolicy}; must not be {@code null}
771     *
772     * @exception NullPointerException if {@code value} is {@code
773     * null}
774     */
775    ImagePullPolicy(final String value) {
776      Objects.requireNonNull(value);
777      this.value = value;
778    }
779
780
781    /*
782     * Instance methods.
783     */
784    
785
786    /**
787     * Returns the valid Kubernetes value for this {@link
788     * ImagePullPolicy}.
789     *
790     * <p>This method never returns {@code null}.</p>
791     *
792     * @return the valid Kubernetes value for this {@link
793     * ImagePullPolicy}; never {@code null}
794     */
795    @Override
796    public final String toString() {
797      return this.value;
798    }
799  }
800  
801}