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} 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 {@code
230   * gcr.io/kubernetes-helm/tiller:v2.5.0} will be used instead
231   *
232   * @param imagePullPolicy an {@link ImagePullPolicy} specifying how
233   * the Tiller image should be pulled; may be {@code null} in which
234   * case {@link ImagePullPolicy#IF_NOT_PRESENT} will be used instead
235   *
236   * @param hostNetwork the value to be used for the {@linkplain
237   * PodSpec#setHostNetwork(Boolean) <code>hostNetwork</code>
238   * property} of the Tiller Pod's {@link PodSpec}
239   *
240   * @param tls whether Tiller's conversations with Kubernetes will be
241   * encrypted using TLS
242   *
243   * @param verifyTls whether, if and only if {@code tls} is {@code
244   * true}, additional TLS-related verification will be performed
245   *
246   * @param tlsKeyUri a {@link URI} to the public key used during TLS
247   * communication with Kubernetes; may be {@code null} if {@code tls}
248   * is {@code false}
249   *
250   * @param tlsCertUri a {@link URI} to the certificate used during
251   * TLS communication with Kubernetes; may be {@code null} if {@code
252   * tls} is {@code false}
253   *
254   * @param tlsCaCertUri a {@link URI} to the certificate authority
255   * used during TLS communication with Kubernetes; may be {@code
256   * null} if {@code tls} is {@code false}
257   *
258   * @exception IOException if a communication error occurs
259   *
260   * @see #install(String, String, String, Map, String, String,
261   * ImagePullPolicy, boolean, boolean, boolean, URI, URI, URI)
262   *
263   * @see #upgrade(String, String, String, String, String,
264   * ImagePullPolicy, Map)
265   */
266  public void init(final boolean upgrade,
267                   String namespace,
268                   String deploymentName,
269                   String serviceName,
270                   Map<String, String> labels,
271                   String serviceAccountName,
272                   String imageName,
273                   final ImagePullPolicy imagePullPolicy,
274                   final boolean hostNetwork,
275                   final boolean tls,
276                   final boolean verifyTls,
277                   final URI tlsKeyUri,
278                   final URI tlsCertUri,
279                   final URI tlsCaCertUri)
280    throws IOException {
281    namespace = normalizeNamespace(namespace);
282    deploymentName = normalizeDeploymentName(deploymentName);
283    serviceName = normalizeServiceName(serviceName);
284    labels = normalizeLabels(labels);
285    serviceAccountName = normalizeServiceAccountName(serviceAccountName);
286    imageName = normalizeImageName(imageName);
287    
288    try {
289      this.install(namespace,
290                   deploymentName,
291                   serviceName,
292                   labels,
293                   serviceAccountName,
294                   imageName,
295                   imagePullPolicy,
296                   hostNetwork,
297                   tls,
298                   verifyTls,
299                   tlsKeyUri,
300                   tlsCertUri,
301                   tlsCaCertUri);
302    } catch (final KubernetesClientException kubernetesClientException) {
303      final Status status = kubernetesClientException.getStatus();
304      if (status == null || !"AlreadyExists".equals(status.getReason())) {
305        throw kubernetesClientException;
306      } else if (upgrade) {
307        this.upgrade(namespace,
308                     deploymentName,
309                     serviceName,
310                     serviceAccountName,
311                     imageName,
312                     imagePullPolicy,
313                     labels);
314      }
315    }
316  }
317
318  public void install() {
319    try {
320      this.install(null, null, null, null, null, null, null, false, false, false, null, null, null);
321    } catch (final IOException willNotHappen) {
322      throw new AssertionError(willNotHappen);
323    }
324  }
325
326  public void install(String namespace,
327                      final String deploymentName,
328                      final String serviceName,
329                      Map<String, String> labels,
330                      final String serviceAccountName,
331                      final String imageName,
332                      final ImagePullPolicy imagePullPolicy,
333                      final boolean hostNetwork,
334                      final boolean tls,
335                      final boolean verifyTls,
336                      final URI tlsKeyUri,
337                      final URI tlsCertUri,
338                      final URI tlsCaCertUri)
339    throws IOException {
340    namespace = normalizeNamespace(namespace);
341    labels = normalizeLabels(labels);
342    final Deployment deployment =
343      this.createDeployment(namespace,
344                            normalizeDeploymentName(deploymentName),
345                            labels,
346                            normalizeServiceAccountName(serviceAccountName),
347                            normalizeImageName(imageName),
348                            imagePullPolicy,
349                            hostNetwork,
350                            tls,
351                            verifyTls);
352        
353    this.kubernetesClient.extensions().deployments().inNamespace(namespace).create(deployment);
354
355    final Service service = this.createService(namespace, normalizeServiceName(serviceName), labels);
356    this.kubernetesClient.services().inNamespace(namespace).create(service);
357    
358    if (tls) {
359      final Secret secret =
360        this.createSecret(namespace,
361                          tlsKeyUri,
362                          tlsCertUri,
363                          tlsCaCertUri,
364                          labels);
365      this.kubernetesClient.secrets().inNamespace(namespace).create(secret);
366    }
367    
368  }
369
370  public void upgrade() {
371    this.upgrade(null, null, null, null, null, null, null);
372  }
373  
374  public void upgrade(String namespace,
375                      final String deploymentName,
376                      String serviceName,
377                      final String serviceAccountName,
378                      final String imageName,
379                      final ImagePullPolicy imagePullPolicy,
380                      final Map<String, String> labels) {
381    namespace = normalizeNamespace(namespace);
382    serviceName = normalizeServiceName(serviceName);
383
384    this.kubernetesClient.extensions()
385      .deployments()
386      .inNamespace(namespace)
387      .withName(normalizeDeploymentName(deploymentName))
388      .edit()
389      .editSpec()
390      .editTemplate()
391      .editSpec()
392      .editContainer(0)
393      .withImage(normalizeImageName(imageName))
394      .withImagePullPolicy(normalizeImagePullPolicy(imagePullPolicy))
395      .and()
396      .withServiceAccountName(normalizeServiceAccountName(serviceAccountName))
397      .endSpec()
398      .endTemplate()
399      .endSpec()
400      .done();
401
402    // TODO: this way of emulating install.go's check to see if the
403    // Service exists...not sure it's right
404    final Service service = this.kubernetesClient.services()
405      .inNamespace(namespace)
406      .withName(serviceName)
407      .get();
408    if (service == null) {
409      this.createService(namespace, serviceName, normalizeLabels(labels));
410    }
411    
412  }
413  
414  protected Service createService(final String namespace,
415                                  final String serviceName,
416                                  Map<String, String> labels) {
417    labels = normalizeLabels(labels);
418
419    final Service service = new Service();
420    
421    final ObjectMeta metadata = new ObjectMeta();
422    metadata.setNamespace(normalizeNamespace(namespace));
423    metadata.setName(normalizeServiceName(serviceName));
424    metadata.setLabels(labels);
425    
426    service.setMetadata(metadata);
427    service.setSpec(this.createServiceSpec(labels));
428
429    return service;
430  }
431
432  protected Deployment createDeployment(String namespace,
433                                        final String deploymentName,
434                                        Map<String, String> labels,
435                                        final String serviceAccountName,
436                                        final String imageName,
437                                        final ImagePullPolicy imagePullPolicy,
438                                        final boolean hostNetwork,
439                                        final boolean tls,
440                                        final boolean verifyTls) {
441    namespace = normalizeNamespace(namespace);
442    labels = normalizeLabels(labels);
443
444    final Deployment deployment = new Deployment();
445
446    final ObjectMeta metadata = new ObjectMeta();
447    metadata.setNamespace(namespace);
448    metadata.setName(normalizeDeploymentName(deploymentName));
449    metadata.setLabels(labels);
450    deployment.setMetadata(metadata);
451
452    deployment.setSpec(this.createDeploymentSpec(labels, serviceAccountName, imageName, imagePullPolicy, namespace, hostNetwork, tls, verifyTls));
453    return deployment;
454  }
455
456  protected Secret createSecret(final String namespace,
457                                final URI tlsKeyUri,
458                                final URI tlsCertUri,
459                                final URI tlsCaCertUri,
460                                final Map<String, String> labels)
461    throws IOException {
462    
463    final Secret secret = new Secret();
464    secret.setType("Opaque");
465
466    final Map<String, String> secretData = new HashMap<>();
467    
468    try (final InputStream tlsKeyStream = read(tlsKeyUri)) {
469      if (tlsKeyStream != null) {
470        secretData.put("tls.key", Base64.getEncoder().encodeToString(toByteArray(tlsKeyStream)));
471      }
472    }
473
474    try (final InputStream tlsCertStream = read(tlsCertUri)) {
475      if (tlsCertStream != null) {
476        secretData.put("tls.crt", Base64.getEncoder().encodeToString(toByteArray(tlsCertStream)));
477      }
478    }
479    
480    try (final InputStream tlsCaCertStream = read(tlsCaCertUri)) {
481      if (tlsCaCertStream != null) {
482        secretData.put("ca.crt", Base64.getEncoder().encodeToString(toByteArray(tlsCaCertStream)));
483      }
484    }
485
486    secret.setData(secretData);
487
488    final ObjectMeta metadata = new ObjectMeta();
489    metadata.setNamespace(normalizeNamespace(namespace));
490    metadata.setName(SECRET_NAME);
491    metadata.setLabels(normalizeLabels(labels));
492    secret.setMetadata(metadata);
493    
494    return secret;
495  }
496  
497  protected DeploymentSpec createDeploymentSpec(final Map<String, String> labels,
498                                                final String serviceAccountName,
499                                                final String imageName,
500                                                final ImagePullPolicy imagePullPolicy,
501                                                final String namespace,
502                                                final boolean hostNetwork,
503                                                final boolean tls,
504                                                final boolean verifyTls) {    
505    final DeploymentSpec deploymentSpec = new DeploymentSpec();
506    final PodTemplateSpec podTemplateSpec = new PodTemplateSpec();
507    final ObjectMeta metadata = new ObjectMeta();
508    metadata.setLabels(normalizeLabels(labels));
509    podTemplateSpec.setMetadata(metadata);
510    final PodSpec podSpec = new PodSpec();
511    podSpec.setServiceAccountName(normalizeServiceAccountName(serviceAccountName));
512    podSpec.setContainers(Arrays.asList(this.createContainer(imageName, imagePullPolicy, namespace, tls, verifyTls)));
513    podSpec.setHostNetwork(Boolean.valueOf(hostNetwork));
514    final Map<String, String> nodeSelector = new HashMap<>();
515    nodeSelector.put("beta.kubernetes.io/os", "linux");
516    podSpec.setNodeSelector(nodeSelector);    
517    if (tls) {
518      final Volume volume = new Volume();
519      volume.setName(DEFAULT_NAME + "-certs");
520      final SecretVolumeSource secretVolumeSource = new SecretVolumeSource();
521      secretVolumeSource.setSecretName(SECRET_NAME);
522      volume.setSecret(secretVolumeSource);
523      podSpec.setVolumes(Arrays.asList(volume));
524    }
525    podTemplateSpec.setSpec(podSpec);
526    deploymentSpec.setTemplate(podTemplateSpec);    
527    return deploymentSpec;
528  }
529
530  protected Container createContainer(final String imageName,
531                                      final ImagePullPolicy imagePullPolicy,
532                                      final String namespace,
533                                      final boolean tls,
534                                      final boolean verifyTls) {
535    final Container container = new Container();
536    container.setName(DEFAULT_NAME);
537    container.setImage(normalizeImageName(imageName));
538    container.setImagePullPolicy(normalizeImagePullPolicy(imagePullPolicy));
539
540    final ContainerPort containerPort = new ContainerPort();
541    containerPort.setContainerPort(Integer.valueOf(44134));
542    containerPort.setName(DEFAULT_NAME);
543    container.setPorts(Arrays.asList(containerPort));
544
545    final List<EnvVar> env = new ArrayList<>();
546    
547    final EnvVar tillerNamespace = new EnvVar();
548    tillerNamespace.setName("TILLER_NAMESPACE");
549    tillerNamespace.setValue(normalizeNamespace(namespace));
550    env.add(tillerNamespace);
551
552    if (tls) {
553      final EnvVar tlsVerify = new EnvVar();
554      tlsVerify.setName("TILLER_TLS_VERIFY");
555      tlsVerify.setValue(verifyTls ? "1" : "");
556      env.add(tlsVerify);
557      
558      final EnvVar tlsEnable = new EnvVar();
559      tlsEnable.setName("TILLER_TLS_ENABLE");
560      tlsEnable.setValue("1");
561      env.add(tlsEnable);
562
563      final EnvVar tlsCerts = new EnvVar();
564      tlsCerts.setName("TILLER_TLS_CERTS");
565      tlsCerts.setValue(TILLER_TLS_CERTS_PATH);
566      env.add(tlsCerts);
567    }
568    
569    container.setEnv(env);
570    
571
572    final IntOrString port44135 = new IntOrString(Integer.valueOf(44135));
573    
574    final HTTPGetAction livenessHttpGetAction = new HTTPGetAction();
575    livenessHttpGetAction.setPath("/liveness");
576    livenessHttpGetAction.setPort(port44135);
577    final Probe livenessProbe = new Probe();
578    livenessProbe.setHttpGet(livenessHttpGetAction);
579    livenessProbe.setInitialDelaySeconds(ONE);
580    livenessProbe.setTimeoutSeconds(ONE);
581    container.setLivenessProbe(livenessProbe);
582
583    final HTTPGetAction readinessHttpGetAction = new HTTPGetAction();
584    readinessHttpGetAction.setPath("/readiness");
585    readinessHttpGetAction.setPort(port44135);
586    final Probe readinessProbe = new Probe();
587    readinessProbe.setHttpGet(readinessHttpGetAction);
588    readinessProbe.setInitialDelaySeconds(ONE);
589    readinessProbe.setTimeoutSeconds(ONE);
590    container.setReadinessProbe(readinessProbe);
591
592    if (tls) {
593      final VolumeMount volumeMount = new VolumeMount();
594      volumeMount.setName(DEFAULT_NAME + "-certs");
595      volumeMount.setReadOnly(true);
596      volumeMount.setMountPath(TILLER_TLS_CERTS_PATH);
597      container.setVolumeMounts(Arrays.asList(volumeMount));
598    }
599
600    return container;
601  }
602
603  protected ServiceSpec createServiceSpec(final Map<String, String> labels) {
604    final ServiceSpec serviceSpec = new ServiceSpec();
605    serviceSpec.setType("ClusterIP");
606
607    final ServicePort servicePort = new ServicePort();
608    servicePort.setName(DEFAULT_NAME);
609    servicePort.setPort(Integer.valueOf(44134));
610    servicePort.setTargetPort(new IntOrString(DEFAULT_NAME));
611    serviceSpec.setPorts(Arrays.asList(servicePort));
612
613    serviceSpec.setSelector(normalizeLabels(labels));
614    return serviceSpec;
615  }
616
617  protected final String normalizeNamespace(String namespace) {
618    if (namespace == null || namespace.isEmpty()) {
619      namespace = this.tillerNamespace;
620      if (namespace == null || namespace.isEmpty()) {
621        namespace = DEFAULT_NAMESPACE;
622      }
623    }
624    return namespace;
625  }
626
627
628  /*
629   * Static methods.
630   */
631
632  
633  protected static final Map<String, String> normalizeLabels(Map<String, String> labels) {
634    if (labels == null) {
635      labels = new HashMap<>(7);
636    }
637    if (!labels.containsKey("app")) {
638      labels.put("app", "helm");
639    }
640    if (!labels.containsKey("name")) {
641      labels.put("name", DEFAULT_NAME);
642    }
643    return labels;
644  }
645  
646  protected static final String normalizeDeploymentName(final String deploymentName) {
647    if (deploymentName == null || deploymentName.isEmpty()) {
648      return DEFAULT_DEPLOYMENT_NAME;
649    } else {
650      return deploymentName;
651    }
652  }
653  
654  protected static final String normalizeImageName(final String imageName) {
655    if (imageName == null || imageName.isEmpty()) {
656      return DEFAULT_IMAGE_NAME;
657    } else {
658      return imageName;
659    }
660  }
661  
662  private static final String normalizeImagePullPolicy(ImagePullPolicy imagePullPolicy) {
663    if (imagePullPolicy == null) {
664      imagePullPolicy = DEFAULT_IMAGE_PULL_POLICY;      
665    }
666    assert imagePullPolicy != null;
667    return imagePullPolicy.toString();
668  }
669
670  protected static final String normalizeServiceAccountName(final String serviceAccountName) {
671    return serviceAccountName == null ? "" : serviceAccountName;
672  }
673  
674  protected static final String normalizeServiceName(final String serviceName) {
675    if (serviceName == null || serviceName.isEmpty()) {
676      return DEFAULT_DEPLOYMENT_NAME; // yes, DEFAULT_*DEPLOYMENT*_NAME
677    } else {
678      return serviceName;
679    }
680  }
681
682  private static final InputStream read(final URI uri) throws IOException {
683    final InputStream returnValue;
684    if (uri == null) {
685      returnValue = null;
686    } else {
687      final URL url = uri.toURL();
688      assert url != null;
689      final InputStream uriStream = url.openStream();
690      if (uriStream == null) {
691        returnValue = null;
692      } else if (uriStream instanceof BufferedInputStream) {
693        returnValue = (BufferedInputStream)uriStream;
694      } else {
695        returnValue = new BufferedInputStream(uriStream);
696      }
697    }
698    return returnValue;
699  }
700
701  private static final byte[] toByteArray(final InputStream inputStream) throws IOException {
702    // Interesting historical anecdotes at https://stackoverflow.com/a/1264737/208288.
703    byte[] returnValue = null;
704    if (inputStream != null) {
705      final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
706      returnValue = new byte[4096]; // arbitrary size
707      int bytesRead;
708      while ((bytesRead = inputStream.read(returnValue, 0, returnValue.length)) != -1) {
709        buffer.write(returnValue, 0, bytesRead);
710      }      
711      buffer.flush();      
712      returnValue = buffer.toByteArray();
713    }
714    return returnValue;
715  }
716
717
718  /*
719   * Inner and nested classes.
720   */
721
722
723  /**
724   * An {@code enum} representing valid values for a Kubernetes {@code
725   * imagePullPolicy} field.
726   *
727   * @author <a href="https://about.me/lairdnelson"
728   * target="_parent">Laird Nelson</a>
729   */
730  public static enum ImagePullPolicy {
731
732
733    /**
734     * An {@link ImagePullPolicy} indicating that a Docker image
735     * should always be pulled.
736     */
737    ALWAYS("Always"),
738
739    /**
740     * An {@link ImagePullPolicy} indicating that a Docker image
741     * should be pulled only if it is not already cached locally.
742     */
743    IF_NOT_PRESENT("IfNotPresent"),
744
745    /**
746     * An {@link ImagePullPolicy} indicating that a Docker image
747     * should never be pulled.
748     */
749    NEVER("Never");
750
751    /**
752     * The actual valid Kubernetes value for this {@link
753     * ImagePullPolicy}.
754     *
755     * <p>This field is never {@code null}.</p>
756     */
757    private final String value;
758
759
760    /*
761     * Constructors.
762     */
763
764
765    /**
766     * Creates a new {@link ImagePullPolicy}.
767     *
768     * @param value the valid Kubernetes value for this {@link
769     * ImagePullPolicy}; must not be {@code null}
770     *
771     * @exception NullPointerException if {@code value} is {@code
772     * null}
773     */
774    ImagePullPolicy(final String value) {
775      Objects.requireNonNull(value);
776      this.value = value;
777    }
778
779
780    /*
781     * Instance methods.
782     */
783    
784
785    /**
786     * Returns the valid Kubernetes value for this {@link
787     * ImagePullPolicy}.
788     *
789     * <p>This method never returns {@code null}.</p>
790     *
791     * @return the valid Kubernetes value for this {@link
792     * ImagePullPolicy}; never {@code null}
793     */
794    @Override
795    public final String toString() {
796      return this.value;
797    }
798  }
799  
800}