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