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.Closeable;
020import java.io.IOException;
021
022import java.net.InetAddress;
023import java.net.MalformedURLException;
024
025import java.util.Collections;
026import java.util.HashMap;
027import java.util.Map;
028import java.util.Objects;
029
030import java.util.concurrent.TimeUnit;
031
032import hapi.services.tiller.ReleaseServiceGrpc;
033import hapi.services.tiller.ReleaseServiceGrpc.ReleaseServiceBlockingStub;
034import hapi.services.tiller.ReleaseServiceGrpc.ReleaseServiceFutureStub;
035import hapi.services.tiller.ReleaseServiceGrpc.ReleaseServiceStub;
036import hapi.services.tiller.Tiller.GetVersionResponse;
037
038import hapi.version.VersionOuterClass.VersionOrBuilder;
039
040import io.fabric8.kubernetes.client.Config;
041import io.fabric8.kubernetes.client.ConfigAware;
042import io.fabric8.kubernetes.client.DefaultKubernetesClient; // for javadoc only
043import io.fabric8.kubernetes.client.HttpClientAware;
044import io.fabric8.kubernetes.client.KubernetesClient;
045import io.fabric8.kubernetes.client.KubernetesClientException; // for javadoc only
046import io.fabric8.kubernetes.client.LocalPortForward;
047
048import io.grpc.ManagedChannel;
049import io.grpc.ManagedChannelBuilder;
050import io.grpc.Metadata;
051
052import io.grpc.health.v1.HealthGrpc;
053import io.grpc.health.v1.HealthGrpc.HealthBlockingStub;
054import io.grpc.health.v1.HealthGrpc.HealthFutureStub;
055import io.grpc.health.v1.HealthGrpc.HealthStub;
056
057import io.grpc.stub.MetadataUtils;
058
059import okhttp3.OkHttpClient;
060
061import org.microbean.development.annotation.Issue;
062
063import org.microbean.kubernetes.Pods;
064
065/**
066 * A convenience class for communicating with a <a
067 * href="https://docs.helm.sh/glossary/#tiller"
068 * target="_parent">Tiller server</a>.
069 *
070 * @author <a href="https://about.me/lairdnelson"
071 * target="_parent">Laird Nelson</a>
072 *
073 * @see ReleaseServiceGrpc
074 */
075public class Tiller implements ConfigAware<Config>, Closeable {
076
077
078  /*
079   * Static fields.
080   */
081
082
083  /**
084   * The version of Tiller {@link Tiller} instances expect.
085   *
086   * <p>This field is never {@code null}.</p>
087   */
088  public static final String VERSION = "2.8.2";
089
090  /**
091   * The Kubernetes namespace into which Tiller server instances are
092   * most commonly installed.
093   *
094   * <p>This field is never {@code null}.</p>
095   */
096  public static final String DEFAULT_NAMESPACE = "kube-system";
097
098  /**
099   * The port on which Tiller server instances most commonly listen.
100   */
101  public static final int DEFAULT_PORT = 44134;
102
103  /**
104   * The Kubernetes labels with which most Tiller instances are
105   * annotated.
106   *
107   * <p>This field is never {@code null}.</p>
108   */
109  public static final Map<String, String> DEFAULT_LABELS;
110
111  /**
112   * The maximum size, in bytes, that messages destined for Tiller may
113   * be.
114   */
115  public static final int MAX_MESSAGE_SIZE = 20 * 1024 * 1024;
116  
117  /**
118   * A {@link Metadata} that ensures that certain Tiller-related
119   * headers are passed with every gRPC call.
120   *
121   * <p>This field is never {@code null}.</p>
122   */
123  private static final Metadata metadata = new Metadata();
124
125
126  /*
127   * Static initializer.
128   */
129  
130
131  /**
132   * Static initializer; initializes the {@link #DEFAULT_LABELS}
133   * {@code static} field (among others).
134   */
135  static {
136    final Map<String, String> labels = new HashMap<>();
137    labels.put("name", "tiller");
138    labels.put("app", "helm");
139    DEFAULT_LABELS = Collections.unmodifiableMap(labels);
140    metadata.put(Metadata.Key.of("x-helm-api-client", Metadata.ASCII_STRING_MARSHALLER), VERSION);
141  }
142
143
144  /*
145   * Instance fields.
146   */
147
148
149  /**
150   * The {@link Config} available at construction time.
151   *
152   * <p>This field may be {@code null}.</p>
153   *
154   * @see #getConfiguration()
155   */
156  private final Config config;
157
158  /**
159   * The {@link LocalPortForward} being used to communicate (most
160   * commonly) with a Kubernetes pod housing a Tiller server.
161   *
162   * <p>This field may be {@code null}.</p>
163   *
164   * @see #Tiller(LocalPortForward)
165   */
166  private final LocalPortForward portForward;
167
168  /**
169   * The {@link ManagedChannel} over which communications with a
170   * Tiller server will be conducted.
171   *
172   * <p>This field is never {@code null}.</p>
173   */
174  private final ManagedChannel channel;
175
176
177  /*
178   * Constructors.
179   */
180
181
182  /**
183   * Creates a new {@link Tiller} that will use the supplied {@link
184   * ManagedChannel} for communication.
185   *
186   * @param channel the {@link ManagedChannel} over which
187   * communications will be conducted; must not be {@code null}
188   *
189   * @exception NullPointerException if {@code channel} is {@code
190   * null}
191   */
192  public Tiller(final ManagedChannel channel) {
193    super();
194    Objects.requireNonNull(channel);
195    this.config = null;
196    this.portForward = null;
197    this.channel = channel;
198  }
199
200  /**
201   * Creates a new {@link Tiller} that will use information from the
202   * supplied {@link LocalPortForward} to establish a communications
203   * channel with the Tiller server.
204   *
205   * @param portForward the {@link LocalPortForward} to use; must not
206   * be {@code null}
207   *
208   * @exception NullPointerException if {@code portForward} is {@code
209   * null}
210   */
211  public Tiller(final LocalPortForward portForward) {
212    super();
213    Objects.requireNonNull(portForward);
214    this.config = null;
215    this.portForward = null; // yes, null
216    this.channel = this.buildChannel(portForward);
217  }
218
219  /**
220   * Creates a new {@link Tiller} that will forward a local port to
221   * port {@code 44134} on a Pod housing Tiller in the {@code
222   * kube-system} namespace running in the Kubernetes cluster with
223   * which the supplied {@link KubernetesClient} is capable of
224   * communicating.
225   *
226   * <p>The {@linkplain Pods#getFirstReadyPod(Listable) first ready
227   * Pod} with a {@code name} label whose value is {@code tiller} and
228   * with an {@code app} label whose value is {@code helm} is deemed
229   * to be the pod housing the Tiller instance to connect to.  (This
230   * duplicates the default logic of the {@code helm} command line
231   * executable.)</p>
232   *
233   * @param <T> a {@link KubernetesClient} implementation that is also
234   * an {@link HttpClientAware} implementation, such as {@link
235   * DefaultKubernetesClient}
236   *
237   * @param client the {@link KubernetesClient}-and-{@link
238   * HttpClientAware} implementation that can communicate with a
239   * Kubernetes cluster; must not be {@code null}
240   *
241   * @exception MalformedURLException if there was a problem
242   * identifying a Pod within the cluster that houses a Tiller instance
243   *
244   * @exception NullPointerException if {@code client} is {@code null}
245   */
246  public <T extends HttpClientAware & KubernetesClient> Tiller(final T client) throws MalformedURLException {
247    this(client, DEFAULT_NAMESPACE, DEFAULT_PORT, DEFAULT_LABELS);
248  }
249
250  /**
251   * Creates a new {@link Tiller} that will forward a local port to
252   * port {@code 44134} on a Pod housing Tiller in the supplied
253   * namespace running in the Kubernetes cluster with which the
254   * supplied {@link KubernetesClient} is capable of communicating.
255   *
256   * <p>The {@linkplain Pods#getFirstReadyPod(Listable) first ready
257   * Pod} with a {@code name} label whose value is {@code tiller} and
258   * with an {@code app} label whose value is {@code helm} is deemed
259   * to be the pod housing the Tiller instance to connect to.  (This
260   * duplicates the default logic of the {@code helm} command line
261   * executable.)</p>
262   *
263   * @param <T> a {@link KubernetesClient} implementation that is also
264   * an {@link HttpClientAware} implementation, such as {@link
265   * DefaultKubernetesClient}
266   *
267   * @param client the {@link KubernetesClient}-and-{@link
268   * HttpClientAware} implementation that can communicate with a
269   * Kubernetes cluster; must not be {@code null}; no reference to
270   * this object is retained by this {@link Tiller} instance
271   *
272   * @param namespaceHousingTiller the namespace within which a Tiller
273   * instance is hopefully running; if {@code null}, then the value of
274   * {@link #DEFAULT_NAMESPACE} will be used instead
275   *
276   * @exception MalformedURLException if there was a problem
277   * identifying a Pod within the cluster that houses a Tiller instance
278   *
279   * @exception NullPointerException if {@code client} is {@code null}
280   *
281   * @exception KubernetesClientException if there was a problem
282   * connecting to Kubernetes
283   *
284   * @exception TillerException if a ready Tiller pod could not be
285   * found and consequently a connection could not be established
286   */
287  public <T extends HttpClientAware & KubernetesClient> Tiller(final T client, final String namespaceHousingTiller) throws MalformedURLException {
288    this(client, namespaceHousingTiller, DEFAULT_PORT, DEFAULT_LABELS);
289  }
290
291  /**
292   * Creates a new {@link Tiller} that will forward a local port to
293   * the supplied (remote) port on a Pod housing Tiller in the supplied
294   * namespace running in the Kubernetes cluster with which the
295   * supplied {@link KubernetesClient} is capable of communicating.
296   *
297   * <p>The {@linkplain Pods#getFirstReadyPod(Listable) first ready
298   * Pod} with labels matching the supplied {@code tillerLabels} is
299   * deemed to be the pod housing the Tiller instance to connect
300   * to.</p>
301   *
302   * @param <T> a {@link KubernetesClient} implementation that is also
303   * an {@link HttpClientAware} implementation, such as {@link
304   * DefaultKubernetesClient}
305   *
306   * @param client the {@link KubernetesClient}-and-{@link
307   * HttpClientAware} implementation that can communicate with a
308   * Kubernetes cluster; must not be {@code null}; no reference to
309   * this object is retained by this {@link Tiller} instance
310   *
311   * @param namespaceHousingTiller the namespace within which a Tiller
312   * instance is hopefully running; if {@code null}, then the value of
313   * {@link #DEFAULT_NAMESPACE} will be used instead
314   *
315   * @param tillerPort the remote port to attempt to forward a local
316   * port to; normally {@code 44134}
317   *
318   * @param tillerLabels a {@link Map} representing the Kubernetes
319   * labels (and their values) identifying a Pod housing a Tiller
320   * instance; if {@code null} then the value of {@link
321   * #DEFAULT_LABELS} will be used instead
322   *
323   * @exception MalformedURLException if there was a problem
324   * identifying a Pod within the cluster that houses a Tiller instance
325   *
326   * @exception NullPointerException if {@code client} is {@code null}
327   *
328   * @exception KubernetesClientException if there was a problem
329   * connecting to Kubernetes
330   *
331   * @exception TillerException if a ready Tiller pod could not be
332   * found and consequently a connection could not be established
333   */
334  public <T extends HttpClientAware & KubernetesClient> Tiller(final T client,
335                                                               String namespaceHousingTiller,
336                                                               int tillerPort,
337                                                               Map<String, String> tillerLabels) throws MalformedURLException {
338    super();
339    Objects.requireNonNull(client);
340    this.config = client.getConfiguration();
341    if (namespaceHousingTiller == null || namespaceHousingTiller.isEmpty()) {
342      namespaceHousingTiller = DEFAULT_NAMESPACE;
343    }
344    if (tillerPort <= 0) {
345      tillerPort = DEFAULT_PORT;
346    }
347    if (tillerLabels == null) {
348      tillerLabels = DEFAULT_LABELS;
349    }
350    final OkHttpClient httpClient = client.getHttpClient();
351    if (httpClient == null) {
352      throw new IllegalArgumentException("client", new IllegalStateException("client.getHttpClient() == null"));
353    }
354    LocalPortForward portForward = null;
355    
356    this.portForward = Pods.forwardPort(httpClient, client.pods().inNamespace(namespaceHousingTiller).withLabels(tillerLabels), tillerPort);
357    if (this.portForward == null) {
358      throw new TillerException("Could not forward port to a Ready Tiller pod's port " + tillerPort + " in namespace " + namespaceHousingTiller + " with labels " + tillerLabels);
359    }
360    this.channel = this.buildChannel(this.portForward);
361  }
362
363
364  /*
365   * Instance methods.
366   */
367
368
369  /**
370   * Returns any {@link Config} available at construction time.
371   *
372   * <p>This method may return {@code null}.</p>
373   *
374   * @return a {@link Config}, or {@code null}
375   */
376  @Override
377  public Config getConfiguration() {
378    return this.config;
379  }
380  
381
382  /**
383   * Creates a {@link ManagedChannel} for communication with Tiller
384   * from the information contained in the supplied {@link
385   * LocalPortForward}.
386   *
387   * <p><strong>Note:</strong> This method is (deliberately) called
388   * from constructors so must have stateless semantics.</p>
389   *
390   * <p>This method never returns {@code null}.</p>
391   *
392   * <p>Overrides of this method must not return {@code null}.</p>
393   *
394   * @param portForward a {@link LocalPortForward}; must not be {@code
395   * null}
396   *
397   * @return a non-{@code null} {@link ManagedChannel}
398   *
399   * @exception NullPointerException if {@code portForward} is {@code
400   * null}
401   *
402   * @exception IllegalArgumentException if {@code portForward}'s
403   * {@link LocalPortForward#getLocalAddress()} method returns {@code
404   * null}
405   */
406  @Issue(id = "42", uri = "https://github.com/microbean/microbean-helm/issues/42")
407  protected ManagedChannel buildChannel(final LocalPortForward portForward) {
408    Objects.requireNonNull(portForward);
409    @Issue(id = "43", uri = "https://github.com/microbean/microbean-helm/issues/43")
410    final InetAddress localAddress = portForward.getLocalAddress();
411    if (localAddress == null) {
412      throw new IllegalArgumentException("portForward", new IllegalStateException("portForward.getLocalAddress() == null"));
413    }
414    final String hostAddress = localAddress.getHostAddress();
415    if (hostAddress == null) {
416      throw new IllegalArgumentException("portForward", new IllegalStateException("portForward.getLocalAddress().getHostAddress() == null"));
417    }
418    return ManagedChannelBuilder.forAddress(hostAddress, portForward.getLocalPort())
419      .idleTimeout(5L, TimeUnit.SECONDS)
420      .keepAliveTime(30L, TimeUnit.SECONDS)
421      .maxInboundMessageSize(MAX_MESSAGE_SIZE)
422      .usePlaintext(true)
423      .build();
424  }
425
426  /**
427   * Closes this {@link Tiller} after use; any {@link
428   * LocalPortForward} or {@link ManagedChannel} <strong>used or
429   * created</strong> by or for this {@link Tiller} instance will be
430   * closed or {@linkplain ManagedChannel#shutdown() shut down}
431   * appropriately.
432   *
433   * @exception IOException if there was a problem closing the
434   * underlying connection to a Tiller instance
435   *
436   * @see LocalPortForward#close()
437   *
438   * @see ManagedChannel#shutdown()
439   */
440  @Override
441  public void close() throws IOException {
442    if (this.channel != null) {
443      this.channel.shutdown();
444    }
445    if (this.portForward != null) {
446      this.portForward.close();
447    }
448  }
449
450  /**
451   * Returns the gRPC-generated {@link ReleaseServiceBlockingStub}
452   * object that represents the capabilities of the Tiller server.
453   *
454   * <p>This method will never return {@code null}.</p>
455   *
456   * <p>Overrides of this method must never return {@code null}.</p>
457   *
458   * @return a non-{@code null} {@link ReleaseServiceBlockingStub}
459   *
460   * @see ReleaseServiceBlockingStub
461   */
462  public ReleaseServiceBlockingStub getReleaseServiceBlockingStub() {
463    ReleaseServiceBlockingStub returnValue = null;
464    if (this.channel != null) {
465      returnValue = MetadataUtils.attachHeaders(ReleaseServiceGrpc.newBlockingStub(this.channel), metadata);
466    }
467    return returnValue;
468  }
469
470  /**
471   * Returns the gRPC-generated {@link ReleaseServiceFutureStub}
472   * object that represents the capabilities of the Tiller server.
473   *
474   * <p>This method will never return {@code null}.</p>
475   *
476   * <p>Overrides of this method must never return {@code null}.</p>
477   *
478   * @return a non-{@code null} {@link ReleaseServiceFutureStub}
479   *
480   * @see ReleaseServiceFutureStub
481   */
482  public ReleaseServiceFutureStub getReleaseServiceFutureStub() {
483    ReleaseServiceFutureStub returnValue = null;
484    if (this.channel != null) {
485      returnValue = MetadataUtils.attachHeaders(ReleaseServiceGrpc.newFutureStub(this.channel), metadata);
486    }
487    return returnValue;
488  }
489
490  /**
491   * Returns the gRPC-generated {@link ReleaseServiceStub}
492   * object that represents the capabilities of the Tiller server.
493   *
494   * <p>This method will never return {@code null}.</p>
495   *
496   * <p>Overrides of this method must never return {@code null}.</p>
497   *
498   * @return a non-{@code null} {@link ReleaseServiceStub}
499   *
500   * @see ReleaseServiceStub
501   */  
502  public ReleaseServiceStub getReleaseServiceStub() {
503    ReleaseServiceStub returnValue = null;
504    if (this.channel != null) {
505      returnValue = MetadataUtils.attachHeaders(ReleaseServiceGrpc.newStub(this.channel), metadata);
506    }
507    return returnValue;
508  }
509
510  public HealthBlockingStub getHealthBlockingStub() {
511    HealthBlockingStub returnValue = null;
512    if (this.channel != null) {
513      returnValue = MetadataUtils.attachHeaders(HealthGrpc.newBlockingStub(this.channel), metadata);
514    }
515    return returnValue;
516  }
517
518  public HealthFutureStub getHealthFutureStub() {
519    HealthFutureStub returnValue = null;
520    if (this.channel != null) {
521      returnValue = MetadataUtils.attachHeaders(HealthGrpc.newFutureStub(this.channel), metadata);
522    }
523    return returnValue;
524  }
525  
526  public HealthStub getHealthStub() {
527    HealthStub returnValue = null;
528    if (this.channel != null) {
529      returnValue = MetadataUtils.attachHeaders(HealthGrpc.newStub(this.channel), metadata);
530    }
531    return returnValue;
532  }
533
534  public VersionOrBuilder getVersion() throws IOException {
535    final ReleaseServiceBlockingStub stub = this.getReleaseServiceBlockingStub();
536    assert stub != null;
537    final GetVersionResponse response = stub.getVersion(null);
538    assert response != null;
539    return response.getVersion();
540  }
541  
542}