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